【问题标题】:Backgrounded subshells use incrementally more memory后台子shell 使用增量内存
【发布时间】:2018-08-17 14:39:09
【问题描述】:

我在后台循环启动 1000 个子shell。我假设他们使用大致相同数量的内存。

for i in `seq 1000`; do
  (
    echo $i;
    sleep 100;
  )&
done;

但是,他们没有。每个新的子shell 比前一个子shell 占用更多的内存。他们的内存使用量正在增加。

$ ps -eo size,command --sort -size | grep subshell | head -n2
  624 /bin/bash /tmp/subshells.sh
  624 /bin/bash /tmp/subshells.sh
$ ps -eo size,command --sort -size | grep subshell | tail -n2
  340 /bin/bash /tmp/subshells.sh
  340 /bin/bash /tmp/subshells.sh

最小的 subshel​​l 使用 340KB,而最大的需要 624KB。

这里发生了什么?有没有办法避免这种情况?我很伤心,因为我的并行计算的组织方式需要数千个后台子shell,而且我的内存快用完了。

【问题讨论】:

  • 您使用(...)& 而不是{...}& 是否有原因。 () 在这里充其量是毫无意义的,最坏的情况是浪费了一个额外的分叉。
  • 另外,你真的不应该在 bash 中使用 `。更喜欢$(...)。 Bash 也可以使用 c 样式的 for 循环或 {1..100} 来完成 seq 本身所做的事情
  • 我无法重现这个。您问题中的代码是否正是/tmp/subshell.sh 的内容?您使用的是哪个版本的 bash?另外:你能证明大小是“缓慢增加”,而不是随机的吗?
  • @DTSCode 没有理由,我不擅长 bash。不过,感谢您的提示。但是,{...}& 会以完全相同的内存占用量重现该问题。
  • @rici GNU bash, version 4.3.11(1)-release (x86_64-pc-linux-gnu)。我犯了一个复制粘贴错误,内存占用对应于seq 1000。这就是我知道它在增加而不是随机的方式:我在后台放置的子shell 越多,每个子shell 需要的内存就越多。我试过seq 4000,每个子shell的内存使用量在656K到1772K之间。 5000 个子壳:748K-2224K。此时我的内存不足。

标签: bash subshell


【解决方案1】:

这里的基本问题是,当 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

【讨论】:

  • 美丽。发光。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-09
  • 2012-01-24
  • 1970-01-01
相关资源
最近更新 更多