バッチ処理のためにいくつかのコマンドに引数として渡すことができるファイルの数を計算する方法は?

バッチ処理のためにいくつかのコマンドに引数として渡すことができるファイルの数を計算する方法は?

たとえば、私のディレクトリには、次のように生成された複数のファイルが含まれています。

touch files/{1..10231}_file.txt

私はそれらを新しいディレクトリに移動したいと思いますnew_files_dir

最も簡単な方法は次のとおりです。

for filename in files/*; do
    mv "${filename}" -t "new_files_dir"
done

このスクリプトは以下で動作します。10私のコンピュータから数秒。非常に遅いです。各ファイルに対するコマンドの実行により、mv速度が遅くなります。

###修正開始###

私の場合、最も簡単な方法

mv files/* -t new_files_dir

または、「パラメータリストが長すぎます」の場合:

printf '%s\0' files/* | xargs -0 mv -t new_files_dir

しかし、上記のケースはミッションの一部です。全体的な作業は次の質問にあります。Linuxでは、ファイル名に基づいて多数のファイルをディレクトリに移動する。したがって、ファイルをそのサブディレクトリに移動する必要があり、サブディレクトリの対応はファイル名の番号に基づいています。これはfor私のコードスニペットで繰り返しやその他の奇妙な現象の原因です。

###編集終了###

mv次のように、単一ファイルではなく複数のファイルをコマンドに渡すことで、このプロセスを高速化できます。

batch_num=1000

# Counting of files in the directory
shopt -s nullglob
file_list=(files/*)
file_num=${#file_list[@]}

# Every file's common part
suffix='_file.txt'

for((from = 1, to = batch_num; from <= file_num; from += batch_num, to += batch_num)); do
    if ((to > file_num)); then
        to="$file_num"
    fi  

    # Generating filenames by `seq` command and passing them to `xargs`
    seq -f "files/%.f${suffix}" "$from" "$to" |
    xargs -n "${batch_num}" mv -t "new_files_dir"
done

この場合、スクリプトは次のように動作します。0.2第二。したがって、パフォーマンスは50倍向上します。

しかし、問題があります。このファイル名のセットが最大許容長より小さいという保証はないため、いつでも「パラメータリストが多すぎる」ため、プログラムは動作を拒否する可能性があります。

私の考え計算は次のとおりですbatch_num

batch_num = "max allowable length" / "longest filename length"

その後batch_numで使用してくださいxargs

したがって、質問:許容される最大長はどのように計算されますか?


私はいくつかのことをしました:

  1. 全長は以下で確認できます。

     $ getconf ARG_MAX
     2097152
    
  2. 環境変数もパラメータサイズに影響を与えるため、次の値を除く必要がありますARG_MAX

     $ env | wc -c
     3403
    
  3. 正しい値を見つける前に、異なる数のファイルを試して同じサイズの最大ファイル数を決定する方法(バイナリ検索を使用)が開発されました。

     function find_max_file_number {
         right=2000000
         left=1
         name=$1
         while ((left < right)); do
             mid=$(((left + right) / 2))
    
             if /bin/true $(yes "$name" | head -n "$mid") 2>/dev/null; then
                 left=$((mid + 1))
             else
                 right=$((mid - 1))
             fi
         done
         echo "Number of ${#name} byte(s) filenames:" $((mid - 1))
     }
    
     find_max_file_number A
     find_max_file_number AA
     find_max_file_number AAA
    

    出力:

     Number of 1 byte(s) filenames: 209232
     Number of 2 byte(s) filenames: 190006
     Number of 3 byte(s) filenames: 174248
    

    しかし、私はこれらの結果の論理/関係を理解できませんでした。

  4. この値が試行されました。回答計算には適していません。

  5. 書いたプログラムは、渡されたパラメーターの合計サイズを計算します。このプログラムの結果は似ていますが、計算されていないバイトが残ります。

     $ ./program {1..91442}_file.txt
    
     arg strings size: 1360534
     number of pointers to strings 91443
    
     argv size:  1360534 + 91443 * 8 = 2092078
     envp size:  3935
    
     Overall (argv_size + env_size + sizeof(argc)):  2092078 + 3935 + 4 = 2096017
     ARG_MAX: 2097152
    
     ARG_MAX - overall = 1135 # <--- Enough bytes are
                              # left, but no additional
                              # filenames are permitted.
    
     $ ./program {1..91443}_file.txt
     bash: ./program: Argument list too long
    

    プログラム.c

     #include <stdio.h>
     #include <string.h>
     #include <unistd.h>
    
     int main(int argc, char *argv[], char *envp[]) {
         size_t chr_ptr_size = sizeof(argv[0]);
         // The arguments array total size calculation
         size_t arg_strings_size = 0;
         size_t str_len = 0;
         for(int i = 0; i < argc; i++) {
             str_len = strlen(argv[i]) + 1;
             arg_strings_size += str_len;
     //      printf("%zu:\t%s\n\n", str_len, argv[i]);
         }
    
         size_t argv_size = arg_strings_size + argc * chr_ptr_size;
         printf( "arg strings size: %zu\n"
                 "number of pointers to strings %i\n\n"
                 "argv size:\t%zu + %i * %zu = %zu\n",
                  arg_strings_size,
                  argc,
                  arg_strings_size,
                  argc,
                  chr_ptr_size,
                  argv_size
             );
    
         // The enviroment variables array total size calculation
         size_t env_size = 0;
         for (char **env = envp; *env != 0; env++) {
           char *thisEnv = *env;
           env_size += strlen(thisEnv) + 1 + sizeof(thisEnv);
         }
    
         printf("envp size:\t%zu\n", env_size);
    
         size_t overall = argv_size + env_size + sizeof(argc);
    
         printf( "\nOverall (argv_size + env_size + sizeof(argc)):\t"
                 "%zu + %zu + %zu = %zu\n",
                  argv_size,
                  env_size,
                  sizeof(argc),
                  overall);
         // Find ARG_MAX by system call
         long arg_max = sysconf(_SC_ARG_MAX);
    
         printf("ARG_MAX: %li\n\n", arg_max);
         printf("ARG_MAX - overall = %li\n", arg_max - (long) overall);
    
         return 0;
     }
    

    私はこのプログラムの正確性についてStackOverflowに質問しました:argv、envp、argc(コマンドライン引数)の最大要約サイズは、常にARG_MAX制限から離れています。

答え1

xargsに計算をさせます。

printf '%s\0' files/* | xargs -0 mv -t new_files_dir

答え2

あなたの質問は、実際には2つの制限の組み合わせである実際の「パラメータ数の制限」があると仮定しているようです。

  1. コマンドライン引数の文字列長の合計そして終了 NUL バイトを含む環境変数。

  2. 単一のコマンドライン引数の最大文字列長。

たとえば、1文字パラメータ200000個、2文字パラメータ100000個を使用してコマンドを呼び出すことはできますが、128kバイトを超える単一パラメータは使用できません。

xargsGNU coreutilsからインポートされたと仮定すると、xargs --show-limits </dev/nullシステムにこれらの制限が表示されます。

xargsどのシステムでもいいえコマンドラインを作成するときは、システムの最大制限を使用しますが、合理的なものを選択してください(この方法でシステムにストレスを与える必要はありません)。

答え3

本当に重要な場合は、batch-moveファイルのリストを標準入力として使用し、関連するUnixシステムコールを使用してファイルを移動するプログラムをCに直接書くことができます。

そうでなければ「限界を求めて目標に向けて努力せよ」ということだ。正確にxargs(1)(ここではLinuxのGNUバージョン)私はあなたがより速く得ることができるかどうか疑問に思う。

答え4

mvただ組み込んだり組み込んだりできるシェルを使えば問題ないだろう。 (これはexecve()システムコールの制限なので、外部コマンドしか使用できません。)何回呼び出すかは重要ではありませんmv

zsh、、(製造方法に応じて)はこれらのシェルの一部ですbusybox shksh93そしてzsh

#! /bin/zsh -

zmodload zsh/files # makes mv and a few other file manipulation commands builtin
batch=1000
files=(files/*(N))

for ((start = 1; start <= $#files; start += batch)) {
  (( end = start + batch - 1))
  mkdir -p ${start}_${end} || exit
  mv -- $files[start,end] ${start}_${end}/ || exit
}

E2BIGexecve()制限の適用はシステム(およびそのバージョン)によって異なり、スタックサイズ制限などの要因によって異なります。通常、argv[]各文字列のサイズ(NUL終了文字を含む)と通常、これらのポインタ配列(および終了NULLポインタ)のサイズを考慮しますenvp[](したがって、引数のサイズと数によって異なります)。シェルは最後の瞬間にいくつかの環境変数を設定することもできます(たとえば、_一部のシェルは変数を実行中のコマンドのパスに設定します)。

また、実行可能ファイルの種類(ELF、スクリプト、binfmt_misc)によって異なります。たとえば、スクリプトを使用すると、通常は長いパラメータリスト(goes)を使用して2番目の操作を実行できますexecve()execve()["myscrip", "arg", NULL]["/path/to/interpreter" or "myscript" depending on system, "-<option>" if any on the shebang, "myscript", "arg"]

また、一部のコマンドは、同じパラメーター・リストといくつかの追加の環境変数を使用して別のコマンドを実行します。たとえば、その環境内で実行しますsudo cmd arg(引数リストを保持するために必要なスペースの2倍)。cmd argSUDO_COMMAND=/path/to/cmd arg

現在のLinuxカーネルバージョン、現在のシェルバージョン、および渡すことができる引数の数を最大化するために、実行したい特定のコマンドに適したアルゴリズムを考えることができますが、execve()これはもはや真ではないかもしれません。カーネル/シェル/コマンドは次のバージョンで有効です。より良いアプローチは、アプローチをとり、これらxargsすべての追加の変更または使用を説明するのに十分な余裕を可能にすることですxargs

GNUには、これを処理する方法を詳しく説明するオプションがxargsあります。--show-limits

$ getconf ARG_MAX
2097152
$ uname -rs
Linux 5.7.0-3-amd64
$ xargs --show-limits < /dev/null
Your environment variables take up 3456 bytes
POSIX upper limit on argument length (this system): 2091648
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2088192
Size of command buffer we are actually using: 131072
Maximum parallelism (--max-procs must be no greater): 2147483647

ARG_MAX私の場合は2MiBであることがわかります。xargs使える最大値だと思いましたが、2088192128KiBに制限することにしました。

次のように:

$ yes '""' | xargs -s 230000 | head -1 | wc -c
229995
$ yes '""' | strace -fe execve xargs -s 240000 | head -1 | wc -c
[...]
[pid 25598] execve("/bin/echo", ["echo", "", "", "", ...], 0x7ffe2e742bf8 /* 47 vars */) = -1 E2BIG (Argument list too long)
[pid 25599] execve("/bin/echo", ["echo", "", "", "", ...], 0x7ffe2e742bf8 /* 47 vars */) = 0
[...]
119997

239,995個の空の引数(NULで区切られた合計文字列サイズは239,995バイトなので240,000個のバッファに収まります)を渡すことに失敗したため、引数の半分を使用して再試行しました。これは少量のデータですが、これらの文字列へのポインタのリストが8倍大きいことを考慮する必要があり、これを合計すると2MiBを超えることになります。

6年前に同じ試験をしたときQ&AはこちらLinux 3.11 では、最近変更された他の動作が表示されます。これは、渡される引数の数を最大にするために正しいアルゴリズムを見つける練習が少し意味がないことを示唆しています。

ここで、平均ファイルパスサイズは32バイト、バッファは128KiBで、まだ4096個のファイル名が渡され、すべてのファイルの名前を変更または移動するコストと比較してmv開始コストは無視できます。mv

あまり保守的でないバッファサイズ(に渡されますxargs -s)ですが、少なくとも以前のバージョンのLinuxでは、すべての引数リストにまだ有効にするには、次のようにします。

$ (env | wc; getconf ARG_MAX) | awk '
  {env = $1 * 8 + $3; getline; printf "%d\n", ($0 - env) / 9 - 4096}'
228499

環境で使用されるスペースの高い推定値を計算します(出力の行数は、少なくとも私たちが渡したポインタのenvenvp[]と同じくらい大きくする必要があり、envそれぞれについて8バイトとそのサイズ(NULを含む)を計算します。)envNL))に置き換えます。値を減算してARG_MAX9で割って、空の引数リストの最悪のケースを処理し、4KiBの空き時間を追加します。

スタックサイズを4MiB以下(例:)に制限すると、limit stacksize 4Mこれはzsh次のようになります。もっとGNUのデフォルトのバッファサイズよりも保守的ですxargs(私の場合はまだ128Kであり、空の変数のリストを正しく渡していません)。

$ limit stacksize 4M
$ (env | wc; getconf ARG_MAX) | awk '
  {env = $1 * 8 + $3; getline; printf "%d\n", ($0 - env) / 9 - 4096}'
111991
$ xargs --show-limits < /dev/null |& grep actually
Maximum length of command we could actually use: 1039698
Size of command buffer we are actually using: 131072
$ yes '""' | xargs  | head -1 | wc -c
65193
$ yes '""' | xargs -s 111991 | head -1 | wc -c
111986

関連情報