我正在检查有关 bash 脚本的 pid 泄漏的可能性,该脚本不断创建后台作业但不调用 wait 命令,我碰巧发现(通过 strace)Bash 监视 SIGCHLD 并自动调用 wait4(...),尽管我的脚本没有调用等待命令。这就是没有 pid 泄漏的原因,这很好。但是后来我开始想,如果我为那个后台 pid 调用等待命令怎么办?它不存在于 /proc 中,它应该返回错误,Bash 是如何处理的?我在 Bash 4.4.19 和 5.1.16 上做了一些实验,事实证明 Bash wait 命令实际上是从后台作业缓存中获取结果,我还检查了源代码,例如Bash 5.1.16 ,请参阅 builtins/wait.def 行253
status = wait_for_single_pid (pid, wflags|JWAIT_PERROR);
然后 job.c 行 2611
r = bgp_search (pid);
意思是
/* Search for PID in the list of saved background pids; return its status if
found. If not found, return -1. We hash to the right spot in pidstat_table
and follow the bucket chain to the end. */
.
我的实验是
测试1:
bash <<'EOF'
bash -c 'sleep 1; exit 9' &
PID=$!
echo $PID
sleep 2
ls -d /proc/$PID
wait $PID
echo wait result: $?
EOF
结果是:
16079
ls: cannot access '/proc/16079': No such file or directory
wait result: 9
这是 Bash wait 命令使用缓存的证据(我也使用 strace 来验证,它清楚地显示了 wait4
返回 -1 的最后一个系统调用。
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 9}], 0, NULL) = 397
wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 399
wait4(-1, 0x7ffd79fc3fd0, WNOHANG, NULL) = -1 ECHILD (No child processes)
当然,如果我disown -a
之前运行过wait
,那么wait
将返回代码 127:,wait: pid xxxxxx is not a child of this shell
这也验证了一旦从后台作业列表中删除了后台 pid,等待命令将不会正确退出代码。
所以这就是 Bash wait 命令正在使用后台作业管理信息中的缓存结果的结论。
那么我的问题就变成了:如果脚本连续创建后台作业,例如,
测试2:
while true; do
echo hi &
done
那么后台的job cache会越来越大,那会不会变成内存泄漏呢?
我已经测试了脚本,似乎没有内存泄漏,那为什么没有泄漏呢?
编辑:让我说得更清楚,上面的脚本应该会耗尽内存,但实际上并没有像我观察的那样,为什么?
编辑:上面test2
仍然是最有趣的问题,为什么它不会耗尽内存?
编辑:我做了另一个测试,几秒钟后它确实耗尽了内存:
测试3:
bash <<'EOF'
while true; do
sleep 10 &
echo $!
done
EOF
结果是
...
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable
bash: fork: Interrupted system call
好的,现在它的行为符合预期:内存不足。
抱歉,我的问题变成了:这是设计使然吗?我从未听说过不断创建后台作业的警告。到目前为止,我知道的唯一解决方案是使用disown
停止管理后台作业,或使用其他技巧,例如在(cmd&)
不作为后台作业进行管理的情况下启动进程。
编辑:我自己回答:这应该是设计使然,它只是意味着 Bash 跟踪所有活动作业,如果在短时间内,有很多活动作业,那么它就会耗尽内存。所以这并不矛盾test2
。
编辑:添加了另一个测试以表明 Bash 后台作业退出代码缓存不仅缓存活动作业之外最后一个作业的退出代码,它还缓存所有作业的退出代码。
测试4:
bash -x <<'EOF'
bash -c '/bin/sleep 3; exit 1' &
PID1=$!
bash -c '/bin/sleep 6; exit 2' &
PID2=$!
wait $PID1
echo exit code of first process is: $?
wait $PID2
echo exit code of second process is: $?
wait $PID1
echo Get exit code of first process again, result is: $?
EOF
结果是:
+ PID1=2357449
+ bash -c '/bin/sleep 3; exit 1'
+ PID2=2357450
+ wait 2357449
+ bash -c '/bin/sleep 6; exit 2'
+ echo exit code of first process is: 1
exit code of first process is: 1
+ wait 2357450
+ echo exit code of second process is: 2
exit code of second process is: 2
+ wait 2357449
+ echo Get exit code of first process again, result is: 1
Get exit code of first process again, result is: 1
这不是泄漏,不,因为记忆没有丢失。该 shell 跟踪 PID,因此理论上它最终可能会耗尽内存,但所有这些都是预期的和管理的内存使用。
在 POSIX 下,shell 只跟踪活动的 PID 和最后一个 PID 退出的结果:
但是,在(至少 4.4.12 到 5.2)的情况下
bash
,你是正确的,因为它不遵循 POSIX,即使bash --posix
行为不符合 POSIX。相反,保留所有后台进程状态。这已在您的“test4”中成功演示。使用符合 POSIX 标准的对比结果dash
:查看
bash
文件中的源代码,nojobs.c
尤其是函数(从中alloc_pid_list
调用),每个托管 PID 在数组中使用一个额外的 12 字节条目。由于此阵列的大小在您出于其他原因用完之前不断增加,因此您极不可能用尽系统资源。wait_builtin
wait.def
pid_list
首先,内存永远不会用完
/proc/sys/kernel/pid_max
,因为最大 pid(您可以在您bash
将保留在内存中的 pids将小于 32768。在
bash
大小上也取决于你的nproc
(最大用户进程数)限制。您可以使用以下脚本在您的 bash 中轻松确认它是真的:
在这种情况下,我在后台运行 4096+100=4196 个作业并等待每个作业完成,同时将 pids 保存在 pids_list 数组中。所有作业完成后,我遍历 pids_list 数组并检查 bash 是否仍保持其状态。
在我的例子中,我的默认最大 proc 限制是 4096:
如果我运行这段代码(作为脚本,或者如果你获取它),它将确认它会丢弃前 100 个 pids 的状态,并且只在内存中保留最后 4096 个 pids:
如果我将限制减少到 1024,那就是它将保留的进程数。
如果我增加限制,它会保留所有 pids(但同样 - 达到您的
pid_max
限制)。bash中进程表占用多少内存
bash
您还可以检查需要保留的不同数量的 pid消耗了多少内存。我在这里使用time(1)
命令来检查进程使用的内存bash
。%M
在time
检查Maximum resident set size of the process during its lifetime, in Kbytes.
您可以看到,每个 1K 进程块都会增加大约 32K 的内存消耗
bash
,这意味着每个进程条目使用 32 位。然而,当我们接近 32 KB(这是限制
max_pid
)时,您可以看到内存变为静态。那是因为,正如我所说,最终 pids 被回收(并且我的系统上已经运行了很多进程)。