两个直观的感受,你在终端运行一个程序然后摁一下Ctrl+c就是向正在运行的程序发送了一个终止信号,程序就被终止了;在终端kill一个pid相当于发送9号杀死这个进程;在终端运行kill -l 就可以查看系统的所有信号。
那么信号本质是什么呢?信号本质上是一种向一个进程通知发生异步事件的机制,是在软件层次上对中断的一种模拟。这种通知机制可以用于通知硬件消息like上面的感受1,也可以用来进行进程间通信like上面的感受2,还可以用来通知一些程序错误如除0、非法内存访问。异步是说进程没有对信号进行实时监控,不必等待信号到来,事实上进程也根本不知道信号什么时候会来。一个进程本来在欢乐的跑着突然就被你一个ctrl+c给杀死了,飞来横祸呀。至于说是一种软中断,是因为在原理上,一个进程受到一个信号与处理器收到一个中断请求可以说是一样的,本来在欢乐的跑着就从你一个脚上给你来一个高电平。
软中断信号(signal,又简称为信号)用来通知进程发生了异步事件。在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是进程间通信机制中唯一的异步通信机制,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。进程之间可以互相通过系统调用kill发送软中断信号。内核也可以因为内部事件而给进程发送信号,通知进程发生了某个事件。信号机制除了基本通知功能外,还可以传递附加信息。
通过kill -l可以看到linux现在支持64个信号,注意一下信号不是从0编号的而是从1编号。其中前32为标准(Standard)信号,后32为实时(Real-time)信号。好吧,什么是标准信号,什么又是实时信号。
在遥远的古代是只有标准信号的,那时候它也不叫标准信号,就叫信号,它是一种十分简单的机制。先说一下信号的运行,当信号发送到程序时并不是立即执行而是等待某个时机再执行,在这个时机还没到来的时候你一个类型的信号无论发多少个都只记录一个,就好比有32个信箱每个信箱只能收一封信,多的就扔吧;另一方面信号的响应也是不保证顺序的,你发送信号的顺序和信号响应的顺序可能根本就没什么关系,因为古代人类都比较简单嘛。后来人类不断发展又想要可以响应一个类型的多个信号又想保证响应顺序,实时信号就诞生了,其实就是加了个sigqueue这么个队列数据结构,需求就被满足了。但是之前的简单信号已经成为了实际上的标准,而实时信号的应用也还不如前者广,两者就共存了。
UNIX对前32个信号都有默认的响应方式,分为以下5类:
Term:终止进程
Ign:忽略该信号
Core:终止进程并保存内存信息
Stop:停止进程
Cont:有停止就有恢复进程
当然只有5个响应方法怎么够呢,not fashion 于是sigaction()这个系统调用就上了,通过它可以给一个信号绑定一个函数来当作信号处理函数,你就可以在这个函数里面胡作非为了。可是你胡作非为了内核开发人员又感觉不爽了于是就设了两个信号你是改不了的,以显示他们不可动摇的地位,这两个信号就是9号SIGKILL和19号SIGSTOP,所以你也就不能定义Ctrl+c和Ctrl+z发送出来的信号的处理方式了。
最简单的理解,一个程序给另一个程序发了个短信,通过中国移不动或者中国联不通的网络,另一个程序的手机就收到了,一个信号就算发送成功了。具体来说就是一个程序调用一个发送信号的系统调用例如。然后内核就扮演运营商的角色把信号扔给另一个进程。我们知道进程在内存里还是有很多家当的,主要维护了一个进程描述符,里面有着pid呀,进程状态呀,优先级呀一堆不可告人的秘密
pending 和 signal 是两个挂起信号队列,为什么要有两个呀?因为一个是私有的队列一个是共享的队列。为什么有私有和共享之分呀?因为一个是针对轻量级进程的一个是针对线程组的?这两个又是什么东西呀?本小农发现这里开始不好说了.为什么有私有和共享之分呀?因为一个是针对轻量级进程的一个是针对线程组的?这两个又是什么东西呀?本小农发现这里开始不好说了……简单说,Linux是没有进程和线程的,有的只有轻量级进程,如果一组轻量级进程之间可以共享资源,那么就组成一个相当于线程组的东西,也就相当于一个线程,换句话说Linux是用轻量级进程这个东西模拟多线程,感兴趣同学可以看一下LWP,总之这里知道有两个信号挂起队列就好了,如果前面LWP的东西没看懂,你这里可以认为一个是记录的给线程的信号,一个记录的给进程的信号。sighand就简单多了就是记录64个信号对应的处理函数的入口地址,当然还有其他好多辅助的数据结构,但主要就是这个功能。如果能大致看懂下面这张图说明你还没晕。
回到手机短信,内核把短信发给进程是干了什么事呢?就是找个队列把信号插进去。当然进程也是有尊严的,不会让你随便插的如果是标准信号的话你只能插一次,如果这个信号还在的话就不让你插了,不像对实时信号那么随便想插多少插多少。有的进程比较专一,如果有一个信号插进来他会设置一个屏蔽位不让别的信号插,可以对比一下中断,处理器有时也会设一个中断屏蔽位有木有。
短信发过来了,不一定就被看到了,这也是异步说的意思,那么什么时候进程才会发现我有一个新的短消息呢?原来进程是在从内核态这个黑暗的角落到用户态切换之前偷偷的看一眼短信,看看都有谁插了进来,然后把他们处理掉,再回到用户态光明正大的去接客。那么什么时候进程会去内核态这个小黑屋呢,主要有三种情况:
执行系统调用
处理中断异常
进程调度上CPU
出小黑屋前,会看一眼手机,如果有短消息就啪啪啪的处理短信,如果没有的话就伤感的回用户态。如果是比较规矩的短信只是执行五种规定的标准动作那么在小黑屋解决就好了,但是有的短信比较坏调用了sigaction告诉进程要出来到这个地方来玩,然后进程就把手机扔小黑屋里拔腿就跑到用户态去玩了,玩完想起来手机还在家,就又回趟家看看还有没有其他短信,没有再去用户态光明正大的见人。具体过程如下图。
不管你知不知道,进程从用户态进入内核态是要再内核态保存一份用户态堆栈的副本的,其中最重要的就是保存当前的pc这样进程从内核态返回的时候把pc还原就可以按照原来的指令流行进了;不管你知不知道,当进程从内核态回到用户态的时候这个堆栈的副本是被清空的。于是当进程在收到一个出去玩的短信出去之后,他原来的用户态返回地址就被默默的清空了,然后他玩完回到小黑屋就发现找不到回用户态的路了,一辈子就被关在这个阴冷黑暗的小黑屋,永世不得见光,这个故事告诉我们节操是很重要的。然而这个恐怖的故事没有限制住任何一个进程寻欢作乐,进程们出去玩之前先往外发了个消息把用户态的返回地址,堆栈信息什么的都发出去了,等玩完回家再等小哥把信息发回来,就又可以光明正大的回用户态了,所以节操这个东西……
好了,上面都是为了加强理解的段子,下面是正儿八经的原理介绍,需要有对堆栈和函数调用机制有一些了解,你会发现还是节操比较好说。
我们知道,当进程陷入内核态的时候,会在堆栈中保存中断现场。因为用户态和内核态是两个运行级别,所以要使用两个不同的栈。当用户进程通过系统调用刚进入内核的时候,CPU会自动在该进程的内核栈上压入下图所示的内容:(图来自《Linux内核完全注释》)
在处理完系统调用以后,就要调用do_signal()函数进行设置frame等工作。这时内核堆栈的状态应该跟下图左半部分类似(系统调用将一些信息压入栈了):
在找到了信号处理函数之后,do_signal函数首先把内核堆栈中存放返回执行点的eip保存为old_eip,然后将eip替换为信号处理函数的地址,然后将内核中保存的“原ESP”(即用户态栈地址)减去一定的值,目的是扩大用户态的栈,然后将内核栈上的内容保存到用户栈上,这个过程就是设置frame.值得注意的是下面两点:
以上这些搞清楚之后,下面的事情就顺利多了。这时进程返回用户空间,就会根据内核栈中的EIP值执行信号处理函数。那么,信号处理程序执行完后,怎么返回程序继续执行呢?
信号处理程序执行完毕之后,进程会主动调用sigreturn()系统调用再次回到内核,查看有没有其他信号需要处理,如果没有,这时内核就会做一些善后工作,将之前保存的frame恢复到内核栈,恢复eip的值为old_eip,然后返回用户空间,程序就能够继续执行。至此,内核遍完成了一次(或几次)信号处理工作。
到信号的进程对各种信号有不同的处理方法。处理方法可以分为三类:
在linux里面每个进程都是按照进程描述符task_struct结构创建的,在进程描述符task_struct里面,有一项是Signal_Struct,在Signal_Strct这里面有一项list_head的描述符,在这里面有一个sigset_t表,定义了64种信号的所代表的含义。也就是说在每个进程中,都有一个表,里面存着各种信号所代表的含义。
上面提到在进程的表项中有一个软中断信号域,该域中每一位对应一个信号。内核给每一个进程发送软中断信号的方法,是在进程所在进程表项的信号域设置对应于该信号的位,如果信号发送给一个正在睡眠的进程,如果进程睡眠在可被中断的优先级上,则唤醒进程,否则仅设置进程表中信号域相应的位,而不唤醒进程。如果是发送给一个处于可运行状态的进程,则只设置相应的域即可。进程的task_struct结构中有关于本进程未决信号的数据成员,struct sigpending:
struct sigpending{
struct sigqueue *head, *tail;
sigset_t signal;
};
第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。
当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。
总之信号注册与否,与发送信号的函数(如kill()或sigqueue()等)以及信号安装函数(signal()及sigaction())无关,只与信号值有关(信号值小于SIGRTMIN的信号最多只注册一次,信号值在SIGRTMIN及SIGRTMAX之间的信号,只要被进程接收到就被注册)
由于信号的触发和发送是随机的,接收进程无法预知什么时间会收到哪个信号。举个例子,如果有A,B两个进程,A进程接收到触发条件,开始发送信号给B进程,信号并不是直接从进程A发送给进程B,而是通过内核来转发,这样做也是为了安全考虑,因为有些信号比如SIGSTOP和SIGKILL可以将接收该信号的进程直接停掉,肯定是需要一定的权限才能发送此类信号,不能随随便便让一个进程发送信号给其他进程。
A进程发送的信号信息,其实就是根据上面的那个信号表,根据需要对相应的表项进行设置,内核接受到这个信号信息后,会先检查A进程是否有权限对B进程的信号表对应的项进行设置,如果可以,就会对B进程的信号表进行设置。
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。
对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。否则待该信号的所有sigqueue处理完毕后再在进程的未决信号集中删除该信号。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
整个流程可参考下图
如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。
signal()
#include
void (*signal(int signum, void (*handler))(int)))(int);
如果该函数原型不容易理解的话,可以参考下面的分解方式来理解:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler));
第一个参数指定信号的值,第二个参数指定针对前面信号值的处理,可以忽略该信号(参数设为SIG_IGN);可以采用系统默认方式处理信号(参数设为SIG_DFL);也可以自己实现处理方式(参数指定一个函数地址)。
如果signal()调用成功,返回最后一次为安装信号signum而调用signal()时的handler值;失败则返回SIG_ERR。
内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料
学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈
原文作者:精通Linux内核
原文地址:Linux内核:进程管理——进程信号处理流程 - 知乎(版权归原文作者所有,侵权留言联系删除)