0%

进程学习小记: 僵尸进程和孤儿进程

进程的状态

在操作系统中,我们学习过进程的几种状态,分别为就绪态、运行态和阻塞态。然而在实际的系统中,往往还会有两种状态,即新建态和终止态。 因此,一共有新建态、就绪态、运行态、阻塞态和终止态 5 种进程状态。

Linux 下使用 ps 能够查询的状态

在 linux 和 unix 中,可以使用 ps -l 命令来查看进程状态,在实际的操作系统中,进程的状态要比预期的复杂的多。

1
2
3
4
5
6
7
8
9
10
11
12
D  不可中断睡眠 (通常是在IO操作) 收到信号不唤醒和不可运行, 进程必须等待直到有中断发生  
R 正在运行或可运行(在运行队列排队中)
S 可中断睡眠 (休眠中, 受阻, 在等待某个条件的形成或接受到信号)
T 已停止的 进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行
X 死进程 (未开启)
Z 僵尸进程 进程已终止, 但进程描述符存在, 直到父进程调用
< 高优先级
N 低优先级
L 有些页被锁进内存
s 有子进程
l 多线程,克隆线程
+ 前台进程

fork 产生子进程

在多线程中,我们需要生成新的进程,会通过使用 fork 来产生子进程。在子进程产生后,如果父进程处理方式不正确或者提前退出,则可能会出现僵尸进程孤儿进程

在进程运行过程中,会将进程的一些信息(进程号,执行时间,退出码,打开的文件等)存入进程表,进程结束时会传递给父进程,父进程调用 wait 或者 waitpid 来释放符号表中的信息。

孤儿进程

在父进程创建了子进程后,子进程会在结束时将进程表中的信息传递给父进程,然后将进程表中的信息移除。当子进程还没有结束,父进程提前退出时,子进程这时就被称为”孤儿进程”。孤儿进程会被进程号为1的init进程收养,代替其父进程完成善后工作。因此,孤儿进程不会对系统造成危害。

僵尸进程

僵尸进程产生的原因

在父进程创建子进程后,子进程已经结束,但是仍然占用符号表的状态,被称作”僵尸进程”。 这一过程任何进程都会遇到(除了 init 进程),但是如果父进程立即进行处理,这一时间会很短暂的。如果父进程迟迟不做处理,这时候就可以看到很多状态为 Z 的僵尸进程。

僵尸进程无疑是有危害的,因为僵尸进程长时间占用进程号,如果系统长时间运行,可能会导致进程号耗尽的情况。

如何消除僵尸进程

僵尸进程本身已经结束了,所以调用 kill 命令去结束僵尸进程是没有用的。其实僵尸进程的根源是父进程,因为父进程不去收养这些进程而形成了长时间存在的僵尸进程。

因此,杀死父进程可以结束僵尸进程。父进程被杀死后,init 进程会收养这些进程并移除其进程表中的信息。

使用两次 fork 避免僵尸进程

当我们 fork() 一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:

  • 父进程调用 waitpid() 等函数来接收子进程退出状态。
  • 父进程先结束,子进程则自动托管到 init 进程(pid = 1)

第一种方法,有两种情况:

  • 父进程在创建子进程后等待子进程结束,然后执行自身业务 => 如果子进程时间比较长,父进程的业务无法完成;
  • 父进程在执行自己业务后等待子进程结束 => 子进程会长时间处于僵尸进程状态。

第二种方法父进程如果有自身业务将无法进行,不可取。

因此,我们可以通过两次 fork 来避免僵尸进程。

  • 第一次 fork: 父进程产生子进程 ,等待子进程结束;
  • 第二次 fork: 子进程产生孙子进程;
  • 子进程立即退出,孙子进程变成孤儿进程;
  • 父进程收到子进程结束信号,继续执行后续逻辑;

这样,孙子进程由 init 进程负责收尸,子进程在创建孙子进程后立即由父进程收尸,便不会产生僵尸进程的问题了。