一.基础知识
信号产生的条件
a. 终端按键产生。如:ctrl+c(SIGINT信号),ctrl+\(SIGQUIT信号),ctrl+z(SIGTSTP信号)......
b. 系统命令和函数。如:kill(2)函数,raise函数,abort函数(SIGABRT信号)(就像exit函数一样,abort函数总是会成功的,所以没有返回值)。
c.软硬件产生。如:当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程;闹钟超时产生SIGALRM信号;向读端已关闭的管道写数据时产生SIGPIPE信号; 若不想按默认动作处理信号,可调用sigaction(2)函数告诉内核如何处理某种信号......
信号处理动作
a.忽略
b.执行默认动作
c.自定义动作(捕捉信号)
阻塞信号
a.信号递达(Delivery):实际执行信号的处理动作。
b.信号未决(Pending):信号从产生到递达之间的状态。
c.信号阻塞: 进程可以选择阻塞(Block)某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略表示信号已经递达,因为忽略是在递达之后可选的一种处理动作。
信号在内核中的表示可以看作是这样的:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
(1). SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
(2). SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除 阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
(3). SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。
在进程解决对某信号的阻塞状态前,信号可能过多次,Linux是这样实现的:常规信号是采用只记录一次,而实时信号将这些信号保存在一个队列中。
5.函数
1)sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何 有效信号。
2)sigfillset初始化set所指向的信号集。
在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定 的状态。
3)sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
以上4个函数成功返回0,出错返回-1。
4)sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返 回1,不包含则返回0,出错返回-1。
5)sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。形式如下,成功返回0,出错回-1。
6)sigpending读取当前进程的未决信号集。调用成功则返回0,出错则返回-1。
6.代码验证
1 #include<stdio.h> 2 #include<signal.h> 3 void printsigset(sigset_t* set) 4 { 5 int i=1; 6 for(;i<32;i++) 7 { 8 if(sigismember(set,i)) 9 { 10 putchar('1'); 11 } 12 else 13 { 14 putchar('0'); 15 } 16 } 17 printf("\n"); //效果等价于puts(""); puts()函数用来向标准输出设备(屏幕)写字符串并换行。 18 } 19 int main() 20 { 21 sigset_t s,p; 22 sigemptyset(&s); 23 sigaddset(&s,SIGINT); 24 sigprocmask(SIG_BLOCK,&s,NULL); 25 while(1) 26 { 27 sigpending(&p); 28 printsigset(&p); 29 sleep(1); 30 } 31 return 0; 32 }
输出结果:
结果分析:
每秒钟把各信号的未决状态打印一遍,由于我们阻塞了SIGINT信号,按Ctrl-C将会使SIGINT信号处于未决状态,按Ctrl-\,因为SIGQUIT信号没有阻塞,仍然可以终止程序。
二.捕捉
1.内核如何实现信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
(1). 用户程序注册了SIGQUIT信号的处理函数sighandler。
(2). 当前正在执行main函数,这时发生中断或异常切换到内核态。
(3). 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。
(4). 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
(5). sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。
(6). 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
就像是倒写的8,如图
2.函数
(1)a.sigaction函数可以读取和修改与指定信号相关联的处理动作;
b.成功返回0,出错返回- 1。
(2)a.signum是指定信号的编号;
b.若act指针非空,则根据act修改该信号的处理动作;
c.若oldact指针非空,则通过oldact传出该信号原来的处理动作;
d.act和oldact指向sigaction结构体。
(3)a.将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号;
b.赋值为常数SIG_DFL表示执行系统默认动作;
c.赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数。(该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。)
(1)pause函数使调用进程挂起直到有信号递达。
(2)如果信号的处理动作是终止进程,则进程止,pause函数没有机会返回;
(3)如果信号的处理动作是忽略,则进程继续处于挂起状态,pause不返回;
(4)如果信号的处理动作是捕捉,则调用了信号处理函数之后pause返回-1,errno设置为EINTR, 所以pause只有出错的返回值。(错误码EINTR表示“被信号中断”)。
3.代码实现
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<signal.h> 4 5 void sig_alrm(int signum) 6 {} 7 8 unsigned int mysleep(unsigned int nsecs) 9 { 10 struct sigaction new,old; 11 unsigned int unslept=0; 12 new.sa_handler=sig_alrm; 13 sigemptyset(&new.sa_mask); 14 new.sa_flags=0; 15 16 sigaction(SIGALRM,&new,&old); 17 alarm(nsecs); 18 pause(); 19 unslept=alarm(0); 20 sigaction(SIGALRM,&old,NULL); 21 return unslept; 22 } 23 int main() 24 { 25 while(1) 26 { 27 mysleep(3); 28 printf("3 seconds passed!\n"); 29 } 30 return 0; 31 }
输出结果:
结果分析:
(1). main函数调用mysleep函数,后者调用sigaction注册了SIGALRM信号的处理函数sig_alrm。
(2). 调用alarm(nsecs)设定闹钟。
(3). 调用pause等待,内核切换到别的进程运行。
(4). nsecs秒之后,闹钟超时,内核发SIGALRM给这个进程。
(5). 从内核态返回此进程的用户态之前处理未决信号,发现有SIGALRM信号,其处理函数是sig_alrm。
(6). 切换到用户态执行sig_alrm函数,进入sig_alrm函数时SIGALRM信号被自动屏蔽, 从sig_alrm函数返回时SIGALRM信号自动解除屏蔽。然后自动执行系统调用sigreturn再次进入内核,再返回用户态继续执行进程的主控制流程(main函数调用的mysleep函数)。
(7). pause函数返回-1,然后调用alarm(0)取消闹钟,调用sigaction恢复SIGALRM信号以前处理动作。
三.
1.sig_atomic_t类型与volatile限定符
(1)sig_atomic_t类型
例:对全局数据的访问只有一行代码,是不是原子操作呢?查看汇编
虽然C代码只有一行,但是在32位机上对一个64位的long long变量赋值需要两条指令完成,因此不是原子操作。同样地,读取这个变量到寄存器需要两个32位寄存器才放得下,也需要两条指令,不是原子操作。若main和sighandler都对这个变量a赋值,最后变量a的值可能会发生错乱。
如果在程序中需要使用一个变量,要保证对它的读写都是原子操作,C标准定义了一个类型sig_atomic_t,在不同平台的C语言库中取不同的类型,例如在32位机 上定义sig_atomic_t为int类型。
(2)sig_atomic_t类型 -> volatile限定符
在main函数中首先要注册某个信号的处理函 数sighandler,然后在一个while死循环中等待信号发生,如果有信号递达则执行sighandler, 在sighandler中将a改为1,这样再次回到main函数时就可以退出while循环,执行后续处理。用上面的方法编译和反汇编这个程序,在main函数的指令中有:
将全局变量a从内存读到eax寄存器,对eax和eax做AND运算,若结果为0则跳回循环开头,再次从内存读变量a的值,可见这三条指令等价于C代码的while(!a);循环。但编译器会优化,如下:
优化之后省去了每次循环读内存的操作。第一条指令将全局变量a的内存单元直接和0比较,如果相等,则第二条指令成了一个死循环,注意,这是一个真正的死循环:即使sighandler将a改为1,只要没有影响Zero标志位,回到main函数后仍然死在第二条指令上,因为不会再次从内存读取变量a的值。
编译器无法识别程序中存在多个执行流程。之所以程序中存在多个执行流程,是因为调用了特定平台上的特定库函数,比如sigaction、pthread_create,这些不是C语言本 身的规范,不归编译器管,程序员应该自己处理这些问题。C语言提供了volatile限定符,如果将 上述变量定义为volatile sig_atomic_t a=0;那么即使指定了优化选项,编译器也不会优化掉对变 量a内存单元的读写。
(3)必须使用volatile限定符
a.程序中存在多个执行流程访问同一全局变量
b.变量的内存单元中的数据不需要写操作就可以自己发生变化,每次读上来的值都可能不一样
c.即使多次向变量的内存单元中写数据,只写不读,也并不是在做无用功,而是有特殊意义的什么样的内存单元会具有这样的特性呢?肯定不是普通的内存,而是映射到内存地址空间的硬件寄存器,例如串口的接收寄存器属于上述第一种情况,而发送寄存器属于上述第二种情况。sig_atomic_t类型的变量应该总是加上volatile限定符,因为要使用sig_atomic_t类型的理由也正 是要加volatile限定符的理由。
2.竞态条件
(1)竞态条件(Race Condition): 如果我们写程序时考虑不周密,就可能由于时序问题而导致错误。
(2)sigsuspend:包含了pause的挂起等待功能,同时解决了竞态条件的问题,在对时序要求严格的场合下,应调用sigsuspend而不是pause。
a.sigsuspend没有成功返回值,只有执行了一个信号处理函数之后sigsuspend才返回,返回值为-1,errno设置为EINTR。
b.调用sigsuspend时,进程的信号屏蔽字由sigmask参数指定,可以通过指定sigmask来临时解除 对某 个信号的屏蔽,然后挂起等待,当sigsuspend返回时,进程的信号屏蔽字恢复为原来的值,如果原来对该信号是屏蔽的,从sigsuspend返回后仍然是屏蔽的。
3.代码实现(用sigsuspend重新实现my_sleep函数)
1 #include<stdio.h> 2 #include<signal.h> 3 #include<unistd.h> 4 void sig_alrm(int signum) 5 {} 6 unsigned int my_sleep(unsigned int nsecs) 7 { 8 struct sigaction new,old; 9 sigset_t newmask,oldmask,suspmask; 10 unsigned int unslept; 11 12 new.sa_handler=sig_alrm; 13 sigemptyset(&new.sa_mask); 14 new.sa_flags=0; 15 sigaction(SIGALRM,&new,&old); 16 17 sigemptyset(&newmask); 18 sigaddset(&newmask,SIGALRM); 19 sigprocmask(SIG_BLOCK,&newmask,&oldmask); 20 21 alarm(nsecs); 22 23 suspmask=oldmask; 24 sigsuspend(&suspmask); 25 26 unslept=alarm(0); 27 sigaction(SIGALRM,&old,NULL); 28 29 sigprocmask(SIG_SETMASK,&oldmask,NULL); 30 return unslept; 31 } 32 int main() 33 { 34 my_sleep(3); 35 printf("3 seconds passed!\n"); 36 return 0; 37 }
输出结果:
(3)SIGCHLD信号
子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。