使用 xargs 的并行 shell:在 UNIX 和 Windows 上充分利用您的 CPU 核心

Parallel Shells With xargs Unix

简介

UNIX shell 的一个特殊痛点是无法轻松调度多个并发任务,以充分利用现代系统上的 CPU 核心。本文重点关注文件压缩的示例,但许多计算密集型任务也存在此问题,例如图像/音频/媒体处理、密码破解和哈希分析、数据库 提取、转换和加载以及备份活动。当机器的大部分处理能力处于空闲状态时,等待 gzip * 在单个 CPU 核心上运行时,这令人沮丧,这是可以理解的。

这可以理解为 Research UNIX 最初十年的一个弱点,它不是在具有 SMP 的机器上开发的。Bourne shell 从第 7 版中出现时,没有任何用于内聚管理后台进程资源消耗的本机语法或控件。

实用程序已经零星地发展以执行其中一些功能。GNU 版本的 xargs 能够对分配后台进程进行一些原始控制,这在文档中进行了相当详细的讨论。虽然 GNU 对 xargs 的扩展已经扩散到许多其他实现中(特别是 BusyBox,包括 Microsoft Windows 的版本,示例如下),但它们不符合 POSIX.2 标准,并且可能在商业 UNIX 上找不到。

xargs 的历史用户会记得它是一个有用的工具,用于处理包含太多文件而无法使用 echo * 或其他通配符的目录;在这种情况下,调用 xargs 以重复批量处理一组文件,并使用单个命令。随着 xargs 超越 POSIX 发展,它承担了新的相关性,这值得探索。


为什么 POSIX.2 如此糟糕?

要清楚地了解 UNIX 中缺乏内聚作业调度的原因,需要了解这些实用程序的演变历史。

POSIX.2 定义的 shell 具有原始的作业控制功能。此功能源于一个来源,即 Bill Joy 编写的 csh,并于 1978 年首次发布,此后没有显着进步,即使在作业控制被 Korn shell 吸收之后也是如此。以下是在 bash 中实现的 [c]sh 作业管理的示例,POSIX.2 shell 仍然受其约束。在此会话中,^Z 和 ^C 表示 Control 键组合。

$ xz -9e users00.dat

^Z

[1]+  Stopped                 xz -9e users00.dat


$ bg

[1]+ xz -9e users00.dat &


$ xz -9e users01.dat

^Z

[2]+  Stopped                 xz -9e users01.dat


$ xz -9e users02.dat

^Z

[3]+  Stopped                 xz -9e users02.dat


$ jobs

[1]   Running                 xz -9e users00.dat &

[2]-  Stopped                 xz -9e users01.dat

[3]+  Stopped                 xz -9e users02.dat


$ bg 3

[3]+ xz -9e users02.dat &


$ jobs

[1]   Running                 xz -9e users00.dat &

[2]+  Stopped                 xz -9e users01.dat

[3]-  Running                 xz -9e users02.dat &


$ fg 2

xz -9e users01.dat

^C


$ jobs

[1]-  Running                 xz -9e users00.dat &

[3]+  Running                 xz -9e users02.dat &

在上面的示例中,启动了三个压缩命令,第二个被取消,其余的被推送到后台。

引发讨论,以下是此设计的明显缺陷的部分列表

  • 没有可用 CPU 的报告或分配来接管工作,因为资源变得可用。

  • 返回非零退出状态或以其他方式异常终止的失败命令未得到良好地传达。将此类情况放在失败队列中以重新运行将很有帮助。

  • 没有可用的全局系统作业调度。任何用户都可以发出后台作业,这些作业会使机器不堪重负,无论是他们自己还是与其他用户一起。

虽然 SMP 最早出现在 1962 年销售的计算机系统中,并且随着与 UNIX 诞生同年出现的 IBM System/370 的发布而得到巩固,但如此强大的机器 对于 被称为 Research UNIX 的“贫困”中的开发人员来说不可用。具有这些功能的系统在许多年后才会普遍流行。

“[UNIX] 系统不支持多处理……IBM 3033AP 处理器满足了需求,其计算能力约为单个 PDP-11/70 处理器的 15 倍。”

似乎第一个支持 SMP 的 UNIX 平台是 Sperry/UNIVAC 1100,这是 AT&T 内部端口,始于 1977 年。此端口和后来的 IBM 在 System/370 上的努力都建立在供应商提供的 OS 组件(EXEC 8TSS)之上,并且似乎不依赖于第 7 版内核中实现的一般 SMP。

“Sperry 提供的任何配置,包括多处理器配置,都可以运行 UNIX 系统。”

由于 csh 不可能在多处理机器上编写,并且 UNIX System V 之前的几年通常没有引入 SMP,因此 shell 作业控制同样无法看到多个处理器,并且未设计为利用它们。

由于 UNIX 战争,这种缺乏进展在 POSIX.2 中得到巩固,在 UNIX 战争中,这些标准是由 IBM、HP 和 DEC(以及其他公司)领导的联盟作为防御措施发布的,从而将 UNIX System V 的功能永远锁定在行业中。对于许多人来说,不允许进行超出 POSIX 的创新。

当 POSIX.2 获得批准时,所有主要参与者都已实施 SMP,但没有找到扩展超出 System V 的 POSIX.2 标准 shell 的动力。这使得 x86 服务器 NUMA 和嵌入式 big.LITTLE 在任何严格符合 POSIX 的实现中都同样没有得到充分体现。

并行发出 gzip 进程仍然是一项非平凡的任务的原因是由于编纂的防御性营销。


GNU xargs

由于 POSIX.2 shell 中缺少现代作业控制,因此可以使用一种 hack,在 GNU xargs 中提供扩展功能。其他解决方案包括 GNU parallel 和 pdsh,此处未介绍。

经典的 xargs 实用程序将标准输入和位置参数组合以 fork 命令。一个简单的 xargs 示例可能是列出一些 inode 号

$ echo /etc/passwd /etc/group | xargs stat -c '%i %n'

525008 /etc/passwd

525256 /etc/group

当处理大量超出 shell 命令行最大大小的文件时,此基本调用非常有用。以下是古代商业 UNIX 中 xargs 用于解决 shell 内存故障的示例

$ uname -a

HP-UX localhost B.10.20 A 9000/800 862741461 two-user license


$ cd /directory/with/lots/of/files


$ chmod 644 *

sh: There is not enough memory available now.


$ ls | xargs chmod 644


$ echo *

sh: There is not enough memory available now.


$ ksh


$ what /usr/bin/ksh | grep Version

        Version 11/16/88


$ echo *

ksh: no space


$ /usr/dt/bin/dtksh


$ echo ${.sh.version}

Version M-12/28/93d


$ echo *

Pid 1954 received a SIGSEGV for stack growth failure.

Possible causes: insufficient memory or swap space,

or stack size exceeded maxssiz.

Memory fault


$ /usr/old/bin/sh


$ ls *

/usr/bin/ls: arg list too long


$ ls * *

no stack space

祝你好运在手册中找到它。

POSIX xargs 存在一个问题,它不能很好地处理标准输入文件中的空格或换行符。UNIX 文件名中唯一普遍禁止的字符是正斜杠 (/)。GNU 扩展 -0 参数将文件分隔符设置为 NUL 或零字节值,这极大地简化了文件处理,并大大提高了安全性。GNU find 具有利用管道中此功能的开关。实际上,缺少 -0 的 xargs 不值得使用。

第二个主要的 GNU 扩展允许使用 -P # 参数进行并行处理。就其本身而言,这不会触发并行处理,但当与 -L 1 选项结合使用时,所有输入文件将与目标程序分开启动,仅运行分配的进程槽数量。

在启动我们的第一个并行脚本之前,请验证此程序,该程序报告 Linux 可见的处理器 CPU 核心数

$ nproc

4

此数字可能不反映物理核心,也可能反映每个核心以倍数实现的 SMT/超线程。某些命令在单个核心上实现的线程中运行时效果不佳。

现在让我们介绍一个并行压缩脚本,该脚本足够灵活,可以生成多种文件格式。它符合 POSIX 标准,并在 Debian DASH 和 BusyBox shell 下运行。

$ cat ~/ppack_lz 

#!/bin/sh


PARALLEL="$(nproc --ignore=1)"


EXT="${0##*_}"

case "$EXT" in

     bz2) CMD='bzip2 -9'                              ;;

     gz)  CMD='gzip -9'                               ;;

     lz)  CMD='lzip -9'                               ;;

     xz)  CMD='xz -9e'                                ;;

     zst) CMD='zstd --rm --single-thread --ultra -22' ;;

esac


if [ -z "$1" ]

then echo "Specify files to pack into ${EXT} files."

else for x

     do printf '%s\0' "$x"

     done | nice xargs -0 -L 1 -P "$PARALLEL" $CMD

fi

关于此示例的一些注意事项

  • 该脚本配置为使用 nproc 报告的所有 CPU,但一个除外。根据机器负载,最好手动设置此值。

  • 该脚本通过脚本文件名中下划线 (_) 后的最后字符检测要执行的压缩类型。如果脚本名为 foo_bz2,则它将执行 bzip2 处理,而不是上面 ppack_lz 选择的 lzip。

  • 指定为脚本参数的要压缩的文件将通过 for 循环在其标准输出上发出,以 NUL 分隔,以供 xargs 调度。

为了观察此脚本的运行情况,拥有一个(几乎符合 POSIX 标准的)shell 函数来搜索 ps 命令的输出很有帮助

psearch () {

  local xx_a xx_b xx_COLUMNS IFS=\|


  [ -z "$COLUMNS" ] && xx_COLUMNS=80 || xx_COLUMNS="$COLUMNS"


  ps -e -o user:7,pid:5,ppid:5,start,bsdtime,%cpu,%mem,args |

  while read xx_a

  do if [ -z "$xx_b" ]

     then printf '%s\n' "${xx_b:=$xx_a}"

     else for xx_b

          do case "$xx_a" in

               *"$xx_b"*)

                 printf '%s\n' "$(expr substr "$xx_a" 1 "$xx_COLUMNS")" ;;

             esac

          done

     fi

  done

}

在准备好该监视器后,我们可以在具有四核 CPU 的一些 WAV 文件上运行此脚本

$ ~/ppack_lz *.wav

在另一个终端中,可以看到正在调度这些命令的 xargs

$ psearch lzip

USER      PID  PPID  STARTED   TIME %CPU %MEM COMMAND

cfisher 29995 29992 16:01:49   0:00  0.0  0.0 xargs -0 -L 1 -P 3 lzip -9

cfisher 30007 29995 16:02:10   0:27  100  2.8 lzip -9 track01.cdda.wav

cfisher 30046 29995 16:02:31   0:05 97.5  1.4 lzip -9 track02.cdda.wav

cfisher 30049 29995 16:02:33   0:04  108  1.2 lzip -9 track03.cdda.wav

正如 xargs 并行 文档中概述的那样,发送 SIGUSER1 和 SIGUSER2 将相反地增加和减少 xargs 调度的并行进程数。添加立即生效,而减少将等待现有进程退出。

上面的 xargs 命令形式具有约束性,因为预定参数和 xargs 提供的参数的顺序无法调整。可以使用 POSIX -I 选项设置更细致的版本,从而实现更大的脚本灵活性,但这需要运行时生成的“元脚本”。

$ cat ~/parallel-pack_gz

#!/bin/sh


PARALLEL="$(nproc --ignore=1)"


S="$(mktemp -t PARALLEL-XXXXXX)"

trap 'rm -f "$S"' EXIT


EXT="${0##*_}"

case "$EXT" in


     7z)  printf '#!/bin/sh \n exec 7za a -bso0 -bsp0 --mx=9 "${1}.7z" "$1"' ;;


     bz2) printf '#!/bin/sh \n exec bzip2 -9 "$1"'                           ;;


     gz)  printf '#!/bin/sh \n exec gzip -9 "$1"'                            ;;


     lz)  printf '#!/bin/sh \n exec lzip -9 "$1"'                            ;;


     xz)  printf '#!/bin/sh \n exec xz -9e "$1"'                             ;;


     zst) printf '#!/bin/sh\nexec zstd --rm --single-thread --ultra -22 "$1"';;


esac > "$S"


chmod 500 "$S"


if [ -z "$1" ]

then echo "Specify files to pack into ${EXT} files."

else for x

     do printf '%s\0' "$x"

     done | nice xargs -0 -P "$PARALLEL" -Ifname "$S" fname

fi

上面,添加了对 7za 的调用,该调用包含在许多平台(Red Hat 用户可以在 EPEL 中找到它)上提供的 p7zip 包中。使用 7-zip 带有几个警告,因为该程序本身是多线程的(使用 1.25-1.5 个核心),并且内存需求增加,因此应减少并行进程的数量。此外,7-zip 具有附加到现有存档的能力(如 Info-ZIP,它旨在替代 Info-ZIP);不要调度多个 7-zip 进程以附加到同一目标文件。7-zip 的加密选项可能在避免备份介质上的 安全漏洞通知法规方面特别令人感兴趣。

虽然本文的标题“并行 shell”在上述用法中在技术上是正确的,但上面的 exec 会立即清除 shell,并且是进程表更有效的使用方式。

有了这个灵活的脚本,我们使用 pigz(多线程 gzip)对 80 个 2 GB 大小的文件(在本例中为 Oracle 数据库数据文件,其中包含随机的表和索引块)进行了基准测试。基本服务器是一台(较旧的)HP DL380 Gen8,具有 8 个可用处理器核心

$ lscpu | grep name

Model name:            Intel(R) Xeon(R) CPU E5-2609 0 @ 2.40GHz

# time pigz -9v users*

users00.dat to users00.dat.gz

users01.dat to users01.dat.gz

users02.dat to users02.dat.gz

...

users77.dat to users77.dat.gz

users78.dat to users78.dat.gz

users79.dat to users79.dat.gz


real   45m51.904s

user   335m15.939s

sys    2m11.146s

在 pigz 运行时,top 实用程序报告了以下进程 CPU 利用率

 PID USER PR NI   VIRT  RES SHR S  %CPU %MEM    TIME+ COMMAND

11162 root 20  0 617616 6864 772 S 714.2  0.0 17:58.21 pigz -9v users01.dat...

与此(理想的)基准相比,即使在 nice CPU 优先级下运行,xargs 脚本也稍快一些,在同一主机上将 PARALLEL 设置为 8

$ time ~/parallel-pack_gz users*


real   44m42.107s

user   341m18.650s

sys    2m47.379s

在 xargs 编排的并行 gzip 运行时,top 报告列出了在单独的 CPU 上调度的所有单线程进程(请注意优先级级别 30,由 nice 降低,而 pigz 为 20)

 PID USER PR NI VIRT RES SHR S  %CPU %MEM   TIME+ COMMAND

14624 root 30 10 4624 828 424 R 100.0  0.0 0:09.85 gzip -9 users00.dat

14625 root 30 10 4624 832 424 R 100.0  0.0 0:09.86 gzip -9 users01.dat

...

14630 root 30 10 4624 832 424 R  99.3  0.0 0:09.76 gzip -9 users06.dat

14631 root 30 10 4624 824 424 R  98.0  0.0 0:09.69 gzip -9 users07.dat

在这种理想情况下,文件数可以被 CPU 数均匀整除,这有助于并行 xargs 击败 pigz;添加另一个文件会导致 xargs 输掉这场比赛。

还有并行版本的 bzip2 (pbzip2)、lzip (plzip)、xz (pixz),zstd 实用程序通常是多线程的,并将利用所有 cpu 核心,但此默认设置在上面被禁用。多线程版本可能显示与使用 xargs 获得的不同性能特征。对于 7za,xargs 是提高机器利用率的明显方法。

并行 xargs 调度在旋转介质上的一个重要的 I/O 问题是碎片。虽然这在 SSD 上不是一个因素,但在传统存储上这是一个问题,如果可能,应定期解决,正如这个结果所观察到的,与 inode 号匹配

# ls -li users46.dat.lz

2684590096 -rw-r--r--. 1 oracle dba 174653599 Jan 28 13:30 users46.dat.lz


# xfs_fsr -v

...

ino=2684590096

extents before:52 after:1 DONE ino=2684590096

...

XFS 文件系统(Red Hat 及其衍生产品的本机文件系统)上的这种碎片是明显的,并且应注意定期解决文件系统上的碎片,其中存在工具来修复它(即 e4defrag、btrfs defrag)。在 ZFS 文件系统上,没有工具来解决碎片,应非常谨慎地对待并行处理,并且仅在维护充足可用空间池内的数据集上进行。

由于此碎片问题,我们放弃了并行解包,但更喜欢单线程、与压缩无关的方法

$ cat unpack

#!/bin/sh


for x

do echo "$x"

   EXT="${x##*.}"

   case "$EXT" in

        bz2) bzip2 -cd "$x" ;;

        gz)  gzip  -cd "$x" ;;

        lz)  lzip  -cd "$x" ;;

        xz)  xz    -cd "$x" ;;

        zst) zstd  -cd "$x" ;;

   esac > "$(basename "$x" ".${EXT}")"

done

最后,此技术可以在 Windows 的 BusyBox 端口上使用,并且可能在 Win32/64 平台上支持 GNU xargs 的其他 (POSIX) shell 实现上使用。BusyBox shell 不实现 nice(从脚本中删除它),BusyBox 中也不存在 nproc(手动设置 PARALLEL)。BusyBox 仅完全实现了 gzip 和 bzip2(存在 xz applet,但不实现数字质量设置)。重新调整 bzip2 更改,这是在我的笔记本电脑上的演示,使用所有 Cygwin .DLL 文件的副本进行测试

C:\Temp>busybox64 sh


C:/Temp $ time sh parallel-pack_bz2 dtest/*.dll

real    0m 58.70s

user    0m 0.00s

sys     0m 0.06s


C:/Temp $ exit


C:\Temp>dir dtest

Volume in drive C is OSDisk

Volume Serial Number is E44B-22EC


Directory of C:\Temp\dtest


02/02/2021  11:10 AM    <DIR>          .

02/02/2021  11:10 AM    <DIR>          ..

02/02/2021  11:09 AM            40,957 cygaa-1.dll.bz2

02/02/2021  11:09 AM           263,248 cygakonadi-calendar-4.dll.bz2

02/02/2021  11:09 AM           289,716 cygakonadi-contact-4.dll.bz2

. . .

02/02/2021  11:10 AM           658,119 libtcl8.6.dll.bz2

02/02/2021  11:10 AM           489,135 libtk8.6.dll.bz2

02/02/2021  11:09 AM             5,942 Xdummy.dll.bz2

            1044 File(s)    338,341,460 bytes

               2 Dir(s)  133,704,908,800 bytes free

结论

IBM 在其 UNIX 移植到 System/370 的工作中写道

UNIX……是唯一可在从单芯片微型计算机到最大的通用大型机的所有设备上运行的操作系统……这至少代表了两个数量级的功率和容量范围……UNIX 系统优雅地跨越从微型计算机到高端大型机的范围的能力是对其十多年前的初始设计及其精心演变的致敬。

与此同时,我们对(System/370 操作系统下的)作业控制感到怀旧,我们不理解这种怀旧。

虽然 Linux 可能达不到 PDP-11 的水平,但它与第 7 版在很大程度上具有相同的属性,同时在从 1970 年代的角度来看速度快得难以想象的机器上运行。但是,POSIX.2 要求我们使用许多工具停留在 1970 年代,这可能会促使用户转向工具(作业)更好的、限制较少的竞争对手。

我在 90 年代初在大学的 Encore Multimax 上开始接触 UNIX SMP,即使是那台机器的用户区也受到 POSIX.2 不合理要求的约束,这是不合理的。即使现在,也接受对现代 SMP 设计的相同限制,这在某种程度上是可憎的。

POSIX 在许多领域被认为是不可侵犯的标准。看到 SELinux 和 systemd 在小范围内超越它,这为我们克服上一代人强加给我们的限制提供了一些希望。也许显而易见的解决方案是 systemd 获得一个新的作业调度系统。虽然可能会争辩说可移植性优先于功能,但创新最终也必须超越传统。可移植性是一个有用的追求,但能力和效率也并非没有价值。

改进的作业调度系统不需要内核参与。添加到 POSIX 的基本用户空间实现可能会受到用户社区的热烈欢迎(并希望比 SIGUSR1/2 更好,用于运行时调整)。POSIX 不允许这样做,但现在是时候将过去抛在脑后了。

由于 UNIX 早期的贫困而被强迫使用晦涩难懂的实用程序进行并行脚本编写是不可接受的。对于功能强大的 shell 和各种用户区实用程序的更新的 POSIX.2 标准早就应该出台了。

在此时期之前,感谢 FSF 的横向方法。

Charles Fisher 拥有爱荷华大学的电气工程学位,并在一家财富 500 强矿业和制造公司担任系统和数据库管理员。

加载 Disqus 评论