信号最早在unix中即被引入,信号是一个数,表现为“发送给一个进程”。POSIX标准除了常规信号以外,还引入了实时信号,在Linux中它们的编码接在常规信号后,从32-64。它们与常规信号的区别在于常规信号不排队,同样的信号,多发了就丢弃,而实时信号多发了就会排队。Linux内核不使用实时信号,“发送”是个抽像的概念,其实由数据结构实现。
进程描述符中与信号相关的数据结构有:sigset_t blocked表示被阻塞信号掩码,在取消相应的阻塞前,进程不接收这个信号。
Pending字段是个表示进程挂起的私有信号集合的结构体,其中的signal字段指出挂起的信号,list字段是个链表头,它将表示各个信号的结构体sigqueue串起。挂起的意思是进程已经感知到信号,但并未作出相应动作。信号有时不仅发给单个进程,也发给多线程下的整个进程组,这时要另一个与私有挂起信号集合类似的共享挂起队列shared_pending。但它不能直接存在进程描述符中,因为对于共享信号而言,有额外的共享信息如共享线程的个数等,分存在各个进程中不合适。引入信号描述符字段,同一进程组的线程指向相同的信号描述符,其中就有shared_pending字段。进程描述符中还有个信号处理程序描述符,用于列出0~63个信号的处理程序入口、标志及运行处理程序时要屏蔽的信号。它有个自旋锁和count字段用于同步,所以不需要将信号处理程序设计成可重入的。
以上是一些重要的数据结构。信号处理过程上分两阶段:1.产生:内核更新目标进程的相关数据结构,以表示已有信号“发出”。2.传递:内核强迫进程对已经“产生”的信号作出反应。
信号产生的大致过程:关中断、获自旋锁,若进程忽略这个信号且未被阻塞、或者非实时信号且挂起队列上已有挂起信号则结束。否则,将信号添加到进程的挂起信号集合。过程是将sigqueue加入链表并初始化,队列的相应位掩码置1。但这有特殊情况:若识别出SIGKILL、SIGSTOP或进程挂起信号数量过多,或是sigqueue分配内存失败的这些情况时,则不向队列中加,但为了kill()能成功调用,队列中的位掩码还是要置。此时设置完成,要“告之”目标进程有新的挂起信号的方法是设置TIF_SIG_PENDING标志位,这样在中断返回时就可以“处理信号”。
传递信号的大致过程:中断一章提到,TIF_SIG_PENDING设置后,内核从中断或异常返回时,会检查挂起的信号。信号的传递要对竞争条件、各种特殊情况详细处理,相当累赘,简而言之就是反复循环地依次考虑私有挂起队列与共享挂起队列中的所有信号,逐个处理并更新之前的TIF_SIGPENDING。信号缺省操作较简单,麻烦的是处理有信号修理函数的信号,因为它必须从内核态执行用户态的代码且最终返回内核态,这牵扯到内核栈与用户栈的数据变动,尤其是切回用户栈后,内核栈中的硬件上下文的存储。Linux的解决方法是将内核栈中的硬件上下文拷至当前用户栈中,但这样用户栈就乱了。为此引入“帧”(frame)的数据结构,它可以有效管理用户栈中的内核栈内容。执行完后,恢复用户栈与内核栈。信号处理程序的另一复杂点在于它可以进行系统调用,从而必须使系统调用返回后回到信号处理程序。还有一个问题是系统调用发出了但并未进行,进程处于TASK_INTERUPTIBLE时,某进程向它发信号,这样在信号处理完后从中断或异常返回时设为了TASK_RUNNING,系统调用服务例程还未完成工作,此时会返回错误码,按照相关的规定决定是否重启系统调用。
版权声明:本文为博主原创文章,未经博主允许不得转载。