信号:类似生活中,看到红绿灯就知道走还是不走
信号具有的特点:
- 信号有很多种
- 不同的信号有不同的处理方式
- 在信号发生之前,每种信号如何处理,我们都知道
kill
kill -l(小写L):查看Linux所有的信号
一共 62 种信号。1-31;34-64;
函数:
int kill(pid_t pid, int sig);
参数:
pid_t pid:进程的id, int sig:进程的信号
返回值:
成功:0 失败:-1
作用:
信号的作用,就相当于神经系统,用来处理进程运行过程中遇到的各种“意外”情况。
【重点】信号产生方式:
- 键盘按键产生:ctrl + c (2信号) ;ctrl + \ (3号信号);ctrl + z(19号信号)
- 硬件产生:
MMU 硬件产生的信号 ->产生 11 号信号,段错误信号
CPU产生的信号 ->产生 8 号信号
- 软件条件产生的信号
PIPE 读端关闭,尝试写,此时会触发SIGPIPE 信号,管道破裂
TCP 一定条件下也会触发 SIGPIPE 信号
- 系统调用产生
kill 命令
信号处理方式:
- 忽略
- 默认处理:大部分信号的默认行为都是终止进程
- 捕捉信号:通过自定义的行为来处理信号
捕捉信号函数:
typedef void(*sighandler_t)(int); //创建函数指针
sighandler_t signal(int signum, sighandler_t handler);
头文件:
#include<signal.h>
参数:
signum:对几号信号进行修改
sighandler_t handler:捕捉之后,进行什么处理
//自定义信号处理 //signal 函数 :捕获信号 typedef void(*sighandler_t)(int);//创建函数指针 sighandler_t signal(int signum, sighandler_t handler); //signum 对几号信号进行修改 //sighandler_t handler 捕捉之后,进行什么处理 void myhandle(int signo) { //自定义方式 printf("signo= %d\n", signo); } int main() { //signal 替换了原来的处理方式,就会通过返回值来返回 signal(2, myhandle); //2号信号 ctrl c 变成 打印signo= 2 signal(3, myhandle); //3号信号 ctrl \ while (1) { //测试 会一直sleep sleep(1); } return 0; }
阻塞信号
可重入函数:
一个函数可重入,就意味着多个执行流中调用没问题。
一个函数不可重入,就意味着多个执行流中调用有问题。
可重入:函数运行一半,又调用函数,再次调用就是再次进入函数。
是否可重入判断标准:
如果一个函数使用了全局变量/静态变量——不可重入。
如果函数使用了 malloc ,不可重入。
如果一个函数调用了不可重入函数,也是不可重入的。
注意:如果一个函数虽然用了全局变量,但是使用了互斥锁,就是可重入函数——这个说法是错误的。
volatile关键字:
功能:保持内存可见性
//自定义信号处理 //signal 函数 :捕获信号 typedef void(*sighandler_t)(int);//创建函数指针 sighandler_t signal(int signum, sighandler_t handler); //signum 对几号信号进行修改 //sighandler_t handler 捕捉之后,进行什么处理 #include<signal.h> int flag = 1; //volatile void myhandle(int sig) { (void)sig; flag = 0; } int main() { //signal 替换了原来的处理方式,就会通过返回值来返回 signal(2, myhandle); //2号信号 ctrl c 变成 函数功能 while (flag); return 0; }
注意:这个代码 flag 没有用关键字 volatile,代码的结果是:你按 ctrl c,结果无效。
原因:编译器的代码有关。
由于 while 循环要频繁读取 flag 到寄存器中,编译器会认为这是一个比较高的开销,另外代码没有检测到哪个代码要修改flag(myhandle 函数内核调用,编译器不知道),编译器就会有错误的判断,把 flag 这个值优化到寄存器中。
#include<stdio.h> #include<unistd.h> #include<signal.h> volatile int flag = 1; //告诉编译器每次都从内存中读取,不优化到寄存器中 //功能:保持内存可见性 //volatile void myhandle(int sig) { (void)sig; flag = 0; } int main() { //signal 替换了原来的处理方式,就会通过返回值来返回 signal(2, myhandle); //2号信号 ctrl c 变成 函数功能 while (flag); return 0; }
加上volatile,就可以了。
另外:
volatile:经常使用在多线程程序中,编译器对于对多执行流的情况不太会判断。
信号其他知识:
wait 阻塞等待
waitpid 阻塞/非阻塞等待
阻塞等待:代码简单,效率低
非阻塞等待:代码复杂,效率高
如要满足代码简单,效率高:利用17号信号 SIGCHLD
void myhandle(int sig) { (void)sig; printf("child exit\n"); wait(NULL); } int main() { signal(17, myhandle); //17号信号SIGCHLD ctrl c 变成 函数功能 pid_t ret = fork(); if (ret > 0) { //father while (1) { sleep(1); } } else if (ret == 0) { //child exit(0); } else { perror("fork"); return 1; } return 0; }
子进程正常退出,不会僵尸进程,各干各的。 结果显示:child exit ,父进程会继续sleep。这是一种方法。
#include<stdio.h> #include<unistd.h> #include<signal.h> int main() { signal(17, SIG_IGN); //17号信号SIGCHLD 子进程直接忽略(SIG_IGN) for (int i = 0; i<20; i++) { pid_t ret = fork(); if (ret == 0) { //child printf("child pid %d\n", getpid()); } while (1) { sleep(1); } } return 0; }
这是避免僵尸进程最好的方法:子进程直接忽略(SIG_IGN),即:signal(SIGCHLD , SIG_IGN)。