Como o ato de fechar descritores de arquivos compartilhados com um processo bloqueado por gravação faz com que ele seja desbloqueado?

0

Dado vários processos, bifurcados de um processo pai com descritores de arquivos compartilhados (representando STDOUT / STDERR), se um dos processos gravar em STDOUT e exceder o buffer de ~ 64K, ele será bloqueado (conforme esperado). Ao fechar todos os seus descritores de arquivos compartilhados em outros processos, o processo desbloqueia e continua gravando no STDOUT.

Como o ato de fechar descritores de arquivos compartilhados faz com que o processo bloqueado por gravação seja desbloqueado? (Eu suponho que o buffer está sendo liberado, mas não consigo encontrar evidências disso)

Para repro, aqui estão dois scripts que configuram o estado. Meu objetivo não é resolver um problema, mas entender como o ato de fechar esses descritores faz com que o processo bloqueado continue. (ou seja, a intenção é não resolver um problema de Python ou subprocesso)

Arquivo: A.py

#!/usr/bin/env python2.6

import subprocess

if __name__ == "__main__":
   subprocess.Popen("./B.sh 70000", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
   subprocess.Popen("./B.sh 100", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
   subprocess.Popen("./B.sh 100", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)

Arquivo: B.sh

#!/usr/bin/env bash

for i in 'seq 1 $1'; do
   echo -n "#"
done

echo ""

while true; do
    echo > /dev/null
done

O subprocesso do Python 2.6.Popen é usado como um meio para estabelecer o estado. Abaixo dele, tubos e garfos (talvez não nessa ordem) o processo atual que duplica seus descritores de arquivo para cada processo bifurcado, criando uma cadeia de processos com descritores de arquivos compartilhados.

O shell script B.sh simplesmente envia os dados para STDOUT e, em seguida, faz um loop (intencionalmente sem repouso, para que você possa distinguir entre estados em execução e em estado de repouso em algo como htop).

Coloque os dois scripts no mesmo diretório de trabalho e execute o A.py para replicar o comportamento (CentOS 6.7, mas eu suspeito que qualquer versão do CentOS 6.X seja reproduzida).

Aqui está uma lista de diretórios dos descritores de arquivos de processos para demonstrar o estado compartilhado, para referência:

# Process 1: ./B.sh 70000
ls -la /proc/4144/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53061]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53061]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh

# Process 2: ./B.sh 100
ls -la /proc/4145/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53063]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53063]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh
lr-x------ 1 root root 64 Sep 28 14:18 3 -> pipe:[53061]

# Process 3: ./B.sh 100
ls -la /proc/4146/fd
total 0
lrwx------ 1 root root 64 Sep 28 14:18 0 -> /dev/pts/0
l-wx------ 1 root root 64 Sep 28 14:18 1 -> pipe:[53065]
l-wx------ 1 root root 64 Sep 28 14:24 10 -> pipe:[53065]
l-wx------ 1 root root 64 Sep 28 14:18 2 -> pipe:[53065]
lr-x------ 1 root root 64 Sep 28 14:18 255 -> /root/B.sh
lr-x------ 1 root root 64 Sep 28 14:18 3 -> pipe:[53061]
lr-x------ 1 root root 64 Sep 28 14:18 4 -> pipe:[53063]

O primeiro processo gerado (Processo 1) gera ~ > 64K de dados para STDOUT fazendo com que ele entre em um estado de suspensão (evidente via htop e anexando strace ao pid) devido a ele estar bloqueado na gravação.

O segundo e terceiro processos (Processo 2 e Processo 3 respectivamente) permanecerão em estado de execução e terão descritores de arquivo duplicados que se referem ao (s) pipe (s) estabelecido (s) como parte do Processo 1.

Mate um dos processos 2 ou 3 e o processo 1 permanece adormecido, mate ambos e o processo 1 destrava (por quê?) e entra em estado de execução.

Reinicie o teste e use gdb para anexar ao Processo 2 ou Processo 3 e, em seguida, fechar p close(#) o descritor de arquivo compartilhado com o Processo 1 e o Processo 1 permanecerá em um estado de suspensão. Anexe ao outro processo e feche o descritor compartilhado e o Processo 1 é desbloqueado e entra em estado de execução.

Assim, o ato de fechar todos os descritores compartilhados com o processo bloqueado faz com que ele seja desbloqueado. O que faz com que esse processo, anteriormente bloqueado por gravação, seja liberado nesta circunstância?

    
por unscannable 28.09.2018 / 18:56

1 resposta

0

Quando a extremidade de leitura do pipe estiver fechada, a tentativa de gravação é um erro. Isso matará o processo com o SIGPIPE. Ou se esse sinal for bloqueado, a gravação retornará imediatamente com errno == EPIPE . Isso deve explicar seu comportamento. É uma das características originais dos canais do UNIX.

Acontece quando a última referência restante para o final de leitura do pipe é fechada. Pode haver outras referências, p. de dup() .

No seu caso, você tem fork() ed um novo processo, então o processo filho começa com todos os mesmos descritores de arquivo. Para o pipe ser fechado, os descritores de arquivo do pai e da criança devem ser fechados. Observe que close() no pai não afeta os descritores de arquivos da criança (ou vice-versa).

Este é um exemplo do conceito geral de contagem de referência. O kernel mantém a contagem de quantos descritores de arquivos se referem à extremidade de leitura do pipe. Reduz a contagem em um para cada chamada close() . Se a contagem cair para zero, o kernel executa a função de limpeza apropriada. No kernel do Linux, esse é um ponteiro de função chamado .release , pois libera todos os recursos associados.

O sistema de contagem de referência é essencial para os descritores de arquivos do UNIX. Por exemplo, eu posso encontrar dup () e fork ( ) usado na pesquisa UNIX V5.

Se você quiser saber por que o SIGPIPE está bloqueado em subprocessos iniciados no python2.6, consulte o link .

Se você ficou surpreso com o fato de os FDs do canal vazarem para P2 e P3, consulte o link . Ou seja para obter um comportamento mais sensato de Popen() , você pode passar close_fds=True .

Caso contrário, se você quiser passar FDs extras específicos para P2 e P3, eu realmente quero explicitar isso usando o parâmetro pass_fds .

Eu vou assumir que você quer, caso contrário eu realmente não vejo o que este programa de exemplo deveria estar fazendo. Você está descartando os objetos de subprocesso e, em seguida, saindo. Portanto, o processo pai está fechando seus FDs de pipe, pelo menos quando sai.

Podemos reproduzir isso no shell, sem depender de detalhes que pareçam ser dependentes da versão específica do python.

$ strace -f sh -c 'cat </dev/zero | { sleep 1& sleep 2& }'
...
[pid 26477] read(0, "
$ strace -f sh -c 'cat </dev/zero | { sleep 1& sleep 2& }'
...
[pid 26477] read(0, "%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%"..., 131072) = 131072
[pid 26477] write(1, "%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%"..., 131072 <unfinished ...>
...
[pid 26480] nanosleep({tv_sec=2, tv_nsec=0},  <unfinished ...>
...
[pid 26479] nanosleep({tv_sec=1, tv_nsec=0}, NULL) = 0
...
[pid 26479] +++ exited with 0 +++
[pid 26480] <... nanosleep resumed> NULL) = 0
...
[pid 26480] +++ exited with 0 +++

[pid 26477] <... write resumed> )       = 65536
[pid 26477] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=26477, si_uid=1001} ---
[pid 26477] +++ killed by SIGPIPE +++
%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%"..., 131072) = 131072 [pid 26477] write(1, "%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%%pre%"..., 131072 <unfinished ...> ... [pid 26480] nanosleep({tv_sec=2, tv_nsec=0}, <unfinished ...> ... [pid 26479] nanosleep({tv_sec=1, tv_nsec=0}, NULL) = 0 ... [pid 26479] +++ exited with 0 +++ [pid 26480] <... nanosleep resumed> NULL) = 0 ... [pid 26480] +++ exited with 0 +++ [pid 26477] <... write resumed> ) = 65536 [pid 26477] --- SIGPIPE {si_signo=SIGPIPE, si_code=SI_USER, si_pid=26477, si_uid=1001} --- [pid 26477] +++ killed by SIGPIPE +++

Eu noto um detalhe aqui que eu não tinha pensado antes. write() está retornando que ele gravou com sucesso apenas 64K no buffer do pipe. O que aconteceria se o chamador desativasse a ação de finalização padrão do SIGPIPE? "gravações curtas" são algo que você geralmente tem que tolerar em pipes ou soquetes, tentando novamente. Por exemplo. isso pode acontecer se o processo receber um sinal não relacionado e houver uma função de manipulador configurada para esse sinal. Assim, o chamador deve continuar tentando novamente write() com os dados restantes, e essa write() chamada retornará imediatamente com errno == EPIPE .

    
por 28.09.2018 / 19:22