Kikimo's blog

An occasional blog on programming

僵尸进程的一些处理方法

1. 僵尸进程及其危害

进程执行结束后, 一般会返回一个返回值, 例如: 程序正常退出 main() 函数返回零值, 非正常退出 main() 函数返回非零值. 父进程通过 wait() 系统调用获取子进程的返回值. 在子进程执行结束后, 子进程的返回值被父进程获取前的这段时间内的子进程就成为僵尸进程(Z 状态,defunct 进程). 子进程的返回值被读取后, 系统会回收该进程的 PCB(Process Control Block) 结构体, 并从进程列表中删除该进程.

如果父进程一直没有读取子进程的返回值, 子进程就会一直处于僵尸状态,它的 PCB 就一直无法释放. PCB 本身是一个庞大的结构体, 包括页表索引、进程打开的文件、进程打开的 socket 等等, 再加上系统为进程分配的内存页表,内存占用相当可观. 如果系统存在大量的且持续增长的僵尸进程, 最终可能耗尽系统内存, 导致系统和运行于系统上的服务都无法正常工作.

2. 处理僵尸进程的一些思路

  1. 僵尸进程是杀不掉的, 尝试 kill 它没用

    它都已经执行结束了,就等着释放资源.

  2. 僵尸进程不是进程僵死

    一定要把僵尸进程和进程僵死两种情况区别开来. 进程僵死是进程尚未执行完毕, 但是因为死锁或者其他原因卡住了. 僵尸进程则是进程已经执行结束, 但是资源没有释放.

  3. 处理僵尸进程,首先要关注的可能并不是僵尸进程本身

    例如,发现大量 salt 相关的僵尸进程,并不一定就是 salt 进程本身的问题.

  4. 处理僵尸进程,重点应该关注僵尸进程的父进程

    父进程执行 wait() 系统调用收集子进程的返回值,然后僵尸进程就可以释放资源彻底退出了. 所以,分析僵尸进程最重要的是分析僵尸进程的父进程: 为什么没有执行 wait() 系统调用.

    1. 找到僵尸进程的父进程, 观察父进程的状态

       $ ps -p ${ppid} -o state | tail -n 1
      
    2. 如果父进程处于 T (stopped) 状态, 那基本可以确定是父进程的问题, 恢复父进程的执行应该就能解决问题

       $ kill -SIGCONT ${ppid}
      

      然后, 下一步要做的是查清楚谁让父进程变成了 T(stopped) 状态, 这才是问题的根源.

    3. 如果父进程处于正常状态, 可以执行 strace 观察它的行为

       $ strace -p ${ppid}
      

      strace 跟踪进程执行的的系统调用, 根据进程的系统调用情况某种程度可以推断当前进程在做什么, 比如, 是否发生了死锁等等. 需要注意的是 strace 会唤醒暂停状态的进程,所以应该先观测进程的状态再尝试执行 strace.

  5. init 进程和孤儿进程

    init 进程是 Linux 启动后运行的第一个进程, init 进程会进一步 fork 出所有用户进程. 当某进程先于它的子进程退出后,其子进程就会变成孤儿进程,这时候 init 进程会收养该孤儿进程成为它的父进程. init 进程会定时收集其子进程的返回值, 从而使子进程释放资源.

    早期的 Linux 内核版本允许用户给他发送信号. 一个以 root 身份运行的脚本或程序, 如果因为实现的 bug 或其他原因向 init 进程发送了 SIGSTOP 信号, init 进程会就此处于暂停运行状态. 处于暂停状态的 init 进程会导致其子进程退出后变成僵尸进程, 堆积的僵尸进程可能会最终导致服务器宕机. 对于此种情况在机器上执行了 $ kill -SIGCONT 1 后就可以让 init 进程恢复运行.

    需要特别指出以下几点重要的信息:

    1. 高版本 Linux 内核的 init 进程是不会受 SIGSTOP 信号影响的

      SIGSTOP 信号应该是被直接忽略了, 同时你也不能再对 init 进程执行 strace 了, 因为对 init 进程执行 ptrace() 被禁止了, root 也不行.

    2. 父进程先于子进程退出, 程序这样做不是 BUG

      Unix/Linux 本来就是特地把 init 进程设计成可以收养孤儿进程, 从而支持用户这么干的, 这不是 BUG.

  6. 僵尸进程的存在并不一定意味着系统或服务有问题

    比如父进程忙于其他事务, 过一段时间后才执行 wait() 系统调用获取子进程返回值, 这种情况可能不是什么大问题.

  7. 僵尸进程有可能是程序设计不周导致, 比如

    父进程一直在运行, 但父进程的代码逻辑中根本不考虑收集子进程的返回值, 这个程序的设计就是有问题了, 这会导致子进程变成僵尸进程. 但是, 还是要和父进程先退出, init 进程收养子进程的这种情况区别开来, 切记, 这不是 BUG.

以上是处理僵尸进程时可以考虑的一些切入点, 具体问题, 还需要根据具体的场景来分析.

comments powered by HyperComments