我正在检查有关 bash 脚本的 pid 泄漏的可能性,该脚本不断创建后台作业但不调用 wait 命令,我碰巧发现(通过 strace)Bash 监视 SIGCHLD 并自动调用 wait4(...),尽管我的脚本没有调用等待命令。这就是为什么没有pid泄漏,这很好。但后来我开始想,如果我为该后台 pid 调用 wait 命令会怎样?它不存在于 /proc 中,它应该返回错误,Bash 如何处理这个问题?我在 Bash 4.4.19 和 5.1.16 上做了一些实验,结果发现 Bash wait 命令实际上从后台作业缓存中获取结果,我还检查了源代码,例如,重击 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,那么 wait 命令将不会正确退出代码。
这就是 Bash wait 命令正在使用后台作业管理信息中缓存的结果的结论。
那么我的问题变成:如果脚本连续创建后台作业,例如,
测试2:
while true; do
echo hi &
done
那么后台作业缓存会越来越大,那么会不会变成内存泄漏呢?
我测试过这个脚本,似乎没有内存泄漏,那为什么没有泄漏呢?
编辑:让我更清楚地说,上面的脚本应该耗尽内存,但实际上并没有像我观察的那样,为什么?
编辑:上面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
答案1
首先,内存永远不会耗尽,因为最大 pid(您可以在 参考资料中看到/proc/sys/kernel/pid_max
)是有限的,通常为 32768。因此,即使您运行更多进程,最终 pid 也会被内核回收,因此最大数量的 pid 会被内核回收。您bash
将保留在内存中的pids将< 32768。
尺寸bash
也取决于你的nproc
(最大用户进程数)限制。
您可以使用以下脚本轻松确认 bash 中的情况是否正确:
#!/bin/bash
declare -a pids_list=()
for i in {1..4196}; do
(exit 0) & waitpid=$! && wait $waitpid
pids_list+=($waitpid)
done
export KEPT=0 DISCARDED=0
for i in "${pids_list[@]}"
do
wait $i 2>/dev/null
if [ $? -ne 127 ] # If the child is not found in the jobs table, wait returns 127
then
let KEPT++
else
let DISCARDED++
KEPT=0
fi
done
echo KEPT=$KEPT DISCARDED=$DISCARDED
在本例中,我在后台运行 4096+100=4196 个作业,并等待每个作业完成,同时将 pid 保留在 pids_list 数组中。所有作业完成后,我循环遍历 pids_list 数组并检查 bash 是否仍保持其状态。
就我而言,我的默认最大进程限制是 4096:
$ ulimit -u
4096
如果我运行此代码(无论是作为脚本,还是源代码),它将确认它将丢弃前 100 个 pid 的状态,并仅将最后 4096 个 pid 保留在内存中:
$ check_pid_table.sh
KEPT=4096 DISCARDED=100
如果我将限制减少到 1024,这就是它将保留的进程数。
$ ulimit -u 1024
$ check_pid_table.sh
KEPT=1024 DISCARDED=3172
如果我增加限制,它将保留所有 pid(但同样 - 达到您的pid_max
限制)。
$ ulimit -u 8192
$ check_pid_table.sh
KEPT=4196 DISCARDED=0
bash中进程表占用多少内存
bash
您还可以检查需要保留的不同数量的 pid消耗了多少内存。我在这里使用的是time(1)
命令查看bash
进程使用的内存。
%M
在time
检查Maximum resident set size of the process during its lifetime, in Kbytes.
$ ulimit -u 65536
$ for i in {1..8} {25..32}; do
> /usr/bin/time -f "number of procs=$i KB, memory=%M KB" bash -c '
> for (( i=$0 ; i>0 ; i-- )); do
> echo >/dev/null & wait $!
> done' $(($i*1024))
> done
number of procs=1 KB, memory=2904 KB
number of procs=2 KB, memory=2936 KB
number of procs=3 KB, memory=2968 KB
number of procs=4 KB, memory=3000 KB
number of procs=5 KB, memory=3032 KB
number of procs=6 KB, memory=3064 KB
number of procs=7 KB, memory=3096 KB
number of procs=8 KB, memory=3128 KB
number of procs=25 KB, memory=3672 KB
number of procs=26 KB, memory=3704 KB
number of procs=27 KB, memory=3736 KB
number of procs=28 KB, memory=3768 KB
number of procs=29 KB, memory=3796 KB
number of procs=30 KB, memory=3796 KB
number of procs=31 KB, memory=3796 KB
number of procs=32 KB, memory=3796 KB
您可以看到,每个 1K 进程块会增加大约 32K 的内存消耗bash
,这意味着每个进程条目使用 32 位。
但是,当我们接近 32 KB(这是限制max_pid
)时,您可以看到内存变成静态的。这是因为,正如我所说,最终 pid 会被回收(并且我的系统上已经有许多进程在运行)。
答案2
这不是泄漏,不,因为内存没有丢失。 shell 跟踪 PID,因此理论上它最终可能会耗尽内存,但所有这些都是预期和托管的内存使用。
在 POSIX 下,shell 只跟踪活动的 PID 和最后一个退出的 PID 的结果:
wait [-n] [n ...]
等待每个指定的子进程并返回其终止状态。每个n
可能是一个进程 ID 或一个作业规范;如果给出了作业规范,则等待该作业管道中的所有进程。如果n
未给出,则等待所有当前活动的子进程,并且返回状态为零。如果-n
提供了该选项,则wait
等待任何作业终止并返回其退出状态。如果n
指定不存在的进程或作业,则返回状态为127。否则,返回状态为最后等待的进程或作业的退出状态。
bash
不过,就(至少 4.4.12 到 5.2)而言,您是正确的不遵循 POSIX,甚至bash --posix
行为不符合 POSIX 标准。相反,保留所有后台进程状态。这在您的“test4”中已成功演示。使用符合 POSIX 标准的方法对比结果dash
:
exit code of first process is: 1
exit code of second process is: 2
Get exit code of first process again, result is: 127
查看bash
文件中 ,的源代码nojobs.c
特别是函数alloc_pid_list
(从wait_builtin
in调用)wait.def
),每个托管 PID 在数组中使用一个额外的 12 字节条目pid_list
。在您因其他原因耗尽系统资源之前,由于该阵列的大小不断增大,您不太可能耗尽系统资源。