什么定义了命令单个参数的最大大小?

什么定义了命令单个参数的最大大小?

我的印象是,单个参数的最大长度并不是这里的问题,而是整个参数数组的总大小加上环境的大小(限制为ARG_MAX.因此我认为类似以下的事情会成功:

env_size=$(cat /proc/$$/environ | wc -c)
(( arg_size = $(getconf ARG_MAX) - $env_size - 100 ))
/bin/echo $(tr -dc [:alnum:] </dev/urandom | head -c $arg_size) >/dev/null

足以- 100解释 shell 中的环境大小和进程之间的差异echo。相反,我得到了错误:

bash: /bin/echo: Argument list too long

玩了一段时间后,我发现最大值小了一个完整的十六进制数量级:

/bin/echo \
  $(tr -dc [:alnum:] </dev/urandom | head -c $(($(getconf ARG_MAX)/16-1))) \
  >/dev/null

当负数被删除时,错误返回。看似单个参数的最大值实际上是ARG_MAX/16-1占位于参数数组中字符串末尾的空字节。

另一个问题是,当参数重复时,参数数组的总大小可以更接近ARG_MAX,但仍然不完全是:

args=( $(tr -dc [:alnum:] </dev/urandom | head -c $(($(getconf ARG_MAX)/16-1))) )
for x in {1..14}; do
  args+=( ${args[0]} )
done

/bin/echo "${args[@]}" "${args[0]:6534}" >/dev/null

此处使用"${args[0]:6533}"会使最后一个参数长 1 个字节并给出Argument list too long错误。这种差异不太可能由环境的大小来解释:

$ cat /proc/$$/environ | wc -c
1045

问题:

  1. 这是正确的行为,还是某个地方存在错误?
  2. 如果没有,这种行为是否记录在任何地方?是否还有另一个参数定义单个参数的最大值?
  3. 这种行为是否仅限于 Linux(甚至特定版本的 Linux)?
  4. 参数数组的实际最大大小加上环境的近似大小与 之间额外约 5KB 的差异是什么原因造成的ARG_MAX

附加信息:

uname -a
Linux graeme-rock 3.13-1-amd64 #1 SMP Debian 3.13.5-1 (2014-03-04) x86_64 GNU/Linux

答案1

答案

  1. 绝对不是一个错误。
  2. 定义一个参数的最大大小的参数是MAX_ARG_STRLEN。除了以下注释之外,没有关于此参数的文档binfmts.h

    /*
     * These are the maximum length and maximum number of strings passed to the
     * execve() system call.  MAX_ARG_STRLEN is essentially random but serves to
     * prevent the kernel from being unduly impacted by misaddressed pointers.
     * MAX_ARG_STRINGS is chosen to fit in a signed 32-bit integer.
     */
    #define MAX_ARG_STRLEN (PAGE_SIZE * 32)
    #define MAX_ARG_STRINGS 0x7FFFFFFF
    

    如图所示,Linux 对命令的参数数量也有(非常大的)限制。

  3. 对单个参数大小的限制(与参数加环境的总体限制不同)似乎是特定于 Linux 的。这文章给出了类 Unix 系统上的详细比较ARG_MAX和等效项。MAX_ARG_STRLEN针对 Linux 进行了讨论,但没有提及任何其他系统上的任何等效内容。

    上述文章还指出了MAX_ARG_STRLENLinux 2.6.23 中引入的内容,以及与命令参数最大值相关的许多其他更改(如下所述)。可以找到提交的日志/差异这里

  4. 目前尚不清楚是什么原因导致了getconf ARG_MAX参数加环境的结果与实际最大可能大小之间的额外差异。Stephane Chazelas 的相关回答,表明部分空间由指向每个参数/环境字符串的指针占用。然而,我自己的调查表明,这些指针不是在execve系统调用的早期创建的,因为它仍然可能E2BIG向调用进程返回错误(尽管指向每个argv字符串的指针肯定是稍后创建的)。

    另外,据我所知,字符串在内存中是连续的,因此这里没有由于对齐而产生的内存间隙。尽管很可能是其中的一个因素用完额外的内存。了解什么使用了额外的空间需要更详细地了解内核如何分配内存(这是有用的知识,所以我将在稍后进行调查和更新)。

ARG_MAX 混乱

自 Linux 2.6.23 起(由于这次提交),处理命令参数最大值的方式发生了变化,这使得 Linux 与其他类 Unix 系统不同。除了添加MAX_ARG_STRLENand之外MAX_ARG_STRINGS,now 的结果getconf ARG_MAX还取决于堆栈大小,并且可能与ARG_MAXin不同limits.h

通常,结果getconf ARG_MAX将是1/4堆栈大小。在bash使用ulimit获取堆栈大小时请考虑以下事项:

$ echo $(( $(ulimit -s)*1024 / 4 ))  # ulimit output in KiB
2097152
$ getconf ARG_MAX
2097152

然而,上述行为略有改变犯罪(在Linux 2.6.25-rc4~121中添加)。 ARG_MAXin limits.hnow 作为 的结果的硬下界getconf ARG_MAX。如果堆栈大小设置为1/4小于ARG_MAXin limits.h,则将limits.h使用该值:

$ grep ARG_MAX /usr/include/linux/limits.h 
#define ARG_MAX       131072    /* # bytes of args + environ for exec() */
$ ulimit -s 256
$ echo $(( $(ulimit -s)*1024 / 4 ))
65536
$ getconf ARG_MAX
131072

另请注意,如果堆栈大小设置低于可能的最小值ARG_MAX,则堆栈 ( ) 的大小将成为返回RLIMIT_STACK之前参数/环境大小的上限(尽管仍会显示 中的值)。E2BIGgetconf ARG_MAXlimits.h

最后要注意的是,如果内核构建时没有CONFIG_MMU(支持内存管理硬件),则检查ARG_MAX将被禁用,因此该限制不适用。虽然MAX_ARG_STRLEN并且MAX_ARG_STRINGS仍然适用。

进一步阅读

答案2

eglibc-2.18/NEWS

* ARG_MAX is not anymore constant on Linux.  Use sysconf(_SC_ARG_MAX).
Implemented by Ulrich Drepper.

eglibc-2.18/debian/patches/kfreebsd/local-sysdeps.diff

+      case _SC_ARG_MAX:
+   request[0] = CTL_KERN;
+   request[1] = KERN_ARGMAX;
+   if (__sysctl(request, 2, &value, &len, NULL, 0) == -1)
+       return ARG_MAX;
+   return (long)value;

linux/include/uapi/linux/limits.h

#define ARG_MAX       131072    /* # bytes of args + environ for exec() */

131072你的$(getconf ARG_MAX)/16-1,也许你应该从 0 开始。

您正在处理 glibc 和 Linux。为了获得返回的“正确”值,最好也修补 getconf ARG_MAX

编辑:

澄清一点(经过简短而激烈的讨论)

ARG_MAX中定义的常量给出limits.h了 exec 传递的一个参数的最大长度。

getconf ARG_MAX命令返回传递给 exec 的累积参数大小和环境大小的最大值。

答案3

因此,@StephaneChazelas 在下面的评论中正确地纠正了我 - shell 本身并不以任何方式规定您的系统允许的最大参数大小,而是由您的内核设置。

正如其他几个人已经说过的,内核似乎将最大参数大小限制为 128kb,这是您在首次执行新进程时可以从任何其他进程传递给新进程的最大参数大小。由于许多嵌套,您会特别遇到此问题$(command substitution)子 shell 必须就地执行并将其全部输出从一个传递到下一个。

这是一种疯狂的猜测,但由于 ~5kb 的差异似乎非常接近标准系统页面大小,我怀疑它是专用于页面的bash用于处理您的子shell$(command substitution)需要最终交付其输出和/或用于关联您的函数堆栈array table与您的数据。我只能假设两者都不是免费的。

我在下面演示了,虽然可能有点棘手,但可以在调用时将非常大的 shell 变量值传递给新进程,只要您可以设法对其进行流式处理。

为了做到这一点,我主要使用管道。但我也评估了 shell 数组here-document指向cat's stdin. 结果如下。

但最后一点 - 如果您没有特别需要可移植代码,我突然想到mapfile可能会稍微简化你的 shell 工作。

time bash <<-\CMD
    ( for arg in `seq 1 6533` ; do
        printf 'args+=(' ; printf b%.0b `seq 1 6533` ; echo ')'
    done ;
    for arg in `seq 1 6533` ; do
        printf %s\\n printf\ '%s\\n'\ \""\${args[$arg]}"\" ;
    done ) | . /dev/stdin >&2
CMD
bash <<<''  66.19s user 3.75s system 84% cpu 1:22.65 total

也许你可以将其加倍,然后如果你在流中这样做的话再次这样做 - 我还没有病态到发现 - 但如果你流它肯定它有效。

我确实尝试将printf第二行中的生成器部分更改为:

printf \ b%.0b

它也有效:

bash <<<''  123.78s user 5.42s system 91% cpu 2:20.53 total

所以也许我有点病态。我用zero padding here并在前面添加"$arg"值到当前值"$arg"价值。我已经超过6500了...

time bash <<-\CMD
    ( for arg in `seq 1 33` ; do
        echo $arg >&2
        printf 'args+=('"${args[$((a=arg-1))]}$(printf "%0${arg}0d" \
            `seq 1 6533` ; printf $((arg-1)))"')\n'
    done ;
    for arg in `seq 1 33` ; do
        printf '/usr/bin/cat <<HERE\n%s\nHERE\n' "\${args[$arg]}"
    done ) | . /dev/stdin >&2
CMD

bash <<<''  14.08s user 2.45s system 94% cpu 17.492 total

如果我改变cat行看起来像这样:

printf '/usr/bin/cat <<HERE | { printf '$arg'\  ; wc -c ;}
    %s\nHERE\n' "\${args[$arg]}"

我可以从中获取字节数wc.请记住,这些是中每个键的大小args大批。数组的总大小是所有这些值的总和。

1 130662
2 195992
3 261322
4 326652
5 391982
6 457312
7 522642
8 587972
9 653302
10 718633
11 783963
12 849293
13 914623
14 979953
15 1045283
16 1110613
17 1175943
18 1241273
19 1306603
20 1371933
21 1437263
22 1502593
23 1567923
24 1633253
25 1698583
26 1763913
27 1829243
28 1894573
29 1959903
30 2025233
31 2090563
32 2155893
33 2221223

相关内容