stdin
é descritor de arquivo 0
. Fechar um descritor de arquivo de um processo é algo que só pode ser feito ativamente pelo próprio processo. stdin é fechado quando o processo decide fechar o período.
Agora, quando o stdin de um processo é o final de leitura de um pipe, a outra extremidade do pipe pode ser aberta por um ou mais outros processos. Quando todos os descritores de arquivo para o outro lado tiverem sido fechados, a leitura desse pipe lerá os dados restantes ainda nesse pipe, mas acabará retornando nada (em vez de esperar por mais dados) significando fim de arquivo. / p>
Aplicativos como cat
, cut
, wc
... que são lidos de seus stdin geralmente saem quando isso acontece porque o papel deles é processar suas entradas até o fim até que não haja mais entrada.
Não há nenhum mecanismo mágico que faça com que os aplicativos morram quando o final de sua entrada é atingido, apenas eles decidem sair quando isso acontece.
Em:
echo foo | cat
Uma vez que echo
tenha escrito "foo\n"
, sai o que faz com que a extremidade de gravação do canal seja fechada, então o read()
feito por cat
na outra extremidade retorna 0 bytes, o que informa cat
não há mais nada para ler e, em seguida, cat
decide sair.
Em
echo foo | sleep 1
sleep
só sai depois de 1 segundo. Seu stdin se tornando um tubo fechado não tem nenhuma incidência nisso, sleep
não está nem lendo seu stdin.
É diferente no final da escrita dos pipes (ou soquetes para isso).
Quando todos os fds no final da leitura foram fechados, qualquer tentativa de escrever nos fds abertos para o fim da escrita faz com que um SIGPIPE seja enviado para o processo fazendo com que ele morra (a menos que ele ignore o sinal, caso em que o write()
falha com EPIPE
).
Mas isso só acontece quando eles tentam escrever.
Por exemplo, em:
sleep 1 | true
Mesmo que true
saia imediatamente e o final da leitura seja encerrado imediatamente, sleep
não é eliminado porque não tenta gravar em seu stdout.
Agora, sobre /proc/fd/pid/n
em vermelho na saída ls -l --color
(conforme mencionado na primeira versão da sua pergunta ), isso é apenas porque ls
faz um lstat()
no resultado de readlink()
nesse link simbólico para tentar determinar o tipo de destino do link.
Para descritores de arquivos abertos em pipes, ou sockets ou arquivos em outros namespaces ou arquivos excluídos, o resultado de readlink
não será um caminho real no sistema de arquivos, portanto, o segundo lstat()
feito por ls
falhará e ls
vai achar que é um link simbólico quebrado, e links simbólicos quebrados são renderizados em vermelho. Você conseguirá isso com qualquer fd para qualquer extremidade de qualquer tubo, seja a outra extremidade do tubo fechada ou não. Tente com ls --color=always -l /proc/self/fd | cat
, por exemplo.
Para determinar se um fd aponta para um pipe quebrado, no Linux, você pode tentar lsof
com a opção -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
Para o fd 3, o lsof não conseguiu encontrar nenhum outro processo na extremidade de leitura do pipe. Cuidado, você pode obter resultados como:
$ 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
os fds 3 e 5 ainda estão em pipes quebrados, porque não há fd no final da leitura (parece haver um bug no lsof, já que o fato de sleep
também ter seu fd 3 aberto para o cano quebrado não é refletido em todos os lugares).
Para matar um processo assim que o canal aberto em seu stdin perde seu último gravador (fica quebrado), você poderia fazer algo como:
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
}' "$@"
}
O qual procuraria por uma condição de erro no stdin (e no Linux, isso parece acontecer assim que não houver mais nenhum gravador, mesmo se houver dados no pipe) e matar o comando filho assim que isso acontecer. Por exemplo:
sleep 1 | run_under_watch sleep 2
Mataria o processo sleep 2
após 1 segundo.
Agora, geralmente isso é meio bobagem de se fazer. Isso significa que você está potencialmente matando um comando antes de ter tempo de processar o final de sua entrada. Por exemplo, em:
echo test | run_under_watch cat
Você verá que cat
às vezes é eliminado antes de ter tido tempo de saída (ou mesmo de leitura!) "test\n"
. Não há como evitar isso, nosso watcher não sabe quanto tempo o comando precisa para processar a entrada. Tudo o que podemos fazer é dar um período de carência antes do kill "TERM"
esperando que o comando leia o conteúdo deixado no pipe e faça o que ele precisa fazer com ele.