宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

原创 宋宝华 Linux阅码场 4月8日服务器


深度睡眠与浅度睡眠!

众所周知,Linux的进程睡眠有两种常规状态:app

  • TASK_INTERRUPTIBLE(浅度睡眠):能够被等待的资源唤醒,也能被signal唤醒;
  • TASK_UNINTERRUPTIBLE(深度睡眠):能够被等待的资源唤醒,可是不能被signal唤醒。
    简单来讲,深度睡眠的进程必须等待资源来了才能醒,在此以前,甚至你给它发任何的信号,它都不可能醒来。

浅度睡眠的进程,则能够被信号唤醒,对于常规的键盘、串口、触摸屏等等这些I/O设备,显然符合此类模型。因此Linux内核的代码里面常常看到这样的代码模板,笔者在《Linux设备驱动开发详解》一书中也花了大篇幅解释以下模板:ide

宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

调用_ _set_current_state(TASK_INTERRUPTIBLE)并schedule()出去的进程,醒来第一件事每每就是经过signal_pending(current)查看信号是否存在,若是存在,就跳出去处理信号,无需等待I/O的完成(大不了信号处理完了再从新read)。
TASK_INTERRUPTIBLE看起来很理想,不至于在I/O没完成的时候,连CTRL+C都不响应(固然也不会响应其余SIGIO、SIGUSR1等信号)。
那么,有的童鞋就会问,既然浅度睡眠这么好,那么还要TASK_UNINTERRUPTIBLE这种彻底不响应信号的深度睡眠干什么?函数

深度睡眠不可避免

正在读本文的你,可能都有过这样的悲催经历,在NFS文件系统上面运行程序,可是NFS服务器挂了,你怎么都ctrl + c不掉那个进程,由于它就是个深度睡眠的场景。你徘徊,你迷茫,你问能不能直接都改成TASK_INTERRUPTIBLE,完全删除TASK_UNINTERRUPTIBLE呢?测试

对此,祖师爷Linus的答复是:不可能。请看他2002年的邮件:操作系统

宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

对于磁盘读等场景,若是读还没完成,就跳出去响应信号,application可能break,因此深度睡眠必须存在是一个客观的残酷的现实(cold fact)线程

祖师爷还有更猛的一槌定音:
宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)3d

祖师爷没有点明为何磁盘读的时候不该该跑用户态去执行信号处理函数,为何引起application break。我理解其中的一个场景以下:Linux对于代码段、数据段、堆和栈都一般依赖demanding page在发生page fault的时候从磁盘swap进来的,从而致使磁盘读的行为。在这个过程当中,若是咱们执行浅度睡眠并响应信号而跳过去执行应用程序代码段设置的信号处理函数,则此信号处理函数的执行可能再次由于swap in的需求引起进一步的磁盘读,形成double page fault的场景。磁盘的读很大程度上不必定是read系统调用引起的,考虑到代码段、数据段、堆和栈的每每是发生了page fault后才去从磁盘swap进来。磁盘有其特殊性,在Linux这样的操做系统里面,磁盘某种意义上是"内存"。

可是,若是响应信号后,哪怕application break都已经无所谓了呢?若是咱们的目的干脆就是发一个致命的信号,譬如杀死应用的信号(SIGKILL),那么application break这个就显得可有可无了,由于咱们自己就不打算继续玩下去了!这样就使得深度睡眠的进程,还能够被杀死,妈妈不再用担忧NFS服务器挂了后,我痛苦,我孤独,我精分了!code

可杀的深度睡眠

Linux所以推出了一个特殊的深度睡眠状态,叫作blog

  • TASK_KILLABLE(可杀的深度睡眠):能够被等到的资源唤醒,不能被常规信号唤醒,可是能够被致命信号唤醒,醒后即死
    TASK_KILLABLE状态的定义是:

    #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

因此它显然是属于TASK_UNINTERRUPTIBLE的,只是能够被TASK_WAKEKILL。
什么叫致命信号呢?talk is cheap,show me the code。

宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

因此,足够致命的信号就是SIGKILL。SIGKILL何许人也,就是传说中的信号9,没法阻挡没法被应用覆盖的终极杀器:

宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

仅仅从这个代码能够看出来,只有信号9才属于fatal signals。那么是否是只有信号9,才能够杀死TASK_KILLABLE的进程,信号2(CTRL+C)是否无能为力呢?
猜测再多,不如玩一个真实的代码,咱们下面来改造下,把globalfifo.c的read改造为TASK_KILLABLE。

宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

加载这个driver后,咱们来读取它:

# insmod globalfifo.ko 
# insmod globalfifo-dev.ko 
# cat /dev/globalfifo

这个时候,咱们ps命令看一下,能够清楚到看到cat进程处于D状态:

root      7658  0.0  0.0  16800   752 pts/1    D+   19:21   0:00 cat /dev/globalfifo

从前面的代码能够看出,CTRL+C是不该该能够杀死这个cat进程的,由于它不是SIGKILL。可是咱们来实际测试一下:

# cat /dev/globalfifo 
^C
#

实际倒是能够杀死!!!
咱们查看一下咱们加的那个内核打印代码,看一下signal pending的状况:

# dmesg
[ 4670.082548] wake-up by fatal signal 100

明明咱们发的是信号2,可是被置上的就是信号9(0x100的1对应SIGKILL的位)。这里发生了神奇的化学反应!!!
这踏马究竟是怎么回事?不是必定致命的信号2,为何转化为了最最致命的信号9呢?

信号2是如何转化为信号9的?

这个时候咱们重点关注kernel/signal.c内核代码中的complete_signal()函数:

宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

实际上,当Linux内核发现进程(线程组)收到了一个sig_fatal()的信号的时候,会给这个进程中的每一个线程人为地插入一个SIGKILL信号,这个从while_each_thread循环能够看出。
sig_fatal()和fatal_signal_pending()不是一个概念。咱们看看sig_fatal()的代码:
宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)
基本上,一个信号的行为若是是缺省的(SIG_DFL),它又没有被忽略,那么它就是知足sig_fatal()条件的。
以下图,流程大概是:
当咱们给进程P1(假设内部有线程T1和T2,那么每一个线程会有个tast_struct)发送信号2,这个2会填入T1和T2共享的进程级signal pending,因为咱们对信号2没有绑定和忽略而是采用了默认行为,因而致使sig_fatal()条件知足。内核就会在T1和T2的各自独占的一份signal pending里面填入9,从而刺激fatal_signal_pending()条件的知足。
宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)
有的童鞋说,若是个人进程只有一个线程呢?那去掉上图中的T2以及T2独占的signal pending框便可:
宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)
为了进行验证,咱们再也不使用cat。而是本身写个app去访问globalfifo,而在此app里面修改信号2的行为:
宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)
咱们经过signal(2, sigint)给信号2绑定了信号处理函数sigint(),这个时候read(fd, buf, 10)引起TASK_KILLABLE睡眠,咱们不管怎么kill -2,都杀不死上面这个app。2到9的转化过程再也不发生。

下面的修改也可达到相似效果:
宋宝华:能够杀死的深度睡眠TASK_KILLABLE状态(最透彻一篇)

上面咱们是把信号2进行了SIG_IGN的忽略处理。
不只信号2是这样的,其余的不少信号也相似,好比SIGHUP、SIGIO、SIGTERM、SIGPIPE等均可以在没有绑定和忽略的状况下,转化为信号9。可是SIGCHLD显然不同,由于SIGCHLD默认就是忽略的。

(END)

相关文章
相关标签/搜索