Что определяет максимальный размер для одного аргумента команды?

49

У меня сложилось впечатление, что максимальная длина одного аргумента не была проблемой здесь, так как общий размер массива аргументов плюс размер среды, которая ограничена 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этом более чем достаточно, чтобы учесть разницу между размером среды в оболочке и 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 (или даже определенными версиями такого)?
  4. Чем объясняется дополнительное расхождение ~ 5 КБ между фактическим максимальным размером массива аргументов и приблизительным размером среды и 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
Graeme
источник
5
В Linux он жестко запрограммирован на 32 страницы (128 кБ). Смотрите MAX_ARG_STRLEN в источнике.
Стефан Шазелас
1
Большая часть информации, которую вы ищете, содержится в этом ответе CP: максимальное количество исходных файлов, аргументы для утилиты копирования
Стефан Шазелас
1
По крайней мере, на моей машине, getconf ARG_MAXзависит от тока ulimit -s. Установите неограниченное количество и получите потрясающий 4611686018427387903 для ARG_MAX.
Дероберт
почему вы используете путь / proc / $$ / environment? procfs в linux поддерживает symlink / proc / self, тогда вы можете использовать / proc / self / environment. все патчи, назначенные процессу, когда этот процесс проверяет один и тот же процесс, указывают на / proc / self. То же самое с devfs, например внутри / dev, stdout устройства - это символическая ссылка на fd / 1, но fd указывает на / self / fd. многие системы копируют это поведение.
Znik

Ответы:

50

ответы

  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. Эта статья дает подробное сравнение ARG_MAXи эквиваленты на Unix-подобных системах. MAX_ARG_STRLENобсуждается для Linux, но нет никакого упоминания о каком-либо эквиваленте в других системах.

    В вышеприведенной статье также говорится, что она MAX_ARG_STRLENбыла введена в Linux 2.6.23, а также ряд других изменений, связанных с максимумами аргументов команды (обсуждаются ниже). Log / diff для коммита можно найти здесь .

  4. До сих пор не ясно, что объясняет дополнительное расхождение между результатом getconf ARG_MAXи фактическим максимально возможным размером аргументов плюс среда. Ответ Стефана Чазеласа предполагает, что часть пространства учитывается указателями на каждую из строк аргумента / окружения. Тем не менее, мое собственное расследование показывает, что эти указатели не создаются в начале execveсистемного вызова, когда он все еще может вернуть E2BIGошибку вызывающему процессу (хотя указатели на каждую argvстроку, безусловно, создаются позже).

    Кроме того, строки, насколько я вижу, являются смежными в памяти, поэтому никаких пробелов в памяти здесь не происходит. Хотя, скорее всего, это фактор, который израсходует лишнюю память. Понимание того, что использует дополнительное пространство, требует более подробных знаний о том, как ядро ​​распределяет память (что полезно знать, поэтому я буду исследовать и обновлять позже).

ARG_MAX Путаница

Начиная с Linux 2.6.23 (в результате этого коммита ) изменились способы обработки максимумов аргументов команд, что отличает Linux от других Unix-подобных систем. В дополнение к добавлению MAX_ARG_STRLENи MAX_ARG_STRINGS, результат 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_MAXв limits.hнастоящее время служит жесткой нижней границы на результат getconf ARG_MAX. Если размер стека установлен так, что 1/4размер стека меньше, чем ARG_MAXв 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) становится верхним пределом размера аргумента / среды до того, как E2BIGбудет возвращено (хотя getconf ARG_MAXвсе равно будет отображаться значение в limits.h).

Последнее, что следует отметить, это то, что если ядро ​​собрано без CONFIG_MMU(поддержка аппаратного обеспечения управления памятью), то проверка ARG_MAXотключена, поэтому ограничение не применяется. Хотя MAX_ARG_STRLENи MAX_ARG_STRINGSдо сих пор применяются.

Дальнейшее чтение

Graeme
источник
2
Это хороший ответ, конечно, лучше моего - я проголосовал за него. Но ответ, который мы просим, ​​не всегда является ответом, который мы должны получить - вот почему мы спрашиваем, потому что мы не знаем. Это не решает проблему с вашим рабочим процессом, который привел вас к этой проблеме в первую очередь. Я демонстрирую, как это может быть смягчено в моем собственном ответе, и как аргументы строки с одной переменной оболочки длиной более 2 МБ могут быть переданы недавно запущенным процессам всего за пару строк сценария оболочки.
mikeserv
Я создал скрипт Python, который демонстрирует ограничение 32 * 4 КБ = 128 КБ переменных среды в Linux по умолчанию.
nh2
0

В 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.


источник
2
То, что ARG_MAX является минимально гарантированным для предела размера arg + env, это не максимальный размер одного аргумента (хотя это то же самое значение, что и MAX_ARG_STRLEN)
Стефан Шазелас
У тебя есть свидание для твоего eglibc-2.18/NEWSфрагмента? Было бы хорошо связать это с конкретной версией ядра.
Грэм
@StephaneChazelas: Мне просто лень найти часть, но если arg превышает максимальное значение, нет необходимости определять размер env.
@Graeme: у меня также работает несколько старых linux, где значение getconf показывает 131072. Я думаю, что это относится к более новым linux с eglibc> ?? только. Поздравляю, вы нашли ошибку BTW.
2
Вы смотрите на код glibc, это не имеет значения здесь. Libc не волнует, какой размер аргументов вы передаете. Код, который вы цитируете, касается sysconf, API, который дает пользователям представление о максимальном размере (что бы это ни значило) argv + env, передаваемого в execve (2). Это ядро ​​принимает или не принимает список arg и env, передаваемый по системному вызову execve (). Речь getconf ARG_MAXидет о совокупном размере arg + env (переменная в недавнем Linux, см. ulimit -sИ другой вопрос, который я связал), а не о максимальной длине одного аргумента, для которого нет запроса sysconf / getconf.
Стефан Шазелас
-1

Так что @StephaneChazelas справедливо исправляет меня в комментариях ниже - сама оболочка никоим образом не определяет максимальный размер аргумента, разрешенный вашей системой, а скорее, он устанавливается вашим ядром.

Как уже говорили некоторые другие, кажется, что ядро ​​ограничивает максимальный размер аргумента 128 КБ, который вы можете передать новому процессу из любого другого при первом его исключении. Эта проблема возникает именно из-за множества вложенных $(command substitution)подоболочек, которые должны выполняться на месте и передавать весь свой вывод от одного к другому.

И это своего рода дикая догадка, но поскольку расхождение ~ 5 КБ кажется настолько близким к стандартному размеру системной страницы, я подозреваю, что оно предназначено для использования страницей bashдля обработки подоболочки, которая $(command substitution)требуется для окончательной доставки ее выходных данных и / или стек функций, который он использует для сопоставления array tableваших данных. Я могу только предположить, что ни один не выйдет на свободу.

Ниже я продемонстрирую, что, хотя это может быть немного сложнее, существует возможность передавать очень большие значения переменных оболочки при вызове новым процессам, при условии, что вам удастся выполнить их потоковую передачу.

Для этого я в основном использовал трубы. Но я также оценил массив оболочки в here-documentуказанном cat's stdin. ниже результате.

Но последнее замечание: если у вас нет особой необходимости в переносимом коде, мне кажется, что это mapfileможет немного упростить работу вашей оболочки.

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
mikeserv
источник
2
Нет, это не имеет ничего общего с оболочкой, это системный вызов execve (2), возвращающий E2BIG, когда один аргумент превышает 128 кБ.
Стефан Шазелас
Учтите также, что нет ограничений на встроенные функции оболочки - они echo $(tr -dc [:alnum:] </dev/urandom | head -c $(($(getconf ARG_MAX)*10))) >/dev/nullбудут работать нормально. Проблема возникает только при использовании внешней команды.
Грэм
@ Grame Хорошо, я сделал это с кошкой также - без проблем. Переменная оценивается в heredoc в конце. Смотрите мое последнее редактирование. Я сократил общее количество до 33, потому что каждый раз добавляю последнее значение. И заполнение нулями ...
mikeserv
@StephaneChazelas - так я могу обойти это, оценив аргумент в потоке heredoc? Или bashэто как-то сжимает?
mikeserv
1
@mikeserv, я нигде не вижу в твоем коде ни одного случая, когда ты выполняешь команду с большим списком аргументов. printfявляется встроенным, так что не выполняется , и AFAICT, ваш catне дает никаких аргументов.
Стефан Шазелас