如何在管道中获取进程的PID

15

考虑以下简化的例子:


my_prog|awk '...' > output.csv &
my_pid="$!" #获取的是awk的PID而不是my_prog的PID
sleep 10
kill $my_pid #my_prog仍然有未被awk处理的数据在其缓冲区中,这些数据将会丢失!

在Bash中,$my_pid指的是awk的PID。然而,我需要my_prog的PID。如果我杀死了awkmy_prog不知道要刷新其输出缓冲区,数据将会丢失。那么,如何获取my_prog的PID呢?请注意,ps aux|grep my_prog将无法工作,因为可能存在多个my_prog

1
我并不真正地将管道连接到cat,这只是一个简化的示例。实际上它是一个丑陋的awk脚本,但它们都表现得一样。 - User1
你想要实现什么目标?我相信一定有更好的方法。 - msw
我现在面临的问题正是这个!这个地方真是太棒了! - Martian Puss
2
可能是重复的问题:如何在Bash中获取管道到另一个进程的进程的PID? - Jan Matějka
是的,我们也需要这样做——杀死我的程序... "kill"手册说我们可以使用进程组ID(gpid)进行杀死。因此,一种方法就是如何找到整个命令行的GPID。不幸的是,它并不与"$!"发出的PID相同。 - will
显示剩余2条评论
9个回答

12

我遇到了同样的问题。我的解决方案:

process_1 | process_2 &
PID_OF_PROCESS_2=$!
PID_OF_PROCESS_1=`jobs -p`

只需确保 process_1 是第一个后台进程即可。否则,您需要解析 jobs -l 的完整输出。


如果使用 jobs -l 命令,可以这样解析。稍后执行:PID_OF_PROCESS_1=jobs -l | grep process_1 | cut -f2 -d" " - rfranr

6

这里提供一种不需要包装器或临时文件的解决方案。这仅适用于背景管道,其输出被捕获到包含脚本的stdout之外,就像您的情况一样。假设您想要执行以下操作:

cmd1 | cmd2 | cmd3 >pipe_out &
# do something with PID of cmd2

如果bash能提供${PIPEPID[n]}就好了!我找到的替代“hack”如下:

PID=$( { cmd1 | { cmd2 0<&4 & echo $! >&3 ; } 4<&0 | cmd3 >pipe_out & } 3>&1 | head -1 )

如果需要的话,您也可以使用3>&-4<&-关闭fd 3(用于cmd*)和fd 4(用于cmd2)。如果这样做,对于cmd2,请确保在将fd 0重定向到它之后再仅关闭fd 4。

6
我使用mkfifo来显式命名管道,解决了这个问题。
步骤1:mkfifo capture 步骤2:运行此脚本

my_prog > capture &
my_pid="$!" #现在,我有了my_prog的PID!
awk '...' capture > out.csv & 
sleep 10
kill $my_pid #杀死my_prog
wait #等待awk完成。

我不喜欢使用mkfifo管理。希望有人能提供更简单的解决方案。

为什么要杀死一个你想要输出的进程? - msw
该进程是一个硬件监控程序,将一直运行直到被终止。当该进程接收到终止信号时,它会刷新其缓冲区。实际上,当测试结束(由上面的sleep语句表示)时,bash脚本将会杀死my_prog。 - User1

4

在你的命令周围添加一个shell包装器并捕获pid。 我的例子使用iostat。

将您的命令包装在shell中,并捕获该进程的pid。以iostat为例。
#!/bin/sh
echo $$ > /tmp/my.pid
exec iostat 1

Exec会用新进程替换shell,同时保留pid。

test.sh | grep avg

同时执行以下操作:

$ cat my.pid 
22754
$ ps -ef | grep iostat
userid  22754  4058  0 12:33 pts/12   00:00:00 iostat 1

因此,您可以:

sleep 10
kill `cat my.pid`

这样更优雅吗?


不需要包装器,这样只会增加不必要的复杂性。还有几种其他解决方案。 - Jan Matějka

3

通过一行代码改进@Marvin@Nils Goroll的答案,将管道中所有命令的pid提取到shell数组变量中:

# run some command
ls -l | rev | sort > /dev/null &

# collect pids
pids=(`jobs -l % | egrep -o '^(\[[0-9]+\]\+|    ) [ 0-9]{5} ' | sed -e 's/^[^ ]* \+//' -e 's! $!!'`)

# use them for something
echo pid of ls -l: ${pids[0]}
echo pid of rev: ${pids[1]}
echo pid of sort: ${pids[2]}
echo pid of first command e.g. ls -l: $pids
echo pid of last command e.g. sort: ${pids[-1]}

# wait for last command in pipe to finish
wait ${pids[-1]}

在我的解决方案中,${pids[-1]} 包含通常在 $! 中可用的值。请注意使用 jobs -l % 输出仅“当前”作业,默认情况下是最后启动的作业。
示例输出:
pid of ls -l: 2725
pid of rev: 2726
pid of sort: 2727
pid of first command e.g. ls -l: 2725
pid of last command e.g. sort: 2727

更新于2017年11月13日: 改进了pids=...命令,更适用于复杂(多行)命令。


2

受@Demosthenex答案启发:使用子shell:

$ ( echo $BASHPID > pid1; exec vmstat 1 5 ) | tail -1 & 
[1] 17371
$ cat pid1
17370
$ pgrep -fl vmstat
17370 vmstat 1 5

2
基于您的评论,我仍然无法理解为什么您更喜欢杀死my_prog而不是让它有序完成。在多处理系统中,十秒钟是非常任意的测量标准,my_prog的输出行数可能取决于系统负载而生成10k行或0行。
如果您想将my_prog的输出限制为更确定的内容,请尝试:
my_prog | head -1000 | awk

不需要从shell中分离。最糟糕的情况是head将关闭其输入并且my_prog将收到一个SIGPIPE信号。在最好的情况下,更改my_prog以使您获得所需的输出量。

针对评论添加

只要您可以控制my_prog,就给它一个可选的-s duration参数。然后在主循环中的某个位置放置谓词:

if (duration_exceeded()) {
    exit(0);
}

在此,退出将依次正确刷新输出文件。如果情况危急且没有地方放置谓词,则可以使用alarm(3)来实现,但我故意不显示它,因为它很糟糕。

你的问题的核心是my_prog永远运行。这里的所有其他内容都是为了解决这个限制而进行的黑客攻击。


1
请看我的回答中的评论。我想我本可以在原问题中提供更多细节。上面的解决方案可能适用于某些情况,但这种情况有点不同。非常感谢你迄今为止所提供的所有帮助。我希望你能告诉我比我的答案更简单的解决方案。 - User1

1
我的解决方案是查询jobs并使用perl进行解析。
在后台启动两个流水线:
$ sleep 600 | sleep 600 |sleep 600 |sleep 600 |sleep 600 &
$ sleep 600 | sleep 600 |sleep 600 |sleep 600 |sleep 600 &

查询后台作业:
$ jobs
[1]-  Running                 sleep 600 | sleep 600 | sleep 600 | sleep 600 | sleep 600 &
[2]+  Running                 sleep 600 | sleep 600 | sleep 600 | sleep 600 | sleep 600 &

$ jobs -l
[1]-  6108 Running                 sleep 600
      6109                       | sleep 600
      6110                       | sleep 600
      6111                       | sleep 600
      6112                       | sleep 600 &
[2]+  6114 Running                 sleep 600
      6115                       | sleep 600
      6116                       | sleep 600
      6117                       | sleep 600
      6118                       | sleep 600 &

解析第二个工作 %2 的作业列表。解析可能会出错,但在这些情况下它能够工作。我们的目标是捕获跟随空格的第一个数字。它被存储为数组变量pids,使用括号:

$ pids=($(jobs -l %2 | perl -pe '/(\d+) /; $_=$1 . "\n"'))
$ echo $pids
6114
$ echo ${pids[*]}
6114 6115 6116 6117 6118
$ echo ${pids[2]}
6116
$ echo ${pids[4]}
6118

对于第一个流水线:
$ pids=($(jobs -l %1 | perl -pe '/(\d+) /; $_=$1 . "\n"'))
$ echo ${pids[2]}
6110
$ echo ${pids[4]}
6112

我们可以将这个内容封装成一个小的别名/函数:

function pipeid() { jobs -l ${1:-%%} | perl -pe '/(\d+) /; $_=$1 . "\n"'; }
$ pids=($(pipeid))     # PIDs of last job
$ pids=($(pipeid %1))  # PIDs of first job

我已在bashzsh中进行了测试。不幸的是,在bash中,我无法将pipeid的输出导入到另一个命令中。可能是因为该管道在子shell中运行,无法查询作业列表?

0
我曾经拼命寻找一个好的解决方案来获取管道作业中的所有PID,但是一个有希望的方法失败了(请参见此答案的先前修订)。
因此,不幸的是,我能想到的最好的方法就是使用GNU awk解析“jobs -l”输出:
function last_job_pids {
    if [[ -z "${1}" ]] ; then
        return
    fi

    jobs -l | awk '
        /^\[/ { delete pids; pids[$2]=$2; seen=1; next; }
        // { if (seen) { pids[$1]=$1; } }
        END { for (p in pids) print p; }'
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接