3. 信号模型及信号屏蔽
3.1 信号的可靠性
(1)与信号相关的数据结构
①struct sigpending结构体:。包括一个信号队列和一个信号位图。其中的signal域保存所有待处理信号的集合。因每个信号占一位,该域也可被为信号未决字或信号接收位图)。
②blocked字段:进程的信号屏蔽字(或信号屏蔽位图),共31位(代表1-31号信号,0没有意义),每一位代表一个信号,初始为0。当某位被置1时表示屏蔽相应的信号,0时表示不屏蔽信号。如果发生信号时,该相应的标志位己经为1(说明正在处理相同的信号)则新的信号不会被立即处理,而会延迟处理(信号未决字相应的标志位被置1)。
③sa_mask: 信号屏蔽位图。在处理信号过程中,可以有选择地将若干种其它信号屏掉。注意同一种信号,不论相应标志位是否为1,总是自动屏蔽的。除非sa_flags中的SA_NODEFER
或SA_NOMASK为1)。但要注意的是只有sigaction安装的信号处理函数,sa_mask才会起作用。
④action中的sa_handler保存该信号对应的处理函数。早期的Unix在信号发生之后调用信号处理函数之前,会将sa_handler重置默认的信号处理函数(SIG_DFL),这要求每次在信号处理函数时,要自己手动重新注册信号,这也会造成信号的不可靠的。现在的Linux己经在内核层面上保证了信号可靠性。
//早期Unix信号的不可靠性 void sigint_handler(int signo); //声明函数原型 ... signal(SIGINT, sigint_handler); //注册信号处理函数 ... void sigint_handler(int signo){ //在进入sigint_handler函数与再次调用signal之间存在时间窗口 //在这个“窗口期”中发生的SIGINT不会被捕获到,因为信号处理函 //数被系统重置SIG_DFL,从而导致进程终止。 signal(SIGINT, sigint_handler); //需重新注册信号处理函数! }
(2)信号屏蔽设置
①信号在处理过程中blocked字段相应的标志位被置为1,处理完毕解除屏蔽(置0)。
②现在Linux信号在内核层面上都是可靠的。进程在处理信号期间,若发生了同类型的信号不会丢失(相同类型只保留一次,信号未决字相应的标志位被置1),但不会被立即处理,而是延迟处理。
③若发生不同类型的信号则会被保留并直接处理,处理完后再处理原有信号。
【编程实验】信号的系统层面可靠性
//signal_rel1.c
#include <signal.h> #include <stdio.h> #include <stdlib.h> //演示处理信号过程中,再次发送相同或不同信号时,信号处理的机制。 //注意,由于现在Linux己经保证了信号的可靠性,所以在sig_handler函数中并没有重新注册信号 //处理函数。 void sig_handler(int signo) { static int i1=0, i2=0; if(signo == SIGINT){ printf("process the SIGINT "); sleep(5); //睡眠过程中可按ctrl-c或ctrl-z再次发送相同或不同的信号 //以测试信号处理机制 printf("%d pid=%d catch SIGINT ", ++i1, getpid()); printf("process the SIGINT finished "); } if(signo == SIGTSTP){ printf("process the SIGTSTP "); sleep(5); //睡眠过程中可按ctrl-c或ctrl-z再次发送相同或不同的信号 //以测试信号处理机制 printf("%d pid=%d catch SIGINT ", ++i2, getpid()); printf("process the SIGTSTP finished "); } } int main(void) { //ctrl-c if(signal(SIGINT, sig_handler) == SIG_ERR){ perror("signal sigint error"); } //ctrl-z if(signal(SIGTSTP, sig_handler) == SIG_ERR){ perror("signal sigtstp error"); } printf("begin running "); while(1) pause(); //进程暂停等待信号 printf("end running "); return 0; } /*输出结果: begin running ^Cprocess the SIGINT //连续按两次ctrl-c,第2次会延迟处理 ^C1 pid=1509 catch SIGINT //即同一类型信号会延迟处理 process the SIGINT finished process the SIGINT 2 pid=1509 catch SIGINT process the SIGINT finished ^Zprocess the SIGTSTP //连续按两次ctrl-z,第2次会延迟处理 ^Z1 pid=1509 catch SIGINT process the SIGTSTP finished process the SIGTSTP 2 pid=1509 catch SIGINT process the SIGTSTP finished //先按ctrl-c,再按ctrl-z,会先执行ctrl-z,再执行ctrl-c ^Cprocess the SIGINT //即不同类型信号,会立即处理 ^Zprocess the SIGTSTP 3 pid=1509 catch SIGINT process the SIGTSTP finished 3 pid=1509 catch SIGINT process the SIGINT finished ^Zprocess the SIGTSTP //先按ctrl-z,再按ctrl-c,会先执行ctrl-c,再执行ctrl-z ^Cprocess the SIGINT 4 pid=1509 catch SIGINT process the SIGINT finished 4 pid=1509 catch SIGINT process the SIGTSTP finished //可以快速按ctrl-c、ctrl-z、ctrl-c、ctrl-z、ctrl-c、ctrl-z做实验: //提示:同类型只被保留一次,所以只会输出前面的4个信号。不同类型会立即处理所以会先执行 */
(3)Linux信号模型的可靠性
①早期的Unix由于信号处理函数执行完,会将处理函数重置为默认的(SIG_DFL),这会造成信号的不可靠。现代的Linux信号己经在系统层面保证了信号的可靠性。
②用户层面也是可靠的,但需要用户自己来维护这种可靠性(将依赖信号而执行的代码放置在信号处理函数中,否则是不可靠的)
【编程实验】用户层面的可靠性
//signal_rel2.c
#include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <signal.h> //信号在用户层面的可靠性 int is_sig_occ = 0; //信号发生的标志 void sig_handler(int signo) { printf("signo occured: %d ", signo); is_sig_occ = 1; } int main(void) { if(signal(SIGINT, sig_handler) == SIG_ERR){ perror("signal sigint error"); } printf("begin running "); //如果信号未发生,则暂停。否则执行while后面的语句 while(is_sig_occ == 0){ //在判断is_sig_occ与pause之间有个“时间窗口”, //如果在这个时间发生信号,则会去执行信号处理函数 //从而将is_sig_occ改变为1。但这时己晚,这个信号无法 //触发while后面的语句执行,而是执行完信号处理函数后 //继续执行后面的pause语句,从而造成信号的不可靠。 //改进方法:将依赖信号执行的printf("I will running "); //代码移到信号处理函数中去执行! sleep(5);//为了模拟这个“时间窗口” pause();//进程暂停等待信号发生 } printf("I will running ");//信号发生要预期要执行的代码,为了保证这行在 //信号发生时一定被执行,应将其放入信号处理 //函数中。 return 0; } /*输出结果对比: [root@localhost]# bin/signal_rel2 begin running //5秒之后按ctrl-c,信号正常触发代码执行 ^Csigno occured: 2 I will running [root@localhost]# bin/signal_rel2 begin running //5秒之内按ctrl-c,信号被丢失 ^Csigno occured: 2 */