僵尸进程是什么?

2025年4月28日 | 阅读 8 分钟

僵尸进程废弃进程是指一个进程已经完成执行(通过exit系统调用),但其在进程表中的条目仍然存在。这种情况发生在子进程中,因为父进程需要该条目来读取子进程的退出状态。一旦通过wait系统调用读取了退出状态,僵尸进程的条目就会从进程表中移除,这个过程称为“收割”。子进程在从资源表中移除之前,总是会先变成僵尸进程。

在大多数情况下,僵尸进程会立即被其父进程等待,然后在正常的系统运行中被系统收割。长时间保持僵尸状态的进程通常是错误的,会导致资源泄露,但它们只占用进程表中的一个条目。

从术语的比喻来看,子进程已经死亡,但尚未被收割。此外,与普通进程不同,kill命令不会影响僵尸进程。

What is Zombie Process

不应将僵尸进程与孤儿进程混淆。孤儿进程是指一个进程仍在执行,但其父进程已死亡。当父进程死亡时,孤儿子进程将被init(进程ID为1)收养。孤儿进程死亡时,不会保留为僵尸进程;相反,它们会被init等待。因此,一个既是僵尸进程又是孤儿进程的进程将被自动收割。

僵尸进程如何工作?

在操作系统中,僵尸进程的工作方式如下:

  • 当一个进程通过exit结束时,与之关联的所有内存和资源都会被释放,以便其他进程使用。
    What is Zombie Process
  • 但是,进程在进程表中的条目仍然存在。父进程可以通过执行wait系统调用来读取子进程的退出状态,此时僵尸进程将被移除。wait调用可能在顺序代码中执行,但更常见的是在处理SIGCHLD信号时执行,父进程会在子进程死亡时收到此信号。
  • 僵尸进程被移除后,其进程标识符(PID)和在进程表中的条目就可以被重新使用。然而,如果父进程未能调用wait,僵尸进程将保留在进程表中,导致资源泄露。在某些情况下,这是期望的行为,父进程希望继续持有此资源。例如,如果父进程创建了另一个子进程,那么新子进程不会被分配相同的PID。
  • 现代类UNIX系统有以下特殊情况。如果父进程通过将处理程序设置为SIG_IGN(而不是默认的忽略信号)显式忽略SIGCHLD信号,或者设置了SA_NOCLDWAIT标志,则所有子进程的退出状态信息将被丢弃,不会留下僵尸进程。
  • 在UNIX“ps”命令的输出中,可以通过“STAT”列中的“Z”来识别僵尸进程。存在时间较长的僵尸进程通常表示父程序存在bug,或者只是不常见地决定不收割子进程。
  • 如果父程序不再运行,僵尸进程通常表示操作系统存在bug。与其他资源泄露一样,少数僵尸进程的存在并不令人担忧,但可能表明在更重的负载下会出现严重问题。由于僵尸进程没有分配内存,唯一的系统内存使用就是进程表条目本身。许多僵尸进程的主要问题不是耗尽内存,而是耗尽进程表条目和实际的进程ID号。
  • 使用kill命令,可以手动向父进程发送SIGCHLD信号来从系统中移除僵尸进程。如果父进程仍然拒绝收割僵尸进程,并且终止父进程是可以接受的,那么下一步可以终止父进程。当一个进程失去其父进程时,init将成为其新的父进程。Init会定期执行wait系统调用来收割所有以init为父进程的僵尸进程。

示例

我们来看一个僵尸进程的例子。

输出:它会给出如下输出,例如

parent9
Child3
Child4
Child2
Child5
Child1
Child6
Child0
Child7
Child8
Child9 // there is a pause here
parent8
parent7
parent6
parent5
parent4
parent3
parent2
parent1
parent0

在第一个循环中,原始(父)进程分叉了10个自身的副本。每个子进程都会打印一条消息,然后休眠并退出。由于父进程在循环中做的很少,所以所有子进程几乎同时创建,因此它们第一次被调度的时间有些随机,导致它们的输出消息顺序混乱。

在循环期间,会构建一个子进程ID数组。所有11个进程都有pids[]数组的副本,但只有在父进程中它是完整的。每个子进程中的副本将缺少较低编号的子进程PID,并且其自身的PID为零。

第二个循环仅在父进程中执行,并等待每个子进程退出。它会等待休眠10秒的子进程,而其他子进程早已退出,因此除了第一条消息之外,所有消息都会很快显示。这里没有随机排序的可能性,因为它是单个进程中的一个循环驱动的。父进程可能会在任何子进程开始执行之前就进入第二个循环。这再次是进程调度器的随机行为——“parent9”消息可能出现在“parent8”之前序列中的任何位置。

Child0到Child8在它们退出和父进程对它们执行waitpid()之间,会花费一秒或更长的时间处于这个状态。父进程在Child9退出之前就已经在等待它,所以那个进程几乎没有时间作为僵尸进程。

僵尸进程的危险

僵尸进程不使用任何系统资源,但它们会保留其进程ID。如果有很多僵尸进程,那么所有可用的进程ID都会被它们垄断。这会阻止其他进程运行,因为没有可用的进程ID。

如果僵尸进程的父进程已不再运行,则僵尸进程通常表明操作系统存在bug。如果只有少数僵尸进程,这不是一个严重的问题,但在更重的负载下可能会给系统带来问题。

预防僵尸进程的方法

我们需要防止创建僵尸进程,因为每个系统只有一个进程表,并且进程表的大小是有限的。如果生成了过多的僵尸进程,那么进程表就会被填满。系统将不会生成任何新进程,然后系统将陷入停顿。因此,我们需要防止僵尸进程的创建。以下是防止僵尸进程创建的不同方法,例如:

What is Zombie Process

1. 使用wait()系统调用

当父进程在创建子进程后调用wait()时,它将等待子进程完成并获取其退出状态。父进程将被挂起(在等待队列中等待),直到子进程终止。必须理解的是,在此期间,父进程不做任何事情,只是等待。

2. 通过忽略SIGCHLD信号

当子进程终止时,会向父进程发送一个相应的SIGCHLD信号。如果我们调用“signal(SIGCHLD,SIG_IGN)”,系统就会忽略SIGCHLD信号,并且子进程的条目会从进程表中删除。这样就不会创建僵尸进程。然而,在这种情况下,父进程无法得知子进程的退出状态。

3. 使用信号处理器

父进程为SIGCHLD信号安装一个信号处理器。信号处理器在其内部调用wait()系统调用。在这种情况下,当子进程终止时,SIGCHLD信号将被传递给父进程。收到SIGCHLD后,相应的处理器将被激活,它会调用wait()系统调用。因此,父进程立即收集退出状态,并且子进程在进程表中的条目会被清除。这样就不会创建僵尸进程。

如何终止僵尸进程?

可以通过使用kill命令向父进程发送SIGCHLD信号来终止僵尸进程。该信号会通知父进程使用wait()系统调用清理僵尸进程。该信号使用kill命令发送。演示如下:

在上面的命令中,pid是父进程的进程ID。

什么是SIGHLD信号?

SIGCHLD是UNIX和类UNIX系统的信号。siginfo_t的代码值如下:

  • 子进程已终止 CLD_EXITED
  • 子进程异常终止(无核心转储) CLD_KILLED
  • 子进程异常终止(有核心转储) CLD_DUMPED
  • 被跟踪时,子进程处于CLD_TRAPPED状态
  • 子进程已停止 CLD_STOPED
  • 已停止的子进程已恢复 CLD_CONTINUED 当一个进程终止或停止时。它会向其父进程发送SIGCHLD信号。默认情况下,此信号将被忽略。如果父进程希望了解其子进程的状态,它应该捕获此信号。信号捕获函数通常会调用wait函数来获取进程ID及其终止状态。