操作系统:信号的由来和实现原理
Linux 为什么要引入信号?
信号是用户进程感知外部事件的一种方式。内核可以发送信号给用户程序,当然用户程序之间也可以互相发送信号,进程通过对某个信号绑定Handler实现对信号的响应。
所以说信号也属于进程通信的一种方式,但是这种通信比较简单直接,目标进程只能知道信号来源的PID,无法直接附带其他数据。
信号传递的原理
每个进程的TCB里都有一条链表存该进程等待的所有信号,给某个进程发送信号就表示为挂一个节点到目标进程的此链表上,内核发送信号当然可以直接操作,进程之间的话会转换成系统调用间接完成。当目标进程被调度时会检查并处理等待的所有信号。
目标进程只能同时有一个同种类型的信号处于挂起状态,也就是说,如果上一个同种信号没有被处理,那么之后到来的同类信号会被忽略。
更详细的说,这个信号的队列(链表)不止一条,分为进程组共享和进程私有的挂起队列。
才能实现某些信号是发送给整个进程组的,比如kill()
,而一些是指定某个进程的,
比如tkill()
.
信号被处理
对目标进程来说,它可以提前设置自定义的handler,所以在其TCB中还需要记录对于每个信号的处理方式。可能有三种:ignore, default handler, user-defined handler。
当进程被调度获得CPU时,在返回用户态执行代码之前会检查是否有挂起的信号。如果有则执行对应的handler。
这个过程对应内核函数
do_signal()
。
有一个问题是,自定义的信号处理函数是在用户态的, 而do_signal()
是发生在
内核态,所以内核要做一些特殊的操作:
- 创建一个临时的用户栈,不能破坏保存的原来用户态环境
- ELR(返回地址) = 自定义处理程序,和其他的用户态环境构建
- 返回用户态,CPU 会执行处理函数
- 执行完毕后,通过之前对用户栈的特殊构建,使得程序接下来会运行一个 syscall
(
sys_sigreturn
), 返回内核态 - 如上述操作检查完所有挂起的信号
- 当所有信号都被处理完成后,则恢复用户进程的原有环境,继续执行