linux 信号 实现原理

发布时间 2023-09-24 22:48:18作者: 流水灯

信号处理函数执行时刻

当我们对一个进程发送信号后,会将这个信号暂时存放到这个进程所对应的task_structpending队列中,此时,进程并不知道有新的信号过来了,这也就是异步的意思。那么进程什么时候才能得知并处理这个信号呢?有两个时机,一个是进程从内核态返回到用户态时,一个是进程从睡眠状态被唤醒。让信号看起来是一个异步中断的关键就是,正常的用户进程是会频繁的在用户态和内核态之间切换的,所以信号能很快的得到执行。

下图为信号相关的一些结构:

 

 

当进程从内核空间返回用户空间时,会调用syscall_exit_to_user_mode函数,最终经历一系列调用,会走到exit_to_user_mode_loop函数中

 

如何实现信号处理函数在用户态执行

用户自定义信号处理函数实际上是在用户空间执行的,目的是为了防止用户利用内核空间的ring 0特权等级做一些意想不到的事,处理过程如下两图所示:

整个过程如图中所见,进程由于系统调用或中断进入内核空间,在内核空间完成工作后返回用户空间的前夕,检查信号队列,如果检查有信号并且有自定义的信号处理函数,返回到用户空间执行信号处理函数,处理完后再返回内核空间,最后再回到用户空间之前代码执行到的地方继续运行

可以看到,这一套流程经历了4次用户态与内核态的切换,比较复杂,那么内核是如何做到的呢?为什么信号处理函数执行完后还要返回内核空间呢?

 

Linux中,在用户态和内核态运行的进程使用的是不同的栈,分别为用户栈和内核栈,当一个进程陷入内核态时,需要将用户栈的信息保存到内核栈中,x86,会将ssspflagscsip等值依次压入栈中,保存为pt_regs结构,然后设置CPU堆栈寄存器的地址为内核栈顶,这样,后续使用的栈便变成了内核栈,当系统调用结束,需要从内核态切换到用户态时,再将之前压入栈中的寄存器值弹出,将pt_regs中保存的值恢复到相应的寄存器中,这里改变了sp寄存器的值,即完成了换栈,cs:ip这两个寄存器分别指向用户态代码段以及用户态指令指针,后续CPU便会执行之前用户态的代码了

 

但是在系统调用完后,回到的将是syscall后的下一条指令,那么如何才能让程序去执行信号处理函数呢?信号处理函数执行完后,又如何回到之前所执行到的代码呢?我们很容易就能想到,先将pt_regs中的值备份一下,然后改变pt_regs中一些寄存器值,比如说将cs:ip修改成信号处理函数对应地址,这样从内核态返回后,就会自动跳转到信号处理函数了,等到信号处理函数执行完,再进入内核态,恢复pt_regs中的值后回到用户态,这样cpu又会从用户调用syscall后的指令开始正常执行了

 

这里实际上是这么做的,首先,内核默认在用户栈上分配了一个栈帧(如果设置了备用栈的话,则会在备用栈上分配),将pt_regs备份到这个栈帧上,用于后续恢复,然后设置pt_regs,改变其spcsip等值,使程序从内核态返回时,可以跳转到信号处理函数对应的栈和代码指令地址,当信号处理函数返回时会执行sigreturn系统调用再进入内核态,将之前备份到栈帧中的寄存器值重新恢复到pt_regs中,然后再从内核态回到用户态就可以正常继续执行syscall后面的代码了