linux中的僵尸进程与孤儿进程

linux环境下不可避免的会遇到僵尸进程与孤儿进程

1 概念

1.1 僵尸进程

一个进程fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保留在系统中,这种进程称为僵尸进程

1.2 孤儿进程

一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)收养,并由init进程对它们完成状态收集工作。
init进程会循环wait它的已经退出的子进程,因此孤儿进程不会有什么危害。

1.3 僵尸进程的识别与处理

  • 如何查看一个进程是否处于僵尸状态
    ps命令输出的进程状态为Z,就表示进程处于僵尸状态;
    同时procfs提供的status状态信息中的state值也为Z也能识别为僵尸进程

    1
    cat /proc/{pid}/status
  • 清除僵尸进程
    <1> 信号机制
    子进程退出时向父进程发送SIGCHILD信号,父进程处理SIGCHILD信号。在信号处理函数中调用wait进行处理僵尸进程
    <2> fork两次
    父进程退出,init进程接管子进程的信息收集
    备注: kill -9命令也无法杀死僵尸进程

1.4 僵尸进程资源并没有被完全释放

父进程调用fork创建子进程,子进程退出后其实并没有将所有的资源完全释放,比如pid依然被占用,不可被系统分配。
僵尸进程保留的资源有:进程控制块,内核栈等,这些资源不释放是为了提供一些重要的信息,比如进程为何退出,收到信号退出还是正常退出,进程退出码是多少,进程一共消耗了多少系统cpu时间,多少用户cpu时间,多少次上下文切换,产生多少缺页中断等等;如果没有这个僵尸状态,系统没有机会获知该进程的相关信息。
父进程收集信息是通过wait或waitpid

参考资料:https://www.cnblogs.com/Anker/p/3271773.html

2 部分疑问

2.1 父进程是init一定不会变僵尸进程吗

会出现僵尸进程。
在多线程的进程中,只有线程组的主线程才有资格通知父进程,线程组的其他线程终止时,并不通知父进程,也就不会保留资源进入僵尸状态。
父进程只认子进程的主线程,因此在线程组中,如果主线程终止,但是线程组中还有其他线程,那么它就不会通知主进程来进行回收资源,直到线程组中的最后一个线程退出。
所以当主线程退出但是其他线程还在运行,这时主线程就会变为Z状态,纵使init接管了主线程

  • 解决办法
    1> 杀死整个进程 kill {pid}
    2> 杀死所有的其他线程

当线程组的最后一个线程退出时,如果发现:

  • 该线程不是线程组的主线程
  • 线程组的主线程已经退出,且处于僵尸状态
  • 自己是最后一个线程
    同时满足这三个条件的时候,该子线程就需要冒充线程组的组长,即以子进程的主线程的身份来通知父进程

3 wait与waitpid

3.1 wait函数

我们知道Linux提供了wait函数来获取子进程的退出状态,父进程中等待回收子进程的资源,而防止僵尸进程的产生

1
2
#include <sys/wait.h>
pid_t wait(int * status)

如果status不是一个空指针,则终止进程的终止状态将存储在该指针所指向的内存单元中。如果不关心终止状态,可以将 status参数设置为NULL。
wait函数出错的情况

errno 说明
ECHLD 调用进程时发现没有子进程需要等待
EINTR 函数被信号中断

errno是一个全局变量,但是需要保证其在多线程/多进程下的安全性

3.1.1 wait的返回值

子进程退出与父进程wait获取子进程的退出状态是两个独立的事件:
1> 子进程先退出:
2> 父进程先调用wait
第一种情况会正常的回收子进程的资源,第二种情况父进程会在调用时因为无子进程退出而陷入阻塞状态,直到某个子进程退出。
这里会发现,wait函数等待的是任意一个子进程,当多个子进程处于僵尸状态的时候,wait函数获取到其中一个子进程的信息后立刻返回。

  • 进程等待所有的子进程退出,有三种可能:
    1> 等到了子进程退出,获取退出信息,返回子进程的进程ID
    2> 等待过程受到了信号,信号打断了系统调用。
    3> 已经成功等到了所有子进程,没有子进程的退出信息需要接收,wait函数返回-1

3.1.2 wait函数局限性

  • 不能等待特定的子进程
  • 如果不存在子进程退出,wait只能阻塞
  • wait只能发现子进程的终止事件,但是不能探知子进程的暂停,也无法感知子进程的恢复执行

3.2 waitpid函数

1
2
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int * status, int options)

可以看到waitpid多了一个pid参数,可以明确指定等待哪个子进程的退出,

  • pid>0
    需要等待进程id为pid的子进程的退出
  • pid=0
    等待与调用进程同一个进程组的任意子进程组的任意子进程;
  • pid=-1
    等待任意子进程
  • pid<-1
    等待所有子进程中,进程组id与pid绝对值相等的所有子进程;
    waitpid能够关注进程的暂停与恢复,linux中提供了SIGSTOP(信号值19)和SIGCONT(信号值18)来完成暂停和恢复的动作。

3.3退出状态status

wait和waitpid函数入参都有退出状态,只有传入NULL空指针的时候才表示不关注退出状态;事实上退出状态多种,为了保证可移植性,一般不直接解析status值获取退出状态,所以系统提供了相应的宏;

3.3.1 正常退出

宏 解释
WITEXITED(status) 子进程正常退出,返回true,否则返回false
WEXITSTATUS(status) 子进程正常退出,该宏获取进程的退出状态

3.3.2 收到信号退出

宏 解释
WIFSIGNALED(status) 进程被信号杀死,返回true,否则返回false
WTERMSIG(status) 进程被信号杀死,返回杀死进程的信号的值
WCOREDUMP(status) 子进程产生了core dump,返回true

3.3.3 进程收到信号,被停止

宏 解释
WIFSTOPPED(status) 子进程收到信号,暂停执行,处于停止状态,返回true,否则返回false
WSTOPSIG(status) 子进程处于停止状态,该宏返回导致子进程停止的信号的值

3.3.4 子进程恢复执行

宏 解释
WIFCONTINUED(status) 如果由于SIGCONT信号的递送,子进程恢复执行,返回true,否则返回false