今天开始写写APUE读书笔记,又不是从第一章开始。好了,书归正传,APUE这本书非常好,不过有些内容过于老旧,因此结合书本知识与动手实践,做一点读书笔记
第十章主要对信号进行分析,既然谈到信号首先让我们来看看什么是信号,这里书中并没有给出太明确的答案,APUE中只是说:“首先,每个信号都有一个名字”,我认为这个说法并没有交代清楚信号的本质,仅是强调了信号类似于某种“宏定义”。在这里给大家还是先分享一篇blog:http://blog.chinaunix.net/uid-24774106-id-4061386.html
这是一个系列blog共四篇,这四篇blog对signal机制的源码进行了一定程度的分析与对比,我本人的水平还没有达到分析源码的程度,所以就站在前人的肩膀,进一步学习,在此也向我曾经引用的博主们致以我最真诚的感谢(今天怎么这么感性)。又跑题了,上述博客中对于信号的理解是“ 信号是一种机制,是在软件层次对中断机制的一种模拟,内核让某进程意识到某特殊事情发生了。”
通过上面的分析,我们可以知道,信号首先代表着某种事件的发生,根据APUE,信号共包括以下四种情况:
对于以上四种情况的信号,共有三种处理方法,分别是:
好了分析到此,就产生了一个问题,我的系统中到底支持多少种信号?
对于以上问题可通过两种方法,一种是分析程序源码,另一种是通过命令。首先来看通过命令的方法,通过“kill -l”命令可查看当前系统中所支持的信号的种类。
当前系统中所支持的信号又可以分为两类分别为“实时信号”与“非实时信号”。
以上两点内容中所谈到的注册不是指通过singal函数在信号值与信号处理函数间建立映射关系的过程,(前半句看不懂可以忽略)此处的注册是指在进程控制块中对尚未处理的信号值进行记录。
将我参考的博客分享给大家http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html
再进一步已经知道了信号的种类,那么每种信号的默认操作是什么?在此就不给大家做一一讲解了,分享一篇blog吧http://www.jb51.net/LINUXjishu/173601.html
最后给大家补充一点关于kill的知识http://www.cnblogs.com/peida/archive/2012/12/20/2825837.html
好了以上就是通过命令的方法,接下来看看通过程序源码的方法,由于我现在还没有安装内核源码树,所以只能看有关于头文件中的内容,在我以前的博客中已经阐述过如何通过头文件查找所需要的内容(安利一下自己的博客)。这里直接给出结果,有关于信号值的定义位于/usr/include/x86_64-linux-gnu/bits/signum,不过以上内容来自于glibc,并非来自于linux kernel。
分析了这么多,接下来要真正开始使用signal函数了,signal定义位于/usr/include/signal.h中,函数定义如下:
/* Type of a signal handler. */ typedef void (*__sighandler_t) (int); /* The X/Open definition of `signal' specifies the SVID semantic. Use the additional function `sysv_signal' when X/Open compatibility is requested. */ extern __sighandler_t __sysv_signal (int __sig, __sighandler_t __handler) __THROW; #ifdef __USE_GNU extern __sighandler_t sysv_signal (int __sig, __sighandler_t __handler) __THROW; #endif /* Set the handler for the signal SIG to HANDLER, returning the old handler, or SIG_ERR on error. By default `signal' has the BSD semantic. */ __BEGIN_NAMESPACE_STD #ifdef __USE_MISC extern __sighandler_t signal (int __sig, __sighandler_t __handler) __THROW; #else /* Make sure the used `signal' implementation is the SVID version. */ # ifdef __REDIRECT_NTH extern __sighandler_t __REDIRECT_NTH (signal, (int __sig, __sighandler_t __handler), __sysv_signal); # else # define signal __sysv_signal # endif #endif __END_NAMESPACE_STD #ifdef __USE_XOPEN /* The X/Open definition of `signal' conflicts with the BSD version. So they defined another function `bsd_signal'. */ extern __sighandler_t bsd_signal (int __sig, __sighandler_t __handler) __THROW; #endif #ifdef __USE_MISC /* SVID names for the same things. */ extern __sighandler_t ssignal (int __sig, __sighandler_t __handler) __THROW;
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
还有多种不同的定义形式。其中__handler的值是常量SIG_IGN、SIG_DFL或接到此信号后要调用的函数的地址。首先让我们来看看signal函数的原型,signal 函数要求两个参数,返回一个函数指针,而该指针所指向的函数无返回值。这里signal的返回值是一个函数地址,该函数有一个整型参数(即最后的int)。这里要区分开一件事:首先signal是一个函数,既然是函数就要有函数参数与返回值,其中参数部分就是一个整型数与一个函数指针,该函数指针指向信号处理程序;而signal函数的返回值同样也是一个函数指针,但该指针所指向的函数仅需要一个函数(也就是最后的int),并且无返回值(注意与signal函数的返回值相区别);最后signal函数所返回的函数指针就是参数中的信号处理程序。
其中对于常量的定义如下:
/* Fake signal functions. */ #define SIG_ERR ((__sighandler_t) -1) /* Error return. */ #define SIG_DFL ((__sighandler_t) 0) /* Default action. */ #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
再来看看signal.c文件,signal.c来自于glibc源码,相关内容如下:
__sighandler_t __bsd_signal (int sig, __sighandler_t handler) { struct sigaction act, oact; /* Check signal extents to protect __sigismember. */ if (handler == SIG_ERR || sig < 1 || sig >= NSIG) { __set_errno (EINVAL); return SIG_ERR; } act.sa_handler = handler; if (__sigemptyset (&act.sa_mask) < 0 || __sigaddset (&act.sa_mask, sig) < 0) return SIG_ERR; act.sa_flags = __sigismember (&_sigintr, sig) ? 0 : SA_RESTART; if (__sigaction (sig, &act, &oact) < 0) return SIG_ERR; return oact.sa_handler; } weak_alias (__bsd_signal, bsd_signal) weak_alias (__bsd_signal, signal) weak_alias (__bsd_signal, ssignal)
这里还有两点问题需要注意:
以上内容就是对10.1到10.3节内容的总结。
接下来看10.4这一小节,本小节主要分析了早期的unix信号机制存在缺陷,但glibc中已经对存在的问题进行了修正,我分享的第一篇博客中就对这一部分进行了详细分析。
总结起来glibc的优势主要体现在以下三点:
1. 传统的signal系统调用,他的信号处理函数是一次性的,执行过后,该信号的信号处理函数就变成了SIG_DFL。而glibc中信号处理函数一次安装之后可多次利用(根据上文中的分析,exec函数会将原先设置为要捕捉的信号都更改为默认动作)。传统的signal系统调用之所以会在第二次信号到来时采取默认方式是由于signal系统调用首先设置了标志位“SA_ONESHOT| SA_NOMASK”,而“SA_ONESHOT”的作用就是指明信号处理函数一旦被调用过就恢复到默认的信号处理函数去。
源码如下(这是我直接摘抄第一篇博客中贴出的源码,正确性我还没有验证):
if (ka->sa.sa_flags & SA_ONESHOT) ka->sa.sa_handler = SIG_DFL;可以看到若已经置位SA_ONESHOT,则将信号处理函数变为默认方式。
2.早期的signal函数,没有屏蔽正在处理的信号。早期的signal函数在调用时会设置SA_NOMASK|SA_ONESHOT标志,这两个标志位一定要结合起来看才有意义,首先SA_NOMASK 表示不启用位图屏蔽信号,即这个时候可以运行信号嵌套(所谓信号嵌套是指信号处理函数结束前,无法中断当前的信号处理函数而对新一次到来的信号进行处理,类似于中断嵌套)。若设置了SA_NOMASK 则所处理的信号的另一次出现,可以中断信号处理程序,而对于新信号的处理方式由于设置了SA_ONESHOT标志位,因此信号函数在此时已变为默认方式。这也就解释了第一篇博客中给出的实例,当中断信号再次来临时程序为什么会中断而不再输出。而glibc并未设置SA_NOMASK,则不屏蔽相应的信号,内核采取的处理方式是自动阻塞这个信号,并将上述信号加入等待队列中,直到一次信号处理程序结束,才从等待队列中取出下一个要处理的信号(这句话其实并不准确,对于实时信号,则相同的信号再次到来时,每一次都会在进程中注册;而对于非实时信号来说,无论收到多少次信号,都会视为只收到一个信号,只在进程中注册一次)。
3. 早期的signal,会中断系统调用。APUE中所写的内容是“如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行”,此处的低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:
原因在于没有SA_RESTART标志位,根据APUE中给出的说法“POSIX.1要求只有中断信号的SA_RESTART标志有效时,实现才重启系统调用”。
以上就是APUE中10.4到10.5节的内容,总结起来就是:glibc主要解决了kernel中信号机制的缺陷,主要体现在以下三点:glibc 中的信号安装并不是一次性的;glibc 屏蔽了正在处理的信号,对于相同信号的另一次出现,glibc 给出的做法阻塞该信号,待进程准备好后再进行下一次处理;glibc 对于中断的系统调用可重启。
针对以上第一点与第三点的内容在此给出一个我写的测试程序,源码如下:
#include <stdio.h> #include <signal.h> #include <unistd.h> void signal_handler(int signo) { if(signo==SIGUSR1) printf("received SIGUSR1\n"); else if(signo==SIGUSR2) printf("received SIGUSR2\n"); else fprintf(stderr,"unknown no\n"); } int main() { char temp[10]; if(signal(SIGUSR1,signal_handler)==SIG_ERR) fprintf(stderr,"can't catch SIGUSR1\n"); if(signal(SIGUSR2,signal_handler)==SIG_ERR) fprintf(stderr,"can't catch SIGUSR2\n"); read(STDIN_FILENO,temp,10); return 0; }
调用read函数从标准输入读入,则进程处于阻塞状态,发送SIGUSR1后中断系统调用,但glibc中signal可以重启系统调用,则收到信号后,read进程能够重启,则进程在此进入阻塞状态,此时再次发送SIGUSR2信号,测试中断处理程序是否是一次性的。运行结果如下:
窗口一运行结果:
./test_signal_1 received SIGUSR1 received SIGUSR2此时程序尚未退出,等待输入。
窗口二运行结果,连续发送两个信号。
kill -10 4966 kill -12 4966
写到这里要给上面的内容做一点补充:在我分享的第一篇blog中给出这样一个例子,我稍加修改把循环次数增大了十倍,代码如下:
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <string.h> #include <errno.h> #define MSG "OMG , I catch the signal SIGINT\n" #define MSG_END "OK,finished process signal SIGINT\n" int do_heavy_work() { int i ; int k; srand(time(NULL)); for(i = 0 ; i < 1000000000;i++) { k = rand()%1234589; } } void signal_handler(int signo) { write(2,MSG,strlen(MSG)); do_heavy_work(); write(2,MSG_END,strlen(MSG_END)); } int main() { char input[1024] = {0}; #if defined TRADITIONAL_SIGNAL_API if(syscall(SYS_signal ,SIGINT,signal_handler) == -1) #elif defined SYSTEMV_SIGNAL_API if(sysv_signal(SIGINT,signal_handler) == -1) #else if(signal(SIGINT,signal_handler) == SIG_ERR) #endif { fprintf(stderr,"signal failed\n"); return -1; } printf("input a string:\n"); if(fgets(input,sizeof(input),stdin)== NULL) { fprintf(stderr,"fgets failed(%s)\n",strerror(errno)); return -2; } else { printf("you entered:%s",input); } return 0; }
input a string: ^COMG , I catch the signal SIGINT ^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^C^COK,finished process signal SIGINT OMG , I catch the signal SIGINT OK,finished process signal SIGINT a you entered:a
那么问题又来了为什么在一次信号处理程序运行过程中,还能对一个信号进行处理,还是在网上找到的答案:
http://blog.sina.com.cn/s/blog_7a9cae0101010hth.html
这其实与处理信号的机制有关,根据上述博文,信号是先在进程中注销,而后再调用信号处理函数,所以对于上面的例子,第一次发出SIGINT信号,信号经历产生->在进程中注册->在进程中销毁,才开始执行信号处理函数,而此时SIGINT已经处于未注册状态。因此在第一个信号处理函数运行过程中,再次产生SIGINT信号,又会经历上述流程,直到在进程中销毁这一步(为什么?后面的中断信号不响应就说明前面的没销毁)。那么此时问题又来了,为什么进程运行到此时又不销毁了?这回我真不知道答案
再来补充一个有关于第二点的例子,代码如下:
#include <stdio.h> #include <signal.h> #include <unistd.h> void signal_handler(int signo) { if(signo==SIGUSR1){ printf("received SIGUSR1 start pause()\n"); pause(); printf("received SIGUSR1 start pause()\n"); } else if(signo==SIGUSR2){ printf("received SIGUSR2 start pause()\n"); pause(); printf("received SIGUSR2 start pause()\n"); } else fprintf(stderr,"unknown no\n"); } int main() { int n; char temp[10]; if(signal(SIGUSR1,signal_handler)==SIG_ERR) fprintf(stderr,"can't catch SIGUSR1\n"); if(signal(SIGUSR2,signal_handler)==SIG_ERR) fprintf(stderr,"can't catch SIGUSR2\n"); while((n=read(STDIN_FILENO,temp,10))!=0){ printf("%s",temp); } return 0; }
窗口一:
received SIGUSR1 start pause()
kill -10 6210 kill -10 6210
让我们从已知的结论出发:glibc 会屏蔽当前正在处理的信号,因此当一次信号处理程序调用pause()函数使进程进入睡眠状态,内核认为此时信号仍处于处理过程中,因此阻塞后来的信号;另一方面,根据APUE,pause函数只有执行了一个信号处理程序并从其返回时,pause才返回,也就是才输出。但现在没有办法使信号处理程序返回,则pause无法返回;pause不返回,则信号处理程序无法返回,注意此时已构成死锁。信号处理程序的无法返回造成了后来的信号无法被处理。以上程序即使换成实时信号也不行,原因可采用同样的思路分析。
最后说一点,以上分析都是我根据网上与书上的内容自己分析总结的,因此有不对的地方欢迎大家批评。对于有关于程序的内容,最权威的资料就是源码,不过很可惜,本人的水平还没有达到。