内核情景分析:强制杀死一个进程的执行过程

前言

我们常常需要强制杀死一个进程,这种情况不同于正常退出的方式,一些退出流程将不会被执行。

按照正常的逻辑,这样的行为应该会导致一些资源没有得到释放,可是实际操作中多次强制杀死进程也没有出现啥异常现象。

那么问题来了: kill -9 杀死进程在内核中到底有怎样的处理过程呢?本文中,我将逐步的揭开这个问题的谜团。

信号的基础工作原理

信号模拟了硬件中断的处理流程。cpu 在每条指令执行完成后检测中断引脚,判断是否有中断到来,检测到有中断发生后打断当前执行的任务并保存现场然后跳转到中断服务程序开始运行。

信号与硬件中断的处理过程有类似之处,却也有显著的区别,它的主要步骤如下:

  1. 向某个进程发送信号事件,信号事件对应的结构被挂入到目标进程的 sigpending 链表中,并置位信号状态掩码中对应的位
  2. 目标进程在从内核态返回用户态的过程中检测是否有挂起的信号,发现有挂起的信号则从链表中每次拿出一个信号事件进行处理直到链表为空
  3. 获取到一个信号事件后,根据信号类型分发到不同的逻辑中,主要有一下三种大类
    • 对于设定为 SIG_IGN 状态的信号直接忽略
    • 对于有通过 signal、sigaction 注册信号处理函数的信号,设定堆栈后跳转到用户态的信号处理函数开始执行
    • 对于设定为 SIG_DFL、其它类型的信号执行杀死进程的操作
  4. 对于有注册信号处理函数的信号,内核在设定好堆栈后返回到用户态后直接从用户态信号处理函数开始执行,此函数返回后触发一个 sigreturn 系统调用后再次回到内核,然后恢复旧的堆栈继续运行

对于 SIGKILL、SIGSTOP 这两种不可被用户程序捕获的信号以及设定了 SIG_IGN、SIG_DEF 行为的信号而言,这些信号的处理过程均在内核态完成。

由于信号处理函数是在用户态程序的代码段中,当用户注册了一个非默认值的可捕获信号信号处理函数时,才会进入用户态执行,这里的过程实际上涉及一些相对复杂的架构依赖性操作,与这里要探讨的问题关系不大,不展开描述了。

信号在何时被处理

信号不同于硬件中断,它是软件上的行为,不能做到在每条指令执行完成后都进行检测并响应。一般来说,它只在内核态返回用户态的过程中被检测并处理,主要有如下两种情况:

  1. 当前进程由于系统调用、中断、异常而进入系统空间后,从系统空间返回用户空间的前夕
  2. 当前进程在内核中进入睡眠以后刚被唤醒的时候,由于信号的存在而提前返回到用户空间

kill -9 信号的处理过程

kill -9 表示发送 SIGKILL 信号,这个信号是不能被用户程序捕获的,它的处理过程完全在内核态完成,核心过程在于调用 do_group_exit 来执行所谓的“组退出”过程杀死整个线程组。

do_group_exit 函数会杀死 current 线程组中的其他进程(如果存在的话),它会向所有不同于 current 的同一个 tgid 中的其它进程发送 SIGKILL 信号,这些进程最终都将调用 do_exit 函数,从而终止运行。

do_exit 是一个相当复杂的函数,它的主要目的是回收进程使用的资源,这也是我们调用了 kill -9 没有出问题的根本原因——内核替我们完成了这些必要的回收工作。

在进一步描述前,先回忆回忆之前研究过的实时操作系统中任务退出函数的执行过程与原理。

实时操作系统中的任务退出函数

我在 rt-thread 与 ucos 中任务退出时如何调用退出函数 这篇博客中描述了实时操作系统中任务退出函数调用的过程,它其实是在每个任务的栈中预先设定了一个调用栈,将此栈的返回地址设置为进程退出的函数,这样当进程主函数执行完成后,弹栈过程会将预设的返回地址赋值给 pc 从而执行退出函数

rt-thread 实时操作系统中也有类似 linux 中延后释放 tcb 的过程,它实际是在 idle 任务中来回收进程的 tcb 的

对于实时操作系统来说,它占用的资源并不像 linux 系统那样多,其中最重要的应当是 ipc 资源了,对这些资源的回收也是其中的主要逻辑。

任务退出函数的复杂性

任务退出函数在某种意义上要比任务创建函数更为复杂。例如对于 ipc 来说,如果有其它进程在等待当前进程占用的 ipc 资源而睡眠,当前任务退出的时候必须考虑到这种情况,必须唤醒相关的进程。

试想如果它不做任何操作就悄无声息的死亡了,占用的 ipc 资源没有被回收,那么这些等待这些 ipc 资源的进程将一直睡眠,这是我们不愿意看到的结果。

再次回到 do_exit

进程在退出系统之前要释放所有的资源,在任务创建过程中从父进程继承的资源有存储空间、打开文件、工作目录、信号处理表等等,相应的在 do_exit 中就有 __exit_mm()、__exit_files()、__exit_sighand。

对于其它非继承的资源如信号量等也需要进行释放。这里有这样一个准则:在 task_struct 结构体中,只要是一个指针,在进程创建时以及运行过程中要为其在内核中分配一个数据结构或缓冲区,而且这个指针又是通向这个数据结构或缓冲区的唯一途径,那就一定要把它释放掉,不然就会造成内核的存储空间泄露。(摘自 《Linux 内核源代码情景分析》)

正是因为内核在 do_exit 中针对用户态程序使用的不同资源进行了回收,这才让 kill -9 这样的方式不至于导致存储空间泄露。

malloc 与 free 对应堆空间的回收

我们可以想想在使用 c 语言编写用户态程序时中一般要求 malloc 与 free 成对存在,如果只调用了 malloc 而不调用 free,则会产生存储空间的泄露,这里的泄露实际上针对的是持续运行过程的说法

malloc 申请的动态内存空间会被映射到程序虚拟内存的堆中,堆也只是程序虚拟内存中的页面,与其它存储区域一样都是通过底层的 mmap 映射到虚拟内存中的,通过执行 pmap、查看 /proc/pid/maps 可以看到。这些页面在 __exit_mm 函数中最终调用到的 exit_mmap 中被释放。

文件描述符的释放

与此类似的还有打开与关闭文件的过程,这个过程也非常常见,一般来说仍旧要成对存在。对于一个持续运行的程序,不断打开新的文件却不关闭会造成文件句柄泄露。在程序退出时,内核调用 __exit_files 来关闭已经打开的文件描述符。

task_struct 与进程内核栈的释放

do_exit 中没有回收 task_struct tcb 结构体与进程内核栈的过程,实际上这个过程是由程序的父进程完成的。在 do_exit 中会调用 exit_notify 来向父进程发送 SIGCHLD 信号以通知父进程,让父进程料理后事。

父进程通过执行 wait 系统调用来等待子进程死亡,子进程在死亡时负责唤醒父进程,让父进程来为自己”收尸“。

完成了 exit_notify 后,子进程最终调用 schedule 让出 cpu,此时由于它已经从就绪表中被移除,因此从 schedule 切出就是它最后一次执行的代码。

总结

强制杀死一个进程的过程与信号处理与进程退出流程的知识强相关,这里的处理过程完全是在内核态完成的。

kill -9 这种强制杀死进程的方式会导致程序正常的退出过程不能得到执行,这应该会造成一些问题。但是我们可以看到操作系统在背后做的大量的资源回收工作,正是这些隐含的动作才不致于让 kill -9 这样的行为出现异常。

不过,我们必须意识到的是,kill -9 强制杀死一个进程的方式与进程主动死亡的方式其背后的行为是不同的,在一些情况下可能需要深入的分析这一过程以定位某些疑难杂症。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值