这里的基本问题是,当 bash 启动子 shell 时,它只是克隆自己,而不是从头开始执行新的 shell。这意味着子shell出生时分配了父shell中的所有临时数据结构。
子shell需要这样做才能继承当前的执行环境:shell函数和变量,以及其他shell设置。它通常也更有效,因为它避免了相当大的外壳启动成本。
Unix 写时复制 (COW) 语义避免了复制所有这些数据结构的一些内存成本。但由于 COW 是在完整的页面上工作,而不是在单独的分配上工作,所以它无法完全避免复制。
减少内存消耗可以做的一件简单的事情是将for 循环更改为计算得到的for,它看起来很像带有额外括号的C for:
for ((i=0; i<5000; ++i)); do
您的 for 循环 (for i in $(seq 5000); do) 必须首先将 seq 5000 的输出扩展为一个字符串(大约 30kb),然后将其拆分为 5000 个单词,每个单词都是一个单独的分配,以及一个5000 个元素的指针向量。分配开销意味着每个单词的成本将超过 40 个字节,即使每个字符串只有 5 个字节长。由于这些是单独的分配,它们会分散一点,其他分配将在同一个 VM 页面中进行,从而触发 COW。
虽然这些数字看起来很小,但通过使用 N 个词向量制作 N 个 shell 的克隆,你正在将所有内容相乘,这意味着总内存消耗是 N 的二次方。如果你有 2500 万个词,那么加起来甚至会增加很多如果每个字只占用几个字节:每个字占 40 个字节,那就是一个千兆字节。而二次增长让它快速增长。
当我尝试更改 for 语句时,它(总共)节省了大约三分之一的已用内存。
这是一个不费吹灰之力的大胜利,但它并没有真正解决根本问题。父 shell 还需要跟踪它产生的所有子代,它通过保存关于每个子代的一些数据来做到这一点。每次产生一个新孩子时都会修改该内存结构,因此每个新孩子出生时都有不同的数据结构。在这种情况下,COW 根本没有帮助,总内存消耗将是严格的二次方。
解决这个问题将取决于您在循环中实际执行的操作。
正如 Charles Duffy 在(现已删除的)评论中所建议的,一个简单的解决方法是使用 disown 命令从作业表中删除并行任务:
for ((i=0; i<5000; ++i)); do
(
echo $i;
sleep 100;
)&
disown
done;
另一方面,如果你正在做的只是启动一个外部命令——或者即使这是你做的最后一件事而且其他一切都很快——你可以使用exec来替换子shell内存带有外部命令的图像:
for ((i=0; i<5000; ++i)); do
(
echo $i;
exec sleep 100;
)&
done;
您甚至可以使用完整的脚本执行 exec,但调用内存密集度较低的 shell,例如 dash。
实验结果(总进程大小,以千字节为单位):
fix for fix for fix for
Only fix + disown + exec + exec
N Original for loop children sleep dash
4000 4655956 3148792 1601428 1233212 1265224
5000 6768896 4404432 2001428 1541460 1581540
6000 9241116 5837660 2401428 1849692 1897768
7000 12056056 7443052 2801428 2158752 2213992
8000 15235688 9220568 3201428 2466104 2530180
很明显,前两列在 N 中大致是二次方,后三列是线性的。
我使用以下助手来收集这些统计信息;您可以在各种 case 子句中看到精确的循环。对于所有测试,大小相加的进程数为 N+1(因此它包括驱动程序):
#!/bin/bash
case $1 in
o*)
printf "Original: " >> /dev/stderr
for i in $(seq $2); do ( echo $i; sleep 10; )& done
ps -osize=,cmd= | grep '[s]ubshell' | awk '{s+=$1}END{print NR, s}' 1>&2
sleep 15
;;
f*)
printf "Fix for loop: " >> /dev/stderr
for ((i = 0; i < $2; ++i)); do ( echo $i; sleep 10; )& done
ps -osize=,cmd= | grep '[s]ubshell' | awk '{s+=$1}END{print NR, s}' 1>&2
sleep 15
;;
d*)
printf "Also disown: " >> /dev/stderr
for ((i = 0; i < $2; ++i)); do ( echo $i; sleep 10; )& disown; done
ps -osize=,cmd= | grep '[s]ubshell' | awk '{s+=$1}END{print NR, s}' 1>&2
sleep 15
;;
e*)
printf "Exec external: " >> /dev/stderr
for ((i = 0; i < $2; ++i)); do ( echo $i; exec sleep 10; )& done
ps -p$$ -Csleep -osize= | awk '{s+=$1}END{print NR, s}' 1>&2
sleep 15
;;
a*)
printf "Exec dash: " >> /dev/stderr
for ((i = 0; i < $2; ++i)); do ( exec /bin/dash -c "echo $i; sleep 10"; )& done
ps -p$$ -Cdash -osize= | awk '{s+=$1}END{print NR, s}' 1>&2
sleep 15
;;
*)
echo "First argument should be original, forloop, disown, exec or ash."
;;
esac