xargsコマンドの引数分割のおかしな挙動についての質問
Q&A
Closed
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
の値が半分だから?)。
$ 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) 近くまで大きくしても問題ないはずなのですが。