背景
要操作一个按行分割的大文件, 单进程跑比较慢, 这时候用多进程的方式来做, 需要先分割这个大文件, 然后逐进程处理, 需要保证行格式一致并且任务之间相对独立.
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 - 输入为绝对路径(便于资源管理)
- 需要配置 shebang, 例如 python 就是
ref
-
Linux Shell 核心编程指南- 3.5
非常重要的 IFS - 4.4
常犯错误的 SubShell - 4.9
控制进程数量的核心技术——文件描述符和命名管道
- 3.5