如何计算可以作为参数传递给某些命令进行批处理的文件数量?

如何计算可以作为参数传递给某些命令进行批处理的文件数量?

例如,我的目录包含通过以下方式创建的多个文件:

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. 写了一个C程序计算传递参数的总大小。该程序的结果很接近,但留下了一些未计数的字节:

     $ ./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

您的问题似乎假设存在实际的“参数数量限制”,而实际上它是两个限制的组合:

  1. 命令行参数的字符串长度之和环境变量,包括其终止 NUL 字节。

  2. 单个命令行参数的最大字符串长度。

例如,您可以使用 200000 个单字母参数、100000 个两字母参数调用命令,但不能使用超过 128k 字节的单个参数。

假设xargs来自 GNU coreutils,xargs --show-limits </dev/null将显示您的系统上的这些限制。

在任何系统上,xargs都会不是在构建命令行时使用系统的最大限制,但会选择合理的东西(以这种方式对系统施加压力是没有意义的)。

答案3

如果真的很重要,您可以用 C 语言编写自己的batch-move程序,该程序将文件列表作为标准输入,并使用相关的 Unix 系统调用进行移动。

如果没有,“找出限制并努力达到目标”就是确切地xargs(1)(这里是 Linux 上的 GNU 版本)的作用。我非常怀疑你会变得更快。

答案4

mv只需使用内置或可以内置的shell ,就不会出现问题(这是execve()系统调用的限制,因此只能使用外部命令)。您拨打多少次也并不重要mv

zshbusybox shksh93(取决于它的构建方式)是其中一些 shell。和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[]字符串的大小envp[](包括终止 NUL 字符),通常还会考虑这些指针数组(以及终止 NULL 指针)的大小(因此它取决于参数的大小和数量)。请注意,shell 也可以在最后一刻设置一些环境变量(例如_某些 shell 设置为正在执行的命令的路径的变量)。

它还可能取决于可执行文件的类型(ELF、脚本、binfmt_misc)。例如,对于脚本,execve()最终会execve()使用通常更长的参数列表(["myscrip", "arg", NULL]变为["/path/to/interpreter" or "myscript" depending on system, "-<option>" if any on the shebang, "myscript", "arg"])执行第二次操作。

另请注意,某些命令最终会执行具有相同参数列表和可能一些额外环境变量的其他命令。例如,在其环境中sudo cmd arg运行(将保存参数列表所需的空间加倍)。cmd argSUDO_COMMAND=/path/to/cmd arg

您也许能够为当前的 Linux 内核版本、当前的 shell 版本和您想要执行的特定命令提出正确的算法,以最大化您可以传递给 的参数数量execve(),但这可能不再适用对内核/shell/命令的下一版本有效。更好的方法是采取xargs方法并给予足够的余裕来考虑所有这些额外的变化或使用xargs

GNUxargs有一个--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认为它最多可以使用2088192,但选择将其限制为 128KiB。

正如:

$ 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 年前做过同样类型的测试时这里的问答在 Linux 3.11 中,我得到了一种不同的行为,该行为最近已经发生了变化,这表明提出正确的算法来最大化要传递的参数数量的练习有点毫无意义。

在这里,平均文件路径大小为 32 字节,缓冲区为 128KiB,仍然传递了 4096 个文件名,并且与重命名/移动所有这些文件的成本相比,mv启动成本已经变得可以忽略不计。mv

对于不太保守的缓冲区大小(传递给xargs -s),但至少对于过去版本的 Linux 的任何 arg 列表仍然有效,您可以这样做:

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

我们计算环境使用的空间的高估计(输出中的行数env应该至少与envp[]我们传递给的指针数一样大env,并且我们为每个字节计算 8 个字节,加上它们的大小(包括 NUL)替换env为 NL)),减去该值ARG_MAX并除以 9 以涵盖空参数列表的最坏情况,并添加 4KiB 的松弛。

请注意,如果将堆栈大小限制为 4MiB 或以下(例如limit stacksize 4Min 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

相关内容