点击打开链接
本文简单介绍下Linux信号处理机制,为介绍二进制翻译下信号处理机制做一个铺垫。
本文主要参考书目《Linux内核源代码情景分析》《独辟蹊径品内核:Linux内核源代码导读》
首先,先说一下什么是信号。信号本质上是在软件层次上对中断机制的一种模拟,其主要有以下几种来源:
在Linux下,可以通过以下命令查看系统所有的信号:
可以通过类似下面的命令显式的给一个进程发送一个信号:
上面的命令将2号信号发送给进程id为pid的进程。不存在编号为0的信号。
目前Linux支持64种信号。信号分为非实时信号(不可靠信号)和实时信号(可靠信号)两种类型,对应于 Linux 的信号值为 1-31 和 34-64。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。本文着重于Linux的信号处理机制,对信号更多的介绍可以参考这里。
一般情况下一个进程接受到信号后,会有如下的行为:
进程对信号的响应
注册信号处理函数
如果想要进程捕获某个信号,然后作出相应的处理,就需要注册信号处理函数。同中断类似,内核也为每个进程准备了一个信号向量表,信号向量表中记录着每个信号所对应的处理机制,默认情况下是调用默认处理机制。当进程为某个信号注册了信号处理程序后,发生该信号时,内核就会调用注册的函数。
注册信号处理函数是通过系统调用signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现, 是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实 现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。关于这方面的内容,如果想获取更多,也可参考这里。
进程如何发现和接受信号?
我们知道,信号是异步的,一个进程不可能等待信号的到来,也不知道信号会到来,那么,进程是如何发现和接受信号呢?实际上,信号的接收不是由用户进程来完成的,而是由内核代理。当一个进程P2向另一个进程P1发送信号后,内核接受到信号,并将其放在P1的信号队列当中。当P1再次陷入内核态时,会检查信号队列,并根据相应的信号调取相应的信号处理函数。如下图所示:
信号检测和响应时机
刚才我们说,当P1再次陷入内核时,会检查信号队列。那么,P1什么时候会再次陷入内核呢?陷入内核后在什么时机会检测信号队列呢?
进入信号处理函数
发现信号后,根据信号向量,知道了处理函数,那么该如何进入信号处理程序,又该如何返回呢?
我们知道,用户进程提供的信号处理函数是在用户态里的,而我们发现信号,找到信号处理函数的时刻处于内核态中,所以我们需要从内核态跑到用户态去执行信号处理程序,执行完毕后还要返回内核态。这个过程如下图所示:
如图中所见,处理信号的整个过程是这样的:进程由于 系统调用或者中断 进入内核,完成相应任务返回用户空间的前夕,检查信号队列,如果有信号,则根据信号向量表找到信号处理函数,设置好“frame”后,跳到用户态执行信号处理函数。信号处理函数执行完毕后,返回内核态,设置“frame”,再返回到用户态继续执行程序。
在上面这段话中,我提到“frame”,frame是什么?那么为什么要设置frame?为什么在执行完信号处理函数后还要返回内核态呢?
什么叫Frame?
在调用一个子程序时,堆栈要往下(逻辑意义上是往上)伸展,这是因为需要在堆栈中保存子程序的返回地址,还因为子程序往往有局部变量,也要占用堆栈中的空间。此外,调用子程序时的参数也是在堆栈中。子程序调用嵌套越深,则堆栈伸展的层次也越多。在堆栈中的每一个这样的层次,就称为一个”框架”,即frame。
一般来说,当子程序和调用它的程序在同一空间中时,堆栈的伸展,也就是堆栈中框架的建立,过程主要如下:
为什么以及怎么设置frame?
我们知道,当进程陷入内核态的时候,会在堆栈中保存中断现场。因为用户态和内核态是两个运行级别,所以要使用两个不同的栈。当用户进程通过系统调用刚进入内核的时候,CPU会自动在该进程的内核栈上压入下图所示的内容:(图来自《Linux内核完全注释》)
在处理完系统调用以后,就要调用do_signal()函数进行设置frame等工作。这时内核堆栈的状态应该跟下图左半部分类似(系统调用将一些信息压入栈了):
在找到了信号处理函数之后,do_signal函数首先把内核堆栈中存放返回执行点的eip保存为old_eip,然后将eip替换为信号处理函数的地址,然后将内核中保存的“原ESP”(即用户态栈地址)减去一定的值,目的是扩大用户态的栈,然后将内核栈上的内容保存到用户栈上,这个过程就是设置frame.值得注意的是下面两点:
以上这些搞清楚之后,下面的事情就顺利多了。这时进程返回用户空间,就会根据内核栈中的EIP值执行信号处理函数。那么,信号处理程序执行完后,怎么返回程序继续执行呢?
信号处理函数执行完后怎么办?
信号处理程序执行完毕之后,进程会主动调用sigreturn()系统调用再次回到内核,查看有没有其他信号需要处理,如果没有,这时内核就会做一些善后工作,将之前保存的frame恢复到内核栈,恢复eip的值为old_eip,然后返回用户空间,程序就能够继续执行。至此,内核遍完成了一次(或几次)信号处理工作。
欢迎大家进行讨论,如果有问题,可以留言或者发信给我,我将最快时间内答复!
另外:这是我在CSDN上问别人时得到的答案,暂时没有全懂,先记下来,仔细看几遍书,再来理解
问题:
(2)另外,信号的捕获以及信号处理函数的执行是不是由内核执行的?它在执行时是不是跟当前的应用程序是并行执行的呢?我在UNP第五章中看了这样一段,多进程服务器为了避免出现僵尸子进程而对SIGCHLD信号捕获后进行了这样的处理
{ while (pid = waitpid( - 1 , & stat,WNOHANG) > 0 ) printf("sdsdssdsdsd\n"); return ; }
waitpid如果设置了WNHONG的话,waitpid非阻塞,并且当没有子进程时会返回0,那么在这个while循环中,要退出除非出错或者所有子进程都被处理了,那么假设这样一个情形,有两个客户连接到多进程服务器上去,现在关闭一个客户端,这时服务器会收到SIGCHLD信号,然后进入sig_child函数,由于此时waitpid一直>0,该循环不会退出,那么这样的话,sig_child函数一直在运行,也就是说服务器原始进程一直处于sig_child中,那么这时如果又有一个新连接过来,由于服务器没办法执行accept函数,是不是说服务器就再没有办法去接收一个新的连接了啊?但事实上,经过测试运行后,服务器照常可以接收新连接啊?对信号处理函数的运行机制不是很了解,踏实独立于我们自己写的应用程序运行的吗?
附加:pid_t waitpid(pid_t pid,int *status,int options)
waitpid的返回值比wait稍微复杂一些,一共有3种情况:
(1) 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
(2) 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
(3) 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD 其它: 调用 wait&waitpid 来处理终止的子进程: pid_t wait(int * statloc); pid_t waitpid(pid_t pid,int *statloc, int options); 两个函数都返回两个值:函数的返回值和终止的子进程ID,而子进程终止的状态则是通过statloc指针返回的。 wait&waitpid 的区别是显而易见的,wait等待第一个终止的子进程,而waitpid则可以指定等待特定的子进程。
回复:
一、信号处理函数是中断线程执行的,不是并行的。几个概念你再去看一下APUE梳理和巩固一下:RETURN VALUE wait(): on success, returns the process ID of the terminated child; on error, - 1 is returned. waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned. On error, - 1 is returned. waitid(): returns 0 on success or if WNOHANG was specified and no child(ren) specified by id has yet changed state; on error, - 1 is returned. Each of these calls sets errno to an appropriate value in the case of an error.
三、如果不是循环waitpid,导致的结果就是多个进程同时结束或者较快时间内同时结束,导致某些SIGCHLD信号丢失,但会保证保留一个SIGCHLD信号,所以你只wait一次不就造成了若干僵尸进程了吗。
如果楼主看不懂这段分析,就应该好好的调整一下学习方式,静下心从头再读书了。