Shell多进程执行脚本

 
Category: Linux-Shell

背景

要操作一个按行分割的大文件, 单进程跑比较慢, 这时候用多进程的方式来做, 需要先分割这个大文件, 然后逐进程处理, 需要保证行格式一致并且任务之间相对独立.

shell 中可以用 split 分割这个大文件, 然后脚本需要批量读取分割好的小文件用于多进程处理, shell 的具名管道和文件描述符操作可以很好地实现这一点.



[optional] split 大文件

input_file=20241120_update
input_file_line_num=$(wc -l $input_file)
split_file_num=$(($input_file_line_num / 25000)) ## 整除

output_dir=${input_file%_*}

## -n l/16:表示将文件按 行数平均分成 16 份。l 代表 "lines"。
## --numeric-suffixes=1  后缀从 01 开始
##  --suffix-length=2  后缀长度为 2, 例如 01, 02
## 最后的输出为 part-01, part-02, ...
split -n l/$split_file_num --numeric-suffixes=1 --suffix-length=2 $input_file $output_dir/part-



多进程执行脚本(模板)

shell 中所有的多进程都可以用这个模版实现, 替换需要处理的命令(script_path), 以及输入列表(input_list) 即可



#!/bin/bash

if (($# != 3)); then
    echo "Usage: $0 script_path input_list result_dir"
    exit -1
fi

script_path=$1
input_list=$2
result_dir=$3
mkdir -p $result_dir

tmp_fifofile="$$.fifo"
mkfifo $tmp_fifofile
exec 6<>$tmp_fifofile
rm $tmp_fifofile
parallel_num=50

## create process placeholders
for ((i = 0; i < $parallel_num; i++)); do
    echo
done >&6

while read -r line; do
    read -u6
    {
        ${script_path} "$line" $result_dir/result.tmprun.$!
        echo >&6
    } &
done < $input_list
wait
exec 6>&-

# post-process, summarize result
## do sth.
exit 0




已知问题与修复

  • 使用for file in $(cat $input_list)读取文件列表, 如果列表中的文件名包含空格等特殊字符, 此时会导致文件名被分隔开, 导致读取失败
  • 使用cat $input_list | while read line; do 读取文件列表, 由于管道符号|启动了子 shell 执行后台进程, 导致 *wait 不生效*(未等到全部子进程退出就开始执行后续命令)



目前方案

  • 采用 while 读取的方式, 设置 IFS 为空(仅在循环中设置, 所以不会影响全局IFS), 并且 read 加上 -r 选项, 保留文件名中的转义字符.
  • 由于需要串接脚本, 这里面的 script_path 的输入有一些要求:
    • 需要配置 shebang, 例如 python 就是 #!/usr/bin/env python3, shell #!/bin/bash
    • 文件需要可执行, chmod +x script_name
    • 输入为绝对路径(便于资源管理)





ref

  • Linux Shell 核心编程指南
    • 3.5 非常重要的 IFS
    • 4.4 常犯错误的 SubShell
    • 4.9 控制进程数量的核心技术——文件描述符和命名管道