标准输入关闭时进程未关闭

Tur*_*bit 6 process pipe

我正在用一个程序开始一个过程。我希望进程在程序执行时终止,因为它丢失了标准输入。

我终止了程序,然后去进程的proc/pid/fd,发现它的stdin仍然链接到/dev/pts/2。

为什么在这种情况下进程不会关闭?更好的是,是否有我可以使用的包装器或技术来确保程序在其标准输入管道关闭时关闭?

Sté*_*las 24

stdin是文件描述符0。关闭进程的文件描述符是只能由进程本身主动完成的事情。当进程决定关闭它时,stdin 被关闭。

现在,当进程的 stdin 是管道的读取端时,管道的另一端可以被一个或多个其他进程打开。当另一端的所有文件描述符都已关闭时,从该管道读取将读取仍在该管道中的剩余数据,但最终将不返回任何内容(而不是等待更多数据),这意味着文件结束。

cat, cut, wc... 这样从其标准输入读取的应用程序通常会在发生这种情况时退出,因为它们的作用是将输入处理到最后,直到没有更多输入为止。

没有什么神奇的机制会导致应用程序在输入结束时死亡,只有他们决定在发生这种情况时退出。

在:

echo foo | cat
Run Code Online (Sandbox Code Playgroud)

一旦echowrite "foo\n",它退出,这会导致管道的写入端关闭,然后另一端的read()done bycat返回 0 字节,这cat表明没有更多可读取的内容,然后cat决定退出。

echo foo | sleep 1
Run Code Online (Sandbox Code Playgroud)

sleep仅在 1 秒后退出。它的标准输入变成了一个封闭的管道,这sleep与它无关,甚至没有从它的标准输入中读取。

它在管道(或套接字)的写入端有所不同。

当读取端的所有 fds 都已关闭时,任何在写入端打开的 fds 上写入的尝试都会导致 SIGPIPE 被发送到导致它死亡的进程(除非它忽略信号,在这种情况下write()失败EPIPE) .

但这只有在他们尝试写作时才会发生。

例如,在:

sleep 1 | true
Run Code Online (Sandbox Code Playgroud)

即使立即true退出并立即关闭读取端,sleep也不会被杀死,因为它不会尝试写入其标准输出。


现在,关于/proc/fd/pid/nls -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
Run Code Online (Sandbox Code Playgroud)

对于 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
Run Code Online (Sandbox Code Playgroud)

fds 3 和 5 仍然指向损坏的管道,因为读取端没有 fd(lsof 中似乎有一个错误,因为sleep它的 fd 3 也对损坏的管道开放的事实并未在任何地方反映出来)。


要在其 stdin 上打开的管道丢失其最后一个写入器(损坏)时立即终止进程,您可以执行以下操作:

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
     }' "$@"
 }
Run Code Online (Sandbox Code Playgroud)

这将监视 stdin 上的错误情况(在 Linux 上,一旦没有写入器,即使管道中有数据,这似乎也会发生)并在发生时立即终止子命令。例如:

 sleep 1 | run_under_watch sleep 2
Run Code Online (Sandbox Code Playgroud)

sleep 2在 1 秒后终止进程。

现在,通常这样做有点愚蠢。这意味着您可能会在命令有时间处理其输入结束之前终止该命令。例如,在:

 echo test | run_under_watch cat
Run Code Online (Sandbox Code Playgroud)

你会发现它cat有时在它有时间输出(甚至阅读!)之前就被杀死了"test\n"。没有办法解决这个问题,我们的观察者无法知道命令需要多少时间来处理输入。我们所能做的就是在kill "TERM"希望它足以让命令读取管道中剩余的内容并做它需要做的事情之前给出一个宽限期。

  • 另一个有用的例子是`sort`,它甚至不能*开始*产生输出,直到它读取*所有*其输入之后,它可能必须在这之间做大量的工作。 (4认同)