Nota: A resposta reflete meu próprio entendimento desses mecanismos atualizados, acumulados durante a pesquisa e leitura das respostas pelos colegas deste site e do unix.stackexchange.com , e serão atualizados com o passar do tempo. Não hesite em fazer perguntas ou sugerir melhorias nos comentários. Eu também sugiro que você tente ver como os syscalls funcionam no shell com o strace
comando Além disso, não se deixe intimidar pela noção de chamadas internas ou de syscalls - você não precisa saber ou ser capaz de usá-las para entender como o shell faz as coisas, mas elas definitivamente ajudam a entender.
TL; DR
|
pipes não estão associados a uma entrada no disco, portanto, não possuem um número de inode do sistema de arquivos em disco (mas possuem inode no sistema de arquivos virtual pipefs no espaço do kernel), mas os redirecionamentos geralmente envolvem arquivos que possuem entradas de disco e, portanto, têm o correspondente inode.
- os pipes não são
lseek()
'capazes', de modo que os comandos não podem ler alguns dados e depois retroceder, mas quando você redireciona >
ou <
geralmente é um arquivo que é um lseek()
objeto capaz, os comandos podem navegar da maneira que desejarem.
- redirecionamentos são manipulações nos descritores de arquivos, que podem ser muitos; pipes possuem apenas dois descritores de arquivo - um para o comando esquerdo e outro para o comando direito
- o redirecionamento em fluxos e tubulações padrão são armazenados em buffer.
- os tubos quase sempre envolvem bifurcação e, portanto, pares de processos estão envolvidos; redirecionamentos - nem sempre, embora nos dois casos os descritores de arquivo resultantes sejam herdados por subprocessos.
- os pipes sempre conectam descritores de arquivo (um par), redirecionamentos - use um nome de caminho ou descritores de arquivo.
- pipes são o método de comunicação entre processos, enquanto os redirecionamentos são apenas manipulações em arquivos abertos ou objetos semelhantes a arquivos
- ambos empregam
dup2()
syscalls embaixo do capô para fornecer cópias dos descritores de arquivo, onde ocorre o fluxo real de dados.
- os redirecionamentos podem ser aplicados "globalmente" com o
exec
comando interno (veja isso e isso ); portanto, se você fizer isso, exec > output.txt
todos os comandos gravarão a output.txt
partir de então. |
os pipes são aplicados apenas ao comando atual (o que significa comando simples ou subconjuntos, como seq 5 | (head -n1; head -n2)
comandos compostos.
Quando o redirecionamento é feito nos arquivos, coisas como echo "TEST" > file
e echo "TEST" >> file
usam o open()
syscall nesse arquivo ( veja também ) e obtêm o descritor de arquivos para o qual ele é passado dup2()
. Os tubos |
usam apenas pipe()
e dup2()
syscall.
No que diz respeito à execução de comandos, os pipes e o redirecionamento não passam de descritores de arquivos - objetos semelhantes a arquivos, nos quais eles podem escrever às cegas, ou manipulá-los internamente (o que pode produzir comportamentos inesperados; apt
por exemplo, tende a nem mesmo gravar no stdout se souber que há redirecionamento).
Introdução
Para entender como esses dois mecanismos diferem, é necessário entender suas propriedades essenciais, a história por trás dos dois e suas raízes na linguagem de programação C. De fato, é essencial saber o que são os descritores de arquivos e como as chamadas de sistema dup2()
e de pipe()
trabalho lseek()
. O shell é uma maneira de tornar esses mecanismos abstratos para o usuário, mas aprofundar mais do que a abstração ajuda a entender a verdadeira natureza do comportamento do shell.
As origens dos redirecionamentos e tubulações
De acordo com o artigo de Dennis Ritche, Petroglyphs proféticos , os canos se originaram de um memorando interno de 1964 de Malcolm Douglas McIlroy , na época em que estavam trabalhando no sistema operacional Multics . Citar:
Para resumir minhas preocupações mais fortes:
- Deveríamos ter algumas maneiras de conectar programas, como mangueira de jardim - enroscar em outro segmento quando se tornar necessário massagear dados de outra maneira. Este é o caminho do IO também.
O que é aparente é que, na época, os programas eram capazes de gravar no disco, mas isso era ineficiente se a saída fosse grande. Para citar a explicação de Brian Kernighan no vídeo do Unix Pipeline :
Primeiro, você não precisa escrever um grande programa maciço - você tem programas menores existentes que já podem fazer parte do trabalho ... Outro é que é possível que a quantidade de dados que você está processando não se encaixe você armazenou em um arquivo ... porque lembre-se, estamos de volta aos dias em que esses discos tinham, se você tivesse sorte, um megabyte ou dois dados ... Portanto, o pipeline nunca teve que instanciar toda a saída .
Assim, a diferença conceitual é aparente: os tubos são um mecanismo para fazer os programas conversarem entre si. Redirecionamentos - são uma maneira de escrever no arquivo no nível básico. Nos dois casos, o shell facilita essas duas coisas, mas por baixo do capô há muita coisa acontecendo.
Indo mais fundo: syscalls e funcionamento interno do shell
Começamos com a noção de descritor de arquivo . Os descritores de arquivo descrevem basicamente um arquivo aberto (seja um arquivo no disco, na memória ou no arquivo anônimo), representado por um número inteiro. Os dois fluxos de dados padrão (stdin, stdout, stderr) são descritores de arquivo 0,1 e 2, respectivamente. De onde eles vêm ? Bem, nos comandos shell, os descritores de arquivo são herdados de seus pais - shell. E é verdade em geral para todos os processos - o processo filho herda os descritores de arquivo dos pais. Para daemons , é comum fechar todos os descritores de arquivos herdados e / ou redirecionar para outros locais.
Voltar para o redirecionamento. O que é realmente? É um mecanismo que instrui o shell a preparar descritores de arquivo para o comando (porque os redirecionamentos são feitos pelo shell antes da execução do comando) e aponte-os para onde o usuário sugeriu. A definição padrão de redirecionamento de saída é
[n]>word
Que [n]
existe o número do descritor de arquivo. Quando você faz echo "Something" > /dev/null
o número 1 está implícito lá, e echo 2> /dev/null
.
Sob o capô, isso é feito duplicando o descritor de arquivo por meio de uma dup2()
chamada do sistema. Vamos levar df > /dev/null
. O shell criará um processo filho onde será df
executado, mas antes disso será aberto /dev/null
como o descritor de arquivo nº 3 e dup2(3,1)
será emitido, o que fará uma cópia do descritor de arquivo 3 e a cópia será 1. Você sabe como possui dois arquivos file1.txt
e file2.txt
, e quando você cp file1.txt file2.txt
tiver dois arquivos iguais, mas poderá manipulá-los independentemente? É o mesmo que acontece aqui. Freqüentemente, você pode ver que, antes de executar, bash
será dup(1,10)
necessário criar um descritor de arquivo de cópia nº 1, que é stdout
(e essa cópia será o arquivo nº 10), a fim de restaurá-lo posteriormente. Importante é observar que, quando você considera comandos internos(que fazem parte do próprio shell e não têm arquivo em /bin
outro local) ou comandos simples no shell não interativo , o shell não cria um processo filho.
E então temos coisas como [n]>&[m]
e [n]&<[m]
. Isso está duplicando os descritores de arquivo, que possuem o mesmo mecanismo que dup2()
apenas agora está na sintaxe do shell, convenientemente disponível para o usuário.
Uma das coisas importantes a serem observadas sobre o redirecionamento é que seu pedido não é fixo, mas é significativo na maneira como o shell interpreta o que o usuário deseja. Compare o seguinte:
# Make copy of where fd 2 points , then redirect fd 2
$ ls -l /proc/self/fd/ 3>&2 2> /dev/null
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
lrwx------ 1 runner user 64 Sep 13 00:08 3 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/29/fd
# redirect fd #2 first, then clone it
$ ls -l /proc/self/fd/ 2> /dev/null 3>&2
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
l-wx------ 1 user user 64 Sep 13 00:08 3 -> /dev/null
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/31/fd
O uso prático destes em scripts de shell pode ser versátil:
e muitos outros.
Encanamento com pipe()
edup2()
Então, como os tubos são criados? Via pipe()
syscall , que terá como entrada uma matriz (também conhecida como lista) chamada pipefd
de dois itens do tipo int
(inteiro). Esses dois números inteiros são descritores de arquivo. O pipefd[0]
final da leitura do pipe e pipefd[1]
o final da gravação. Assim df | grep 'foo'
, grep
obterá uma cópia pipefd[0]
e df
receberá uma cópia de pipefd[1]
. Mas como ? Claro, com a magia do dup2()
syscall. No df
nosso exemplo, digamos que pipefd[1]
tenha o número 4, então o shell criará um filho, faça dup2(4,1)
(lembra do meu cp
exemplo?) E depois execute execve()
para realmente executar df
. Naturalmente,df
herdará o descritor de arquivo nº 1, mas não saberá que não está mais apontando para o terminal, mas sim o nº 4, que é realmente o final de gravação do canal. Naturalmente, a mesma coisa ocorrerá, grep 'foo'
exceto com diferentes números de descritores de arquivos.
Agora, pergunta interessante: poderíamos fazer pipes que redirecionam também o FD # 2, e não apenas o FD # 1? Sim, na verdade é isso que |&
acontece no bash. O padrão POSIX requer linguagem de comandos shell para suportar df 2>&1 | grep 'foo'
sintaxe para esse fim, mas bash
faz |&
bem.
O que é importante observar é que os pipes sempre lidam com descritores de arquivos. Existe FIFO
ou pipe nomeado , que tem um nome de arquivo no disco e de você usá-lo como um arquivo de deixar, mas se comporta como um tubo. Mas os |
tipos de pipes são conhecidos como pipe anônimo - eles não têm nome de arquivo, porque na verdade são apenas dois objetos conectados. O fato de não estarmos lidando com arquivos também cria uma implicação importante: os pipes não são lseek()
capazes. Os arquivos, na memória ou no disco, são estáticos - os programas podem usar o lseek()
syscall para pular para o byte 120, depois voltar para o byte 10 e depois avançar até o final. Os pipes não são estáticos - eles são seqüenciais e, portanto, você não pode retroceder os dados que obtém deles comlseek()
. É isso que faz com que alguns programas saibam se estão lendo do arquivo ou do pipe e, portanto, podem fazer os ajustes necessários para obter um desempenho eficiente; em outras palavras, um prog
pode detectar se eu faço cat file.txt | prog
ou prog < input.txt
. Exemplo de trabalho real disso é cauda .
As outras duas propriedades muito interessantes dos pipes são que eles têm um buffer, que no Linux tem 4096 bytes , e eles realmente têm um sistema de arquivos conforme definido no código-fonte do Linux ! Eles não são simplesmente um objeto para a transmissão de dados, são eles mesmos uma estrutura de dados! De fato, como existe o sistema de arquivos pipefs, que gerencia os pipes e os FIFOs, os pipes têm um número de inode em seu respectivo sistema de arquivos:
# Stdout of ls is wired to pipe
$ ls -l /proc/self/fd/ | cat
lrwx------ 1 user user 64 Sep 13 00:02 0 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:02 1 -> pipe:[15655630]
lrwx------ 1 user user 64 Sep 13 00:02 2 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:02 3 -> /proc/22/fd
# stdin of ls is wired to pipe
$ true | ls -l /proc/self/fd/0
lr-x------ 1 user user 64 Sep 13 03:58 /proc/self/fd/0 -> 'pipe:[54741]'
No Linux, os pipes são unidirecionais, assim como o redirecionamento. Em algumas implementações do tipo Unix - existem tubos bidirecionais. Embora com a mágica do script de shell, você também possa criar tubos bidirecionais no Linux .
Veja também:
thing1 > temp_file && thing2 < temp_file
que é mais fácil usar canos. Mas por que não reutilizar o>
operador para fazer isso, por exemplo,thing1 > thing2
para comandosthing1
ething2
? Por que um operador extra|
?