在开始新的子进程之前等待子进程完成

Wait for a child process to finish before starting new child process

我必须处理十个非常大的文件。 my_profiler 处理每个文件大约需要两天时间。我可以并行化工作,以便 my_profiler 运行 分别处理每个文件,从而使用我系统的所有核心。我并行化工作的方法是 运行 同时在三个不同的终端中处理三个进程。我无法一次处理超过四个文件,或者我的系统开始变得无响应(挂起)。

我的目标是编写一个 shell 脚本,以大小为 3 的批次处理十个文件。一旦一个文件的处理完成,终端应该关闭,新文件的处理应该在另一个终端开始。作为终端,我想使用 gnome-terminal.

目前我被以下脚本所困,运行所有进程并行:

for j in $jobs
do
    gnome-terminal -- bash -c "my_profiler $j"
done

我如何才能等到 shell 脚本 运行ning 在 gnome-terminal 实例中完成?

我的第一个想法是,一旦他们的工作完成,我可能需要从旧终端发送信号。

如果我理解正确...

我认为您可以使用 wait $job 来完成作业。

举个例子。 以下脚本将启动最大值。 3 个并行的工作,在后台。 一旦这 3 个工作中的一个结束,它将开始另一个。

#!/bin/bash

THREADS='3';
FILES=$(find source_dir_path -type f -name "your files*")

for file in ${FILES}
do
 NUMPROC=$(ps -ef |grep -i [y]our_process_name| wc -l |tr -d ' ')
 while (( $NUMPROC >= 3))
 do
  sleep 60
  NUMPROC=$(ps -ef |grep -i [y]our_process_name| wc -l |tr -d ' ')
 done
 echo "Starting: " $file;
 #your file processing command below, I assume this would be:
 my_profiler $file &
done

for job in `jobs -p`
do
 wait $job
done

我不太清楚为什么你必须为每个工作开始一个新的 gnome-terminal。但是您可以将 xargs-P [1] 结合使用。 运行三个my_profiler同时并联:

echo "${jobs}" | xargs -P3 -I{} gnome-terminal --wait -e 'bash -c "my_profiler {}"'

这里重要的是用 --wait 启动 gnome-terminal 否则终端会自我妖魔化,这将导致 xargs 启动下一个进程。 --waitgnome-terminal 3.27.1 一起引入。

xargs-I{} 选项定义了一个占位符 ({}),xargs 将用 运行 命令之前的文件名替换 [=62] =][2]。在上面的示例中,xargs 扫描命令字符串 (gnome-terminal --wait -e 'bash -c "my_profiler {}"') 以查找 {} 并将找到的实例替换为来自标准输入 (echo "${jobs}" | ...) 的第一个文件。然后执行生成的字符串。 xargs 将执行此操作三次 (-P3),然后它开始等待至少一个进程完成。如果发生这种情况,xargs 将开始下一个过程。


[1]: 来自 man xargs

-P max-procs, --max-procs=max-procs

Run up to max-procs processes at a time; the default is 1. If max-procs is 0, xargs will run as many processes as possible at a time. Use the -n option or the -L option with -P; otherwise chances are that only one exec will be done. While xargs is running, you can send its process a SIGUSR1 signal to increase the number of commands to run simultaneously, or a SIGUSR2 to decrease the number. You cannot increase it above an implementation-defined limit (which is shown with --show-limits). You cannot decrease it below 1. xargs never terminates its commands; when asked to decrease, it merely waits for more than one existing command to terminate before starting another.

Please note that it is up to the called processes to properly manage parallel access to shared resources. For example, if more than one of them tries to print to stdout, the ouptut will be produced in an indeterminate order (and very likely mixed up) unless the processes collaborate in some way to prevent this. Using some kind of locking scheme is one way to prevent such problems. In general, using a locking scheme will help ensure correct output but reduce performance. If you don't want to tolerate the performance difference, simply arrange for each process to produce a separate output file (or otherwise use separate resources).

[2]: 来自 man xargs

-I replace-str

Replace occurrences of replace-str in the initial-arguments with names read from standard input. Also, unquoted blanks do not terminate input items; instead the separator is the newline character. Implies -x and -L 1.

另一种方法,因为根据进程 table 中的子字符串计算进程可能会有问题。特别是如果您在脚本中启动子流程,计数可能不可靠。 您还写道,进程 运行 持续了 2 天,因此您有时可能会遇到问题,您需要从先前的点重新启动。

你可以用稍微复杂一点的方式来做到这一点。您需要一个脚本来启动您的进程并在它们看起来仍然健康时监视它们(进程没有崩溃 --> 否则它会重新启动它们)。这需要一个初始化脚本、填充进程队列的脚本和对探查器脚本的小修改。

脚本 1:初始化进程

创建一个作业目录,每个作业一个文件,以便自动跟踪进度。如果可以毫无问题地处理所有作业,稍后将自动删除它。

#!/bin/bash
tmpdir=/tmp/
jobdir=${tmpdir}/jobs
num_jobs=3
mkdir -p ${jobdir}

i=1
for file in $jobs ; do
    ((i++))
    echo "${file}" > ${jobdir}/${i}.open
done

脚本 2:启动实际流程

#!/bin/bash
jobdir=${tmpdir}/jobs
num_jobs=3

function fill_process_queue() {
    # arg1: num_jobs
    # arg2: jobdir
    # arg3...: open jobs
    num_jobs=
    jobdir=
    shift 2
    while [[ $(ls ${jobdir}/*.running.* | wc -l) -lt ${num_jobs} -a $# -gt 0 ]] ; do
        job_file=
        shift 1
        gnome-terminal -- bash -c "my_profiler $(cat ${jobdir}/${job_file}) ${jobdir}/${job_file}"
        # now give the called job some time to
        # mark it's territory (rename the job file)
        sleep 5s
    done
}

while [[ -z $(ls ${jobdir}) ]] ; do
    # still files present, so first check if
    # all started processes are still running
    for started_job in $(ls ${jobdir}/*.running.* 2>/dev/null) ; do
        # check if the running processes are still alive
        pid= "{started_job//[0-9]\.running\.}"
        jobid= "{started_job//\.running\.[0-9]*}"
        if ! kill -0 ${pid} 2> /dev/null ; then
            # process is not running anymore
            # don't worry kill -0 doesn't harm your
            # process
            mv ${jobdir}/${started_job} ${jobdir}/${jobid}
        fi
    done
    fill_process_queue ${num_jobs} ${jobdir} ${jobdir}/*.open
    sleep 30s
done
# if the directory is empty, it will be removed automatically by rmdir, if non-empty, it remains
rmdir ${jobdir}

分析器脚本的变化

探查器脚本需要重命名作业文件,因此它在脚本的开头包含探查器脚本的 pid,并且需要在成功完成后删除该文件。文件名在作业参数之后作为额外参数传递(因此它应该是参数 2)。 这些更改如下所示:

# at the very beginning of your script
process_file=${2//\.open/}.running.$$
mv  ${process_file}

# at the very end of your script, if everything went fine
rm ${process_file}

Each file takes about 2 days to process

运行 它们在图形中 window 是最昂贵的操作。刷新终端 window 可能很昂贵,如果您的进程输出大量标准输出(如 cp -vr /bigfolder /anotherfolder),您将看到性能差异。此外,运行使用后台作业使 X 应用程序依赖于 X 服务器 - 如果您的 X 服务器崩溃,您将失去工作。这一切都与您正在尝试做的事情无关。

对于单个 运行 工作负载(运行&忘记),我会选择 xargs -Pjobs。我会添加一些 ionice nice 以使系统可用白色进程是 运行ning。进程 stdout 输出可以被丢弃,与一些添加的前缀 ex 交错。 | sed 's/^/'"${job}: "'/',保存到文件中。或者更好,| logger 重定向到系统记录器。

如果这是一份一次性工作,我会打开一个 tmuxscreen 会话,输入:

printf "%s\n" $jobs | ionice nice xargs -t -P$(nproc) sh -c 'my_profiler ""' --

并丢弃 tmuxscreen 会话供以后使用。 3天后在我的phone上设置闹钟,3天后查看。

ionice nice 将使您的系统在处理进程时以某种方式可用。 -P$(nproc) 将进程限制为核心数。如果 my_profiler 高度 I/O 依赖并且您不关心 运行 作业时的系统性能,有时建议 运行 更多的作业然后核心,因为他们无论如何都会阻止 I/O。

您可以将 | logger -p local0.info --id=$$ 添加到 xargs 之后或在子 sh shell 内部 xargs 的末尾,这样它将输出重定向到系统日志使用 local0.info 优先级和当前 shell.

的 PID 的 id

我认为更好的方法是创建一个 systemd 服务文件。创建这样的 my_profiles@.service 文件:

[Unit]
Description=Run my_profiler for %i
[Service]
# full path to my_profiler
ExecStart=/usr/bin/my_profiler %i
CPUSchedulingPolicy=batch
Nice=19
IOSchedulingClass=best-effort

使用 systemd link my_profiler@.service 将服务添加到搜索路径或将其创建为 /var/run/systemd/system 中的插入服务文件。然后用 printf "%s\n" $jobs | xargs -I{} -t systemctl start ./my_profiler@{}.service.

开始 运行ning 它

这样我就可以从 journalctl -u my_profiler@job.service 获得我需要的所有日志,并且日志永远不会填满我的磁盘 space 的 100%,因为 journalctl 会检查这一点。使用 systemd list-failedsystemd status my_profiler@job.service.

可以很容易地报告和检查错误