我们时常遇到这样的需求:要杀死一个正在运行运行的进程。这时候可以在终端输入
1
|
|
(其中9的意思是SIGKILL,完整的linux信号请看这里)之后你再用ps查看进程的时候,会发现那个进程已经被杀掉了。
本文将说明在LINUX系统下,用户在终端输入kill -9 <PID>
之后,整个系统到底发生了什么,我们将深入到内核代码。一开始我在想这个问题的时候遇到了一些问题,比如进程是怎么知道自己收到信号的?在执行进程工作代码的同时还要不断轮询有没有新到的信号吗?代价也太大了吧?那是不是基于什么异步通知的方案呢?在说明LINUX是怎么做的之前,先解释一点基础的概念。
什么是信号(SIGNAL)
我自己的理解:信号之于进程,就好比中断之于CPU,是一种信息传递的方式。官方的解释是A signal is an asynchronous notification sent to a process or to a specific thread within the same process in order to notify it of an event that occurred.
一个程序在运行的时候,你可以发各种信号给这个进程,进程对这个信号做出响应。比如你发个SIGKILL给一个进程,该进程就知道用户要杀死它,然后就会终止进程。
一个更常见的例子,你在终端运行一个进程以后,如果是非后台进程,它会在console输出一些log,这时候shell也不能接受输入了,这时候你按下control+c
,进程就被终止了,在这个过程中你就给这个进程发送了一个信号(SIGINT,interrupt signal),在默认情况下,是终止改进程。
那什么时候是非默认情况呢?这里需要引入信号处理器(signal handler)的概念,你可以为一部分信号编写特定的处理函数,比如在默认情况下,SIGINT是结束进程,你可以修改这个默认行为使它什么都不做(即一个空函数),但是有些信号的行为是无法修改的,比如SIGKILL。
kill 命令
在LINUX下有一个kill
的命令,第一次用的同学会以为这是一个“杀死”某个进程的命令,其实并不是很准确。这个命令的作用就是给指定PID的进程发送信号,到底发送什么信号也是由参数指定的,如果不指定信号,默认是发送SIGTERM,它的默认行为是终止进程。其实kill
也是个程序,它内部会调用system call的kill来发起真正信号传递过程。
更详细的介绍请man 2 kill
shell fork进程
当你敲下命令,按下回车,程序就执行了,其实这里也是个很复杂的过程。涉及到了shell的运行原理,每一个shell的实现都不一样,但核心原理是不变的:fork
一个子进程,再调用execve
那一系列系统调用。想了解一个shell是怎么写的,我觉得最好的资料是《Unix/Linux编程实践教程》第八章。本文不会详细解释shell/fork/execve
,我会在另一篇博客里详细解释当你执行fork
时,系统发生了什么。
好了,基础知识差不多介绍完了,下面我们进入下一阶段。
kill -9 PID
我们先讲原理再深入实现细节。所有内核代码都基于3.16.3,本文出现的所有内核代码是我删除了一些错误处理,加锁,临界判断后的结果,所以是比较核心的代码。
执行kill -9 <PID>
,进程是怎么知道自己被发送了一个信号的?首先要产生信号,执行kill程序需要一个pid,根据这个pid找到这个进程的task_struct(这个是Linux下表示进程/线程的结构),然后在这个结构体的特定的成员变量里记下这个信号。
这时候信号产生了但还没有被特定的进程处理,叫做Pending signal。
等到下一次CPU调度到这个进程的时候,内核会保证先执行do\_signal
这个函数看看有没有需要被处理的信号,若有,则处理;若没有,那么就直接继续执行该进程。所以我们看到,在Linux下,信号并不像中断那样有异步行为,而是每次调度到这个进程都是检查一下有没有未处理的信号。
当然信号的产生不仅仅在终端kill的时候才产生的。总结起来,大概有如下三种产生方式:
- 硬件异常:比如除0
- 软件通知:比如当你往一个已经被对方关闭的管道中写数据的时候,会发生SIGPIPE
- 终端信号:你输入
kill -9 <PID>
,或者control+c
就是这种类型
大概原理就是这个样子的,接下来我们来看一看内核的实现。
实现
首先,你在shell里输入kill
这个命令,它本身就是个程序,是有源代码的,它的代码可以在Linux的coreutils里找到。代码很长,我就不全复制过来了,有兴趣的可以去仔细看看。它的核心代码是长这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
我们看到最后调用了系统调用kill
,其代码在Linux内核linux-3.16.3/kernel/signal.c
中实现。在看kill源码之前,先把这个函数最终要操作的结构体看一下,这个struct很长,只列出了信号相关的部分:
1 2 3 4 5 6 7 8 9 10 11 |
|
继续看kill系统调用,我将核心代码列在了下面,想看完整版的点这里。为了方便理解,我给核心逻辑增加了注释。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
因为这个kill_something_info
函数会根据pid的正负来决定是发给特定的进程还是一个进程组,我们下面主要来看发给一个特定进程的情况,即调用kill_pid_info
:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
注意这个函数,出现了我们上文提到的task_strcut
,这个是Linux下表示每个进程/线程的结构体,根据struct pid
找到这个结构后,就调用了group_send_sig_info
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
|
可以看到,最终调用到__send_signal
,设置信号的数据结构,wake up需要处理信号的进程,整个信号传递的过程就结束了。这时候信号还没有被进程处理,还是一个pending signal。
信号的处理
内核调度到该进程时,会调用do_notify_resume
来处理信号队列中的信号,之后这个函数又会调用do_signal
,再调用handle_signal
,具体过程就不用代码说明了,最后会找到每一个信号的处理函数,问题是这个怎么找到?
还记得在上文提到的task_struct吗,里面有一个成员变量sighand_struct
就是用来存储每个信号的处理函数的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
其中sa_handler
就指向了信号的处理程序。
为某个信号注册处理函数
Linux提供了修改信号的处理函数的system call,具体如何使用这些system call不是本文的重点,如果你有兴趣可以参考《Computer System: A programmer’s perspective》8.5节或者参考资料[6],里面提供了非常详细的例子。
总结
这篇文章基于Linux 3.16.3讲述了从shell敲下kill -9 <PID>
后整个系统发生了什么。主要涉及从用户态的shell程序开始,执行coreutils中kill,之后陷入到内核代码,分析了相关的数据结构,信号产生和传递的原理以及核心代码。
参考
[1] http://en.wikipedia.org/wiki/Unix_signal
[2] http://stackoverflow.com/questions/1860175/how-does-a-process-come-to-know-that-it-has-received-a-signal
[3] http://www.linuxjournal.com/article/3985
[4] http://blog.csdn.net/walkingman321/article/details/6167435
[5] http://blog.csdn.net/morphad/article/details/9236975
[6] http://www.alexonlinux.com/signal-handling-in-linux