什么是进程通讯?
进程是一个独立的资源分配单元,不同进程(这里所说的进程通常指的是用户进程)之间的资源是独立的,没有关联的,不能在一个进程中直接访问另一个进程的资源。但是进程也不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。
进程间通信简称 IPC(Interprocess communication),进程间通信就是在不同进程之间传播或交换信息。
为什么要需要进程间的通信?
进程间通讯的本质
由于各个运行进程之间具有独立性,这个独立性主要体现在数据层面,而代码逻辑层面可以私有也可以公有(例如父子进程),因此各个进程之间要实现通信是非常困难的。
各个进程之间若想实现通信,一定要借助第三方资源,这些进程就可以通过向这个第三方资源写入或是读取数据,进而实现进程之间的通信。第三方资源实际上就是操作系统提供的一段内存区域,如下图所示:
因此,进程间通信的本质就是,让不同的进程看到同一份资源(内存,文件内核缓冲等)。 由于这份资源可以由操作系统中的不同模块提供,因此出现了不同的进程间通信方式,在下文将分别介绍。
Linux 操作系统支持的主要进程间通信的通信机制:
介绍的第一种进程间通讯方式是管道,它是最古老的进程间通讯方式。管道又可以细分为有名管道与无名管道。
(1)概念理解
管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,几乎所有的 UNIX 系统都支持这种通信机制。我们把从一个进程连接到另一个进程的数据流称为一个“管道”,我们可以类比现实生活中管子,管子的一端塞东西,管子的另一端取东西。
例如:我们统计一个目录中文件的数目,需要执行一下命令,`ls | wc –l
为了上图的命令,shell 创建了两个进程来分别执行 ls 和 wc。当它们运行起来后就变成了两个进程,ls 进程通过标准输出将数据打到“管道”当中,wc 进程再通过标准输入从“管道”当中读取数据,至此便完成了数据的传输,进而完成数据的进一步加工处理。
【注意】ls 命令用于查看当前目录下的所有文件夹名和目录名,wc -l 用于统计当前的个数。
(2)管道的特点
管道其实是一个在内核内存(由 Linux 内核维护)中维护的缓冲器,这个缓冲器的存储能力是有限的,不同的操作系统大小不一定相同。
管道也可以看做一种特殊类型的文件,其拥有文件的特质:读操作、写操作,匿名管道没有文件实体,有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。所以管道在应用层体现为两个打开的文件描述符。
一个管道是一个字节流,使用管道时不存在消息或者消息边界的概念,从管道读取数据的进程可以读取任意大小的数据块,而不管写入进程写入管道的数据块的大小是多少。
通过管道传递的数据是顺序的,从管道中读取出来的字节的顺序和它们被写入管道的顺序是完全一样的,写入管道中的数据遵循先入先出的规则。
在管道中的数据的传递方向是单向的,一端用于写入,一端用于读取,管道是半双工的。
【补充】单工通信、半双工通信、全双工通信:
从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据,在管道中无法使用 lseek()
(lseek 函数详细请看参考:[[03_Linux 常用 API 函数#6.5 lseek 函数| lseek 函数介绍]]) 来随机的访问数据。
匿名管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
管道内部实现的数据结构:循环队列。
(3)为什么可以使用管道进行进程间通信?
理解了管道大致的概念,我们深入探讨一下,为什么可以使用管道进行进程间通信?
进程间通信的本质就是让不同的进程看到同一份资源,而使用匿名管道实现父子进程间通信的原理类似,就是让两个父子进程先看到同一份被打开的文件资源:子进程在 fork 之后会完全拷贝父进程的内存空间,因此子进程与父进程相当于共享文件描述符,然后父子进程就可以对该文件进行写入或是读取操作,进而实现父子进程间通信。
(1)pipe 函数
#include int pipe(int pipefd[2]);
功能:创建无名管道,用来进程间通信。参数:pipefd : 为 int 型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。pipefd[0] 对应的是管道的读端pipefd[1] 对应的是管道的写端,一般文件 I/O 的函数都可以用来操作管道 ( lseek() 除外)。返回值:成功:0失败:-1, 并设置 errno
示例:子进程通过无名管道给父进程传递一个字符串数据
// test.c :
#include
#include
#include
#include #define SIZE 64
// 父子进程使用无名管道进行通信:父进程写管道 子进程读管道
int main() {int ret = -1;int fds[2];char buf[SIZE];pid_t pid = -1;// 1、创建无名管道,注意:一定要在 fork 之前创建管道ret = pipe(fds);if (-1 == ret) {perror("pipe");return 1;}// 2、创建子进程pid = fork();if (-1 == pid) {perror("fork");return 1;}// 子进程 读管道if (0 == pid) {// 关闭写端close(fds[1]);memset (buf, 0,SIZE);// 读管道的内容ret = read ( fds[0], buf, SIZE);if (ret < 0 ) {perror("read");exit(-1);}printf("child process buf: %s\n", buf);//关闭读端close(fds[0]);//进程退出exit(0);}// 父进程 写管道// 关闭读端close(fds[0]);// 写管道ret = write(fds[1], "ABCDEGHIJK", 10);if (ret < 0 ) {perror("write");exit(1);}printf("parent process wirte len: %d\n", ret);// 关闭写端close(fds[1]);return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
parent process wirte len: 10
child process buf: ABCDEGHIJK
(2)查看管道缓冲大小命令
可以使用 ulimit -a
命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。
如上图所示,该管道有8块,每块有 512 bytes,所以一共有 4 k 的缓存大小。
(3)查看管道缓冲大小函数
#include long fpathconf(int fd, int name);
功能:该函数可以通过name参数查看不同的属性值
参数:fd:文件描述符name:_PC_PIPE_BUF,查看管道缓冲区大小_PC_NAME_MAX,文件名字字节数的上限
返回值:成功:根据 name 返回的值的意义也不同。失败: -1, 并设置 errno
#include
#include
#include
#include
#include int main() {int pipefd[2];int ret = pipe(pipefd);// 获取管道的大小long size = fpathconf(pipefd[0], _PC_PIPE_BUF);printf("pipe size : %ld\n", size);close(pipefd[0]);close(pipefd[1]);return 0;
}
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
pipe size : 4096
/*实现 ps aux | grep xxx 父子进程间通信,步骤如下:1、pipe()2、父进程:获取到数据,过滤3、子进程: ps aux, 子进程结束后,将数据发送给父进程子进程将标准输出 stdout_fileno 重定向到管道的写端。 execlp()
*/#include
#include
#include
#include
#include
#include int main() { int fd[2];int ret = pipe(fd); // 创建一个管道if(ret == -1) {perror("pipe");exit(0);}pid_t pid = fork(); // 创建子进程if(pid > 0) {// 父进程 close(fd[1]); // 关闭写端char buf[1024] = {0}; // 从管道中读取int len = -1;while((len = read(fd[0], buf, sizeof(buf) - 1)) > 0) {// 过滤数据输出printf("%s", buf);memset(buf, 0, 1024);}wait(NULL);} else if(pid == 0) { // 子进程 close(fd[0]); // 关闭读端dup2(fd[1], STDOUT_FILENO); // 文件描述符的重定向 stdout_fileno -> fd[1]execlp("ps", "ps", "aux", NULL); // 执行 ps aux,如果写入的数据大于4k(管道只有4k大小),将会有数据被被忽略,所以如果写入数据大于 4k,这里需要循环的。perror("execlp");exit(0);} else {perror("fork");exit(0);}return 0;
}
使用管道需要注意以下4种特殊情况(假设都是阻塞 I/O 操作,没有设置 O_NONBLOCK 标志):
以上无名管道的读写特点,可以总结为:
设置方法:
//获取原来的flags
int flags = fcntl(fd[0], F_GETFL);
// 设置新的flags
flag |= O_NONBLOCK; // 位或:表示追加的方式
// flags = flags | O_NONBLOCK;
fcntl(fd[0], F_SETFL, flags);
结论: 如果写端没有关闭,读端设置为非阻塞, 如果没有数据,直接返回-1。
#include
#include
#include
#include
#include
#include int main() {int pipefd[2]; // 在fork之前创建管道int ret = pipe(pipefd);if(ret == -1) {perror("pipe");exit(0);}pid_t pid = fork(); // 创建子进程if(pid > 0) { // 父进程printf("i am parent process, pid : %d\n", getpid());close(pipefd[1]); // 关闭写端char buf[1024] = {0}; // 从管道的读取端读取数据int flags = fcntl(pipefd[0], F_GETFL); // 获取原来的flagflags |= O_NONBLOCK; // 修改flag的值fcntl(pipefd[0], F_SETFL, flags); // 设置新的flagwhile(1) {int len = read(pipefd[0], buf, sizeof(buf));printf("len : %d\n", len);printf("parent recv : %s, pid : %d\n", buf, getpid());memset(buf, 0, 1024);sleep(1);}} else if(pid == 0) { // 子进程printf("i am child process, pid : %d\n", getpid());close(pipefd[0]); // 关闭读端char buf[1024] = {0};while(1) {// 向管道中写入数据char * str = "hello,i am child";write(pipefd[1], str, strlen(str));sleep(5);}}return 0;
}
无名管道,由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO文件。
有名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
一旦打开了 FIFO,就能在它上面使用与操作匿名管道和其他文件的系统调用一样的 I/O 系统调用了,如 read()
、write()
和 close()
。与管道一样,FIFO 也有一个写入端和读取端,并且从管道中读取数据的顺序与写入的顺序是一样的。FIFO 的名称也由此而来:先入先出。
有名管道(FIFO) 和无名管道(pipe)有一些特点是不相同的:
(1)有名管道使用的流程
#include
#include
int mkfifo(const char *pathname, mode_t mode);
lseek()
等文件定位操作。(2)创建有名管道
通过命令创建有名管道
通过 API 函数创建有名管道
#include
#include int mkfifo(const char *pathname, mode_t mode);
功能:命名管道的创建。
参数:pathname : 普通的路径名,也就是创建后 FIFO 的名字。mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同是一个八进制的数 。
返回值:成功:0 状态码失败:如果文件已经存在,则会出错且返回 -1, 并设置 errno。
#include
#include
#include
#include
#include int main(void) {int ret = access("fifo1", F_OK); // 判断文件是否存在if (-1 == ret) {ret = mkfifo("fifo", 0644); // 创建一个有名管道, 管道名字为fifoif (-1 == ret) {perror("mkfifo");return 1;}}return 0;
}
(3)有名管道读写操作
一旦使用 mkfifo 创建了一个 FIFO,就可以使用 open 打开它,常见的文件I/O函数都可用于 fifo,如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),对管道及 FIFO 的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如 lseek()
等文件定位操作。
#include
#include
#include
#include
#include
#include
#include // 向管道中写数据
int main() {// 1.判断文件是否存在int ret = access("fifo", F_OK);if(ret == -1) {printf("管道不存在,创建管道\n");// 2.创建管道文件ret = mkfifo("fifo", 0664);if(ret == -1) {perror("mkfifo");exit(0);} }// 3.以只写的方式打开管道int fd = open("test", O_WRONLY);if(fd == -1) {perror("open");exit(0);}// 写数据for(int i = 0; i < 100; i++) {char buf[1024];sprintf(buf, "hello, %d\n", i);printf("write data : %s\n", buf);write(fd, buf, strlen(buf));sleep(1);}close(fd);return 0;
}
#include
#include
#include
#include
#include
#include // 从管道中读取数据
int main() {// 1.以只读的方式打开管道文件int fd = open("test", O_RDONLY);if(fd == -1) {perror("open");exit(0);}// 读数据while(1) {char buf[1024] = {0};int len = read(fd, buf, sizeof(buf));if(len == 0) {printf("写端断开连接了...\n");break;}printf("recv buf : %s\n", buf);}close(fd);return 0;
}
有名管道实现简单版聊天功能:这个聊天功能非常简单,进程 A 发送一条数据,进程 B 收到该条数据后再向进程 A 回复一条数据,进程 A 再接收回复数据。
单进程有名管道实现聊天程序示例:
//talkA.C
#include
#include
#include
#include
#include
#include
#include int main() {// 1.判断有名管道文件是否存在int ret = access("fifo1", F_OK);if(ret == -1) {// 文件不存在printf("管道不存在,创建对应的有名管道\n");ret = mkfifo("fifo1", 0664);if(ret == -1) {perror("mkfifo");exit(0);}}ret = access("fifo2", F_OK);if(ret == -1) {// 文件不存在printf("管道不存在,创建对应的有名管道\n");ret = mkfifo("fifo2", 0664);if(ret == -1) {perror("mkfifo");exit(0);}}// 2.以只写的方式打开管道fifo1int fdw = open("fifo1", O_WRONLY);if(fdw == -1) {perror("open");exit(0);}printf("打开管道fifo1成功,等待写入...\n");// 3.以只读的方式打开管道fifo2int fdr = open("fifo2", O_RDONLY);if(fdr == -1) {perror("open");exit(0);}printf("打开管道fifo2成功,等待读取...\n");char buf[128];// 4.循环的写读数据while(1) {memset(buf, 0, 128);// 获取标准输入的数据fgets(buf, 128, stdin);// 写数据ret = write(fdw, buf, strlen(buf));if(ret == -1) {perror("write");exit(0);}// 5.读管道数据memset(buf, 0, 128);ret = read(fdr, buf, 128);if(ret <= 0) {perror("read");break;}printf("buf: %s\n", buf);}// 6.关闭文件描述符close(fdr);close(fdw);return 0;
}
// talkB.c
#include
#include
#include
#include
#include
#include
#include int main() {// 1.判断有名管道文件是否存在int ret = access("fifo1", F_OK);if(ret == -1) {// 文件不存在printf("管道不存在,创建对应的有名管道\n");ret = mkfifo("fifo1", 0664);if(ret == -1) {perror("mkfifo");exit(0);}}ret = access("fifo2", F_OK);if(ret == -1) {// 文件不存在printf("管道不存在,创建对应的有名管道\n");ret = mkfifo("fifo2", 0664);if(ret == -1) {perror("mkfifo");exit(0);}}// 2.以只读的方式打开管道fifo1int fdr = open("fifo1", O_RDONLY);if(fdr == -1) {perror("open");exit(0);}printf("打开管道fifo1成功,等待读取...\n");// 3.以只写的方式打开管道fifo2int fdw = open("fifo2", O_WRONLY);if(fdw == -1) {perror("open");exit(0);}printf("打开管道fifo2成功,等待写入...\n");char buf[128];// 4.循环的读写数据while(1) {// 5.读管道数据memset(buf, 0, 128);ret = read(fdr, buf, 128);if(ret <= 0) {perror("read");break;}printf("buf: %s\n", buf);memset(buf, 0, 128);// 获取标准输入的数据fgets(buf, 128, stdin);// 写数据ret = write(fdw, buf, strlen(buf));if(ret == -1) {perror("write");exit(0);}}// 6.关闭文件描述符close(fdr);close(fdw);return 0;
}
如果想要进程 A 不断地发送数据,进程 B 不断接受数据,就不能把都和写放到同一个进程中,因为放到同一个进程中,读和写必定有一个是阻塞的,不能同时被执行。 可以将进程A中读写管道分别放入到父进程和子进程中,比如父进程读,子进程写,将进程B中读写管道也分别放入到父进程和子进程中,与进程A的读写管道相反,父进程写,子进程读。
内存映射(Memory-mapped I/O)使得一个磁盘文件与存储空间中的一个缓冲区相映射,相当于将磁盘文件的数据映射到内存中,用户通过修改内存就能修改磁盘文件。
于是当从缓冲区中取数据,就相当于读文件中的相应字节。以此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不使用 read()
和 write()
函数的情况下,使用地址(指针)完成 I/O 操作(通过内存操作函数完成I/O操作),如下图所示:
内存映射也是进程间通讯的一种方式,而且效率比较高,因为它相当于直接对内存进行操作。其原理是把磁盘文件中的数据映射到内存当中,映射之后返回映射地址,在程序中就可以直接操作这块内存,操作过程中会把数据同步到磁盘文件中,这样可以实现进程间通讯。
(1)mmap 函数
#include void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:一个文件或者其它对象映射进内存中
参数:addr : 指定映射的起始地址, 通常设为NULL, 由系统指定。【补充】如果 addr 为 NULL,则内核会自行挑选一个页对齐的地址;如果 addr 不为 NULL ,则内核只是将其作为一个提示。length:映射到内存的文件长度,这个值不能为 0(即文件大小 > 0);建议直接使用文件的长度。【补充】获取文件长度可通过 stat()、lseek() 等函数prot: 映射区的保护方式(【注意】要操作映射内存,必须要有读的权限):a) 读:PROT_READb) 写:PROT_WRITEc) 读写:PROT_READ | PROT_WRITEflags: 映射区的特性, 可以是a) MAP_SHARED : 写入映射区的数据会复制回文件, 即映射区的数据会自动和磁盘文件同步;且允许其他映射该文件的进程共享,所以进程间通信,必须要设置这个选项。b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write),对此区域所做的修改不会写回原文件,即映射区的数据会自动和磁盘文件不同步。fd:由 open() 返回的文件描述符, 代表要映射的文件。注意点如下:a) 文件的大小不能为 0; b) open() 指定的权限不能和 prot 参数冲突(即映射区的权限 <= 文件打开的权限):prot: PROT_READ open:只读/读写 prot: PROT_READ | PROT_WRITE open:读写offset:以文件开始处的偏移量, 必须是4k的整数倍;一般不用,所以通常为0, 表示从文件头开始映射(4k是页大小)
返回值:成功:返回创建的映射区首地址失败:MAP_FAILED宏
(2)munmap 函数
#include
int munmap(void *addr, size_t length);
功能:释放内存映射区
参数:addr:使用 mmap 函数创建的映射区的首地址length:映射区的大小,即要释放的内存的大小,要和mmap函数中的length参数的值一样。
返回值:成功:0失败:-1, 并设置 errno
(3)API 使用注意事项
(1)有关系的进程间通信
内存映射实现父子进程间通信
参考示例:创建一个 test.txt 文件,并保证该文件大小大于 0。
#include
#include
#include
#include
#include
#include
#include
#include
int main() {// 1.打开一个文件int fd = open("test.txt", O_RDWR);// 打开一个文件int len = lseek(fd, 0, SEEK_END);//获取文件大小// 2.创建内存映射区void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (ptr == MAP_FAILED) {perror("mmap error");exit(-1);}close(fd); //关闭文件// 创建子进程pid_t pid = fork();if (pid == 0) {//子进程sleep(1); //保证父进程先执行// 读数据printf("%s\n", (char*)ptr);} else if (pid > 0) {//父进程// 写数据strcpy((char*)ptr, "i am u father!!");// 回收子进程资源wait(NULL);}// 释放内存映射区int ret = munmap(ptr, len);if (ret == -1) {perror("munmap error");exit(-1);}// 关闭文件close(fd);return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
i am u father!!
yxm@192:~$
(2)没有关系的进程间通信
内存映射实现不同进程间通讯
参考示例:创建一个 test.txt 文件,并保证该文件大小大于 0。
// write.c
#include
#include
#include
#include
#include
#include
#include
#include int main(void) {int fd = -1;int ret = -1;pid_t pid = -1;void *addr = NULL;// 1 以读写的方式打开一个文件fd = open("test.txt", O_RDWR);if(-1 == fd) {perror("open");return 1;}int len = lseek(fd, 0, SEEK_END);//获取文件大小// 2 将文件映射到内存addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (addr == MAP_FAILED) {perror("mmap");return 1;}printf("文件存储映射ok.....\n");// 3 关闭文件close(fd);// 4 写入到存储映射区memcpy(addr, "1234567890", 10); // 5断开存储映射munmap(addr, 1024);return 0;
}
// read.c
#include
#include
#include
#include
#include
#include
#include
#include int main(void) {int fd = -1;int ret = -1;pid_t pid = -1;void *addr = NULL;// 1 以读写的方式打开一个文件fd = open("test.txt", O_RDWR);if(-1 == fd) {perror("open");return 1;}int len = lseek(fd, 0, SEEK_END);//获取文件大小// 2 将文件映射到内存addr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);if (addr == MAP_FAILED) {perror("mmap");return 1;}printf("文件存储映射ok.....\n");// 3 关闭文件close(fd);// 4 读存储映射区数据printf("addr:%s\n", (char*)addr);// 5断开存储映射munmap(addr, 1024);return 0;
}
运行结果:
yxm@192:~$ gcc write.c -o write
yxm@192:~$ gcc read.c -o read
yxm@192:~$ ./write
文件存储映射ok.....
yxm@192:~$ ./read
文件存储映射ok.....
addr:1234567890
yxm@192:~$
通过使用我们发现,使用内存映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个大小不为 0 的文件才能实现。
通常为了建立映射区要 open()
一个 temp 文件,创建好了再 unlink、close 掉,比较麻烦。其实 Linux 系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区,这样可以直接使用匿名映射来代替前面提到的内存映射,【注意】匿名映射只能用于具有血缘关系的进程间通讯 。
匿名映射同样需要借助标志位参数 flags 来指定,使用 MAP_ANONYMOUS 或 MAP_ANON(MAP_ANON 已经被废弃) 特性即可实现。
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
程序示例:
#include
#include
#include
#include
#include
#include
#include
#include
#include int main() { // 创建匿名内存映射区int len = 4096;void *ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANON, -1, 0);if (ptr == MAP_FAILED) {perror("mmap error");exit(1);}// 创建子进程pid_t pid = fork();if (pid > 0) {//父进程// 写数据strcpy((char*)ptr, "hello mike!!");// 回收wait(NULL);} else if (pid == 0) {// 子进程sleep(1); //保证父进程先执行// 读数据printf("%s\n", (char*)ptr);}// 释放内存映射区int ret = munmap(ptr, len);if (ret == -1) {perror("munmap error");exit(-1);}return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
hello, world
共享内存除了可以实现进程间通讯外,还可以实现文件操作。不过,很少有人使用内存映射的方式操作文件,此处只简单举例说明:
// 使用内存映射实现文件拷贝的功能
/*思路:1.对原始的文件进行内存映射2.创建一个新文件(拓展该文件)3.把新文件的数据映射到内存中4.通过内存拷贝将第一个文件的内存数据拷贝到新的文件内存中5.释放资源
*/
#include
#include
#include
#include
#include
#include
#include
#include int main() {// 1.对原始的文件进行内存映射int fd = open("english.txt", O_RDWR);if(fd == -1) {perror("open");exit(0);}// 获取原始文件的大小int len = lseek(fd, 0, SEEK_END);// 2.创建一个新文件(拓展该文件)int fd1 = open("cpy.txt", O_RDWR | O_CREAT, 0664);if(fd1 == -1) {perror("open");exit(0);}// 对新创建的文件进行拓展truncate("cpy.txt", len);write(fd1, " ", 1);// 3.分别做内存映射void * ptr = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);void * ptr1 = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd1, 0);if(ptr == MAP_FAILED) {perror("mmap");exit(0);}if(ptr1 == MAP_FAILED) {perror("mmap");exit(0);}// 内存拷贝memcpy(ptr1, ptr, len);// 释放资源munmap(ptr1, len);munmap(ptr, len);close(fd1);close(fd);return 0;
}
运行结果:
yxm@192:~$ gcc test.c -o test
yxm@192:~$ ./test
yxm@192:~$ ls -l
total 332
-rw-rw-r-- 1 yxm yxm 129772 Sep 6 02:45 cpy.txt
-rw-rw-r-- 1 yxm yxm 129772 Sep 6 02:44 english.txt
.....
-rwxrwxr-x 1 yxm yxm 9016 Sep 6 02:45 test
-rw-rw-r-- 1 yxm yxm 1546 Sep 6 02:44 test.c
mmap()
函数会出错:返回MAP_FAILED。open()
函数中的权限需要和 prot 参数的权限保持一致,权限不能和 prot 参数冲突。lseek()
truncate()
函数对新的文件进行扩展。(1)信号的概念
信号全称是软件中断信号,是 Linux 进程间通信的最古老的方式之一,是事件发生时对进程的通知机制。它是在软件层次上对中断机制的一种模拟,是一种异步通信的方式 。信号可以导致一个正在运行的进程被另一个正在运行的异步进程中断,转而处理某一个突发事件。
看了上面的概念,你可能还是比较晕,不妨举个例子:“中断”其实在我们生活中经常遇到,譬如,我正在房间里打游戏,突然有一个电话、者短信或者敲门声,通知你去取快递或者外卖,无奈,我只能把正在玩游戏的我给暂停了,然后去签收快递,处理完成后再继续玩我的游戏。
我们学习的“信号”也是类似的。我们在终端上敲“Ctrl+c”,就产生一个“中断”,相当于产生一个信号,接着就会处理这么一个“中断任务”(默认的处理方式为中断当前进程)。
(2)信号的目的与特点
与外卖小哥想让你知道外卖已到达,需要你快来取走外卖的目的类似,使用信号的两个主要目的是:
【注意】这里信号的产生,注册,注销是信号的内核的机制,而不是信号的函数实现。
同时看到上面的例子,我们不难看出信号有一下特点:
每个信号必备4要素,分别是:1、编号 ;2、名称 ;3、事件 ;4、默认处理动作
(1)信号编号与名称
linux 中信号种类很多,为了方便使用和管理,操作系统给它们分别编号入库,即系统定义的信号列表。
我们可以使用 linux 提供的命令查看系统定义的信号列表,通过 kill –l (“l” 为字母)查看相应的信号:
如上图展示的 1) SIGHUP
, 1 是该信号的编号,SIGHUP就是该信号的名称,所以一共有 62 种信号,不存在编号为 0 的信号:
通过 linux 提供的 man 文档可以查询所有信号的详细信息:
# 查看man文档的信号描述
$ man 7 signal
【注意】仔细观察上图,可以发现有些信号对应着多个编号,这是因为不同的操作系统定义了不同的系统信号:第一个值通常对 alpha 和 sparc 架构有效,中间值针对 x86、arm 和其他架构,最后一个应用于 mips 架构。一个‘-’表示在对应架构上尚未定义该信号。现在,我们的 linux 操作系统基本上都是 Intel X86 架构或 ARM 架构,所以需要看中间一列的编号即可。
linux 常规信号一览表 ,高亮的信号是常用的信号:
编号 | 信号 | 对应事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 当用户退出 shell 时,由该 shell 启动的所有进程将收到这个信号 | 终止进程 |
2 | SIGINT | 当用户按下了 | 终止进程 |
3 | SIGQUIT | 用户按下 | 终止进程 |
4 | SIGILL | CPU 检测到某进程执行了非法指令 | 终止进程并产生core文件 |
5 | SIGTRAP | 该信号由断点指令或其他 trap 指令产生 | 终止进程并产生core文件 |
6 | SIGABRT | 调用 abort 函数时产生该信号 | 终止进程并产生core文件 |
7 | SIGBUS | 非法访问内存地址,包括内存对齐出错 | 终止进程并产生core文件 |
8 | SIGFPE | 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等所有的算法错误 | 终止进程并产生core文件 |
9 | SIGKILL | 无条件终止进程。本信号不能被忽略,处理和阻塞 | 终止进程,可以杀死任何进程 |
10 | SIGUSE1 | 用户定义的信号。即程序员可以在程序中定义并使用该信号 | 终止进程 |
11 | SIGSEGV | 指示进程进行了无效内存访问(段错误) | 终止进程并产生core文件 |
12 | SIGUSR2 | 另外一个用户自定义信号,程序员可以在程序中定义并使用该信号 | 终止进程 |
13 | SIGPIPE | Broken pipe 向一个没有读端的管道写数据 | 终止进程 |
14 | SIGALRM | 定时器超时,超时的时间 由系统调用alarm设置 | 终止进程 |
15 | SIGTERM | 程序结束信号,与 SIGKILL 不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号 | 终止进程 |
16 | SIGSTKFLT | Linux 早期版本出现的信号,现仍保留向后兼容 | 终止进程 |
17 | SIGCHLD | 子进程结束时,父进程会收到这个信号 | 忽略这个信号 |
18 | SIGCONT | 如果进程已停止,则使其继续运行 | 继续/忽略 |
19 | SIGSTOP | 停止进程的执行。信号不能被忽略,处理和阻塞 | 为终止进程 |
20 | SIGTSTP | 停止终端交互进程的运行。按下 | 暂停进程 |
21 | SIGTTIN | 后台进程读终端控制台 | 暂停进程 |
22 | SIGTTOU | 该信号类似于 SIGTTIN,在后台进程要向终端输出数据时发生 | 暂停进程 |
23 | SIGURG | 套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达 | 忽略该信号 |
24 | SIGXCPU | 进程执行时间超过了分配给该进程的 CPU 时间 ,系统产生该信号并发送给该进程 | 终止进程 |
25 | SIGXFSZ | 超过文件的最大长度设置 | 终止进程 |
26 | SIGVTALRM | 虚拟时钟超时时产生该信号。类似于 SIGALRM,但是该信号只计算该进程占用CPU的使用时间 | 终止进程 |
27 | SGIPROF | 类似于 SIGVTALRM,它不公包括该进程占用 CPU 时间还包括执行系统调用时间 | 终止进程 |
28 | SIGWINCH | 窗口变化大小时发出 | 忽略该信号 |
29 | SIGIO | 此信号向进程指示发出了一个异步 IO 事件 | 忽略该信号 |
30 | SIGPWR | 关机 | 终止进程 |
31 | SIGSYS | 无效的系统调用 | 终止进程并产生core文件 |
34~64 | SIGRTMIN ~ SIGRTMAX | LINUX的实时信号,它们没有固定的含义(可以由用户自定义) | 终止进程 |
(2)信号默认动作与信号状态
信号有 5 中默认处理动作:
信号有三种状态:产生、未决、递达:
【注意】SIGKILL 和 SIGSTOP 信号不能被捕捉、阻塞或者忽略,只能执行默认动作。
信号有 5 中默认处理动作,其中 Core 动作会终止进程,生成 Core 文件,具体方法如下:
// core.c
#include
#include int main() {char * buf;strcpy(buf, "hello"); // buf 没有分配内存,这样拷贝是错误的。return 0;
}
yxm@192:~/myshare$ ls
core.c
yxm@192:~/myshare$ gcc core.c -g #需要加上-g 参数,否则后续无法使用gdb 调试
yxm@192:~/myshare$ ls
a.out core.c
yxm@192:~/myshare$ ./a.out
Segmentation fault (core dumped) # 发生段错误
yxm@192:~/myshare$ ls # 注意,此时并不会生成 core 文件,因为 core 默认大小为 0 字节
a.out core.c
yxm@192:~/myshare$ ulimit -a
core file size (blocks, -c) 0 #core 文件大小为0,所以 上面步骤不会生成core文件
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
...
yxm@192:~/myshare$ ulimit -c 1024 # 修改 core 文件的大小为 1024
yxm@192:~/myshare$ ./core
Segmentation fault (core dumped)
yxm@192:~/myshare$ ls #此时就会生成core 文件
a.out core core.c
yxm@192:~/myshare$ gdb a.out
GNU gdb (Ubuntu 8.1.1-0ubuntu1) 8.1.1
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later
......
For help, type "help".
Type "apropos word" to search for commands related to "word"...
"/home/yxm/myshare/core": not in executable format: File format not recognized
(gdb) core-file core # 输出 core 文件的错误信息
[New LWP 22010]
Core was generated by `./core'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00000000004004be in ?? ()
(gdb)
(1)kill 函数
kill 既一个函数也是一个命令,关于命令详细请参考:[[09_Linux 进程基础#kill 命令| kill 命令]]
#include
#include int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号(不一定杀死,比如有些信号是忽略信号)参数:pid : 取值有 4 种情况 :pid > 0: 将信号传送给进程 ID 为pid的进程。pid = 0 : 将信号传送给当前进程所在进程组中的所有进程。pid = -1 : 将信号传送给系统内所有的进程。pid < -1 : 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。sig : 信号的编号(即数字编号),也可以填信号的宏值(即信号名字)。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。如果 sig 为 0 表示不发送任何信号。返回值:成功:0失败:-1, 并设置 errno
【注意】对于 kill ,super 用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的(没有权限)。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 普通用户只能向自己创建的进程发送信号。
#include
#include
int main() {pid_t pid = fork();if (-1 == pid) {perror("fork");return 1;}if (0 == pid) {//子进程 int i = 0;for (i = 0; i< 5; i++) {printf("in son process\n");sleep(1);}} else {//父进程printf("in father process\n");sleep(2);printf("kill son process now \n");kill(pid, SIGINT);}return 0;
}
(2) raise 函数
#include int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:sig:信号的编号(即数字编号),也可以填信号的宏值(即信号名字)。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。
返回值:成功:0;失败:非0值, 并设置 errno
【注意】对于 kill ,super 用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的(没有权限)。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 普通用户只能向自己创建的进程发送信号。
#include
#include
#include int main(void) {int i = 0;while (1) {printf("do working %d\n", i);if (4 == i) {raise(SIGTERM);}i++;sleep(1);}return 0;
}
(3)abort 函数
#include void abort(void);
功能:发送异常终止信号 SIGABRT 给当前进程,默认是杀死当前进程,并产生core文件, 等价于 kill(getpid(), SIGABRT);【补充】core文件的目的是为了方便对程序的错误进行调试:core 文件中会记录错误信息,程序运行终止之后想要知道错误原因、终止的信息,可以读取 core 文件中的信息,具体如下面的例子所示。参数:无
返回值:无
【注意】对于 kill ,super 用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的(没有权限)。同样,普通用户也不能向其他普通用户发送信号,终止其进程。 普通用户只能向自己创建的进程发送信号。
#include
#include
#include int main(void) {int i = -1;while (1) {printf("do working %d\n", i);if (4 == i) {abort(); // 给自己发送一个编号为6的信号,默认的行为就是终止进程}i++;sleep(1);}return 0;
}
(4)alarm 函数—闹钟
即当检测到某种软件条件已发生,并将其通知有关进程时产生信号,类似于闹钟的功能。
#include unsigned int alarm(unsigned int seconds);
功能:设置定时器 (闹钟)。在指定 seconds(秒)后,内核会给当前进程发送 SIGALRM信号。进程收到该信号的默认动作是终止当前进程。 【注意】alarm 不会阻塞当前进程。参数:seconds:指定的时间,以秒为单位。如果参数为0,定时器无效(不进行倒计时,也不会发送信号)取消一个定时器通过 alarm(0),此时返回旧闹钟余下秒数。返回值:之前没有定时器返回0;之前有定时器则返回剩余的秒数
【注意】
alarm(20);// 返回0
过了一秒钟
alarm(5); // 返回19,此时此处定义的定时器会覆盖上面的定时器(上一个定时器失效)并开始5秒的倒计时
#include
#include int main() {int seconds = 0;seconds = alarm(5);printf("seconds = %d\n", seconds); // seconds 值为 0sleep(2);seconds = alarm(5); // 之前没有超时的闹钟被新的设置的闹钟给覆盖了printf("seconds = %d\n", seconds); // seconds 值为 3while (1);return 0;
}
(5)setitimer 函数—定时器
#include int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
参数:which:指定定时方式(即定时器以什么时间计时):a) 自然定时:ITIMER_REAL → 时间到了发送 SIGALRM 信号,计算自然时间,最常用;b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 时间到了发送 SIGVTALRM 信号,只计算进程占用 cpu 的时间;c) 运行时计时(用户 + 内核):ITIMER_PROF → 时间到了发送 SIGPROF 信号,计算占用 cpu 及执行系统调用的时间;new_value:负责设定 timeout 时间(即时间到了,触发定时器),使用结构体表示:struct itimervalstruct itimerval { // 定时器的结构体struct timerval it_interval; // 闹钟触发周期:每个阶段的时间,间隔时间struct timerval it_value; // 闹钟触发时间:延迟多长时间执行定时器// 过10秒后,每隔2秒定是一次:10秒指 it_value;2秒指 it_interval};struct timeval { // 时间的结构体long tv_sec; // 秒数long tv_usec; // 微秒}old_value: 存放上一次定时的 timeout 值,一般不使用,所以常指定为NULL返回值:成功:0失败:返回 -1,并设置错误号【注意】:setitimer 函数和 alarm 一样都是非阻塞的函数
#include
#include
#include int main() {struct itimerval new_value;//定时周期,每隔 1 秒钟new_value.it_interval.tv_sec = 1;new_value.it_interval.tv_usec = 0;//第一次触发的时间new_value.it_value.tv_sec = 2;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); //定时器设置if(ret == -1) {perror("setitimer");exit(0);}while (1);return 0;
}
一个用户进程常常需要对多个信号做出处理,为了方便对多个信号进行处理,在 Linux 系统中引入了信号集(信号的集合),信号集用数据结构 sigset_t
来表示。这个信号集有点类似于我们的 QQ 群,一个个的信号相当于 QQ 群里的一个个好友。
进程的虚拟地址空间,分为用户区和内核区,内核区中有一个 PCB 进程控制块, 是一个结构体:task_struct,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要是两个信号集:阻塞信号集和未决信号集。
在 PCB 中的两个非常重要的信号集:一个称之为 “阻塞信号集” ,另一个称之为“未决信号集” 。这两个信号集都是内核使用位图机制来实现的。但操作系统不允许我们直接对这两个信号集进行位操作。而需自定义另外一个集合,借助信号集操作函数来对 PCB 中的这两个信号集进行修改。
阻塞信号集和未决信号集发生过程举例:
Ctrl + C
, 产生 2 号信号SIGINT (信号被创建)信号集是一个能表示多个信号的数据类型,sigset_t set,set 即一个信号集。sigset_t 实际上就是一个64位的整数(位图),因为有 64 号信号编号(实际上31、32号缺失)。既然是一个集合,就需要对集合进行添加/删除等操作。相关函数说明如下:
#include int sigemptyset(sigset_t *set); 功能:将 set 集合置空,即将信号集中的所有的标志位置为 0参数:传出参数,需要操作的信号集返回值:成功返回0, 失败返回-1
int sigfillset(sigset_t *set); 功能:将所有信号加入 set 集合,即将信号集中的所有的标志位置为 1参数:传出参数,需要操作的信号集返回值:成功返回0, 失败返回-1
int sigaddset(sigset_t *set, int signo); 功能:将 signo 信号加入到set集合,即设置 signo 信号对应的标志位为1,表示阻塞这个信号参数:set:传出参数,需要操作的信号集signo:需要设置阻塞的那个信号返回值:成功返回0, 失败返回-1
int sigdelset(sigset_t *set, int signo); 功能:从set集合中移除signo信号,即设置 signo 信号对应的标志位为 0,表示不阻塞这个信号参数:set:传出参数,需要操作的信号集signo:需要设置不阻塞的那个信号返回值:成功返回0, 失败返回-1
int sigismember(const sigset_t *set, int signo); 功能:判断某个信号是否存在,即判断某个信号是否阻塞参数:传出参数,需要操作的信号集set:传出参数,需要操作的信号集signo:需要设置阻塞的那个信号返回值:1 : signum被阻塞0 : signum不阻塞-1 : 失败, 并设置 errno。sigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
【注意】由于只能设置阻塞信号集不能设置未决信号集,所以 sigaddset()
相当于阻塞某个信号,sigdelset()
表示不阻塞某个信号
#include
#include int main() {sigset_t set; // 定义一个信号集变量int ret = 0;sigemptyset(&set); // 清空信号集的内容// 判断 SIGINT 是否在信号集 set 里ret = sigismember(&set, SIGINT);if (ret == 0) {printf("SIGINT is not a member of set \nret = %d\n", ret);}sigaddset(&set, SIGINT); // 把 SIGINT 添加到信号集 setsigaddset(&set, SIGQUIT);// 把 SIGQUIT 添加到信号集 set// 判断 SIGINT 是否在信号集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGINT);if (ret == 1) {printf("SIGINT is a member of set \nret = %d\n", ret);}sigdelset(&set, SIGQUIT); // 把 SIGQUIT 从信号集 set 移除// 判断 SIGQUIT 是否在信号集 set 里// 在返回 1, 不在返回 0ret = sigismember(&set, SIGQUIT);if (ret == 0) {printf("SIGQUIT is not a member of set \nret = %d\n", ret);}return 0;
}
我们可以通过 sigprocmask()
修改当前的阻塞信号集来改变信号的阻塞情况。
#include int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能:将自定义信号集中的数据设置到内核中:检查或修改信号阻塞集,根据 how 指定的方法对进程的阻塞信号集进行修改,新的信号阻塞集由 set 指定, 而原先的信号阻塞集合由 oldset 保存(保留旧的阻塞集是为了方便还原)。参数:how : 信号阻塞集合的修改方法,有 3 种情况:SIG_BLOCK: 将用户设置的阻塞信号集添加到内核中:向信号阻塞集合中添加 set 信号集,新的阻塞信号集是 set 和旧阻塞信号集的并集。相当于 mask = mask|set。SIG_UNBLOCK:从当前内核的阻塞信号集中去除 set 中的信号。相当于 mask = mask & ~ set。被去除的信号相当于解除了阻塞。SIG_SETMASK:将内核中原有阻塞信号集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。set : 要操作的信号集地址。若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。oldset : 保存原先信号阻塞集地址,可以为 NULL。返回值:成功:0,失败:-1,, 并设置 errno。失败时错误代码只可能是 EINVAL,表示参数 how 不合法。
#include
#include
#include
#include int main() {// 设置2、3号信号阻塞sigset_t set;sigemptyset(&set);// 将2号和3号信号添加到信号集中sigaddset(&set, SIGINT);sigaddset(&set, SIGQUIT);// 修改内核中的阻塞信号集sigprocmask(SIG_BLOCK, &set, NULL);int num = 0;while(1) {num++;// 获取当前的未决信号集的数据sigset_t pendingset;sigemptyset(&pendingset);sigpending(&pendingset);// 遍历前32位for(int i = 1; i <= 31; i++) {if(sigismember(&pendingset, i) == 1) {printf("1");} else if(sigismember(&pendingset, i) == 0) {printf("0");} else {perror("sigismember");exit(0);}}printf("\n");sleep(1);if(num == 10) {// 解除阻塞sigprocmask(SIG_UNBLOCK, &set, NULL);}}return 0;
}
本函数不常用,了解即可。
#include int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:set:未决信号集
返回值:成功:0失败:-1, 并设置 errno。
一个进程收到一个信号的时候,可以用如下方法进行处理:
内核实现信号捕捉过程:
信号捕捉的特性
接下来将介绍三种信号捕捉 API,基本可以覆盖日常工作需求。
#include typedef void(*sighandler_t)(int); // 函数指针
sighandler_t signal(int signum, sighandler_t handler);
功能:设置某个信号的捕捉行为:注册信号处理函数,即确定收到信号后处理函数的入口地址。此函数不会阻塞。参数:signum:要捕捉的信号,可以是编号(即数字编号),也可以填信号的宏值(即信号名字)。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。handler : 捕捉到的信号如何处理,取值有 3 种情况:SIG_IGN:忽略该信号SIG_DFL:执行系统默认动作信号处理函数名:自定义信号处理函数(回调函数)这个函数不是程序员调用,而是当信号产生,由内核调用;程序员只负责写这个函数(函数的类型根据实际需求,看函数指针的定义);写的内容为:捕捉到信号后如何处理信号,如:func 回调函数的定义如下:void func(int signo) {// signo 为触发的信号,为 signal() 第一个参数的值}返回值:成功:第一次返回 NULL,下一次返回此信号的上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。失败:返回 SIG_ERR,设置错误号【注意】SIGKILL SIGSTOP不能被捕捉,不能被忽略。
【注意】由 ANSI 定义,由于历史原因在不同版本的 Unix 和不同版本的 Linux 中,signal 函数可能有不同的行为。因此应该尽量避免使用它,取而代之使用 sigaction 函数。
#include
#include
#include
#include // 信号处理函数
void signal_handler(int signo) {if (signo == SIGINT) {printf("recv SIGINT\n");} else if (signo == SIGQUIT) {printf("recv SIGQUIT\n");}
}int main() {printf("wait for SIGINT OR SIGQUIT\n");/* SIGINT: Ctrl+c ; SIGQUIT: Ctrl+\ */// 信号注册函数signal(SIGINT, signal_handler);signal(SIGQUIT, signal_handler);while (1); //不让程序结束return 0;
}
#include int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:检查或修改指定信号的设置(或同时执行这两种操作),即信号捕捉。参数:signum:要操作的信号。可以是编号(即数字编号),也可以填信号的宏值(即信号名字)。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。act:捕捉到信号之后的处理动作,即设置对信号的新处理方式(传入参数)。oldact:原来对信号的处理方式,一般不使用,传递NULL(传出参数)。如果 act 指针非空,则要改变指定信号的处理方式(设置);如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。返回值:成功:0失败:-1, 并设置 errno。【注意】SIGKILL SIGSTOP不能被捕捉,不能被忽略。
【注意】由 ANSI 定义,由于历史原因在不同版本的 Unix 和不同版本的 Linux 中,signal 函数可能有不同的行为。因此应该尽量避免使用它,取而代之使用 sigaction 函数。
struct sigaction结构体:
struct sigaction {void(*sa_handler)(int); // 信号处理函数指针void(*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数指针,不常用sigset_t sa_mask; // 临时信号阻塞集:在信号捕捉函数执行过程中,临时阻塞某些信号。int sa_flags; // 信号处理的方式,即使用哪一个信号处理对捕捉到的信号进行处理// 这个值可以是0,表示使用sa_handler;// 这个值也可以是 SA_SIGINFO 表示使用 sa_sigactionvoid(*sa_restorer)(void);// 已弃用
};
signal()
里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler 两者之一赋值,其取值如下:信号处理函数:
void(*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:signum:信号的编号。info:记录信号发送进程信息的结构体。context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文。
#include
#include
#include
#include void myalarm(int num) {printf("捕捉到了信号的编号是:%d\n", num);printf("xxxxxxx\n");
}// 过3秒以后,每隔2秒钟定时一次
int main() {struct sigaction act;act.sa_flags = 0;act.sa_handler = myalarm;sigemptyset(&act.sa_mask); // 清空临时阻塞信号集// 注册信号捕捉sigaction(SIGALRM, &act, NULL);struct itimerval new_value;// 设置间隔的时间new_value.it_interval.tv_sec = 2;new_value.it_interval.tv_usec = 0;// 设置延迟的时间,3秒之后开始第一次定时new_value.it_value.tv_sec = 3;new_value.it_value.tv_usec = 0;int ret = setitimer(ITIMER_REAL, &new_value, NULL); // 非阻塞的printf("定时器开始了...\n");if(ret == -1) {perror("setitimer");exit(0);}// getchar();while(1);return 0;
}
本函数不常用,了解即可。
#include int sigqueue(pid_t pid, int sig, const union sigval value);
功能:给指定进程发送信号。
参数:pid : 进程号。sig : 信号。可以是编号(即数字编号),也可以填信号的宏值(即信号名字)。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。value : 通过信号传递的参数。union sigval 类型如下:union sigval{int sival_int;void *sival_ptr;};
返回值:成功:0失败:-1, 并设置 errno。
向指定进程发送指定信号的同时,携带数据。但如果传地址需注意:不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。
下面我们做这么一个例子,一个进程在发送信号,一个进程在接收信号的发送。
// 发送信号示例代码如下
int main() {if (argc >= 2) {pid_t pid, pid_self;union sigval tmp;pid = atoi(argv[1]); // 进程号if (argc >= 3) {tmp.sival_int = atoi(argv[2]);} else {tmp.sival_int = 100;}// 给进程 pid,发送 SIGINT 信号,并把 tmp 传递过去sigqueue(pid, SIGINT, tmp);pid_self = getpid(); // 进程号printf("pid = %d, pid_self = %d\n", pid, pid_self);}return 0;
}
接收信号示例代码如下:
// 信号处理回调函数
void signal_handler(int signum, siginfo_t *info, void *ptr) {printf("signum = %d\n", signum); // 信号编号printf("info->si_pid = %d\n", info->si_pid); // 对方的进程号printf("info->si_sigval = %d\n", info->si_value.sival_int); // 对方传递过来的信息
}int main() {struct sigaction act, oact;act.sa_sigaction = signal_handler; //指定信号处理回调函数sigemptyset(&act.sa_mask); // 阻塞集为空act.sa_flags = SA_SIGINFO; // 指定调用 signal_handler// 注册信号 SIGINTsigaction(SIGINT, &act, &oact);while (1) {printf("pid is %d\n", getpid()); // 进程号pause(); // 捕获信号,此函数会阻塞}return 0;
}
两个终端分别编译代码,一个进程接收,一个进程发送。
SIGCHLD 信号产生的条件
SIGCHL 信号用途:解决多进程中僵尸进程的问题。
子进程结束的时候,父进程有责任回收子进程的资源,一般是在父进程不断循环的调用 wait()
或者 waitpid()
去回收子进程的资源。这就导致一个问题:父进程需要不断地循环回收处理,而且 wait 函数是阻塞的,但是父进程也需要做自己的事情,不能一直阻塞等待子进程结束回收资源。如果当子进程结束的时候,给父进程发送一个 SIGCHLD信号,父进程中会默认忽略该信号,但是我们可以捕捉该信号,当捕捉到信号时,说明有子进程结束,此时可以调用 wait()
或者 waitpid()
回收子进程资源。实例如下:
#include
#include
#include
#include
#include
#include void myFun(int num) {printf("捕捉到的信号 :%d\n", num);// 回收子进程PCB的资源// 1、需要添加循环,否则只能回收少量子进程资源,因为常规信号不支持排队// 2、不使用 wait,因为 wait 会导致阻塞,而要使用 waitpid,因为 waitpid 可以设置为非阻塞while(1) {int ret = waitpid(-1, NULL, WNOHANG);if(ret > 0) {printf("child die , pid = %d\n", ret);} else if(ret == 0) {// 说明还有子进程或者break;} else if(ret == -1) {// 没有子进程break; }}
}int main() {// 提前设置好阻塞信号集,阻塞 SIGCHLD,因为有可能子进程很快结束,父进程还没有注册完信号捕捉sigset_t set;sigemptyset(&set);sigaddset(&set, SIGCHLD);sigprocmask(SIG_BLOCK, &set, NULL);// 创建一些子进程pid_t pid;for(int i = 0; i < 20; i++) {pid = fork();if(pid == 0) {break;}}if(pid > 0) {// 父进程// 捕捉子进程死亡时发送的SIGCHLD信号struct sigaction act;act.sa_flags = 0;act.sa_handler = myFun;sigemptyset(&act.sa_mask);sigaction(SIGCHLD, &act, NULL);// 注册完信号捕捉以后,解除阻塞sigprocmask(SIG_UNBLOCK, &set, NULL);while(1) {printf("parent process pid : %d\n", getpid());sleep(2);}} else if( pid == 0) {// 子进程printf("child process pid : %d\n", getpid());}return 0;
}
共享内存介绍:共享内存允许两个或者多个进程共享物理内存的同一块区域(通常被称为段)。由于一个共享内存段会成为一个进程用户空间的一部分,因此这种 IPC(进程间通信) 机制无需内核介入。所以这种 IPC 需要做的就是让一个进程将数据复制进共享内存中,并且这部分数据会对其他所有共享同一个段的进程可用。
共享内存特点:
1、创建共享内存段:shmget 函数
#include
#include int shmget(key_t key, size_t size, int shmflg);功能:创建一个新的共享内存段,或者获取一个既有的共享内存段的标识。新创建的内存段中的数据都会被初始化为0参数:key : key_t类型是一个整形,通过这个找到或者创建一个共享内存。一般使用16进制表示,非0值size: 共享内存的大小shmflg: 属性访问权限附加属性:创建/判断共享内存是不是存在创建:IPC_CREAT判断共享内存是否存在: IPC_EXCL , 需要和IPC_CREAT一起使用:IPC_CREAT | IPC_EXCL | 0664返回值:失败:-1 并设置错误号成功:>0 返回共享内存的引用的ID,后面操作共享内存都是通过这个值。
2、关联函数:shmat 函数
#include
#include void *shmat(int shmid, const void *shmaddr, int shmflg);功能:和当前的进程进行关联参数:shmid : 共享内存的标识(ID),由shmget返回值获取shmaddr: 申请的共享内存的起始地址,指定NULL,内核指定shmflg : 对共享内存的操作读 : SHM_RDONLY, 必须要有读权限读写: 0返回值:成功:返回共享内存的首(起始)地址。 失败:(void *) -1,并设置错误号
3、解除关联:shmdt 函数
#include
#include int shmdt(const void *shmaddr);功能:解除当前进程和共享内存的关联参数:shmaddr:共享内存的首地址返回值:返回值:成功 0失败: -1,并设置错误号
4、操作共享内存:shmctl 函数
#include
#include int shmctl(int shmid, int cmd, struct shmid_ds *buf);功能:对共享内存进行操作(删除共享内存,共享内存要删除才会消失,创建共享内存的进程被销毁了对共享内存是没有任何影响)。参数:shmid: 共享内存的IDcmd : 要做的操作IPC_STAT : 获取共享内存的当前的状态IPC_SET : 设置共享内存的状态IPC_RMID: 标记共享内存被销毁buf:需要设置或者获取的共享内存的属性信息IPC_STAT : buf存储数据IPC_SET : buf中需要初始化数据,设置到内核中IPC_RMID : 没有用,NULL
返回值:返回值:成功 0失败: -1,并设置错误号
5、生成 key 值:ftok 函数
#include
#include key_t ftok(const char *pathname, int proj_id);功能:根据指定的路径名,和int值,生成一个共享内存的key参数:pathname:指定一个存在的路径proj_id: int类型的值,但是这系统调用只会使用其中的1个字节,范围 : 0-255 一般指定一个字符 'a'
使用步骤
1、调用 shmget()
创建一个新共享内存段或取得一个既有共享内存段的标识符(即由其
他进程创建的共享内存段)。这个调用将返回后续调用中需要用到的共享内存标识符。
2、使用 shmat()
来附上共享内存段:使该段成为调用进程的虚拟内存的一部分。
3、此刻在程序中可以像对待其他可用内存那样对待这个共享内存段。为引用这块共享内存,程序需要使用由 shmat()
调用返回的 addr 值,它是一个指向进程的虚拟地址空间中该共享内存段的起点的指针。
4、调用 shmdt()
来分离共享内存段。在这个调用之后,进程就无法再引用这块共享内存了。这一步是可选的,并且在进程终止时会自动完成这一步。
5、调用 shmctl() 来删除共享内存段。只有当当前所有附加内存段的进程都与之分离之
后内存段才会销毁。只有一个进程需要执行这一步。
共享内存示例
// write_shm.c
#include
#include
#include
#include
int main() { // 1.创建一个共享内存int shmid = shmget(100, 4096, IPC_CREAT|0664);printf("shmid : %d\n", shmid);// 2.和当前进程进行关联void * ptr = shmat(shmid, NULL, 0);char * str = "helloworld";// 3.写数据memcpy(ptr, str, strlen(str) + 1);printf("按任意键继续\n");getchar();// 4.解除关联shmdt(ptr);// 5.删除共享内存shmctl(shmid, IPC_RMID, NULL);return 0;
}
// read_shm.c
#include
#include
#include
#include int main() { // 1.获取一个共享内存:必须是100与写端的 id 保持一直,并且大小一般设置为零,用来表示是读端int shmid = shmget(100, 0, IPC_CREAT);printf("shmid : %d\n", shmid);// 2.和当前进程进行关联void * ptr = shmat(shmid, NULL, 0);// 3.读数据printf("%s\n", (char *)ptr);printf("按任意键继续\n");getchar();// 4.解除关联shmdt(ptr);// 5.删除共享内存shmctl(shmid, IPC_RMID, NULL);return 0;
}
共享内存补充
问题1:操作系统如何知道一块共享内存被多少个进程关联?
问题2:可不可以对共享内存进行多次删除 shmctl?
问题3:共享内存和内存映射的区别?
ipcs 用法
1、ipcs -a // 打印当前系统中所有的进程间通信方式的信息
2、ipcs -m // 打印出使用共享内存进行进程间通信的信息
3、ipcs -q // 打印出使用消息队列进行进程间通信的信息
4、ipcs -s // 打印出使用信号进行进程间通信的信息
ipcrm 用法
1、ipcrm -M shmkey // 移除用shmkey创建的共享内存段
2、ipcrm -m shmid // 移除用shmid标识的共享内存段
3、ipcrm -Q msgkey // 移除用msqkey创建的消息队列
4、ipcrm -q msqid // 移除用msqid标识的消息队列
5、ipcrm -S semkey // 移除用semkey创建的信号
6、ipcrm -s semid // 移除用semid标识的信号
参考文章