pipe, {list; } funciona apenas com alguns programas

13

Precisa de explicações de usuários avançados para esse comportamento imprevisível:

ps -eF | { head -n 1;grep worker; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root       441     2  0     0     0   2 paź15 ?       00:00:00 [kworker/2:1H]

tudo parece ok, enquanto

ls -la / | { head -n 1;grep sbin; }

exibe apenas a saída de head

Eu pensei sobre stdout 2>&1 e não funciona nem para mim é estranho, alguma explicação ou sugestão de como lidar com isso?

    
por ast 18.10.2013 / 23:15

4 respostas

9

Eu fiz algumas investigações usando strace e parece ser devido ao modo como o programa no lado esquerdo do pipeline faz a gravação no terminal. Quando o comando ls é executado, ele grava todos os dados em um único write() . Isso faz com que head consuma todo stdin.

Por outro lado, ps grava dados em lotes, portanto, apenas o primeiro write() é consumido por head e, em seguida, existe. Chamadas posteriores para write() irão para o processo grep recém-gerado.

Isso significa que não funcionaria se o processo para o qual você está tentando grep não tivesse ocorrido no primeiro write() , pois grep não consegue ver todos os dados (ele vê até menos que apenas os dados menos a primeira linha).

Aqui está um exemplo de como tentar usar o pid 1 no meu sistema:

$ ps -eF | { head -n2; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | grep '/lib/systemd/systemd$'
root         1     0  0  1697  3768   2 Oct03 ?        00:00:03 /lib/systemd/systemd
$ ps -eF | { head -n1; grep '/lib/systemd/systemd$'; }
UID        PID  PPID  C    SZ   RSS PSR STIME TTY          TIME CMD

Seu exemplo ps -eF só funciona por acaso.

    
por 18.10.2013 / 23:43
6

Isso é causado pelo buffering no glibc. No caso de ls , a saída está em um buffer interno e, como tal, é passada apenas para head . Para o ps -eF , a saída é maior e assim, quando head for concluído, o seguinte grep obterá as partes restantes da saída (mas não toda) de ps .

Você pode se livrar dele descarregando o cachimbo - por exemplo, com sed -u (não tenho certeza se não é uma extensão GNU):

$ ls -al / | sed -u "#" | { head -n 1; grep bin; }
total 76
drwxr-xr-x   2 root root  4096 Oct  2 21:52 bin
drwxr-xr-x   2 root root  8192 Oct  3 01:54 sbin
    
por 18.10.2013 / 23:52
4

O que está acontecendo é que head -n 1 lê mais de uma linha. Para uma taxa de transferência ideal, o head lê trechos de bytes, portanto, ele pode ler 1024 bytes por vez e, em seguida, examinar esses bytes para a primeira quebra de linha. Como a quebra de linha pode ocorrer no meio desses 1024 bytes, o restante dos dados é perdido. Não pode ser colocado de volta no tubo. Então, o próximo processo que executa somente recebe bytes 1025 e on.

Seu primeiro comando é bem-sucedido porque o processo kworker é depois daquele primeiro bloco que head lê.

Para que isso funcione, head teria que ler 1 caractere por vez. Mas isso é extremamente lento, então isso não acontece. A única maneira de fazer algo assim eficientemente é ter um único processo que faça o "head" e o "grep".

Aqui estão duas maneiras de fazer isso:

echo -e '1\n2\n3\n4\n5' | perl -ne 'print if $i++ == 0 || /4/'

ou

echo -e '1\n2\n3\n4\n5' | awk '{if (NR == 1 || /4/) print }'

Existem muito mais ...

    
por 18.10.2013 / 23:58
2

Se você quer apenas a primeira ou a segunda linha, o seguinte tipo de truque funciona e evita os problemas de buffer causados pelo uso de dois diferentes comandos para ler o fluxo de saída:

$ ps -eF   | { IFS= read -r x ; echo "$x" ; grep worker; }
$ ls -la / | { IFS= read -r x ; echo "$x" ; grep sbin; }

O read está embutido no shell e não consome um buffer inteiro de entrada apenas para a saída de uma linha, então usando read deixa todo o resto da saída para o seguinte comando.

Se você quiser acentuar os problemas de buffering mostrados por seus exemplos que usam dois comandos diferentes, adicione um sleep a eles para eliminar os problemas de tempo e permitir que o comando da esquerda para gerar todo o seu saída antes dos comandos à direita tentar ler qualquer um deles:

$ ps -eF   | { sleep 5 ; head -n 1 ; grep worker; }
$ ls -la / | { sleep 5 ; head -n 1 ; grep sbin; }

Agora, ambos os exemplos acima falham da mesma maneira - o head lê um buffer inteiro da saída apenas para produzir a única linha, e que buffer não está disponível para o seguinte grep .

Você pode ver o problema de buffer ainda mais claramente usando alguns exemplos esse número as linhas de saída para que você possa dizer quais linhas estão faltando:

$ ps -eF          | cat -n | { sleep 5 ; head -n 1 ; head ; }
$ ls -la /usr/bin | cat -n | { sleep 5 ; head -n 1 ; head ; }

Uma maneira simples de ver o problema de buffer é usar seq que gera uma lista de números. Podemos facilmente dizer quais números estão faltando:

$ seq 1 100000    | { sleep 5 ; head -n 1 ; head ; }
1

1861
1862
1863
1864
1865
1866
1867
1868
1869

Minha solução truque usando o shell para ler e ecoar os trabalhos de primeira linha corretamente, mesmo com o atraso do sono adicionado:

$ seq 1 100000 | { sleep 5 ; IFS= read -r x ; echo "$x" ; head ; }
1
2
3
4
5
6
7
8
9
10
11

Abaixo está um exemplo completo mostrando os problemas de head do buffer, mostrando como head consome um buffer inteiro da saída apenas para produzir seus cinco linhas de cada vez. Esse buffer consumido não está disponível para o próximo Comando head na sequência:

$ seq 1 100000 | { sleep 5 ; head -5 ; head -5 ; head -5 ; head -5 ; }
1
2
3
4
5

1861
1862
1863
1864
499
3500
3501
3502
3503
7
5138
5139
5140
5141

Olhando para o número 1861 acima, podemos calcular o tamanho do buffer sendo usado por head contando a saída seq de 1 para 1860 :

$ seq 1 1860 | wc -c
8193

Vemos que head está armazenando em buffer lendo um 8KB completo (8 * 1024 bytes) da saída do tubo de cada vez, mesmo para produzir apenas algumas linhas de sua própria saída.

    
por 19.10.2013 / 06:20

Tags