信号的基本概念
signal.h中找到,例如其中有定义#define SIGINT 2。编号34以上的是实时信号,34以下的信号是普通信号。而这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明,在命令行上输入man 7 signal:
产生信号的方式:
进程收到信号后,常见的信号处理动作有以下三种: 忽略此信号。前台进程与后台进程 //sig.c #include int main(){ while(1); return 0; }
2.Ctrl-C产生的信号只能发给前台进程,不能发给后台进程。
前台进程:
后台进程:
3.后台进程使Shell不必等待进程结束就可以接受新的命令。而前台进程运行时占用SHELL,它运行的时候SHELL不能接受其他命令。
4.Shell可以同时运行一个前台进程和任意多个后台进程。
异步(Asynchronous)的。后台进程不是任一进程都能做,要看实际情况。一般来说,如果某进程不需从键盘输入输出(交互少的)或者执行所需时间较长的话,就比较合适做后台进程。 产生信号
产生信号的方式在前面已经提到,简单来说可以归结为以下4点: 通过终端按键(组合键)产生信号
硬件异常产生的信号
调用系统函数向进程发信号
由软件条件产生信号
1.通过终端按键(组合键)产生信号:
Core Dump:
2.调用系统函数向进程发信号:
kill:给某个进程发送某个信号
函数原型: #include int kill(pid_t pid, int signo); //成功返回0,错误返回-1。
模拟实现kill: #include #include #include #include void usage(const char* proc){ printf("Usage: %s signo pid\n", proc); } int main(int argc, char* argv[]){ if(argc != 3){ usage(argv[0]); return 0; } int signo = atoi(argv[1]); int pid = atoi(argv[2]); kill(pid, signo); return 0; }
raise:给自己发送某个信号
函数原型: #include int raise(int signo); //成功返回0,错误返回-1。 #include #include #include #include int main(){ while(1){ sleep(1); raise(3); } return 0; }
执行1秒后:
也就是该进程执行1秒后,遇到raise函数,发出3号信号,导致程序异常终止。
abort:给自己发送 SIGABRT 信号
函数原型: #include #include #include #include int main(){ while(1){ sleep(1); abort(); } return 0; }
执行1秒后,遇到abort函数,发出 SIGABRT 信号,导致程序异常终止
3.由软件条件产生信号:
闹钟信号: #include #include #include #include #include void MyHandler(int sig, uint64_t count){ (void) sig; printf("count = %lu\n", count); exit(0); } int main(){ uint64_t count = 0; signal(SIGALRM, MyHandler); alarm(3); while(1){ ++count; } return 0; } 捕捉信号
Linux内核实现信号捕捉流程:
特点: 整体执行顺序∞
信号处理函数与原有的main函数是两个不同的执行流
信号处理未执行完以前,原有的main函数一直挂起等待
有关捕捉信号的函数:
pause: #include int pause(void);
模拟实现sleep函数: #include #include #include void MyHandler(int sig){ (void) sig; } void MySleep(int second){ //1.捕捉 SIGALRM 信号,并且要把原来的信号处理方法备份下来 __sighandler_t old_handler = signal(SIGALRM, MyHandler); //2.通过 alarm 注册一个闹钟 alarm(second); //3.需要借助一个函数来等待闹钟信号的到来 // 在闹钟信号到来之前,挂起等待 // 一旦闹钟信号来了,继续往后执行 pause(); //4.恢复 SIGALRM 的信号处理方式 // 如果不恢复的话,就可能影响到其他信号 signal(SIGALRM, old_handler); return; } int main(){ printf("before sleep...\n"); MySleep(3); printf("after sleep...\n"); return 0; } 阻塞信号
进程可以选择阻塞(Block )某个信号。
2.信号集操作函数
3.sigprocmask #include int sigprocmask(int how, const sigset_t *set, sigset_t *oset); //返回值:若成功则为0,若出错则为-1
4.sigpending
利用以上函数,进行简单的测试: #include #include #include void PrintSigset(sigset_t* set){ int i = 1; for(; i < 32; ++i){ if(sigismember(set, i)){ //判断指定信号是否在目标集合中 printf("1"); } else{ printf("0"); } } printf("\n"); } void MyHandler(int sig){ printf("sig = %d\n", sig); } int main(){ //1.捕捉 SIGINT 信号 signal(SIGINT, MyHandler); //2.把 SIGINT 信号屏蔽掉 sigset_t set; sigset_t oset; // 清空初始化 sigemptyset(&set); sigaddset(&set, SIGINT); // 设置阻塞信号集,阻塞SIGINT信号 sigprocmask(SIG_BLOCK, &set, &oset); //3.循环读取未决信号集 int count = 0; while(1){ ++count; if(count >= 6){ count = 0; //解除信号屏蔽字,再设置上 printf("解除信号屏蔽字!\n"); sigprocmask(SIG_SETMASK, &oset, NULL); sleep(1); printf("再次设置信号屏蔽字!\n"); sigprocmask(SIG_BLOCK, &set, &oset); } sigset_t pending_set; //获取未决信号集 sigpending(&pending_set); PrintSigset(&pending_set); sleep(1); } return 0; }
实现的效果如下:
同一进程中的同一个函数被不同执行流调用称为重入。
判断依据:是否有逻辑问题
同一个函数在多个执行流中同时调用,没有逻辑问题,称为可重入。
同一个函数在多个执行流中同时调用,有逻辑问题,称为不可重入。
调用了其他不可重入函数。
使用了非常量的全局变量/静态变量。
如果信号处理函数为不可重入函数,就可能出现逻辑问题。 volatile
作用:防止编译器过度优化而导致变量未正常读取(从寄存器中读取) 竞态条件与sigsuspend函数
现在重新审视“mysleep”程序,设想这样的时序: 注册SIGALRM信号的处理函数。
nsecs秒钟之后闹钟超时了,内核发送SIGALRM信号给这个进程,处于未决状态。
可是SIGALRM信号已经处理完了,还等待什么呢?
#include int sigsuspend(const sigset_t *sigmask);
#include #include #include void MyHandler(int sig){ (void) sig; } //这个版本的函数来解决竞态条件问题 void MySleep(int second){ //1.捕捉 SIGALRM 信号,并且要把原来的信号处理方法备份下来 __sighandler_t old_handler = signal(SIGALRM, MyHandler); //2.屏蔽闹钟信号 sigset_t set, oset; sigemptyset(&set); sigaddset(&set, SIGALRM); sigprocmask(SIG_BLOCK, &set, &oset); //3.通过 alarm 注册一个闹钟 alarm(second); //4.需要借助一个函数来等待闹钟信号的到来 // 在闹钟信号到来之前,挂起等待 // 一旦闹钟信号来了,继续往后执行 // 此处不能使用 pause, 而要使用 sigsuspend 来进行替换 // sigsuspend 能够做到原子的进行解除屏蔽+等待 sigset_t tmp_set = oset; // 此处我们基于oset备份了一份tmp_set // 但是 tmp_set 里面可能也包含了对 SIGALRM 的屏蔽 sigdelset(&tmp_set, SIGALRM); sigsuspend(&tmp_set); //5.恢复 SIGALRM 的信号处理方式 // 如果不恢复的话,就可能影响到其他信号 signal(SIGALRM, old_handler); //6.恢复信号屏蔽字 sigprocmask(SIG_SETMASK, &oset, NULL); return; } int main(){ printf("before sleep...\n"); MySleep(3); printf("after sleep...\n"); return 0; } SIGCHLD信号
#include #include #include #include #include void MyHandler(int sig){ (void) sig; //int ret = wait(NULL); //若用wait,会因wait数量不匹配而导致产生僵尸进程 //printf("MyHandler:%d\n", ret); while(1){ int ret = waitpid(-1, NULL, WNOHANG); if(ret > 0){ printf("waitpid %d\n", ret); continue; } else if(ret == 0){ //子进程还存在,没执行完,直接让信号处理函数返回 //如果后续的子进程再结束,还会再次发送 SIGCHLD 信号 break; } else{ //子进程都结束 break; } } } int main(){ signal(SIGCHLD, MyHandler); int i = 0; for(; i < 20; ++i){ pid_t ret = fork(); if(ret < 0){ perror("fork"); return 1; } if(ret == 0){ //child printf("child %d\n", getpid()); sleep(3); exit(0); } } while(1){ printf("father working\n"); sleep(1); } return 0; }