youyichannel

志于道,据于德,依于仁,游于艺!

0%

谈谈什么是Orphaned进程和Zombie进程

什么是进程

开发者编写的代码只是一个存储在硬盘的静态文件,通过编译后就会生成二进制可执行文件,当运行该可执行文件后,它会被转载到内存中,接着 CPU 会执行程序中的每一条指令,这个运行中的程序就被称之为「进程」

进程树

一个进程都是由其他进程创建出来的,每个进程都有各自的 PID,在 Linux 系统的进程之间存在继承关系,所有的进程都是1号进程(init进程)的子进程。可以通过 pstree 命令查看进程的继承关系。

系统中的每个进程必定会有一个父进程,在 Linux 操作系统中,可以在 /proc 文件系统中看到该进程对应的父进程 PID,也可以通过 ps -ef 命令查看。

Linux 进程涉及进程的所有操作都围绕进程描述符展开,就是 task_struct 结构体。

struct task_struct {
// ...
/* Recipient of SIGCHLD, wait4() reports: */
struct task_struct __rcu *parent;

/*
* Children/sibling form the list of natural children:
*/
struct list_head children;
struct list_head sibling;

// ...
}

进程终结

当进程通过 exit() 系统调用终结时,系统需要释放该进程所占有的所有资源,这个 syscall 可能是来自进程内部的 exit(),也可能是来自外部的信号。调用结束后,此时该进程还没有完全从系统中消失,该进程的进程描述符依旧存在于系统中,存在的唯一目的就是向其父进程提供信息。

进程的收尾工作是交由其父进程完成的,父进程会通过 wait() 或者 waitpid() 来获取子进程的状态信息,从而释放该进程最后剩余的进程描述符、slab 缓存等,该调用会阻塞当前父进程,直到某个子进程退出。

孤儿进程

当一个父进程退出,而它的一个或者多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程会被 init 进程收养,并由 init进程对它们完成状态收集工作。

僵尸进程

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

危害

僵尸进程虽然不占用内存空间,但是如果父进程不进行善后操作的话,那么子进程保留的信息就不会释放,其进程号就会一直被占用,而系统所能使用的进程号是有限的,如果有大量的僵尸进程,会导致创建新进程时没有可用的进程号。

相反,孤儿进程是没有父进程的进程,孤儿进程由 init 进程处理善后操作。因此孤儿进程并不会有什么危害。

解决

1)方案一:父进程通过 waitwaitpid 等函数等待子进程结束,但是这会导致父进程阻塞;

2)方案二:信号机制

子进程退出时向父进程发送 SIGCHILD 信号,父进程处理 SIGCHILD 信号,在信号处理函数中调用 wait 处理子进程。

3)方案三:fork 两次

原理是将进程变成孤儿进程,从而从 init进程来处理善后操作。具体操作:

  1. 父进程一次 fork() 后产生一个子进程,随后立即执行 wait(NULL) 来等待子进程结束;
  2. 子进程 fork() 产生孙子进程,随后立即 exit(0),这样子进程顺利终止,此时父进程仅仅给子进程收尸,并不需要子进程的返回值,然后父进程继续执行。

此时孙子进程由于失去了它的父进程,成为了孤儿进程,将被转交给 init 进程托管。这样以来父进程与孙子进程无继承关系了,它们的父进程均为 init 进程。

4)方案四:kill 父进程

严格地来说,僵尸进程并不是问题的根源,罪魁祸首是产生出大量僵尸进程的那个父进程,可以使用命令 ps -aux|grep Z进行查找。kill 掉该进程之后,它产生的僵死进程就变成了孤儿进程,这些孤儿进程会被 init 进程接管,init 进程就会释放它们占用的系统进程表中的资源。

推荐阅读