ko1nksm
@ko1nksm (Koichi Nakashima)

Are you sure you want to delete the question?

Leaving a resolved question undeleted may help others!

xargsコマンドの引数分割のおかしな挙動についての質問

xargs の引数サイズに 410 KB 付近に謎の上限がある?

xargs コマンドは標準入力から受け取った多数の引数を、コマンド引数の最大サイズ (ARG_MAX) を考慮して、いい感じに引数を分割してコマンドを呼び出してくれるコマンドです。その実際の挙動、実際に引数の個数やサイズはいくつごとに呼び出されているのかを確認していたのですが、どうしても腑に落ちない点があったので質問します。腑に落ちない点とは本来サイズの上限がなさそうなところで、謎の 410 KB の壁があるように見えることで、BusyBox xargs にいたっては正常に処理できると思われる範囲内なのにエラー終了してしまいます。

Twitter でも同様の質問をしています(ここ
返信をいただき、この記事のコメントに回答の内容をまとめました。

予備知識 sysconf より

ARG_MAX - _SC_ARG_MAX
exec(3) 関数群の引き数の最大長。 _POSIX_ARG_MAX (4096) 未満であってはならない。

バグ
ARG_MAX を使うのは難しい、なぜなら、 exec(3) の引き数領域 (argument space) のうちどれくらいが ユーザーの環境変数によって使われるかは分からないからである。
いくつかの返り値はとても大きくなることがある。これらを使って メモリーの割り当てを行うのは適当ではない。

基本情報

$ uname -a
Linux server 5.4.0-126-generic #142-Ubuntu SMP Fri Aug 26 12:12:57 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.5 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.5 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal
$ getconf ARG_MAX # Linux の場合は 2 MB だが OS によって異なる
2097152

$ xargs --show-limits < /dev/null
環境変数が 2489 バイトを占めます
POSIX の引数の長さ上限 (このシステム): 2092615
POSIX の最小の引数の長さの上限 (すべてのシステム): 4096
実際に使用できるコマンド長の最大値: 2090126
実際に使用しているコマンドバッファの大きさ: 131072
Maximum parallelism (--max-procs must be no greater): 2147483647

$ xargs --show-limits -s 2000000 < /dev/null # -s オプションを追加
環境変数が 2489 バイトを占めます
POSIX の引数の長さ上限 (このシステム): 2092615
POSIX の最小の引数の長さの上限 (すべてのシステム): 4096
実際に使用できるコマンド長の最大値: 2090126
実際に使用しているコマンドバッファの大きさ: 2000000
Maximum parallelism (--max-procs must be no greater): 2147483647

検証

コマンドバッファの大きさがデフォルトの 131072 (128 KB) だと小さくて都合が悪いので、検証ではすべて -s を 2000000 (≒2MB) に設定している。N は引数の数。

【検証1】以下の結果に疑問点はない。100 KB の引数を 100 個渡している。引数の最大サイズが 2 MB であるため 19 個毎に sh コマンドを呼び出していることがわかる。引数のサイズはおよそ 1.9 MB。-s の値を 2000056 に指定すると N=20、およそ 2.0 MB になった。

$ printf '%0100000d\n' {1..100} | xargs -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=19 1900018bytes # およそ 1.9 MB
N=19 1900018bytes
N=19 1900018bytes
N=19 1900018bytes
N=19 1900018bytes
N=5 500004bytes

# busybox xargs の 結果も上記 GNU xargs と同じ
$  printf '%0100000d\n' {1..100} | busybox xargs -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
...同上...

【検証2】以下の結果にも疑問点はない。1 Byte の引数を 1,000,000 個渡している。この時、引数の数 -n を 最大 200,000 個に制限しており、その結果として引数のサイズはおよそ 400 KB になる。

$ printf '%c\n' {1..1000000} | xargs -n 200000 -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=200000 399999bytes # およそ 400 KB
N=200000 399999bytes
N=200000 399999bytes
N=200000 399999bytes
N=200000 399999bytes

# busybox xargs の 結果も上記 GNU xargs と同じ
$ printf '%c\n' {1..1000000} | busybox xargs -n 200000 -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
...同上...

【検証3】以下の結果がよくわからない。上との違いは -n を 5% 増やし 210,000 個に変更しただけ。引数のサイズも 5% 増えて 420 KB になるが引数の最大サイズ (2 MB) よりも遥かに小さいので制限には引っかからないはずだが、おかしな挙動をしている。さらに BusyBox xargs ではエラーになってしまう(GNU xargs と原因が同じであるかどうかは不明)。

$ printf '%c\n' {1..1000000} | xargs -n 210000 -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=104998 209995bytes
N=105002 210003bytes
N=183750 367499bytes
N=26250 52499bytes
N=203438 406875bytes
N=6562 13123bytes
N=208360 416719bytes
N=1640 3279bytes
N=160000 319999bytes

$ printf '%c\n' {1..1000000} | busybox xargs -n 210000 -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
xargs: sh: Argument list too long # エラーになってしまう

N はランダムな数値に見えるが、上から二つずつペアで考えると意味が理解できる。

  • 104998 + 105002 = 210000
  • 183750 + 26250 = 210000
  • 203438 + 6562 = 210000
  • 208360 + 1640 = 210000
  • 160000

推測だが -s の値を変更しながら検証した所 410 KB あたりに謎の制限があって、それを超えるとおそらく内部でエラーが発生しているためにデータを二つに分割してリトライし、さらに最適な値を探っているようにみえる。BusyBox はこのリトライが実装されていないためにエラーになっていると推測している。データの数を増やすと途中で三つずつのペアに切り替わるが、N の呼び出し回数のパターンが収束した。(ただし最適値とは思えない。別の問題で中央値の計算で小数点以下を切り上げるべき所を切り捨ててしまったバグに見える)

$ printf '%c\n' {1..2000000} | xargs -n 210000 -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=104998 209995bytes
N=105002 210003bytes

N=183750 367499bytes
N=26250 52499bytes

N=203438 406875bytes
N=6562 13123bytes

N=208360 416719bytes
N=1640 3279bytes

N=209386 418771bytes
N=614 1227bytes

N=104742 209483bytes
N=104743 209485bytes
N=515 1029bytes

N=104998 209995bytes
N=104999 209997bytes
N=3 5bytes

N=104998 209995bytes
N=104999 209997bytes
N=3 5bytes

N=104998 209995bytes
N=104999 209997bytes
N=3 5bytes

N=110000 219999bytes

【検証4】おそらくこれが最も核心に近い。(私の環境では)コマンドバッファが 418,911 (≒ 409.09277 KB) までは問題なく 418,912 (= 409.09375 KB) からおかしくなった。やはり 410 KB 付近になにかあるような気がする。410 KB は 419,840 バイト、差分は 928 バイト。よくわからない。検証3は -n で個数を指定して使用するコマンドバッファを 410 KB 以内にすれば問題が発生しないということになるのだろう。

$ printf '%c\n' {1..1000000} | xargs -s 418911 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=209437 418873bytes
N=209437 418873bytes
N=209437 418873bytes
N=209437 418873bytes
N=162252 324503bytes
# BusyBox xargs も問題なし

$ printf '%c\n' {1..1000000} | xargs -s 418912 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=104717 209433bytes
N=104721 209441bytes
N=183259 366517bytes
N=26179 52357bytes
N=202894 405787bytes
N=6544 13087bytes
N=207803 415605bytes
N=1635 3269bytes
N=162248 324495bytes
# BusyBox xargs もエラーになる

ここでふと WSL1、つまり MS 版 Linux カーネルの実装はどうなんだろう?と確認した所、変な動きはしなかった。もしかしたらカーネルの問題なのかもしれない? と思いつつ macOS に Homebrew でインストールした GNU コマンドで確認した所、再現してしまった。ただし -s の値はおよそ半分(ARG_MAX の値が半分だから?)。

macOS
$ printf '%c\n' {1..1000000} | gxargs -s 209345 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=104654 209307bytes
N=104654 209307bytes
N=104654 209307bytes
N=104654 209307bytes
N=104654 209307bytes
N=104654 209307bytes
N=104654 209307bytes
N=104654 209307bytes
N=104654 209307bytes
N=58114 116227bytes

printf '%c\n' {1..1000000} | gxargs -s 209346 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=52326 104651bytes
N=52329 104657bytes
N=91574 183147bytes
N=13081 26161bytes
N=101386 202771bytes
N=3269 6537bytes
N=103839 207677bytes
N=816 1631bytes
N=104452 208903bytes
N=203 405bytes
N=104605 209209bytes
N=50 99bytes
N=104643 209285bytes
N=12 23bytes
N=104653 209305bytes
N=2 3bytes
N=104654 209307bytes
N=1 1bytes
N=58105 116209bytes

これから

strace 使ってシステムコールの様子を見てみたりしたのですが、システムコール周りの知識が足りてないので、正直よくわからん \(^o^)/ 状態です。xargs のソース見たりデバッグしたりして理由を探りたいとは思っていますが、もし理由がわかる方がいましたら願いします m(_ _)m

補足 この問題はおそらく -s オプションでコマンドバッファを大きくしたことで引き起こされているはずなので、デフォルト 128 KB ~ 410 KB の壁までの間で使っていれば問題は起きないはずです。コマンドバッファは仕様上は ARG_MAX (2 MB) 近くまで大きくしても問題ないはずなのですが。

3

3Answer

スタックが溢れているのではないでしょうか?

$ ulimit -s
8192
$ ulimit -s 16384
$ ulimit -s
16384
$ printf '%c\n' {1..1000000} | xargs -n 210000 -s 2000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
N=210000 419999bytes
N=210000 419999bytes
N=210000 419999bytes
N=210000 419999bytes
N=160000 319999bytes
3Like

Comments

  1. @ko1nksm

    Questioner

    コメントありがとうございます。どうやら引数で消費するメモリが、スタックを溢れさせないようにする処理が入っているようですね。確かに `ulimit` で制限を緩くすれば期待通りの動作を行いました。Twitter でも @angel_p_57 さんより同様のコメントを頂きました。

    (え?ここ Markdown 使えないの?)

要約すると、xargs コマンド以前の問題で(少なくとも Linux では)ARG_MAX は単純に引数のサイズではないということです。Linux ではARG_MAX はスタックサイズの 1/4 に設定されています。スタックのサイズは ulimit コマンドなどで動的に変更することが可能ですが、この変更に連動して ARG_MAX も変わります。スタックがデフォルトの 8 MB の場合 ARG_MAX は 2 MB です。引数+環境変数のデータはこの 2 MB を超えてはいけません。

今回の例では 1 つあたりの引数のサイズは 1 文字 + NUL 文字で合計 2 バイトですが、内部的にはその引数の管理のためにポインタ 8 バイト (64 bit) が追加で必要になるため合計 10 バイトとなります。したがって 10 バイト × 210,000 個の引数で ARG_MAX を超えてしまいます(参考 exec.c )。さらにここまでの処理である程度スタックを使用しているはずなので 2 MB よりも少し少ないところ(下記の例では -2,882 バイト)でエラーになるのでしょう。

$ /bin/true $(printf '%c \n' {1..200000}) # 209427 (2,094,270 bytes) まで OK

$ /bin/true $(printf '%c \n' {1..210000}) # 209428 (2,094,280 bytes) から エラー
-bash: /bin/true: 引数リストが長すぎます

今回の例では 1 バイトの引数でしたが、短いファイル名を想定して 11 バイト、1つあたりの引数が 20 バイトで計算すると 10 万個ほどでエラーにになることになります。長い UTF-8 の日本語ファイル名の計算でおよそ 30 文字 ≒ 100 バイトで計算すると 1万ファイル程度になります。一度にそのような数のファイルを処理することはあまりないと思いますが、現実的に十分ありえる数でもあると言えます。

もっとも GNU xargs で処理する場合にはエラーになったら引数の数を分割して実行するので問題はなさそうです。しかし BusyBox ではエラーになりますし、おそらく macOS でも同様の問題があるようです。

macOS 上にて
$ getconf ARG_MAX
1048576

$ printf '%0100d\n' {1..1000000} | xargs -n 10000 -s 1000000 sh -c 'a=$*; echo N=$# ${#a}bytes' sh
xargs: sh: Argument list too long

対策としては -n を大きくしすぎないことかなと思います。スタックを大きくしても回避可能ですが、どこまでスタックの容量を増やせるかなどは環境に依存するので調整が大変でしょう。小さくする分には問題ないでしょう。おそらくデフォルトが一番問題が起きないように調整されていると思います。

まとめとしては「Argument list too long」のようなエラーが発生した時 ARG_MAX の制限だけではなくスタック(他にもあるかも?)の制限によって引数リストが長すぎるというエラーが発生する場合があるということがわかりました。

3Like

@angel_p_57 さんより Twitter 経由

ちょっとソース見てみましたが、execve(2) の E2BIG、つまり引数・環境変数合計容量過多の対策っぽいです。ただ、昔の GNU findutils (4.4.0) では見られなかったので、途中で追加された機能でしょう。

分割してリトライは、おそらく記事での推察通りだと思うのですが、深くは追ってません。find でも xargs 的なことができるようにしてあるためか、findutils の共通サブルーチンでロジックを組んでるようです。

で、閾値の約400KB ですが、これは 200K要素と見た方が良いと思います。結局はデータ容量に依るところではあるのですが。これが、kernel が E2BIG と判断する閾値になっていて、状況により変動します。

閾値については、kernelソースの kernel/fs/exec.c をさらっと見ただけですが、先ほどの数値は、8MiB×1/4÷( 1.0B + 1B + 8B )≒200K要素 という計算です。

8MiB は ulimit -s の設定値です。unlimited だとまた別の値が出てくると思いますが、そこは調べてません。
1/4 は kernel が決めた比率です。
1.0B は引数の平均文字列長、1B は NUL文字区切り分、8B はポインタ分です。
結局、引数とポインタがスタックを食い潰さないようにとの配慮でしょう。

ちなみにポインタというのは、C言語の main(argc,argv) で出てくる、argvで参照するポインタ配列のことです。

E2BIG が起こるかどうかの境目を見たければ、perl ワンライナーで試すのが手っ取り早いかと思います。例えばこんな感じのスクリプトとか。ulimit -s でスタック上限を変えれば、それに大体比例して境目も変わってると思います。
https://ideone.com/RkzRHK

2Like

Your answer might help someone💌