【APUE】Chapter10 Signals
Signal主要分两大部分:
A. 什么是Signal,有哪些Signal,都是干什么使的。
B. 列举了非常多不正确(不可靠)的处理Signal的方式,以及怎么样设计来避免这些错误出现。
10.2 Signal Concepts
1. Signal的实体就是在头文件中定义的正整数(在我使用的linux系统中在/usr/include/bits/signum.h中),如下:
2. 列举了可能会产生Signal的条件:
(1)终端的user的案件操作:如,Ctrl+c,Ctrl+\;由terminal发出。
(2)硬件异常抛出的Signal:如,除0,非法内存引用;由kernel发出。
(3)kill(2) kill(1) :向process或process group发送Signal;由process发出。
(4)软件执行时产生的Signal :比如后面要提到的SIGALRM,定点到时信号。
3. Signal是典型的异步驱动时事件,当Signal出现的时候,可以设定程序来告知kernal去做什么事情:
(1)忽略Signal。这里有两个Signal不能忽略:分别是SIGKILL和SIGSTOP。不能忽略的原因是必须让kernal或root权限可以强制终止进程。如果遇到了hardware exception情况,不去处理这类异常的话,进程后续如何执行就不确定了。
(2)捕获Signal。这里的做法是预先告诉kernal“如果出现了这个信号,用哪个函数去处理”。另,SIGKILL和SIGSTOP是不能被捕获的。
(3)默认处理策略。系统预先指定了几个Signal默认的处理策略。在signum.h中:
具体可以对照Figure10.1中每个Signal的默认处理策略去查看;有些默认策略是terminal+core类型的,可用于系统down了之后的debug。但是,也不一定保证core一定会产生;如果权限不足的话,很可能无法产生core。这个也要留意。
4. 这里插播一个Signal的解释。SIGTSTP,当执行终端命令交互的程序到时候,按下ctrl+z可以触发这个信号,向foreground process group所有的。这个信号名字里虽然叫stop,但是并不是真的给停了。这里与stop相对的是continue(即SIGCONT),只是给暂停的意思。这个信号如果只是望文生义,就容易理解偏差。不仅仅是这个信号,后面还有更无法望文生义的名词解释。
10.3 signal function
signal这个函数的含义就是告诉系统用什么函数处理什么信号。
1. 看一个例子:
- #include "apue.h"
- #include <stdio.h>
- #include <signal.h>
- static void sig_usr(int signo)
- {
- if (signo==SIGUSR1)
- {
- printf("received SIGUSR1\n");
- }
- else if (signo==SIGUSR2)
- {
- printf("received SIGUSR2\n");
- }
- else
- {
- }
- }
- int main(void)
- {
- if (signal(SIGUSR1, sig_usr) == SIG_ERR)
- {
- err_sys("can't catch SIGUSR1");
- }
- if (signal(SIGUSR2, sig_usr) == SIG_ERR)
- {
- err_sys("can't catch SIGUSR2");
- }
- for (;;)
- {
- pause();
- }
- }
执行结果如下:
初步分析如下:
(1)main中给SIGUSR1和SIGUSR2都注册了signal handler
(2)pause()函数的作用是阻塞进程,并一直等着signal到来。
(3)kill跟它叫kill没有关系,纯粹是unix系统的一个misnomer,它的作用就是向process或process group发送信号。因此就是发送SIGUSR1和SIGUSR2信号。
(4)kill默认情况下发送的信号是SIGTERM,即终止进程。
如果连续两次发送SIGUSR1信号会怎样?如下:
通过上面的运行结果可知,用signal给某个信号注册一次handler,只能管一次。即,在信号发生跳转到自定的 handler 处理函数执行后, 系统会自动将此处理函数换回原来系统预设的处理方式。那么有没有注册一次,能够处理多次信号的方法呢?有,改用sigaction函数去注册signal handler。代码如下:
- #include "apue.h"
- #include <stdio.h>
- #include <signal.h>
- static void sig_usr(int signo)
- {
- if (signo==SIGUSR1)
- {
- printf("received SIGUSR1\n");
- }
- else if (signo==SIGUSR2)
- {
- printf("received SIGUSR2\n");
- }
- else
- {
- }
- }
- int main(void)
- {
- struct sigaction sa1, sa2;
- sa1.sa_handler = sig_usr;
- sa2.sa_handler = sig_usr;
- sigemptyset(&sa1.sa_mask);
- sigemptyset(&sa2.sa_mask);
- sigaddset(&sa1.sa_mask, SIGUSR1);
- sigaddset(&sa2.sa_mask, SIGUSR2);
- sigaction(SIGUSR1, &sa1, NULL);
- sigaction(SIGUSR2, &sa2, NULL);
- /*
- if (signal(SIGUSR1, sig_usr) == SIG_ERR)
- {
- err_sys("can't catch SIGUSR1");
- }
- if (signal(SIGUSR2, sig_usr) == SIG_ERR)
- {
- err_sys("can't catch SIGUSR2");
- }
- */
- for (;;) pause();
- }
运行结果如下:
Program Start-Up :
这里面提到了一种情况,如果是由某个程序通过执行exec产生的新的Program,进而替代原来的Process(虽然进程号不变,但是即使是执行原来的程序,程序的地址也变化了);那么,由原来的Porcess注册的各种Signal处理相关的内容,在新的Program中是无效的,因为原来的signal-handler中注册的信号处理函数的地址失效了,不是原来的函数了。
从下面的部分开始,更多的从反面出发,分析signal处理上经历的各种不靠谱设计,从而理解为什么要设计各种靠谱的机制。
10.4 Unreliable Signals
1. 不靠谱伪代码(1):
- int sig_int(); /*signal处理函数*/
- ...
- signal(SIGINT, sig_int); /*注册signal handler*/
- ...
- sig_int()
- {
- signal(SIGINT, sig_int); /*重新注册signal handler以便处理下一次信号*/
- ...
- }
了解过signal函数之后,可以知道,上述代码的意思大概是:每次出现SIGINT信号,都会触发sig_int()函数来处理信号;由于signal函数只能管处理一次SIGINT,因此为了能够连续处理SIGINT信号,在每次进入sig_int()函数时,都重新用signal函数注册一下sig_int。
上面的代码看似是没有问题的,但是却隐含着比较大的漏洞。
考虑如下的情况:如果之前已经来了一个SIGINT信号,系统开始调用注册的sig_int()进行信号处理;如果恰恰在进入sig_int()函数之前,又来了一个SIGINT信号;由于此时系统已经没有用于处理SIGINT信号的函数了,则第二个到来的SIGINT信号就被采用默认的信号处理策略(对于SIGINT这个信号来说,就是直接将进程terminates了),往往达不到我们预期的信号处理效果。这种漏洞的可怕之处还在于,大部分时间程序都是工作正确的,偶尔会出现问题,这种bug是最难排除的。
2. 不靠谱伪代码(2):
- int sig_int(); /*SIGINT信号处理函数*/
- int sig_int_flag; /*SIGINT信号出现时候将其赋值为零*/
- main()
- {
- signal(SIGINT, sig_int); /*注册信号处理函数*/
- ...
- while(sig_int_flay == )
- {
- pause(); /*等着信号到来*/
- }
- }
- sig_int()
- {
- signal(SIGINT, sig_int); /*重新注册信号处理函数*/
- sig_int_flag = ; /*修改环境变量*/
- }
上述的伪代码的本意是:main函数中的while循环就要等着SIGINT信号的到来,并且处理完sig_int_flag标志标量,才往下进行。
代码的本意是好的,但是还是存在漏洞。
考虑如下的情况:如果已经注册完了sig_int函数,首次进入while循环的判断条件,变量为0;而恰恰在执行while循环体中的pause()之前,来了一个SIGINT信号;开始执行sig_int的过程中已经把sig_int_flag设置为1了;sig_int执行完毕,开始执行pause()函数;如果以后再也不来SIGINT信号了,那么pause()就一直等下去了。还是跟上一个不靠谱的代码问题一样,这样的代码大部分时间可以正常运行,偶尔出现问题,非常难debug。
总结一下上面两个不靠谱的代码,其核心问题我认为是:signal是异步出现的,随时都能打断当前执行的程序;而上述代码的设计思路都是传统的同步顺序执行的,所以会遇到各种细节问题:
(1)第一种情况是signal“该来的时候来了,不该来的时候也来了”,即信号处理函数失效。
(2)第二种情况是signal“该来的时候不来,不该来的时候也来了”,即信号丢失。
10.5 Interrupted System Calls
这个部分阐述与System Call相关的signal处理问题。我没太理解深入,暂时记录以下两点:
(1)早期的unix系统中,如果某个process正在被“a slow system call”给阻塞;那么这个时候来个信号,原来正在执行的“a slow system call”就被打断了,返回一个error并且errno被设置为EINTR。
(2)为了避免这样的问题,有的系统提供了处理上述问题的system call restart的机制。可能的做法就是每次执行slow system call的时候,都去检查error的返回值,是否是EINTR,来决定是否启动restart。
有个伪代码如下:
- again:
- if ( (n = read(fd, buf, BUFFSIZE)) < )
- {
- if ( error == EINTR )
- goto again; /*被interrupted的system call*/
- ... /*处理其他问题*/
- }
在后续的14.4节中,select()和poll()函数的时候还会细说interrupted system calls
10.6 Reentrant Functions (可重入函数)
这部分说的是signal处理中安全问题,是否可重入。(可以查阅这个bloghttp://particle128.com/posts/2014/05/reentrant.html)
所谓的安全问题,是指signal到来与处理,打断了原来执行的程序;在执行完信号处理函数后,原来正在执行的程序可能就受到影响了,与预想的结果不太一样,这就是我理解的signal安全。
书上举了两个例子,来说明由于重入某些函数可能带来的signal不安全情况:
(1)如果原进程正执行malloc分配内存呢,分配到一半的时候来个signal信号;然后进入到信号处理函数中,又用到malloc函数动态分配内存了。
(2)如果原进程正执行getpwnam函数呢(简单理解getpwnam往一个static的存数据),正执行到一半突然来个signal信号;然后进入到signal信号处理函数中,又调用getpwnam;结果就是上次存一半的结果被新的覆盖了;再次回到原来的进程顺序执行的时候,getpwnam获得结果就乱套了。
上述的例子只是一个热身,系统些来说,判断一个signal handler函数是不是reentrant的,一般从signal handler function如下的几个方面进行考虑:
(1)是否用到了static data:如果信号发生时正调用getwpnam,并且在signal handler中也调用了getwpnam,则在handler中新获取的值就会覆盖之前进程中获取的值。
(2)是否调用了free或者malloc函数:如果信号发生时正调用malloc(修改堆上的存储空间连接表),并且信号处理程序又调用malloc,会破坏内核的数据结构。
(3)是否调用了standard IO:因为好多standard I/O函数都使用了全局的数据结构(如,printf中用到的文件偏移量是全局的)
(4)是否用了longjmp这类的函数:信号发生时候程序正在修改一个数据结构,longjmp这种完全推倒重来的函数在信号处理中一旦出现,就容易出现改一半就没改完的情况
类unix系统中,如果对于signal是安全的,有两种要求:
(1)首先函数必须是reentrant fucntion标准的(上述的4点),即从自身设计上不要出现signal不安全的漏洞
(2)这些函数执行时已经block各类signal,即从外部影响上主动避开signal出现带来的问题
另外,书上还提醒了一点:即使是调用Figure10.4中的reentrant function,还需要检验errno这个变量值;原因是,可能在信号处理函数中改变了error的实际值。比如,如果信号处理函数中调用了read(参考interrupted system call的内容),就有可能在信号处理完成后改变error的值。
因此,即使在signal handler中调用的是Figure10.4中的reentrant function,也需要在信号处理函数中检验errno的值。
例如,在Figure10.4中有fork函数,它的作用是分叉产生一个child process;当child process执行完了之后,就会产生一个SIGCHLD信号,返回给主进程;而这个信号的signal handler一般都有一个wait function,而wait function改变errno值。
总结一下,以后再设计signal处理函数的时候,一定要注意是否是reentrant的,以及信号处理安全问题。
10.7 SIGCLD Semantics
SIGCLD和SIGCHLD都是与child process结束状态有关的信号。
这重点介绍的是SIGCLD这个信号,有一些类unix系统对于这个SIGCLD信号的处理是比较特殊的。
(1)有时候,我们不想关心一些child process的运行状况,就会把SIGCLD信号的处理方式设置为SIG_IGN。这样做的好处就是,不会产生僵尸进程,“the status of these child processes is discarded”;但是,如果父进程中不小心有wait()函数正等着这个child process,那就会一直阻塞了。
(2)如果注册signal handler来处理SIGCLD信号,在调用signal函数的时候,kernel会检测是否已经有child process正在等待被回收处理。这点特性,也带来了如下的问题。
有问题的示例代码如下:#include "apue.h#include <sys/wait.hstatic void sig_cld(int);
- int main()
- {
- pid_t pid;
- signal(SIGCLD, sig_cld);
- if ( (pid=fork()) == ) /*child process*/
- {
- sleep();
- _exit();
- }
- pause();
- exit();
- }
- static void sig_cld(int signo)
- {
int status;- signal(SIGCLD, sig_cld); /*reestablish handler*/
- ...
pid = wait(&status);- }
main()中的sleep(2)是为了保证parent process先执行pause(),然后child process再执行_exit(0)。sig_cld()函数中在开始就调用signal(SIGCLD, sig_cld)是为了能够连续处理SIGCLD信号。
不考虑其他代码漏洞,上述代码在正常逻辑顺序上有较大的问题:每次调用signal(SIGCLD, XXX)的时候,kernel就会去check是否有child process等待被回收;由于此时child process还没有被回收呢,因此kernel认为还有child process等着被处理,因此再调用sig_cld函数;整个代码陷入了循环。
总结一下,凡是涉及到SIGCLD以及SIGCHLD的信号处理问题,需要参考具体所在系统对于SIGCLD以及SIGCHLD的实现分析。
10.8 10.9 10.10 kill和alarm函数
三个部分放在一起,用一个例子综合在一起。
1. kill函数:kill(pid_t pid, int signo),向某个进程发送信号。
这里注意两点:
(1)调用kill函数的进程是否有权限给另一个进程发信号
(2)kill(pid, 0) 这种形式可以用来验证pid进程号是否存在;但是考虑到类Unix系统,即使是一个进程的资源已经释放了,进程号pid仍然会被占用一段时间,因此这种通过检测pid号的方式来检测进程是否存在也不一定是完全准确的,得看具体的系统实现。
2. alarm函数:alarm(unsigned int seconds),启动一个倒计时器,到达seconds的时间后,会触发SIGALRM的信号;如果没有设定信号处理函数,则默认的行为就是将进程结束。
这里注意两点:
(1)一个进程只能同时有一个有效的alarm函数,如果在一个进程中多次使用alarm函数:后一次的alarm时间会替代上一次的alarm的时间。具体示例如下:
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <signal.h>
- #include <sys/time.h>
- struct timeval start, end;
- void sig_int()
- {
- unsigned int seconds = ;
- seconds = alarm();
- printf("left seconds: %u\n", seconds);
- gettimeofday(&start, NULL);
- pause();
- }
- void sig_alm()
- {
- float time_interval;
- gettimeofday(&end, NULL);
- time_interval = *(end.tv_sec-start.tv_sec)+end.tv_usec-start.tv_usec;
- time_interval = time_interval/;
- printf("Pasue time: %f\n", time_interval);
- }
- int main()
- {
- signal(SIGINT, sig_int);
- signal(SIGALRM, sig_alm);
- alarm();
- pause();
- }
程序执行结果如图:
分析如下:main中执行倒计时100秒;大概过了10秒之后在终端ctrl+c触发interrupt信号,进入SIGINT信号处理函数;在SIGINT信号处理函数中,修改alarm的倒计时为5秒。最后从执行结果看到,第二次执行的alarm(5)替代了前一次的alarm(100)。
有时候为了避免之前设定的alarm无效了,也可以检查类似上述代码中的seconds的值,来保证等够100秒。
(2)在某些情况下,如果产生了资源竞争,调用pause()之前,alarm就执行完了,pasue()就会一直在等着了。为了避免这种情况,书上给出了如下的sleep函数的设计。我在书上代码的基础上稍加修改,为的就是在代码中更好的体现资源竞争。
- #include "apue.h"
- #include <signal.h>
- #include <unistd.h>
- #include <setjmp.h>
- static jmp_buf env_alrm;
- static void sig_alm(int signo)
- {
- longjmp(env_alrm, );
- }
- unsigned int sleep2(unsigned int second)
- {
- if (signal(SIGALRM, sig_alm) == SIG_ERR){
- return second;
- }
- if ( setjmp(env_alrm)== )
- {
- alarm(second);
- kill(getpid(),SIGINT);
- pause();
- }
- return alarm();
- }
- static void sig_int(int signo)
- {
- int i,j;
- volatile int k;
- printf("sig_int starting\n");
- for ( i=; i<; i++)
- {
- for (j=; j<; j++) k += i*j;
- }
- printf("sig_int finished\n");
- }
- int main()
- {
- unsigned int unslept;
- if (signal(SIGINT, sig_int)==SIG_ERR) {
- err_sys("signal(SIGINT) error");
- }
- unslept = sleep2();
- printf("sleep2 returned : %u\n", unslept);
- exit();
- }
程序的执行结果如下:
分析如下:
a. 上述代码体现了setjmp longjmp能够在资源竞争条件下对alarm和pause对进行保护。
上述代码在main()中调用sleep2()函数。
进入到sleep2()函数之后,先注册一个SIGALRM的信号处理函数 → 再在setjmp保护下,alarm(second)设定倒计时 → 随后马上执行kill命令,向进程自身发送一个SIGINT信号,触发sig_int函数 → sig_int函数中执行的任务远超过alarm(second)中second的限制,为的就是模拟资源竞争时alarm已经倒计时完毕的时候,pause()还没开始。
由于sleep2()中保护机制的存在,如果由于资源竞争导致“alarm已经执行完毕,但是pause还没开始”,longjmp就直接跳到setjmp的地方了,并且返回的值为1;这样就跳过了pause()的语句,自然也就不会无限期的等待了。
b. 上述代码也体现了setjmp longjmp这种机制的隐患。
加入不是人为产生资源竞争,而是进程真的在执行某些任务;这个时候由于alarm到时,强制longjmp结束,很有可能造成其他任务还没处理完成,就直接longjmp结束了。
总结一下,要慎用setjmp和longjmp这样的长跳转机制。
10.11 10.12 10.13 sigset_t & sigprocmask & sigpending
1. sigset_t
有时候我们需要设定,一个进程要屏蔽哪些信号、要接受哪些信号、记录进程原来对信号的设定状况等。这个时候,就需要一种信号集合的数据结构,以及围绕其的一些周边函数来完成。其中数据结构就是sigset_t。
2. int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset)
设定信号mask。
how : 修改信号mask的方式,SIG_BLOCK、SIG_UNBLOCK、SIG_SETMASK
ret : 信号mask被设定成设么样
oret : 保留原来的信号mask
返回值表示函数执行成功或失败。
3. int sigpending(sigset_t *set)
获得当前进程有哪些信号被pending了。
看一个例子:
- #include "apue.h"
- static void sig_quit(int signo)
- {
- printf("caught SIGQUIT\n");
- signal(SIGQUIT, SIG_DFL);
- }
- int main()
- {
- sigset_t newmask, oldmask, pendmask;
- signal(SIGQUIT, sig_quit); /*捕捉退出信号*/
- sigemptyset(&newmask); /*初始化新的信号mask*/
- sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信号*/
- sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信号*/
- sleep();
- sigpending(&pendmask); /*获得有当前进程被阻塞的信号*/
- if (sigismember(&pendmask, SIGQUIT)) {
- printf("\nSIGQUIT pending\n");
- }
- sigprocmask(SIG_SETMASK, &oldmask, NULL); /*释放被阻塞的信号*/
- printf("SIGQUIT unblocked\n");
- sleep();
- exit();
- }
代码执行结果如下:
分析如下:
(1)上述代码首先用sigprocmask阻塞了SIGQUIT信号;经过第一个sleep(5)之后,马上执行sigpending,获得了当前被阻塞的信号,并且得到了验证SIGQUIT确实在被阻塞的信号集合pendmask中。
(2)紧接着再用sigprocmask函数释重新恢复了信号mask的值,即释放被阻塞的信号;随后,之前被阻塞的SIGQUIT信号马上就进来了,并且触发了sig_quit信号处理函数;注意,在信号处理函数中恢复了对SIGQUIT的默认处理方式。
(3)即使第一个sleep(5)的时候输入了多个ctrl+\输入信号,最终被处理的SIGQUIT信号也只有一个(最起码在我使用的Linux系统上是这样的)。
在上述代码基础上再扩展一下,如果多个不同的信号被pending住,当unblock的之后,会有什么效果呢?将书上的代码修改如下:
- #include "apue.h"
- static void sig_quit(int signo)
- {
- printf("caught SIGQUIT\n");
- signal(SIGQUIT, SIG_DFL);
- }
- static void sig_int(int signo)
- {
- printf("caught SIGINT\n");
- signal(SIGINT, SIG_DFL);
- }
- int main()
- {
- sigset_t newmask, oldmask, pendmask;
- signal(SIGQUIT, sig_quit); /*捕捉退出信号*/
- signal(SIGINT, sig_int); /*捕捉中断信号*/
- sigemptyset(&newmask); /*初始化新的信号mask*/
- sigaddset(&newmask, SIGQUIT); /*在newmask中添加退出信号*/
- sigaddset(&newmask, SIGINT); /*阻塞interrupt信号*/
- sigprocmask(SIG_BLOCK, &newmask, &oldmask); /*阻塞SIGQUIT信号*/
- sleep();
- sigpending(&pendmask); /*获得有当前进程被阻塞的信号*/
- if (sigismember(&pendmask, SIGQUIT)) {
- printf("\nSIGQUIT pending\n");
- }
- if (sigismember(&pendmask, SIGINT)) {
- printf("\nSIGINT pending\n");
- }
- sigprocmask(SIG_SETMASK, &oldmask, NULL); /*释放被阻塞的信号*/
- printf("SIGQUIT unblocked\n");
- printf("SIGINT unblocked\n");
- sleep();
- exit();
- }
不仅阻塞了SIGQUIT信号,而且还阻塞SIGINT信号。再执行代码如下:
分析如下:
(1)可以看到,对于同类的信号被阻塞信号,只获取一个;对于不同类的阻塞信号,可以把不同类等待的信号分别处理了。
(2)对比两次运行程序,虽然输入信号的顺序是不同的,但是执行信号处理函数的顺序却没有改变。
针对以上两点内容,google了一下相关资料:这个blog的解释非常好http://galex.cn/【apue】信号/
上述的信号是经典信号,不支持排队,而且信号的响应顺序和信号到来的顺序根本没有关系。恩,暂时这样理解。
10.14 10.15 sigaction Function & sigsetjmp siglongjmp Function
1. int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact)
本质上用于信号处理函数的注册,signal(2)的加强改进版。执行成功返回0,出错返回-1。
signo : 要处理的信号
act : 对信号处理函数的新设定
oact : 之前注册的signo信号处理函数的信息
其中sigaction是结构体变量,里面不仅存放了信号处理函数,而且还有需要屏蔽掉的信号集合,以及其他信息。
struct sigaction{
void (*sa_handler) (int); /*信号处理函数*/
sigset_t sa_mask; /*需要屏蔽的信号*/
...
}
这里的屏蔽信号指的是执行信号处理函数前希望屏蔽掉的信号;当信号处理函数执行完并return的时候,信号mask就会恢复到之前的状态。
回想之前在signal handler中重新调用signal的例子,应用sigaction函数就可以做到“Hence, we are guaranteed that whenever we are processing a given signal, another occurrence of that same signal is blocked until we're finished processing the first occurrence”
另外,用sigaction函数注册的信号处理函数,只要注册一次就一直生效;除非显式修改该信号的处理函数。
2. sigsetjmp & siglongjmp
int sigsetjmp(sigjmp_buf env, int savemask)
返回0代表是设置jmp点;返回非零值代表从别处跳回来,具体是什么值在跳的时候决定。
env : 目前还不详
savemask : 执行跳转之前的信号mask值
void siglongjmp(sigjmp_buf env, int val)
跳到sigsetjmp的地方,其中val要求是非零值,即sigsetjmp的返回值。
之前已经有setjmp和longjmp这种终极长跳转函数对了,为什么还要有sigsetjmp和siglongjmp函数对呢?
考虑下面这种情况:
a. 假设进入到了信号处理函数中(此时,假设同类信号已经被blocked了),该类信号已经在mask中设置为blocked了;
b. 如果signal handler被顺利执行完,这时信号mask就恢复到执行signal handler之前的状态了
c. 但是,如果signal handler执行到中间,执行到了longjmp语句了,很可能就把信号mask恢复这个环节就个跳过去了
d. 信号mask没有恢复的问题就是,本来同类信号可以再继续被处理,但是由于没有恢复信号mask,就不能被处理了
上面的情况,也是sigsetjmp和siglongjmp的设计初衷。
下面看一个例子(基于书上Figure 10.20的示例改造的),实际体会一下sigsetjmp和siglongjmp的函数对的作用:
- #include "apue.h"
- #include <setjmp.h>
- #include <time.h>
- #include <errno.h>
- static sigjmp_buf jmpbuf;
- static volatile sig_atomic_t canjmp;
- /*获得当前进程是否mask了某种信号*/
- void pr_mask(const char *str)
- {
- sigset_t sigset;
- int errno_save;
- errno_save = errno;
- sigemptyset(&sigset);
- if ( sigprocmask(,NULL,&sigset)< ) //获得当前的signal set情况
- {
- err_ret("sigprocmask error");
- errno = errno_save;
- return;
- }
- printf("%s",str); //打印调用信息title
- if ( sigismember(&sigset, SIGINT) ) printf(" SIGINT");
- if ( sigismember(&sigset, SIGQUIT) ) printf(" SIGQUIT");
- if ( sigismember(&sigset, SIGUSR1) ) printf(" SIGUSR1");
- if ( sigismember(&sigset, SIGALRM) ) printf(" SIGALRM");
- printf("\n");
- errno = errno_save;
- }
- /*SIGUSR1的信号处理函数*/
- void sig_usr1(int signo)
- {
- time_t starttime;
- if (canjmp==) return;
- pr_mask("starting sig_usr1: ");
- alarm();
- starttime = time(NULL);
- for (; ;) /*busy等待5秒*/
- if (time(NULL)>starttime+)
- break;
- pr_mask("finishing sig_usr1: ");
- canjmp = ;
- siglongjmp(jmpbuf,);
- /*longjmp(jmpbuf,1);*/
- }
- /*SIGALRM信号处理函数*/
- void sig_alarm(int signo)
- {
- pr_mask("in sig_alrm: ");
- }
- int main()
- {
- struct sigaction siga1,siga2;
- siga1.sa_handler = sig_usr1;
- sigemptyset(&siga1.sa_mask);
- sigaddset(&siga1.sa_mask, SIGUSR1);
- siga2.sa_handler = sig_alarm;
- sigemptyset(&siga2.sa_mask);
- sigaddset(&siga2.sa_mask, SIGALRM);
- sigaction(SIGUSR1, &siga1, NULL);
- sigaction(SIGALRM, &siga2, NULL);
- pr_mask("staring main: ");
- if (/*setjmp(jmpbuf)*/sigsetjmp(jmpbuf,)) {
- pr_mask("ending main: ");
- exit();
- }
- canjmp = ;
- for(; ;)
- pause();
- }
程序运行结果如下:
(1)用sigsetjmp和siglongjmp函数对的运行结果:
(2)用setjmp和longjmp函数对的运行结果:
对比以上两个运行结果可以看到,sigsetjmp和siglongjmp函数对确实很好地保护了信号mask现场,不会因为jmp的动作就导致某些信号mask位没有被恢复过来。(另,在我运行的系统上,如果直接用signal还不太行,必须用sigaction函数才可以获得上述的结果;原因就是系统的signal没有实现同类信号屏蔽)
10.18 system Function
这个部分主要说system这个函数与signal相关的内容:POSIX.1标准要求system必须忽略SIGINT和SIGQUIT信号,屏蔽SIGCHLD信号。
下面用正反两个例子来说明为什么有上述的要求。
例子一
- #include <unistd.h>
- #include <signal.h>
- #include <stdio.h>
- #include <errno.h>
- int system(const char *cmdstring) /* version without signal handling */
- {
- pid_t pid;
- int status;
- if (cmdstring == NULL)
- return(); /* always a command processor with UNIX */
- if ((pid = fork()) < ) {
- status = -; /* probably out of processes */
- } else if (pid == ) { /* child */
- execl("/bin/sh", "sh", "-c", cmdstring, (char *));
- _exit(); /* execl error */
- } else { /* parent */
- while (waitpid(pid, &status, ) < ) {
- if (errno != EINTR) {
- status = -; /* error other than EINTR from waitpid() */
- break;
- }
- }
- }
- return(status);
- }
- static void sig_int(int signo)
- {
- printf("caught SIGINT\n");
- }
- static void sig_chld(int signo)
- {
- printf("caught SIGCHILD\n");
- }
- static void sig_usr1(int signo)
- {
- printf("caught SIGUSR1\n");
- }
- int main()
- {
- signal(SIGINT, sig_int);
- signal(SIGCHLD, sig_chld);
- signal(SIGUSR1, sig_usr1);
- system("/bin/ed");
- }
程序运行结果如下:
分析如下:
-------------插播一段ed的背景知识-------------
首先需要补充一下/bin/ed这个程序有关信号的特点:
(1)ed捕获SIGINT和SIGQUIT信号
(2)对于SIGINT信号:prints a question mask
(3)对于SIGQUIT信号的处理方式:ignore
为了验证ed的特性,我做了如下的试验:
两个窗口是tmux开的,左侧是调用/bin/ed的窗口;右侧的是发送signal用的(另,不知道tmux的,可以参考我的这篇blog)
可以看到开启ed程序后,向其发送SIGINT信号,真的再终端反馈输出一个?;向其发送SIGQUIT信号,并么有什么反应。
------------------------插播结束-------------------------
现在开始切入正题。
先分析运行结果
(参考http://blog.csdn.net/windeal3203/article/details/39049291和http://blog.csdn.net/ctthuangcheng/article/details/9258715):
(1)终端输入ctrl+c,前台进程都会受到这个信号;这里的前台进程包括ed shell a.out三个进程(其中shell自动忽略SIGINT信号,不讨论)
(2)ed在收到SIGINT后,自然会输出一个?;而a.out中注册了SIGINT的处理函数,因此也会触发信号处理函数
(3)输入q之后,ed退出,发送一个SIGCHLD信号;由于a.out是ed的父进程(看红框中的pid和ppid),所以自然收到SIGCHLD信号,并触发信号处理函数
请注意,上述的system函数是去除了signal设定的阉割版,并没有符合POSIX.1的标准。
我们需要一直记着:这个/bin/ed是由system函数调用的,这样带来的问题有两个:
(1)由于终端正在运行的是/bin/ed程序,输入的ctrl+c的想法是给ed的,并不是给a.out的,a.out错误接收到了发给ed的SIGINT了;这个时候如果a.out还有其他的任务要执行,并且没有处理SIGINT的机制,就被强制关闭了。
(2)当ed执行完毕退出的时候,相当于child process结束,因此会给parent process的a.out发送一个SIGCHLD信号;其实这个ed一旦由system函数执行上,就跟a.out没有太大关系了,a.out错误接收了发给ed的SIGCHLD了,认为是自己的进程执行完毕了。
我个人理解,system function虽然说某种程度上方便了启动某些程序;但通过上述的分析,system Function也是一个挺别扭的事情。要想知道别扭的原因,需要参考一下system的实现(不考虑signal版):
- int system(const char *cmdstring) /* version without signal handling */
- {
- pid_t pid;
- int status;
- if (cmdstring == NULL)
- return(); /* always a command processor with UNIX */
- if ((pid = fork()) < ) {
- status = -; /* probably out of processes */
- } else if (pid == ) { /* child */
- execl("/bin/sh", "sh", "-c", cmdstring, (char *));
- _exit(); /* execl error */
- } else { /* parent */
- while (waitpid(pid, &status, ) < ) {
- if (errno != EINTR) {
- status = -; /* error other than EINTR from waitpid() */
- break;
- }
- }
- }
- return(status);
- }
(1)显然system的实现利用fork和exec两个重要的函数:fork就是分叉生成一个child process;exec的作用是让child process用shell运行程序;可以说system利用了fork和exec实现了极大的代码便利
(2)但是fork和exec必然会对parent process有影响,比如上面提到的信号问题。
上述两个方面(1)简化了工作,(2)复杂了工作,所以说system Function是个别扭的函数。
例子二
给出另一个system实现(在原书的代码上做了一些修改,主要是方便显示),代码如下:
- #include <sys/wait.h>
- #include <errno.h>
- #include <signal.h>
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- static void sig_int(int signo)
- {
- printf("caught SIGINT\n");
- }
- static void sig_chld(int signo)
- {
- printf("caught SIGCHILD\n");
- }
- int i_system(const char *cmdstring) /* with appropriate signal handling */
- {
- pid_t pid;
- int status;
- struct sigaction ignore, saveintr, savequit;
- sigset_t chldmask, savemask;
- if (cmdstring == NULL)
- return(); /* always a command processor with UNIX */
- ignore.sa_handler = SIG_IGN; /* ignore SIGINT and SIGQUIT */
- sigemptyset(&ignore.sa_mask);
- ignore.sa_flags = ;
- if (sigaction(SIGINT, &ignore, &saveintr) < )
- return(-);
- if (sigaction(SIGQUIT, &ignore, &savequit) < )
- return(-);
- sigemptyset(&chldmask); // now block SIGCHLD
- sigaddset(&chldmask, SIGCHLD);
- if (sigprocmask(SIG_BLOCK, &chldmask, &savemask) < )
- return(-);
- if ((pid = fork()) < ) {
- status = -; /* probably out of processes */
- } else if (pid == ) { /* child */
- /* restore previous signal actions & reset signal mask */
- sigaction(SIGINT, &saveintr, NULL);
- sigaction(SIGQUIT, &savequit, NULL);
- sigprocmask(SIG_SETMASK, &savemask, NULL);
- execl("/bin/sh", "sh", "-c", cmdstring, (char *));
- _exit(); /* exec error */
- } else {
- /* parent */
- while (waitpid(pid, &status, ) < )
- if (errno != EINTR) {
- printf("error\n");
- status = -; /* error other than EINTR from waitpid() */
- break;
- }
- }
- printf("ed ends\n");
- /* restore previous signal actions & reset signal mask */
- if (sigaction(SIGINT, &saveintr, NULL) < )
- return(-);
- if (sigaction(SIGQUIT, &savequit, NULL) < )
- return(-);
- if (sigprocmask(SIG_SETMASK, &savemask, NULL) < )
- return(-);
- printf("return from system\n");
- return(status);
- }
- int main()
- {
- signal(SIGINT, sig_int);
- signal(SIGCHLD, sig_chld);
- i_system("/bin/ed");
- printf("after ed\n");
- }
代码执行结果:
结果分析:
(1)当终端输入q之后,ed进程结束,并向父进程发送SIGCHLD信号
(2)但是由于i_system执行过程中屏蔽了SIGCHLD信号,因此ed结束后先被主进程waitpid收尸,这个时候父进程a.out是不会收到SIGCHLD信号的
(3)最后,当所有与ed相关的内容都处理完了之后,最后一个sigprocmask放开SIGCHLD信号的阻塞
(4)之前由于ed进程结束带来的SIGCHLD信号马上被处理了,触发sig_chld信号处理函数
可以看到,由于system忽略了SIGINT和SIGQUIT信号,在system的执行过程中,不会由于终端发出ctrl+c就会影响父进程a.out的运行;并且由于屏蔽了SIGCHLD信号,不会使得父进程误收到SIGCHLD信号;只有当所system中执行的所有内容都结束后,才会释放对SIGCHLD的阻塞。
system这个函数考虑的问题太多,实际用到的时候要把与signal相关的东西考虑清楚。
【APUE】Chapter10 Signals的更多相关文章
- 【APUE】Chapter1 UNIX System Overview
这章内容就是“provides a whirlwind tour of the UNIX System from a programmer's perspective”. 其实在看这章内容的时候,已经 ...
- 【APUE】Chapter13 Daemon Processes
这章节内容比较紧凑,主要有5部分: 1. 守护进程的特点 2. 守护进程的构造步骤及原理. 3. 守护进程示例:系统日志守护进程服务syslogd的相关函数. 4. Singe-Instance 守护 ...
- 【APUE】Chapter12 Thread Control
今天看了APUE的Chapter12 Thread Control的内容,记录一下看书的心得与示例code. 这一章的内容是对Chapter11 Threads(见上一篇日志)的补充,大部分内容都是理 ...
- 【APUE】Chapter17 Advanced IPC & sign extension & 结构体内存对齐
17.1 Introduction 这一章主要讲了UNIX Domain Sockets这样的进程间通讯方式,并列举了具体的几个例子. 17.2 UNIX Domain Sockets 这是一种特殊s ...
- 【APUE】Chapter16 Network IPC: Sockets & makefile写法学习
16.1 Introduction Chapter15讲的是同一个machine之间不同进程的通信,这一章内容是不同machine之间通过network通信,切入点是socket. 16.2 Sock ...
- 【APUE】Chapter15 Interprocess Communication
15.1 Introduction 这部分太多概念我不了解.只看懂了最后一段,进程间通信(IPC)内容被组织成了三个部分: (1)classical IPC : pipes, FIFOs, messa ...
- 【APUE】Chapter14 Advanced I/O
14.1 Introduction 这一章介绍的内容主要有nonblocking I/O, record locking, I/O multiplexing, asynchronous I/O, th ...
- 【APUE】Chapter5 Standard I/O Library
5.1 Introduction 这章介绍的standard I/O都是ISOC标准的.用这些standard I/O可以不用考虑一些buffer allocation.I/O optimal-siz ...
- 【APUE】Chapter4 File and Directories
4.1 Introduction unix的文件.目录都被当成文件来看待(vi也可以编辑目录):我猜这样把一起内容都当成文件的原因是便于统一管理权限这类的内容 4.2 stat, fstat, fst ...
随机推荐
- [USACO12FEB]牛券Cow Coupons
嘟嘟嘟 这其实是一道贪心题,而不是dp. 首先我们贪心的取有优惠券中价值最小的,并把这些东西都放在优先队列里,然后看[k + 1, n]中,有些东西使用了优惠券减的价钱是否比[1, k]中用了优惠券的 ...
- 2018.12.30 Intellij IDEA设置main方法自动补全
Eclipse与 Intellij IDEA设置方法自动补全 1.首先,点击File-->Settings-->Editor-->Live Templates 设置你想输出的模板 右 ...
- sst上传和下载码云
第一次 Team-----share---->Add----->commit-------remote----->pull 第二次 直接share开始.
- 转载 【MySql】Update批量更新与批量更新多条记录的不同值实现方法
批量更新 mysql更新语句很简单,更新一条数据的某个字段,一般这样写: UPDATE mytable SET myfield = 'value' WHERE other_field = 'other ...
- 【洛谷P1983】车站分级
车站分级 题目链接 首先,可以发现火车停靠站点的大小是没有什么规律的, 火车可以停靠在级别<=当前级别的站点,必须停靠在级别>=当前最高级别的站点 但是所有没有被停靠的站点级别一定比所有被 ...
- 【洛谷P1726】上白泽慧音
上白泽慧音 题目链接 强联通分量模板题,Tarjan求强联通分量,记录大小即可 #include<iostream> #include<cstring> #include< ...
- apache开启.htaccess及使用方法
1 . 如何让的本地APACHE器.htaccess 如何让的本地APACHE呢?其实只要简朴修改一下apache的httpd.conf设置就让APACHE.htaccess开启了,来看看操作 打开h ...
- linux系统命令与常识
之前短期学过linux,用到时才发现已经忘得一干二净了. 现在对学过的和了解到的做一个总结: 先明确一些使用工具: winscp : WinSCP是一个Windows环境下使用SSH的开源图形化SFT ...
- 一款查询天气的WebApp
一.WebApp介绍 1.初始界面 2.搜索结果页面 二.项目代码 1.项目目录 --------app ----------app.component.ts ----------app.compon ...
- JQuery实现聊天对话框
效果图如下: HTML代码如下: <!DOCTYPE html> <html lang="en"> <head> <meta charse ...