我正在用一个程序启动一个进程。我希望进程在程序终止时终止,因为它失去了标准输入。
我终止了程序,然后去proc/pid/fd查看该进程,发现它的stdin仍然链接到/dev/pts/2。
在这种情况下为什么进程不会关闭?更好的是,是否有一个包装器或技术可以用来确保程序在其标准输入管道关闭时关闭?
答案1
stdin
是文件描述符0
。关闭进程的文件描述符只能由进程本身主动完成。当进程决定关闭它时,stdin 就会关闭。
现在,当进程的 stdin 是管道的读取端时,管道的另一端可以由一个或多个其他进程打开。当另一端的所有文件描述符都已关闭时,从该管道读取将读取仍在该管道中的剩余数据,但最终不会返回任何内容(而不是等待更多数据),这意味着文件结束。
当发生这种情况时,像cat
、cut
、wc
... 这样从标准输入读取的应用程序通常会退出,因为它们的作用是处理输入直到最后,直到没有更多的输入。
没有什么神奇的机制会导致应用程序在输入结束时死亡,只有它们在发生这种情况时决定退出。
在:
echo foo | cat
一旦echo
write "foo\n"
,它就会退出,这会导致管道的写入端关闭,然后另一端read()
的 did by返回 0 字节,这表明没有更多内容可读取,然后决定退出。cat
cat
cat
在
echo foo | sleep 1
sleep
仅在 1 秒过去后退出。它的标准输入成为一个封闭的管道与此无关,sleep
甚至没有从它的标准输入中读取。
它在管道(或套接字)的写入端有所不同。
当读取端的所有 fd 都已关闭时,任何对写入端打开的 fd 进行写入的尝试都会导致向进程发送 SIGPIPE,导致进程死亡(除非它忽略该信号,在这种情况下会write()
失败并显示EPIPE
) 。
但这只有当他们尝试写作时才会发生。
例如,在:
sleep 1 | true
即使true
立即退出并且读取端立即关闭,sleep
也不会被杀死,因为它不会尝试写入其标准输出。
现在,关于/proc/fd/pid/n
在输出中显示为红色ls -l --color
(如你的问题的第一个版本),这只是因为对该符号链接的结果ls
执行 a来尝试确定链接目标的类型。lstat()
readlink()
对于在管道、套接字或其他命名空间中的文件或已删除文件上打开的文件描述符,其结果readlink
将不是文件系统上的实际路径,因此第二个lstat()
完成的操作ls
将失败,并ls
会认为这是一个损坏的符号链接,并且损坏了符号链接呈现为红色。无论管道的另一端是否关闭,您都可以通过任何管道任意一端的 fd 获得该结果。ls --color=always -l /proc/self/fd | cat
例如尝试一下。
要确定 fd 是否指向损坏的管道,在 Linux 上,您可以尝试lsof
使用该-E
选项。
$ exec 3> >(:) 4> >(sleep 999)
$ lsof -ad3-4 -Ep "$$"
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
zsh 13155 stephane 3w FIFO 0,10 0t0 5322414 pipe
zsh 13155 stephane 4w FIFO 0,10 0t0 5323312 pipe 392,sleep,0r
对于 fd 3,lsof 无法在管道的读取端找到任何其他进程。但请注意,您可能会得到如下输出:
$ exec 5<&3
$ lsof -ad3-5 -Ep "$$"
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
zsh 13155 stephane 3w FIFO 0,10 0t0 5322414 pipe 13155,zsh,5w
zsh 13155 stephane 4w FIFO 0,10 0t0 5323312 pipe 392,sleep,0r
zsh 13155 stephane 5w FIFO 0,10 0t0 5322414 pipe 392,sleep,3w 13155,zsh,3w
fds 3 和 5 仍然是损坏的管道,因为没有 fd 到读取端(lsof 似乎有一个错误,因为它sleep
的 fd 3 也打开到损坏的管道这一事实并没有在任何地方反映出来)。
要在标准输入上打开的管道失去最后一个写入器(损坏)后立即终止进程,您可以执行以下操作:
run_under_watch() {
perl -MIO::Poll -e '
if ($pid = fork) {
$SIG{CHLD} = sub {
wait;
exit($? & 127 ? ($? & 127) + 128 : $? >> 8);
};
$p = IO::Poll->new; $p->mask(STDIN, POLLERR); $p->poll;
kill "TERM", $pid;
sleep 1;
kill "KILL", $pid;
exit(1);
} else {
exec @ARGV
}' "$@"
}
它会监视 stdin 上的错误情况(在 Linux 上,只要没有编写器离开,即使管道中还有数据,这种情况似乎就会发生),并在发生时立即终止子命令。例如:
sleep 1 | run_under_watch sleep 2
sleep 2
1秒后将终止该进程。
一般来说,这样做有点愚蠢。这意味着您可能会在命令有时间处理其输入末尾之前终止该命令。例如,在:
echo test | run_under_watch cat
您会发现cat
有时在它有时间输出(甚至读取!)之前就被杀死了"test\n"
。没有办法解决这个问题,我们的观察者无法知道命令需要多少时间来处理输入。我们所能做的就是在kill "TERM"
希望命令足以读取管道中剩余的内容并执行它需要执行的操作之前给出一个宽限期。