僵尸进程(Zombie)

文章介绍了什么是僵尸进程,即在Unix系统中,子进程结束后但父进程未调用wait或waitpid函数处理导致的进程状态。僵尸进程占用系统资源,应当避免。通过拦截并处理SIGCHLD信号,使用wait或waitpid函数可以有效清理僵尸进程,避免其存在。示例代码展示了如何在C语言中实现这一过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.僵尸进程

进程完成工作后(执行完 main 函数中的程序后)应被销毁,但有时这些进程将变成僵尸进程,占用系统中的重要资源。这种状态下的进程称作“僵尸进程”,这也是给系统带来负担的原因之一。我们应该消灭这种进程。当然应掌握正确的方法,否则它会死灰复燃。

从下图可以看到,父进程的 PID 是 1166,子进程的 PID 是 1167

在这里插入图片描述

如果我们使用 kill -9 1167 命令将子进程干掉,观察父进程会收到什么信号?

从下图可以看到,父进程收到了 SIGCHLD 信号,子进程变成了僵尸进程。

在这里插入图片描述

在这里插入图片描述

在 Unix 系统中,一个子进程结束了,但是他的父进程还活着,但该父进程没有调用 wait()/waitpid() 函数来进行额外的处置,那么这个子进程就会变成一个僵尸进程。

僵尸进程已经被终止,不干活了,但是依旧没有被内核丢弃掉,因为内核认为父进程可能还需要该子进程的一些信息。作为开发者,坚决不允许僵尸进程的存在。

如何干掉僵尸进程?

  • 重启电脑。
  • 手动地把僵尸进程的父进程干掉,僵尸进程就会自动消失。
  • 一个进程被终止或者停止时,SIGCHLD 信号会被发送给父进程,所以,对于源码中有 fork() 行为的进程,我们应该拦截并处理 SIGCHLD 信号。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

// 信号处理函数
void sig_usr(int signo)
{
    int status;

    switch (signo)
    {
        case SIGUSR1:
            printf("收到了SIGUSR1信号,进程id=%d!\n", getpid());    
            break;
        
        case SIGCHLD:
            printf("收到了SIGCHLD信号,进程id=%d!\n", getpid());

            // waitpid()函数获取子进程的终止状态,这样子进程就不会成为僵尸进程了
            // 第一个参数为-1,表示等待任何子进程
            // 第二个参数保存子进程的状态信息
            // 第三个参数提供额外选项,WNOHANG表示不要阻塞,让这个waitpid()立即返回
            pid_t pid = waitpid(-1, &status, WNOHANG);

            if (pid == 0) return; // 子进程没结束,会立即返回这个数字
            if (pid == -1) return; // 这表示这个waitpid()调用有错误,有错误也立即返回出去
            return; // 走到这里,表示成功,那也return吧
            break;
    }
}

int main(int argc, char* const* argv)
{
    pid_t pid;

    printf("进程开始执行!\n");

    // 系统函数,第一个参数是个信号,第二个参数是个函数指针,代表一个针对该信号的捕捉处理函数
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
    {
        printf("无法捕捉SIGUSR1信号!\n");
        exit(1);
    }

    if (signal(SIGCHLD, sig_usr) == SIG_ERR)
    {
        printf("无法捕捉SIGCHLD信号!\n");
        exit(1);
    }

    pid = fork(); // 创建一个子进程

    // 要判断子进程是否创建成功
    if (pid < 0)
    {
        printf("子进程创建失败,很遗憾!\n");
        exit(1);
    }

    // 现在父进程和子进程同时开始运行了 
    for (;;)
    {
        sleep(1);
        printf("休息1秒,进程id=%d!\n", getpid());
    }

    printf("再见了!\n");

    return 0;
}

在这里插入图片描述

2.产生僵尸进程的原因

向 exit 函数传递的参数值和 main 函数的 return 语句返回的值都会传递给操作系统。而操作系统不会销毁子进程,直到把这些值传递给产生该子进程的父进程。处在这种状态下的进程就是僵尸进程。也就是说,将子进程变成僵尸进程的正是操作系统。

问:此僵尸进程何时被销毁呢?

答:应该向创建子进程的父进程传递子进程的 exit 参数值或 return 语句的返回值。

问:如何向父进程传递这些值呢?

答:操作系统不会主动把这些值传递给父进程。只有父进程主动发起请求(函数调用)时,操作系统才会传递该值。换言之,如果父进程未主动要求获得子进程的结束状态值,操作系统将一直保存,并让子进程长时间处于僵尸进程状态。也就是说,父母要负责收回自己生的孩子。

接下来的示例将创建僵尸进程。

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
	pid_t pid = fork();
	
	if (pid == 0)    // if Child Process
	{
		puts("Hi, I am a child process");
	}
	else
	{
		// 输出子进程ID。可以通过该值查看子进程状态(是否为僵尸进程)。
		printf("Child Process ID: %d\n", pid);
		
		// 父进程暂停30秒。如果父进程终止,处于僵尸状态的子进程将同时销毁。因此,延缓父进程的执行以验证僵尸进程。
		sleep(30);    // Sleep 30 sec.
	}

	if (pid == 0)
		puts("End child process");
	else
		puts("End parent process");
	
	return 0;
}

编译运行:

gcc zombie.c -o zombie
./zombie

在这里插入图片描述

程序开始运行后,将在如上所示状态暂停。跳出这种状态前(30秒内),应验证子进程是否为僵尸进程,该验证在其他控制台窗口进行。

在这里插入图片描述

可以看出,PID 为 1387 的进程状态为僵尸进程(Z+)。另外,经过 30 秒的等待时间后,PID 为 1386 的父进程和 PID 为 1387 的僵尸子进程同时销毁。

3.利用 wait 函数销毁僵尸进程

如前所述,为了销毁子进程,父进程应主动请求获取子进程的返回值。

#include <sys/wait.h>

pid_t wait(int *statloc);

// 成功时返回终止的子进程ID,失败时返回-1

调用此函数时如果已有子进程终止,那么子进程终止时传递的返回值(exit 函数的参数值、main 函数的 return 返回值)将保存到该函数的参数所指内存空间。但函数参数指向的单元中还包含其他信息,因此需要通过下列宏进行分离:

  • WIFEXITED:子进程正常终止时返回“真”(true)。
  • WEXITSTATUS:返回子进程的返回值。

也就是说,向 wait 函数传递变量 status 的地址时,调用 wait 函数后应编写如下代码:

if (WIFEXITED(status))    // 是正常终止的吗?
{
    puts("Normal termination!");
    printf("Child pass num: %d", WEXITSTATUS(status));    // 那么返回值是多少?
}

根据上述内容编写如下示例,在下面示例中,不会再让子进程变成僵尸进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int status;

	// 第11行创建的子进程将在第15行通过main函数中的return语句终止。
	pid_t pid = fork();
	
	if (pid == 0)
	{
		return 3;
	}
	else
	{
		printf("Child PID: %d\n", pid);

		// 第22行中创建的子进程将在第26行通过调用exit函数终止。
		pid = fork();
		
		if (pid == 0)
		{
			exit(7);
		}
		else
		{
			printf("Child PID: %d\n", pid);
			
			// 调用wait函数。之前终止的子进程相关信息将保存到status变量,同时相关子进程被完全销毁。
			wait(&status);

			// 第36行中通过WIFEXITED宏验证子进程是否正常终止。如果正常退出,则调用WEXITSTATUS宏输出子进程的返回值。
			if (WIFEXITED(status))
				printf("Child send one: %d\n", WEXITSTATUS(status));
			
			// 因为之前创建了2个进程,所以再次调用wait函数和宏。
			wait(&status);
			
			if (WIFEXITED(status))
				printf("Child send two: %d\n", WEXITSTATUS(status));
			
			// 为暂停父进程终止而插入的代码。此时可以查看子进程的状态。
			sleep(30); // Sleep 30 sec.
		}
	}

	return 0;
}

编译运行:

gcc wait.c -o wait
./wait

输出结果:

在这里插入图片描述

在这里插入图片描述

可以看出,此时系统中并没有 PID 为 1497 和 1498 的进程,这是因为调用了 wait 函数,完全销毁了该子进程。另外,两个子进程终止时返回的 3 3 3 7 7 7 传递到了父进程。

这就是通过调用 wait 函数消灭僵尸进程的方法。调用 wait 函数时,如果没有已终止的子进程,那么程序将阻塞(Blocking)直到有子进程终止,因此要谨慎调用该函数。

4.使用 waitpid 函数销毁僵尸进程

wait 函数会引起程序阻塞,还可以考虑调用 waitpid 函数。这是防止僵尸进程的第二种方法,也是防止阻塞的方法。

#include <sys/wait.h>

pid_t waitpid(pid_t pid, int *statloc, int options);

// 成功时返回终止的子进程ID(或0),失败时返回-1
// pid:等待终止的目标子进程的ID,若传递-1,则与wait函数相同,可以等待任意子进程终止
// statloc:与wait函数的statloc参数具有相同含义
// options:传递头文件sys/wait.h中声明的常量WNOHANG,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数

下面介绍调用 waitpid 函数的示例。调用 waitpid 函数时,程序不会阻塞。

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
	int status;
	pid_t pid = fork();
	
	if (pid == 0)
	{
		// 调用sleep函数推迟子进程的执行。这会导致程序延迟15秒。
		sleep(15);
		return 24;
	}
	else
	{
		// while循环中调用waitpid函数。向第三个参数传递WNOHANG,因此,若之前没有终止的子进程将返回0。
		while (!waitpid(-1, &status, WNOHANG))
		{
			sleep(3);
			puts("sleep 3sec.");
		}

		if (WIFEXITED(status))
			printf("Child send %d\n", WEXITSTATUS(status));
	}

	return 0;
}

编译运行:

gcc waitpid.c -o waitpid
./waitpid

输出结果:

在这里插入图片描述

可以看出,第 22 22 22 行共执行了 5 5 5 次。另外,这也证明 waitpid 函数并未阻塞。

03-16
<think>嗯,用户现在问的是Linux上的“zombie”,也就是僵尸进程。之前他们问过如何输入竖线符号,可能是在使用命令行时遇到了问题,现在转向进程管理相关的问题了。首先,我需要确认用户对僵尸进程的了解程度,可能他们遇到了僵尸进程的问题,或者只是好奇这个概念。 用户的需求可能是想知道僵尸进程是什么,如何识别,以及如何解决它们。考虑到用户之前的问题是关于基础命令行的操作,他们的知识水平可能属于初级或中级。因此,回答需要既详细又易于理解,避免使用过于专业的术语,或者至少解释清楚每个术语。 接下来,我需要组织回答的结构。通常用户喜欢分步骤的解答,比如先定义僵尸进程,然后讲解成因,再给出检测和解决方法,最后注意事项。这样结构清晰,容易跟随。 需要注意的是,用户可能是在实际使用中遇到了僵尸进程导致系统资源占用的问题,或者在学习过程中遇到了这个概念。因此,回答中要包含实际操作的命令,比如使用ps命令查找僵尸进程,以及如何清理它们。同时,要强调僵尸进程通常是短暂的,一般不需要处理,但大量存在时可能指示程序有问题,这点很重要,避免用户过度担心。 另外,用户可能对如何防止僵尸进程感兴趣,所以需要提到父进程正确处理子进程终止状态的重要性,可能涉及编程方面的建议,比如使用wait()系统调用。不过用户可能不是开发者,所以这部分可以简要说明,避免过于深入。 还要注意中英文术语的转换,比如Zombie Process翻译为僵尸进程,并确保所有命令格式正确,使用代码块包裹,方便用户复制粘贴。同时,检查是否有需要补充的细节,比如使用kill命令无法杀死僵尸进程,因为它们已经终止了,这需要明确说明,避免用户尝试无效的操作。 最后,确认回答符合之前的系统级指令:使用中文,数学公式正确(虽然这个问题可能不需要),结构清晰,分点说明,真实可靠。可能需要提醒用户如果问题持续存在需要检查程序代码,或者寻求进一步帮助,以覆盖所有可能的用户需求。</think>好的!以下是关于 Linux 系统中 **僵尸进程Zombie Process)** 的详细介绍: --- ### 一、什么是僵尸进程? - **定义**:僵尸进程是已终止运行但未被父进程“回收”(读取其退出状态)的进程。 - **特点**: - 占用极少量系统资源(如进程号 PID),但无法被直接杀死。 - 在进程列表(`ps` 或 `top`)中状态标记为 `Z` 或 `Z+`。 - 本质上是内核保留的“尸体”,等待父进程处理。 --- ### 二、僵尸进程的成因 1. **父进程未正确处理子进程终止信号**: - 父进程未调用 `wait()` 或 `waitpid()` 函数回收子进程。 - 父进程忽略 `SIGCHLD` 信号(子进程终止时发送给父进程的信号)。 2. **常见场景**: - 编写不严谨的多进程程序(如未处理子进程退出)。 - 父进程因崩溃或人为终止(如 `kill -9`)而无法回收子进程。 --- ### 三、如何检测僵尸进程? 1. **使用 `ps` 命令**: ```bash ps aux | grep 'Z' # 查看状态为 Z 的进程 ``` 2. **使用 `top` 命令**: - 运行 `top` 后按 `Shift + z`,观察标记为 `Z` 的进程。 --- ### 四、清理僵尸进程的方法 1. **等待父进程回收**: 如果父进程仍运行,可手动通知父进程处理: ```bash kill -SIGCHLD <父进程PID> # 发送 SIGCHLD 信号给父进程 ``` 2. **终止父进程**: - 通过父进程 PID 终止: ```bash kill <父进程PID> # 正常终止 kill -9 <父进程PID> # 强制终止(慎用) ``` - 父进程终止后,僵尸进程会被 `init` 进程(PID=1)接管并自动清理。 --- ### 五、注意事项 1. **无需过度处理**: 少量僵尸进程通常无害,系统重启后会自动清除。 2. **避免产生僵尸进程**: - 编写程序时确保父进程调用 `wait()` 或设置 `SIGCHLD` 信号处理函数。 - 使用 `systemd` 等服务管理器运行程序(自动回收子进程)。 3. **无法直接杀死僵尸进程**: - 僵尸进程已终止,`kill` 命令对其无效。 --- ### 六、示例分析 ```bash # 生成一个僵尸进程(测试用) sleep 100 & kill -9 $(pidof sleep) # 强制终止 sleep 进程,父进程(Shell)可能未及时回收 ps aux | grep 'Z' # 查看僵尸进程 ``` --- 如果遇到大量僵尸进程堆积,建议检查相关程序的代码逻辑或联系开发者修复!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值