1.进程间通讯概念
进程是一个独立的资源分配单元,不同进程之间的资源是独立的,没有关联,不能在一个进程中直接访问另一个进程的资源。
但是,进程不是孤立的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信( IPC:Inter Processes Communication )。
进程间通信的目的:
- 数据传输:一个进程需要将它的数据发送给另一个进程。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。为了做到这一点,需要内核提供互斥和同步机制。
- 进程控制:有些进程希望完全控制另一个进程的执行(如 Debug 进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
2.无名管道
2.1 概述
管道也叫无名管道,它是是 UNIX 系统 IPC(进程间通信) 的最古老形式,所有的 UNIX 系统都支持这种通信机制。
无名管道特点:
- 半双工,数据在同一时刻只能在一个方向上流动。
- 数据只能从管道的一端写入,从另一端读出。
- 写入管道中的数据遵循先入先出的规则。
- 管道所传送的数据是无格式的,这要求管道的读出方与写入方必须事先约定好数据的格式,如多少字节算一个消息等。
- 管道不是普通的文件,不属于某个文件系统,其只存在于内存中。
- 管道在内存中对应一个缓冲区。不同的系统其大小不一定相同。
- 从管道读数据是一次性操作,数据一旦被读走,它就从管道中被抛弃,释放空间以便写更多的数据。
- 管道没有名字,只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程,具有亲缘关系)之间使用。
管道是一种特殊类型的文件,在应用层体现为两个打开的文件描述符。
2.2 创建无名管道
#include <unistd.h>int pipe(int pipefd[2]);
功能:创建无名管道。参数:pipefd : 为int型数组的首地址,其存放了管道的文件描述符 pipefd[0]、pipefd[1]。当一个管道建立时,它会创建两个文件描述符 fd[0] 和 fd[1]。其中 fd[0] 固定用于读管道,而 fd[1] 固定用于写管道。一般文件 I/O的函数都可以用来操作管道(lseek() 除外)。返回值:成功:0失败:-1
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>int main()
{int fd[2];int ret = -1;ret = pipe(fd);if(-1 == ret){perror("pipe");return 1;}printf("fd[0]:%d fd[1]:%d\n", fd[0], fd[1]);//关闭文件描述符close(fd[0]);close(fd[1]);return 0;
}
2.3 使用无名管道通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>
#define SIZE 64//父进程写管道,子进程读管道
int main()
{int ret = -1;int fd[2];char buf[SIZE];pid_t pid = -1;ret = pipe(fd);if(-1 == ret){perror("pipe");return 1;}pid = fork();if(-1 == pid){perror("fork");return 1;}//子进程读管道if(0 == pid){close(fd[1]); //关闭写端 memset(buf, 0, SIZE);ret = read(fd[0], buf, SIZE);if(ret < 0){perror("read");exit(-1);}printf("子进程读取数据 buf:%s\n", buf);close(fd[0]); //关闭读端exit(0);}//父进程写管道close(fd[0]); //关闭读端ret = write(fd[1], "ABCDEFG", 7);if(ret < 0){perror("write");return 1;}printf("父进程写入管道的字节数:%d\n", ret);close(fd[1]); //关闭写端return 0;
}
2.4 管道的读写特点
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
- 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
- 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
- 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
- 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
总结:
读管道:
- 管道中有数据,read返回实际读到的字节数。
- 管道中无数据:
- 管道写端被全部关闭,read返回0 (相当于读到文件结尾)
- 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
写管道:
- 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)
- 管道读端没有全部关闭:
- 管道已满,write阻塞。
- 管道未满,write将数据写入,并返回实际写入的字节数。
2.5 查看管道缓冲区命令
ulimit -a
2.6 查看管道缓冲区函数
#include <unistd.h>long fpathconf(int fd, int name);
功能:该函数可以通过name参数查看不同的属性值
参数:fd:文件描述符name:_PC_PIPE_BUF,查看管道缓冲区大小_PC_NAME_MAX,文件名字字节数的上限
返回值:成功:根据name返回的值的意义也不同。失败: -1
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>int main()
{int fd[2];int ret = -1;ret = pipe(fd);if(-1 == ret){perror("pipe");return 1;}printf("fd[0]:%d fd[1]:%d\n", fd[0], fd[1]);printf("pipe size:%ld\n", fpathconf(fd[0], _PC_PIPE_BUF));printf("pipe size:%ld\n", fpathconf(fd[1], _PC_PIPE_BUF));//关闭文件描述符close(fd[0]);close(fd[1]);return 0;
}
2.7 设置非阻塞
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>
#include <fcntl.h>
#define SIZE 64//父进程写管道,子进程读管道
int main()
{int ret = -1;int fd[2];char buf[SIZE];pid_t pid = -1;ret = pipe(fd);if(-1 == ret){perror("pipe");return 1;}pid = fork();if(-1 == pid){perror("fork");return 1;}//子进程读管道if(0 == pid){close(fd[1]); //关闭写端 memset(buf, 0, SIZE);printf("子进程读取管道的内容\n");//设置非阻塞int flag = fcntl(fd[0], F_GETFL);flag |= O_NONBLOCK;fcntl(fd[0], F_SETFL, flag);ret = read(fd[0], buf, SIZE);if(ret < 0){perror("read");exit(-1);}printf("子进程读取数据 buf:%s\n", buf);close(fd[0]); //关闭读端exit(0);}//父进程写管道close(fd[0]); //关闭读端sleep(1);ret = write(fd[1], "ABCDEFG", 7);if(ret < 0){perror("write");return 1;}printf("父进程写入管道的字节数:%d\n", ret);close(fd[1]); //关闭写端return 0;
}
如果写端没有关闭,读端设置为非阻塞, 如果没有数据,直接返回-1。
3.有名管道
3.1 概述
无名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了命名管道(FIFO),也叫有名管道、FIFO文件。
命名管道(FIFO)不同于无名管道之处在于它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,这样,即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。
有名管道特点:
- FIFO 在文件系统中作为一个特殊的文件而存在,但 FIFO 中的内容却存放在内存中。
- 当使用 FIFO 的进程退出后,FIFO 文件将继续保存在文件系统中以便以后使用。
- FIFO 有名字,不相关的进程可以通过打开命名管道进行通信。
3.2 通过命令创建有名管道
mkfifo fifo #创建一个叫fifo的有名管道
#查看fifo的大小为0
3.3 通过函数创建有名管道
#include <sys/types.h>
#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
功能:命名管道的创建。
参数:pathname : 普通的路径名,也就是创建后 FIFO 的名字。mode : 文件的权限,与打开普通文件的 open() 函数中的 mode 参数相同。(0666)
返回值:成功:0 状态码失败:如果文件已经存在,则会出错且返回 -1。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/types.h>
#include <sys/stat.h>
int main()
{int ret = -1;ret = mkfifo("fifo", 0644);if(-1 == ret){perror("mkfifo");return 1;}printf("成功创建fifo\n");return 0;
}
3.4 有名管道读写
一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。
FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
示例:
新建管道文件mkfifo fifo
写管道(write.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#define SIZE 128int main()
{int fd = -1;int ret = -1;char buf[SIZE];int i = 0;fd = open("fifo", O_WRONLY);if(-1 == fd){perror("open");return 1;}printf("以只写方式打开管道\n");//写管道while(1){memset(buf, 0, SIZE);sprintf(buf, "hello world %d", i++);ret = write(fd, buf, strlen(buf));if(ret <= 0){perror("write");break;}printf("write fifo:%d\n", ret);sleep(1);}close(fd);return 0;
}
读管道(read.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#define SIZE 128int main()
{int fd = -1;int ret = -1;char buf[SIZE];fd = open("fifo", O_RDONLY);if(-1 == fd){perror("open");return 1;}printf("以只读方式打开管道\n");//读管道while(1){memset(buf, 0, SIZE);ret = read(fd, buf, SIZE);if(ret <= 0){perror("read");break;}printf("buf:%s\n", buf);}close(fd);return 0;
}
3.5 有名管道注意事项
- 一个为只读而打开一个管道的进程会阻塞直到另外一个进程为只写打开该管道
2)一个为只写而打开一个管道的进程会阻塞直到另外一个进程为只读打开该管道
读管道:
- 管道中有数据,read返回实际读到的字节数。
- 管道中无数据:
- 管道写端被全部关闭,read返回0 (相当于读到文件结尾)
- 写端没有全部被关闭,read阻塞等待
写管道:
- 管道读端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程终止)
- 管道读端没有全部关闭:
- 管道已满,write阻塞。
- 管道未满,write将数据写入,并返回实际写入的字节数。
3.6 练习
实现简易聊天功能。
新建两个有名管道:
mkfifo fifo1
mkfifo fifo2
talkA:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#define SIZE 128//先读后写,以只读方式打开管道1,以只写方式打开管道2。
int main()
{int fdr = -1;int fdw = -1;int ret = -1;char buf[SIZE];fdr = open("fifo1", O_RDONLY);if(-1 == fdr){perror("open");return 1;}fdw = open("fifo2", O_WRONLY);if(-1 == fdw){perror("open");return 1;}printf("所有管道打开成功\n");//循环读写while(1){//读管道1memset(buf, 0, SIZE);ret = read(fdr, buf, SIZE);if(ret <= 0){perror("read");break;}printf("read:%s\n", buf);//写管道2memset(buf, 0, SIZE);fgets(buf, SIZE, stdin);if('\n' == buf[strlen(buf)-1]){buf[strlen(buf)-1] = '\0';}ret = write(fdw, buf, strlen(buf));if(ret <= 0){perror("write");break;}printf("write ret:%d\n", ret);}close(fdr);close(fdw);return 0;
}
talkB:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>#define SIZE 128//先写后读,以只读方式打开管道2,以只写方式打开管道1。
int main()
{int fdr = -1;int fdw = -1;int ret = -1;char buf[SIZE];fdw = open("fifo1", O_WRONLY);if(-1 == fdw){perror("open");return 1;}fdr = open("fifo2", O_RDONLY);if(-1 == fdr){perror("open");return 1;}printf("所有管道打开成功\n");//循环读写while(1){//写管道1memset(buf, 0, SIZE);fgets(buf, SIZE, stdin);if('\n' == buf[strlen(buf)-1]){buf[strlen(buf)-1] = '\0';}ret = write(fdw, buf, strlen(buf));if(ret <= 0){perror("write");break;}printf("write ret:%d\n", ret);//读管道2memset(buf, 0, SIZE);ret = read(fdr, buf, SIZE);if(ret <= 0){perror("read");break;}printf("read:%s\n", buf);}close(fdr);close(fdw);return 0;
}
4.共享存储映射
4.1 概述
存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。
当从缓冲区中取数据,就相当于读文件中的相应字节。将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。
共享内存可以说是最有用的进程间通信方式,也是最快的IPC形式, 因为进程可以直接读写内存,而不需要任何数据的拷贝。
4.2 存储映射函数
4.2.1 mmap函数
#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:一个文件或者其它对象映射进内存
参数:addr: 指定映射的起始地址, 通常设为NULL, 由系统指定length: 映射到内存的文件长度prot: 映射区的保护方式, 最常用的 :a) 读:PROT_READb) 写:PROT_WRITEc) 读写:PROT_READ | PROT_WRITEflags: 映射区的特性, 可以是a) MAP_SHARED : 写入映射区的数据会复制回文件, 且允许其他映射该文件的进程共享。b) MAP_PRIVATE : 对映射区的写入操作会产生一个映射区的复制(copy - on - write), 对此区域所做的修改不会写回原文件。fd: 由open返回的文件描述符, 代表要映射的文件。offset: 以文件开始处的偏移量, 必须是4k的整数倍, 通常为0, 表示从文件头开始映射
返回值:成功:返回创建的映射区首地址失败:MAP_FAILED宏
常用参数:
-
第一个参数写成NULL
-
第二个参数要映射的文件大小 > 0
-
第三个参数:PROT_READ 、PROT_WRITE
-
第四个参数:MAP_SHARED 或者 MAP_PRIVATE
-
第五个参数:打开的文件对应的文件描述符
-
第六个参数:4k的整数倍,通常为0
4.2.2 munmap函数
#include <sys/mman.h>int munmap(void *addr, size_t length);
功能:释放内存映射区
参数:addr:使用mmap函数创建的映射区的首地址length:映射区的大小
返回值:成功:0失败:-1
4.2.3 示例
新建一个文件:touch txt
txt文件的大小不能为0,否则容易出现总线错误。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>int main()
{int fd = -1;int ret = -1;void *addr = NULL;fd = open("txt", O_RDWR);if(-1 == fd){perror("open");return 1;}addr = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if(addr == MAP_FAILED){perror("mmap");return 1;}printf("文件存储映射成功\n");close(fd);//写文件memcpy(addr, "ABCDEFG", 7);//读文件printf("读取内容:%s\n", (char*)addr); //从内存中读数据,等价于从文件中读取内容munmap(addr, 1024);return 0;
}
4.2.4 注意事项
- 创建映射区的过程中,隐含着一次对映射文件的读操作。
- 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。
- 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。
- 特别注意,当映射文件大小为0时,不能创建映射区。所以,用于映射的文件必须要有实际大小。mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。
- munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。
- 文件偏移量必须为4K的整数倍。
- mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
4.3 共享存储实现父子进程通信
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/wait.h>int main()
{int fd = -1;int ret = -1;void *addr = NULL;pid_t pid = -1;fd = open("txt", O_RDWR);if(-1 == fd){perror("open");return 1;}addr = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);if(addr == MAP_FAILED){perror("mmap");return 1;}printf("文件存储映射成功\n");close(fd);pid = fork();if(-1 == pid){perror("fork");return 1;}//子进程if(0 == pid){memcpy(addr, "ABCDEFG", 7);}else{wait(NULL);printf("%s\n", (char *)addr);}munmap(addr, 1024);return 0;
}
4.4 匿名映射实现父子进程通信
使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉。
可以直接使用匿名映射来代替。Linux系统提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。需要借助标志位参数flags来指定。
示例:
#include <stdio.h>
#include <string.h>#include <unistd.h>
#include <sys/mman.h>
#include <sys/wait.h>int main()
{int ret = -1;pid_t pid = -1;void *addr = NULL;addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);if(MAP_FAILED == addr){perror("mmap");return 1;}pid = fork();if(-1 == pid){perror("fork");munmap(addr, 4096);return 1;}if(0 == pid){memcpy(addr, "ABCDEFG", 7);}else{wait(NULL);printf("父进程读取:%s\n", (char *)addr);}munmap(addr, 4096);return 0;
}
5.信号
5.1 概述
在计算机科学中,信号是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。
信号可以直接进行用户空间进程和内核空间进程的交互,内核进程可以利用它来通知用户空间进程发生了哪些系统事件。
一个完整的信号周期包括四个部分:信号的产生,信号在进程中的注册,信号在进程中的注销,执行信号处理函数。
5.2 信号编号
Unix早期版本就提供了信号机制,但不可靠,信号可能丢失。Berkeley 和 AT&T都对信号模型做了更改,增加了可靠信号机制。但彼此不兼容。POSIX.1对可靠信号例程进行了标准化。
kill -l
查看相应的信号
不存在编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或标准信号),34-64称之为实时信号,驱动编程与硬件相关。
Linux常规信号一览表 :
编号 | 信号 | 对应事件 | 默认动作 |
---|---|---|---|
1 | SIGHUP | 用户退出shell时,由该shell启动的所有进程将收到这个信号 | 终止进程 |
2 | SIGINT | 当用户按下了**<Ctrl+C>**组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号 | 终止进程 |
3 | SIGQUIT | 用户按下**<ctrl+\ >**组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出此信号 | 终止进程 |
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 | 停止终端交互进程的运行。按下<ctrl+z>组合键时发出这个信号 | 暂停进程 |
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的实时信号,它们没有固定的含义(可以由用户自定义) | 终止进程 |
5.3 信号四要素
- 编号
- 名称
- 事件
- 默认处理动作
使用
man 7 signal
命令查看帮助文档
在标准信号中,有一些信号是有三个“Value”,第一个值通常对alpha和sparc架构有效,中间值针对x86、arm和其他架构,最后一个应用于mips架构。一个‘-’表示在对应架构上尚未定义该信号。
不同的操作系统定义了不同的系统信号。因此有些信号出现在Unix系统内,也出现在Linux中,而有的信号出现在FreeBSD或Mac OS中却没有出现在Linux下。这里我们只研究Linux系统中的信号。
Action默认动作:
- Term:终止进程
- Ign: 忽略信号 (默认即时对该种信号忽略操作)
- Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
- Stop:停止(暂停)进程
- Cont:继续运行进程
注意 SIGKILL 和 SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞。
5.4 信号的状态
信号的产生:
-
当用户按某些终端键时,将产生信号。
终端上按“Ctrl+c”组合键通常产生中断信号 SIGINT
终端上按“Ctrl+\”键通常产生中断信号 SIGQUIT
终端上按“Ctrl+z”键通常产生中断信号 SIGSTOP 等。
-
硬件异常将产生信号。
除数为 0,无效的内存访问等。这些情况通常由硬件检测到,并通知内核,然后内核产生适当的信号发送给相应的进程。
-
软件异常将产生信号。
当检测到某种软件条件已发生(如:定时器alarm),并将其通知有关进程时,产生信号。
-
调用系统函数(如:kill、raise、abort)将发送信号。
注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户。
-
运行 kill /killall命令将发送信号。
此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程。
未决状态:没有被处理
递达状态:信号被处理
5.5 信号产生函数
5.5.1 kill函数
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);
功能:给指定进程发送指定信号(不一定杀死)参数:pid : 取值有 4 种情况 :pid > 0: 将信号传送给进程 ID 为pid的进程。pid = 0 : 将信号传送给当前进程所在进程组中的所有进程。pid = -1 : 将信号传送给系统内所有的进程。pid < -1 : 将信号传给指定进程组的所有进程。这个进程组号等于 pid 的绝对值。sig : 信号的编号,这里可以填数字编号,也可以填信号的宏定义,可以通过命令 kill - l("l" 为字母)进行相应查看。不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。返回值:成功:0失败:-1
root用户可以发送信号给任意用户,普通用户不能向系统用户发送信号。普通用户也不能向其他普通用户发送信号,终止其进程,只能向自己创建的进程发送信号。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>
#include <sys/types.h>
#include <signal.h>int main()
{pid_t pid = -1;pid = fork();if(-1 == pid){perror("fork");return 1;}//子进程if(0 == pid){while(1){printf("子进程运行中\n");sleep(1);}exit(0);}//父进程else{sleep(3);printf("父进程kill掉子进程\n");kill(pid, 15);printf("父进程结束\n");}return 0;
}
5.5.2 raise函数
#include <signal.h>int raise(int sig);
功能:给当前进程发送指定信号(自己给自己发),等价于 kill(getpid(), sig)
参数:sig:信号编号
返回值:成功:0失败:非0值
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>
#include <signal.h>
int main()
{int i = 1;while(1){printf("%d\n", i);if(5 == i){//kill(getpid(), SIGTERM);raise(SIGTERM);}i++;sleep(1);}return 0;
}
5.5.3 abort函数
#include <stdlib.h>void abort(void);
功能:给自己发送异常终止信号 6) SIGABRT,并产生core文件,等价于kill(getpid(), SIGABRT);参数:无返回值:无
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>
int main()
{int i = 1;while(1){printf("%d\n", i);if(5 == i){abort();}i++;sleep(1);}return 0;
}
5.5.4 alarm函数
#include <unistd.h>unsigned int alarm(unsigned int seconds);
功能:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14)SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的一个定时器。取消定时器alarm(0),返回旧闹钟余下秒数。
参数:seconds:指定的时间,以秒为单位
返回值:返回0或剩余的秒数
定时与进程状态无关。就绪、运行、挂起(阻塞、暂停)、终止、僵尸……无论进程处于何种状态,alarm都计时。
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>
int main()
{unsigned int ret = 0;ret = alarm(5);printf("上一次闹钟剩下的时间:%u\n", ret); //0sleep(2);ret = alarm(4); //之前没有超时的闹钟被新的闹钟所覆盖printf("上一次闹钟剩下的时间:%u\n", ret); //3printf("按下任意键继续\n");getchar();return 0;
}
5.5.5 setitimer函数
#include <sys/time.h>int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
功能:设置定时器(闹钟)。 可代替alarm函数。精度微秒us,可以实现周期定时。
参数:which:指定定时方式a) 自然定时:ITIMER_REAL → 14)SIGALRM计算自然时间b) 虚拟空间计时(用户空间):ITIMER_VIRTUAL → 26)SIGVTALRM 只计算进程占用cpu的时间c) 运行时计时(用户 + 内核):ITIMER_PROF → 27)SIGPROF计算占用cpu及执行系统调用的时间new_value:struct itimerval, 负责设定timeout时间struct itimerval {struct timerval it_interval; //闹钟触发周期struct timerval it_value; //闹钟触发时间};struct timeval {long tv_sec; //秒long tv_usec; //微秒}itimerval.it_value: 设定第一次执行function所延迟的秒数 itimerval.it_interval: 设定以后每几秒执行functionold_value: 存放旧的timeout值,一般指定为NULL
返回值:成功:0失败:-1
示例:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <sys/time.h>
#include <signal.h>
void myfunc(int sig)
{printf("捕捉到信号:%d\n", sig);
}int main()
{struct itimerval new_value;//定时周期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;signal(SIGALRM, myfunc); //信号处理setitimer(ITIMER_REAL, &new_value, NULL); //定时器设置while (1);return 0;
}
5.6 信号集
5.6.1 概述
在PCB中有两个非常重要的信号集。一个称之为“阻塞信号集”,另一个称之为“未决信号集”。
-
阻塞信号集(信号屏蔽字)
将某些信号加入集合,对他们设置屏蔽,当屏蔽某信号后,再收到该信号,该信号的处理将推后(处理发生在解除屏蔽后)。
-
未决信号集
信号产生,未决信号集中描述该信号的位立刻翻转为1,表示信号处于未决状态。当信号被处理对应位翻转回为0。这一时刻往往非常短暂。
信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称之为未决信号集。在屏蔽解除前,信号一直处于未决状态。
这两个信号集都是内核使用位图机制来实现的。操作系统不允许我们直接对其进行位操作,而需自定义另外一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改。
5.6.2 自定义信号集函数
为了方便对多个信号进行处理,一个用户进程常常需要对多个信号做出处理,在 Linux 系统中引入了信号集。
信号集是一个能表示多个信号的数据类型,sigset_t set,set即一个信号集。既然是一个集合,就需要对集合进行添加/删除等操作。
#include <signal.h> int sigemptyset(sigset_t *set); //将set集合置空
int sigfillset(sigset_t *set); //将所有信号加入set集合
int sigaddset(sigset_t *set, int signo); //将signo信号加入到set集合
int sigdelset(sigset_t *set, int signo); //从set集合中移除signo信号
int sigismember(const sigset_t *set, int signo); //判断信号是否存在
sigset_t类型的本质是位图。不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。
示例:
#include <stdio.h>
#include <signal.h>void show_set(sigset_t *s)
{int i = 0;for(i = 1; i < 32; i++){if(sigismember(s, i))printf("1");elseprintf("0");}putchar('\n');
}
int main()
{sigset_t set;sigemptyset(&set); //清空信号集show_set(&set);sigfillset(&set); //将所有的信号加入setshow_set(&set);sigdelset(&set, SIGINT);sigdelset(&set, SIGQUIT);show_set(&set);sigaddset(&set, SIGINT);show_set(&set);return 0;
}
5.6.3 sigprocmask函数
信号阻塞集也称信号屏蔽集、信号掩码。每个进程都有一个阻塞集,创建子进程时子进程将继承父进程的阻塞集。信号阻塞集用来描述哪些信号递送到该进程的时候被阻塞(在信号发生时记住它,直到进程准备好时再将信号通知进程)。
所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号。
我们可以通过 sigprocmask() 修改当前的信号掩码来改变信号的阻塞情况。
#include <signal.h>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 信号集,从当前信号掩码中去除 set 中的信号。相当于 mask = mask & ~ set。SIG_SETMASK:将信号阻塞集合设为 set 信号集,相当于原来信号阻塞集的内容清空,然后按照 set 中的信号重新设置信号阻塞集。相当于mask = set。set : 要操作的信号集地址。若 set 为 NULL,则不改变信号阻塞集合,函数只把当前信号阻塞集合保存到 oldset 中。oldset : 保存原先信号阻塞集地址返回值:成功:0,失败:-1,失败时错误代码只可能是 EINVAL,表示参数 how 不合法。
示例:
#include <stdio.h>
#include <signal.h>//信号处理函数
void signal_handler(int signum)
{printf("捕捉到信号:%d\n", signum);
}int main()
{int ret = -1;//信号集sigset_t set;sigset_t oldset;signal(SIGINT, signal_handler);printf("按下回车键 屏蔽信号2\n");getchar(); sigemptyset(&oldset);sigemptyset(&set);sigaddset(&set, SIGINT); ret = sigprocmask(SIG_BLOCK, &set, &oldset);if(-1 == ret){perror("sigprocmask");return 1;}printf("屏蔽信号2成功\n");printf("按下回车键 解除信号2屏蔽\n");getchar();ret = sigprocmask(SIG_SETMASK, &oldset, NULL);if(-1 == ret){perror("sigprocmask");return 1;}printf("按下任意键退出\n");getchar();return 0;
}
5.6.4 sigpending函数
#include <signal.h>int sigpending(sigset_t *set);
功能:读取当前进程的未决信号集
参数:set:未决信号集
返回值:成功:0失败:-1
示例:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>int main()
{//自定义信号集sigset_t myset, old;sigemptyset(&myset);//添加要阻塞的信号sigaddset(&myset, SIGINT);sigaddset(&myset, SIGQUIT);sigaddset(&myset, SIGKILL);//自定义信号集设置到内核中的阻塞信号集sigprocmask(SIG_BLOCK, &myset, &old);sigset_t pend;int i = 0;while (1){//读内核中的未决信号集的状态sigpending(&pend);for (int i = 1; i<32; ++i){if (sigismember(&pend, i)){printf("1");}else if (sigismember(&pend, i) == 0){printf("0");}}printf("\n");sleep(1);i++;//10s之后解除阻塞if (i > 10){// sigprocmask(SIG_UNBLOCK, &myset, NULL);sigprocmask(SIG_SETMASK, &old, NULL);}}return 0;
}
5.7 信号捕捉
5.7.1 信号处理方式
一个进程收到一个信号的时候,可以用如下方法进行处理:
-
执行系统默认动作
对大多数信号来说,系统默认动作是用来终止该进程。
-
忽略此信号(丢弃)
接收到此信号后没有任何动作。
-
执行自定义信号处理函数(捕获)
用用户定义的信号处理函数处理该信号。
【注意】:SIGKILL 和 SIGSTOP 不能更改信号的处理方式,因为它们向用户提供了一种使进程终止的可靠方法。
5.7.2 signal函数
#include <signal.h>typedef void(*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能:注册信号处理函数(不可用于 SIGKILL、SIGSTOP 信号),即确定收到信号后处理函数的入口地址。此函数不会阻塞。参数:signum:信号的编号,这里可以填数字编号,也可以填信号的宏定义。handler : 取值有 3 种情况:SIG_IGN:忽略该信号SIG_DFL:执行系统默认动作信号处理函数名:自定义信号处理函数,如:func回调函数的定义如下:void func(int signo){// signo 为触发的信号,为 signal() 第一个参数的值}返回值:成功:第一次返回 NULL,下一次返回此信号上一次注册的信号处理函数的地址。如果需要使用此返回值,必须在前面先声明此函数指针的类型。失败:返回 SIG_ERR
该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数。
示例:
#include <stdio.h>
#include <signal.h>//信号处理函数
void signal_handler(int signo)
{printf("recv SIGINT\n");
}int main()
{//信号注册 Ctrl + csignal(SIGINT, signal_handler);while (1);return 0;
}
5.7.3 sigaction函数
#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:检查或修改指定信号的设置(或同时执行这两种操作)。参数:signum:要操作的信号。act: 要设置的对信号的新处理方式(传入参数)。oldact:原来对信号的处理方式(传出参数)。如果 act 指针非空,则要改变指定信号的处理方式(设置),如果 oldact 指针非空,则系统将此前指定信号的处理方式存入 oldact。返回值:成功:0失败:-1
struct sigaction结构体:
struct sigaction {void (*sa_handler)(int); //旧的信号处理函数指针void (*sa_sigaction)(int, siginfo_t *, void *); //新的信号处理函数指针sigset_t sa_mask; //信号阻塞集int sa_flags; //信号处理的方式void(*sa_restorer)(void); //已弃用
};
结构体变量的说明:
- sa_handler、sa_sigaction:信号处理函数指针,和 signal() 里的函数指针用法一样,应根据情况给sa_sigaction、sa_handler 两者之一赋值,其取值如下:
- SIG_IGN:忽略该信号
- SIG_DFL:执行系统默认动作
- 处理函数名:自定义信号处理函数
- sa_mask:信号阻塞集,在信号处理函数执行过程中,临时屏蔽指定的信号。
- sa_flags:用于指定信号处理的行为,通常设置为0,表示用默认属性。它可以是以下值的“按位或”组合:
- SA_RESTART:使被信号打断的系统调用自动重新发起(已经废弃)
- SA_NOCLDSTOP:使父进程在它的子进程暂停或继续运行时不会收到 SIGCHLD 信号。
- SA_NOCLDWAIT:使父进程在它的子进程退出时不会收到 SIGCHLD 信号,这时子进程如果退出也不会成为僵尸进程。
- SA_NODEFER:使对信号的屏蔽无效,即在信号处理函数执行期间仍能发出这个信号。
- SA_RESETHAND:信号处理之后重新设置为默认的处理方式。
- SA_SIGINFO:使用 sa_sigaction 成员而不是 sa_handler 作为信号处理函数。
新的信号处理函数:
void(*sa_sigaction)(int signum, siginfo_t *info, void *context);
参数说明:signum:信号的编号。info:记录信号发送进程信息的结构体。context:可以赋给指向 ucontext_t 类型的一个对象的指针,以引用在传递信号时被中断的接收进程或线程的上下文。
示例:旧的信号处理函数
#include <stdio.h>
#include <signal.h>void fun(int signum)
{printf("捕捉到信号:%d\n", signum);
}
int main()
{int ret = -1;struct sigaction act;act.sa_handler = fun;act.sa_flags = 0;//信号注册ret = sigaction(SIGINT, &act, NULL);if(-1 == ret){perror("sigaction");return 1;}while(1);return 0;
}
示例:新的信号处理函数
#include <stdio.h>
#include <signal.h>void fun(int signum, siginfo_t *info, void *context)
{printf("捕捉到信号:%d\n", signum);
}
int main()
{int ret = -1;struct sigaction act;act.sa_sigaction = fun; //这里与上面不同act.sa_flags = SA_SIGINFO;//信号注册ret = sigaction(SIGINT, &act, NULL);if(-1 == ret){perror("sigaction");return 1;}while(1);return 0;
}
5.7.4 sigqueue函数(了解)
#include <signal.h>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
向指定进程发送指定信号的同时,携带数据。但如传地址,需注意,不同进程之间虚拟地址空间各自独立,将当前进程地址传递给另一进程没有实际意义。
5.8 概念:不可重入、可重入函数
可重入函数主要用于多任务环境中,一个可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,而返回控制时不会出现什么错误;而不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
可重入函数也可以这样理解,重入即表示重复进入,首先它意味着这个函数可以被中断,其次意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是purecode(纯代码)可重入,可以允许有多个该函数的副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。如果确实需要访问全局变量(包括static),一定要注意实施互斥手段。可重入函数在并行运行环境中非常重要,但是一般要为访问全局变量付出一些性能代价。
满足下列条件的函数多数是不可重入(不安全)的:
- 函数体内使用了静态的数据结构;
- 函数体内调用了malloc() 或者 free() 函数(谨慎使用堆);
- 函数体内调用了标准 I/O 函数。
保证函数的可重入性的方法:
- 在写函数时候尽量使用局部变量(例如寄存器、栈中的变量);
- 对于要使用的全局变量要加以保护(如采取关中断、信号量等互斥方法),这样构成的函数就一定是一个可重入的函数。
Linux常见的可重入函数:
5.9 SIGCHLD信号避免僵尸进程
SIGCHLD信号产生的条件:
- 子进程终止时
- 子进程接收到SIGSTOP信号停止时
- 子进程处在停止态,接受到SIGCONT后唤醒时
利用SIGCHLD信号避免僵尸进程:
- 最简单的方法,父进程通过 wait() 和 waitpid() 等函数等待子进程结束,但是,这会导致父进程挂起。
- 如果父进程要处理的事情很多,不能够挂起,通过 signal() 函数人为处理信号 SIGCHLD , 只要有子进程退出自动调用指定好的回调函数,因为子进程结束后, 父进程会收到该信号 SIGCHLD ,可以在其回调函数里调用 wait() 或 waitpid() 回收。
示例:
void sig_child(int signo)
{pid_t pid;//处理僵尸进程, -1 代表等待任意一个子进程, WNOHANG代表不阻塞while ((pid = waitpid(-1, NULL, WNOHANG)) > 0){printf("child %d terminated.\n", pid);}
}int main()
{pid_t pid;// 创建捕捉子进程退出信号// 只要子进程退出,触发SIGCHLD,自动调用sig_child()signal(SIGCHLD, sig_child);pid = fork();if (pid < 0){ // 出错perror("fork error:");exit(1);}else if (pid == 0){ // 子进程printf("I am child process,pid id %d.I am exiting.\n", getpid());exit(0);}else if (pid > 0){ // 父进程sleep(2); // 保证子进程先运行printf("I am father, i am exited\n\n");system("ps -ef | grep defunct"); // 查看有没有僵尸进程}return 0;
}
- 如果父进程不关心子进程什么时候结束,那么可以用signal(SIGCHLD, SIG_IGN)通知内核,自己对子进程的结束不感兴趣,父进程忽略此信号,那么子进程结束后,内核会回收, 并不再给父进程发送信号。
示例:
int main()
{pid_t pid;// 忽略子进程退出信号的信号// 那么子进程结束后,内核会回收, 并不再给父进程发送信号signal(SIGCHLD, SIG_IGN);pid = fork();if (pid < 0){ // 出错perror("fork error:");exit(1);}else if (pid == 0){printf("I am child process,pid id %d.I am exiting.\n", getpid());exit(0);}else if (pid > 0){sleep(2); // 保证子进程先运行printf("I am father, i am exited\n\n");system("ps -ef | grep defunct"); // 查看有没有僵尸进程}return 0;
}