Linux: bash起后台进程引发的僵尸进程

文章通过一个C语言示例解释了如何产生僵尸进程,分析了bash如何处理&操作符,以及当父进程不调用wait()时,子进程如何变为僵尸。内核通过sys_exit_group系统调用和init进程的介入来回收孤儿进程,防止僵尸进程的产生。若父进程不退出,其子进程会成为僵尸。解决方法是父进程正确使用wait()函数。

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

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 案例

原来的故事是 这样 的,感兴趣的读者可以直接前往。我截取了一段重现故事中问题的代码(对原代码做过小小调整):

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


#define SLEEP_SCRIPT_PATH "/home/bill/Study/qemu-lab/app/issue/1/sleep.sh&"


int main(void)
{
	int pid;

	if ((pid = fork()) == 0) {
		printf("children: %d\n", getpid());
		/* /bin/bash -c /home/bill/Study/qemu-lab/app/issue/1/sleep.sh& */
		execle("/bin/bash", "/bin/bash", 
			"-c", SLEEP_SCRIPT_PATH, (char *)0, NULL);
	}

	printf("parent: %d\n", getpid());

	//printf("waitfing for children... ");
	//wait(NULL);
	//printf("done.\n");

	while (1)
		sleep(1);

	return 0;
}

sleep.sh 的内容如下:

#!/bin/bash

sleep 3

编译并运行:

$ make zombie_issue

$ strace -f -t -e execve ./zombie_issue 
16:28:33 execve("./zombie_issue", ["./zombie_issue"], [/* 69 vars */]) = 0
parent: 11128
strace: Process 11129 attached
children: 11129
[pid 11129] 16:28:33 execve("/bin/bash", ["/bin/bash", "-c", "/home/bill/Study/qemu-lab/app/is"...], NULL) = 0
strace: Process 11130 attached
[pid 11130] 16:28:33 execve("/home/bill/Study/qemu-lab/app/issue/1/sleep.sh", ["/home/bill/Study/qemu-lab/app/is"...], [/* 3 vars */] <unfinished ...>
[pid 11129] 16:28:33 +++ exited with 0 +++
[pid 11128] 16:28:33 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=11129, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
[pid 11130] 16:28:33 <... execve resumed> ) = 0
strace: Process 11131 attached
[pid 11131] 16:28:33 execve("/bin/sleep", ["sleep", "3"], [/* 3 vars */]) = 0
[pid 11131] 16:28:36 --- SIGWINCH {si_signo=SIGWINCH, si_code=SI_KERNEL} ---
[pid 11128] 16:28:36 --- SIGWINCH {si_signo=SIGWINCH, si_code=SI_KERNEL} ---
[pid 11130] 16:28:36 --- SIGWINCH {si_signo=SIGWINCH, si_code=SI_KERNEL} ---
[pid 11131] 16:28:36 +++ exited with 0 +++
[pid 11130] 16:28:36 --- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=11131, si_uid=1000, si_status=0, si_utime=0, si_stime=0} ---
[pid 11130] 16:28:36 +++ exited with 0 +++
16:28:37 --- SIGWINCH {si_signo=SIGWINCH, si_code=SI_KERNEL} ---

$ ps -ef -o pid,ppid,comm
   PID   PPID COMMAND
  9539   2774 bash
 11133   9539  \_ ps
  9439   2774 bash
 11126   9439  \_ strace
 11128  11126      \_ zombie_issue
 11129  11128          \_ bash <defunct>

看看,进程 11129 进程变僵尸了:<defunct> 标注表示进程变僵尸了。用 top 可以观察到变 Z 了:

top - 16:51:36 up  5:39,  1 user,  load average: 0.09, 0.04, 0.01
Tasks:   1 total,   0 running,   0 sleeping,   0 stopped,   1 zombie
%Cpu(s):  0.5 us,  2.1 sy,  0.0 ni, 97.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :  4015908 total,   844272 free,   928832 used,  2242804 buff/cache
KiB Swap:        0 total,        0 free,        0 used.  2735724 avail Mem 

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND                                                                  
 11129 bill      20   0       0      0      0 Z   0.0  0.0   0:00.02 bash

开始分析问题之前,我们先来了解 bash 是怎么处理 & 操作符 的:

If a command is terminated by the control operator &, the shell executes the 
command in the background in a subshell. The shell does not wait for the command 
to finish, and the return status is 0. 

上面是摘自 bash手册 原文,翻译下它的意思:从 bash 启动的命令,如果尾接 & 操作符,则 bash 启动 子 shell 来运行命令,而 bash 本身不等待(即不对命令程序发起 wait() 调用)命令的结束,直接以退出码 0 退出。
我们再来简单了解下,什么样的进程会变成 僵尸进程 :一个进程退出了,其存活的父进程又不对其进行回收(没有对进程发起 wait() 调用),则该进程就会变成 僵尸进程
有了上述对 bash & 操作符僵尸进程 的基础知识,我们就可以来理一理为什么会出现僵尸进程了。我们不关注用来调试的 strace 进程,直接从 zombie_issue 说起。结合 strace 的追踪记录,以及程序 zombie_issue 的输出信息,我们按 进程 PID 来小结一下出现的几个进程:

11128: zombie_issue 进程
11129: zombie_issue 进程 fork 的子进程,用来启动程序 /bin/bash
11130: /bin/bash 的子shell,用来启动脚本 sleep.sh
11131: 运行脚本 sleep.sh 中 sleep 3 语句的进程

上面说了,进程变僵尸,是因为无人对它进行回收。我们一步步来看,为什么 进程 11129 最后变成了僵尸:

1. 脚本 sleep.sh 中执行 sleep 3 语句的进程 11131 运行完成后,
   子shell进程 11130 对其进行了回收,所以它不会变僵尸;
2. 子 shell 进程 11130 等到执行 sleep 3 语句的进程 11131 退出后,它自己也退出了。
   此时因为启动它的父进程程序 /bin/bash 已经退出了,它变成了无人理的孤儿,那么谁来
   回收它呢?针对这种父进程比子进程先结束的情形,Linux内核会将子进程托孤给 始祖进程
   init,由 init进程 负责完成子进程的回收。于是,我们的孤儿进程 11130 也被回收了,
   所以它不会变僵尸;
3. 而启动程序 /bin/bash 的进程 11129 ,自从它退出后,父程序 zombie_issue 进程 11128 
  对它不理睬,任其曝尸荒野,何其惨也,但由于父进程 zombie_issue 又没有退出,Linux内核
  也不会将其托孤给 init 进程,所以只能变僵尸了。

通过上面的分析,我们知道了 进程 11129 为什么变僵尸的原因。
我们再来看一下,在父进程比子进程先退出的情况,Linux内核将子进程托孤给始祖进程 init 的实现细节。进程退出都会经过 exit() 调用,而 exit() 调用系统调用 sys_exit_group()

sys_exit_group(error_code)
	do_group_exit((error_code & 0xff) << 8)
		...
		do_exit(exit_code)
			...
			exit_signals()
				...
				tsk->flags |= PF_EXITING; /* 标记进程正在退出 */
			...
			tsk->exit_code = code; /* 设置进程退出码 */
			...
			exit_notify(tsk, group_dead)
				/* 将 @tsk 的子进程托孤给寻得的新父 */
				forget_original_parent(tsk, &dead)
					reaper = find_child_reaper(father, dead);
						struct task_struct *reaper = pid_ns->child_reaper; /* pid_ns == init_pid_ns */
						...
						if (likely(reaper != father))
							return reaper; /* 在我们的场景下, reaper == &init_task */
						...
					...
					/* 将即将消亡进程 @father 的所有子进程重置新父为 @reaper */
					list_for_each_entry(p, &father->children, sibling) {
						/* 将 @father 当前子进程 @p 所在线程组的所有进程的新父重置为 @reaper */
						for_each_thread(p, t) {
							t->real_parent = reaper; /* 进程 @t 的新父重置为 @reaper */
							if (likely(!t->ptrace))
								t->parent = t->real_parent;
							...
						}
						...
					}
					/* 将即将消亡进程 @father 的所有子孙进程树移交给新父 @reaper */
					list_splice_tail_init(&father->children, &reaper->children);
				...
			...
			/* 
			 * 设置进程状态为最终态: task_struct::state = TASK_DEAD ,
			 * 进程彻底终结放弃 CPU 。
			 */
			do_task_dead();

从上面的代码分析,我们了解到另一个事实,即使父进程没对子进程调用 wait() ,但如果父进程退出,它的子进程仍然会被回收不会变僵尸,这是内核自动完成的。也就是说,如果去掉代码里的如下片段:

while (1)
       sleep(1);

子进程 11129 会被内核托孤回收。
前面的测试代码单独拿出来,就是一个编程 BUG:存活的父进程 理应对子进程发起 wait() 。如果放开对代码中的 wait() 调用的注释,就不会出现僵尸进程。
这其实是一个简单的问题,但放在复杂的环境下,我们确实可能犯这样的错误。其实仅仅是要模拟出现僵尸进程的情形,上面的测试代码还可以简化:

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


int main(void)
{
	int pid;

	if ((pid = fork()) == 0) {
		execle("/bin/bash", "/bin/bash", 
			"-c", "/bin/ls", (char *)0, NULL);
	}

	//wait(NULL);

	while (1)
		sleep(1);

	return 0;
}

3. 参考资料

cron 僵尸进程问题分析
man bash

<think>我们正在回答用户的问题:如何查看Linux中正在运行的后台进程? 根据之前的引用,我们知道有多个命令可以查看进程,特别是后台进程。结合引用[2]、[3]、[4]、[5]中的信息,我们可以总结如下: 1. `ps`命令:最常用的进程查看命令,可以显示当前用户或者所有用户的进程。 2. `jobs`命令:查看当前终端会话中的后台作业(注意:仅限于当前终端启动的后台进程)。 3. `pgrep`命令:通过进程名查找进程ID。 4. `pstree`命令:以树状结构显示进程。 由于用户特别询问“后台进程”,我们需要区分两种情况: a) 当前终端启动的后台进程(使用`&`、`Ctrl+Z`+`bg`等方式放入后台进程) b) 系统所有的后台进程(包括其他终端启动的、系统启动的等) 对于情况a),使用`jobs`命令是最直接的。 对于情况b),则需要使用`ps`命令来查看。 因此,我们将分情况介绍: 一、查看当前终端会话的后台进程(作业) - 使用`jobs`命令:显示当前shell会话中放入后台的作业(包括运行中和暂停的)。 常用选项: `jobs -l`:同时显示作业的PID和命令 `jobs -p`:仅显示作业的进程组ID(PGID)或进程ID(PID) 示例: ```bash $ jobs -l [1]- 12345 Running python train.py & [2]+ 12346 Stopped vim example.txt ``` 二、查看系统中所有的后台进程(包括其他终端启动的) - 使用`ps`命令:可以查看系统所有进程,通过选项组合来筛选后台进程。 常用命令: 1. `ps aux`:查看系统所有进程(包括其他用户的),显示详细信息。 a:显示所有用户的进程 u:显示进程的详细状态 x:包括没有控制终端的进程(即后台进程) 2. `ps -ef`:另一种显示所有进程的方式。 3. 结合`grep`筛选特定进程: `ps aux | grep '进程名'` 4. 注意:后台进程通常没有关联的终端(tty为?),所以我们可以通过`ps`命令查看没有控制终端的进程: `ps aux | grep -v 'pts'` 或者更直接地,查看TTY列为?的进程: `ps aux | awk '$8=="T"'` # 注意:状态为T的进程是已停止的进程,而后台进程状态可能是S(休眠)或R(运行)等,但终端为?(没有控制终端)才是后台进程的特征。 实际上,在`ps aux`的输出中,TTY列显示为`?`的进程就是没有关联终端的进程,通常就是后台进程(守护进程)。 示例: ```bash $ ps aux | less USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 225516 9416 ? Ss May31 0:16 /sbin/init ... ``` 其中,TTY为`?`的进程就是后台进程(守护进程)。 5. 使用`pgrep`:通过进程名查找进程ID,但不会显示详细信息,需要结合其他命令。 `pgrep -l '进程名'` 6. 使用`pstree`:以树状结构显示进程,可以直观地看到父子关系。 `pstree -p` 三、总结命令: 1. 当前终端后台作业:`jobs -l` 2. 系统所有进程(包括后台):`ps aux` 3. 筛选特定后台进程:`ps aux | grep [进程名]` 4. 查看没有控制终端的进程(通常是后台进程):`ps aux | awk '$11=="?"'` # 注意:不同系统ps输出列的位置可能不同,有些系统TTY在第7列,有些在8列?实际上,我们通常用`ps ax`然后看TTY列。 注意:在`ps aux`的输出中,列的顺序是固定的,通常第7列是TTY(在man ps中可以看到格式:USER, PID, %CPU, %MEM, VSZ, RSS, TTY, STAT, START, TIME, COMMAND)。所以我们可以用: `ps aux | awk '$7=="?"'` # 显示TTY为?的进程 但是,注意有些系统可能列的位置不同,为了更通用,我们可以使用: `ps -eo user,pid,tty,stat,args | grep '^[^ ]* *[0-9]* \?'` 或者更简单:`ps -ef | grep -v 'pts'` 但这样可能不准确。 更推荐使用:`ps -eo user,pid,tty,stat,comm | grep -E '\? +S'` # 查找TTY为?且状态为S(休眠)的进程,但后台进程状态不一定是S。 实际上,我们通常只需要知道没有控制终端(TTY=?)的进程就是后台进程(守护进程),而不管其状态。 因此,最简单的方式是使用: `ps aux | grep -v 'tty'` # 但这样会过滤掉所有包含tty的行,不准确 不如直接使用列过滤: `ps aux | awk '$8=="?"'` # 注意:在ps aux中,TTY在第7列?还是第8列?需要确认。 在标准的ps aux输出中,列的顺序是: USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND 所以,TTY是第7列(索引7),STAT是第8列(索引8)。 但是,在有些系统上,VSZ和RSS之后是TTY,然后是STAT,所以第7列是TTY,第8列是STAT。 因此,我们可以用: `ps aux | awk '$7 == "?"'` 但是,注意COMMAND列可能包含空格,所以用数字索引列可能不准确(因为前面的列都是没有空格的)。所以更可靠的方式是使用`ps`的选项自定义输出。 推荐使用: `ps -eo user,pid,tty,stat,args | awk '$3 == "?"'` 其中: -e:显示所有进程 -o:自定义输出格式,这里我们指定:用户、PID、TTY、状态、命令 然后通过awk筛选第三列(TTY)为?的进程。 四、实际应用示例: 1. 查看当前终端后台作业: ```bash $ jobs -l ``` 2. 查看系统中所有后台进程(守护进程): ```bash $ ps -eo user,pid,tty,stat,args | awk '$3 == "?"' ``` 3. 查看特定后台进程(如nginx): ```bash $ ps aux | grep nginx ``` 五、注意事项: - `jobs`命令只能查看当前终端启动的后台作业,如果关闭终端,则无法再通过`jobs`查看(除非使用nohup或disown脱离终端关联的进程会继续运行,但不在当前终端的jobs列表中)。 - 使用`ps`命令查看的是系统全局的进程,不受终端限制。 根据用户的问题,他可能关心的是当前终端放入后台进程,也可能是系统所有的后台进程。因此,我们分情况说明。 最后,我们按照要求生成相关问题。</think>### Linux 查看后台进程的完整指南 在 Linux 中查看后台进程主要有以下几种方法,根据你的需求选择合适的方式: #### 1. 查看**当前终端**的后台进程(推荐) ```bash jobs -l # 显示作业号和进程ID ``` **输出示例**: ``` [1] + 12345 running python train.py & [2] - 12346 suspended vim config.txt ``` **特点**: - 仅显示当前终端启动的后台进程 - 显示状态:`running`(运行中)/`suspended`(已暂停) - `+`表示默认作业,`-`表示下一个默认作业[^5] #### 2. 查看**系统所有**后台进程 ```bash ps aux | grep -v 'tty' # 过滤掉有终端关联的进程 ``` 或更精确的方式: ```bash ps -eo user,pid,tty,stat,args | awk '$3 == "?"' # 筛选无终端(TTY=?)的进程 ``` **输出字段说明**: - `USER`:进程所有者 - `PID`:进程ID - `TTY`:`?`表示无关联终端(后台进程) - `STAT`:进程状态(S=休眠,R=运行中) - `COMMAND`:进程命令[^3] #### 3. 按进程名搜索后台进程 ```bash pgrep -l '进程名' # 如 pgrep -l 'python' pstree -p | grep 进程名 # 树状显示进程关系 ``` #### 4. 查看实时进程状态 ```bash top -u $USER # 只显示当前用户的进程 htop # 增强版top(需安装) ``` ### 关键命令对比 | 命令 | 作用范围 | 最佳场景 | 示例 | |------|----------|----------|------| | `jobs -l` | 当前终端 | 查看刚启动的后台作业 | `jobs -l` | | `ps aux` | 系统全局 | 全面系统监控 | `ps aux \| grep python` | | `pgrep` | 按名称 | 快速查找特定进程 | `pgrep -l 'nginx'` | | `pstree` | 父子关系 | 分析进程依赖 | `pstree -p 12345` | ### 使用示例 1. **启动后台进程**: ```bash python data_processing.py > log.txt 2>&1 & # 输出:[1] 28794 ``` 2. **查看当前终端后台进程**: ```bash jobs -l # 输出:[1] + 28794 running python data_processing.py > log.txt 2>&1 ``` 3. **确认进程仍在运行**: ```bash ps -p 28794 -o pid,stat,tty,cmd # 输出:PID STAT TT COMMAND # 28794 S ? python data_processing.py ``` > **重要提示**:使用 `nohup` 或 `disown` 启动的进程会脱离终端关联,只能通过 `ps` 或 `pgrep` 查看,不会出现在 `jobs` 列表中[^1]。 ### 进程状态解析(STAT列) | 状态 | 含义 | 常见场景 | |------|------|----------| | `S` | 休眠中 | 等待I/O操作 | | `R` | 运行中 | CPU正在处理 | | `T` | 已暂停 | 被`Ctrl+Z`暂停 | | `Z` | 僵尸进程 | 需手动清理 | | `D` | 不可中断休眠 | 硬件设备等待 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值