#+title: 8 -- Fluxos de dados #+author: Blau Araujo #+email: cursos@blauaraujo.com #+options: toc:3 * Objetivos - Compreender o conceito de fluxos de dados. - Entender a relação entre descritores de arquivos e /streams/. - Demonstrar como os mecanismos de pipe e redirecionamento são implementados em baixo nível. - Introduzir a criação de programas em baixo nível que recebem dados interativamente. - Aprender a diferenciar a origem de um fluxo de entrada de dados. * Tabela de descritores de arquivos Um /fluxo de dados/ é a abstração, em software, de todas as possíveis combinações de trocas de dados entre arquivos e processos no espaço de usuário, como a leitura dos dados digitados no teclado do terminal, a impressão (ou exibição) de mensagens no /display/ do terminal o acesso a um arquivo para escrita e/ou leitura de dados e até a mediação da troca de dados entre dois processos. No nível do sistema operacional, esses fluxos são estabelecidos e controlados por diversas estruturas (/structs/), associadas individualmente a cada processo, chamadas de /tabelas de descritores de arquivos/, onde cada entrada é um inteiro sem sinal chamado de /descritor de arquivo/ (ou FD, de /file descriptor/). O diagrama abaixo representa, conceitualmente, como o kernel do Linux organiza os descritores de arquivos e suas estruturas internas para cada processo: #+begin_example TABELA DE DESCRIOTES DE ARQUIVOS ┌─────────────┐ ┌──────────────────┐ ┌──────────────┐ │ ├───────┤ FILE OPETRATIONS │ │ PROCESSO │ ┌──┤ STRUCT FILE ├────┐ └──────────────────┘ ├──────────────┤ │ │ ├──┐ │ ┌──────────────────┐ │ FD0 (STDIN) ├──┘ └─────────────┘ │ └──┤ INODE │ ├──────────────┤ │ └──────────────────┘ │ FD1 (STDOUT) ├── ··· │ ┌──────────────────┐ ├──────────────┤ └────┤ DIR ENTRY │ │ FD2 (STDERR) ├── ··· └──────────────────┘ ├──────────────┤ ··· │ ··· ├── ··· └──────────────┘ #+end_example Nele, nós podemos ver que cada entrada da tabela (um descritor de arquivo) está associada a uma estrutura de dados (/struct file/), gerenciada pelo kernel, onde são encontradas diversas informações sobre um arquivo e as operações que serão realizadas com ele. ** Uma nota sobre tipos de arquivos Sistemas parecidos com o UNIX, como o GNU/Linux, trabalham com 7 tipos de arquivos: - Diretórios :: Arquivos que indexam a localização de outros arquivos. - Arquivos comuns :: Como os arquivos dos nossos textos, programas e imagens. - Ligações simbólicas :: Arquivos que representam outros arquivos. - Dispositivos caractere :: Abstração da comunicação com dispositivos de hardware sem o acúmulo de dados. - Dispositivos bloco :: Abstração da comunicação com dispositivos de hardware com acúmulo de dados. - Pipes/FIFOs :: Arquivos que interfaceiam a troca de dados entre processos. - Sockets :: Arquivos de interfaceiam o acesso a serviços. Então, quando dissermos algo como: /"dispositivo de terminal"/, ou /"ligações simbólicas"/, nós estaremos nos referindo a diferentes tipos de arquivos. ** Descritores de arquivos padrão No sistema de arquivos, já no espaço de usuário, os descritores de arquivos são disponibilizados aos processos como /ligações simbólicas/, e todo processo é iniciado com acesso a três deles -- os /descritores de arquivos padrão/: - *Descritor de arquivo =0=:* para leitura de fluxos de dados; - *Descritor de arquivo =1=:* para escrita de dados; - *Descritor de arquivo =2=:* para escrita de dados relacionados a erros. Esses descritores, assim como todos os outros criados ao longo da execução do processo, podem ser encontrados no diretório virtual =/proc//fd= e, em princípio, eles estão ligados a um /dispositivo de terminal/. Os descritores de arquivos padrão também são expostos no sistema de arquivos pelas ligações simbólicas =stdin=, =stout= e =stderr=, no diretório =/dev= (de /device/): #+begin_example :~$ ls -l /dev/std* lrwxrwxrwx 1 root root 15 mai 28 11:30 /dev/stderr -> /proc/self/fd/2 lrwxrwxrwx 1 root root 15 mai 28 11:30 /dev/stdin -> /proc/self/fd/0 lrwxrwxrwx 1 root root 15 mai 28 11:30 /dev/stdout -> /proc/self/fd/1 #+end_example Isso mostra claramente a relação entre os nomes simbólicos e os descritores de arquivos que representam: - =stdin= → Descritor de arquivo =0= - =stdout= → Descritor de arquivo =1= - =stderr= → Descritor de arquivo =2= Ainda na listagem anterior, note que as ligações apontam para os descritores de arquivos padrão de um processo chamado =self= (/"eu mesmo"/, em inglês), que é um /link mágico/ que expande o PID do processo que acessa o diretório =fd= através das ligações =/dev/std*=. Por exemplo: #+begin_example :~$ grep -E '^(Name:|Pid:)' /proc/self/status Name: grep Pid: 262881 #+end_example Isso fez com que o arquivo =status=, no diretório do processo do próprio =grep=, fosse filtrado para imprimir apenas as linhas iniciadas com =Nome:= e =Pid:=. Nós também podemos usar =self= para expandir a lista de arquivos no diretório =fd= do processo do shell em execução em um terminal: #+begin_example :~$ printf '%s\n' /proc/self/fd/* /proc/self/fd/0 /proc/self/fd/1 /proc/self/fd/2 /proc/self/fd/255 /proc/self/fd/3 #+end_example #+begin_quote No Bash, =printf= é um comando interno. #+end_quote Além dos descritores padrão (=0=, =1= e =2=), nós temos os descritores =255=, criado pelo shell para uso interno, e =3=, que é criado pelo kernel para resolver o caminho expandido por =self=, como podemos demonstrar listando os descritores do processo do =ls= utilizado para listar seu próprio diretório =fd=: #+begin_example :~$ ls -l /proc/self/fd total 0 lr-x------ 1 blau blau 64 mai 30 07:16 3 -> /proc/263784/fd lrwx------ 1 blau blau 64 mai 30 07:16 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 07:16 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 07:16 2 -> /dev/pts/0 #+end_example Sabendo o número do PID, nós podemos listar o conteúdo do diretório =fd= associado ao processo sem usar =fd=. Por exemplo, nós podemos descobrir o PID do processo do shell em execução no terminal com: #+begin_example :~$ echo $$ 252271 #+end_example Em seguida, nós utilizamos o PID encontrado: #+begin_example :~$ ls -l /proc/252271/fd total 0 lrwx------ 1 blau blau 64 mai 30 05:56 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 05:56 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 05:56 2 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 05:56 255 -> /dev/pts/0 #+end_example #+begin_quote A expansão do PID do shell também poderia ser feita diretamente na linha de comando com: =ls -l /proc/$$/fd=. #+end_quote Como podemos ver, as ligações simbólicas =0=, =1= e =2= estão presentes e apontam para o dispositivo de terminal =/dev/pts/0=. Além deles, nós também podemos ver que o shell criou um quarto descritor de arquivo (=255=) para seu uso interno, mas o descritor =3= não existe, porque nós não usamos =self=. ** Abstração em alto nível (/stream/) Acima da interface de baixo nível, a biblioteca padrão da linguagem C (=glibc=) introduz uma camada de abstração bastante conveniente por meio de /streams/ (de /fluxos/, em inglês), representadas por ponteiros do tipo =FILE *= para uma estrutura de controle que, entre outras coisas, encapsula os descritores de arquivos. Para entender a diferença entre descritores de arquivos e /streams/, imagine um arquivo como um livro numa biblioteca: - *Descritor de arquivo:* é como ter apenas o número de catálogo do livro no sistema da biblioteca -- você sabe onde o livro está, mas precisa ir até ele e manuseá-lo por conta própria. - *Stream:* é como ter um assistente que encontra o livro, traz até você, lembra em que página você parou, faz anotações enquanto você lê, e ainda avisa se houver algum problema com o conteúdo. A =glibc=, portanto, fornece diversas funções de alto nível utilizando /streams/ para abertura e fechamento de ligações com arquivos e tarefas de leitura e escrita, além de recursos como bufferização automática, verificação de erros e manipulação de arquivos. Os fluxos padrão (=stdin=, =stdout= e =stderr=) também são inicializados automaticamente como /streams/ assim que o programa é executado, apontando para os descritores =0=, =1= e =2=, respectivamente. * Uso dos fluxos de dados padrão no shell O shell utiliza o fluxo padrão de entrada (=stdin=) para ler os comandos digitados no terminal e os fluxos padrão de saída para imprimir, também no terminal, as saídas de comandos (=stdout=) e mensagens de erro (=stderr=). Os programas executados na linha de comandos, por sua vez, herdarão os descritores de arquivos do shell e também utilizarão os fluxos padrão para ler e escrever no mesmo terminal: #+begin_example :~$ ls -l /proc/self/fd total 0 lr-x------ 1 blau blau 64 mai 30 07:49 3 -> /proc/266285/fd lrwx------ 1 blau blau 64 mai 30 07:49 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 07:49 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 07:49 2 -> /dev/pts/0 :~$ ls -l /proc/$$/fd total 0 lrwx------ 1 blau blau 64 mai 30 05:56 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 05:56 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 05:56 2 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 05:56 255 -> /dev/pts/0 #+end_example Veja que o processo do programa =ls= (primeira listagem) herdou o acesso ao pseudo terminal =/dev/pts/0= de seu processo pai -- o processo do shell, na segunda listagem. Contudo, existem dois mecanismos, implementados como operadores do shell, que possibilitam alterar os arquivos associado aos descritores de arquivo padrão: /redirecionamentos/ e /pipes/. ** Mecanismo de redirecionamento Com os redirecionamentos, nós podemos indicar os arquivos que substituirão o terminal como origem e destino de fluxos de dados. Por exemplo, em vez de imprimir a saída do =ls= no terminal, nós podemos desviar o fluxo em =stdout= para o arquivo =exemplo.txt= com os operadores =>= ou =>>=. Ambos criam o arquivo de destino se ele não existir, mas lidam de formas diferentes com arquivos existentes: - *Operador de escrita em =stdout= (=>=):* trunca (apaga) o conteúdo do arquivo antes de iniciar a escrita. - *Operador de /append/ em =stdout= (=>>=):* inclui as novas linhas após o conteúdo existente no arquivo. Como o nosso arquivo ainda não existe, tanto faz o operador, portanto... #+begin_example :~$ ls -l /proc/self/fd > exemplo.txt :~$ #+end_example Nada foi impresso no terminal, mas o arquivo =exemplo.txt= foi criado e a saída do =ls= foi escrita nele: #+begin_example :~$ cat exemplo.txt total 0 lr-x------ 1 blau blau 64 mai 30 08:04 3 -> /proc/267394/fd lrwx------ 1 blau blau 64 mai 30 08:04 0 -> /dev/pts/0 l-wx------ 1 blau blau 64 mai 30 08:04 1 -> /home/blau/exemplo.txt lrwx------ 1 blau blau 64 mai 30 08:04 2 -> /dev/pts/0 #+end_example Observe que o descritor de arquivo =1= (=stdout=) estava ligado ao arquivo =/home/blau/exemplo.txt= durante a execução do =ls=. O mesmo poderia ser feito com a saída padrão de erros, mas nós teríamos que informar o número do descritor de arquivos (=2=) junto ao operador. Por exemplo, a tentativa de listar um arquivo inexistente causaria um erro e imprimiria uma mensagem no terminal via =stderr=: #+begin_example :~$ ls banana.txt ls: não foi possível acessar 'banana.txt': Arquivo ou diretório inexistente #+end_example Esta saída poderia ser redirecionada para o arquivo =exemplo.txt=: #+begin_example :~$ ls -l /proc/self/fd banana.txt 2> exemplo.txt /proc/self/fd: total 0 lr-x------ 1 blau blau 64 mai 30 08:14 3 -> /proc/268233/fd lrwx------ 1 blau blau 64 mai 30 08:14 0 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 08:14 1 -> /dev/pts/0 l-wx------ 1 blau blau 64 mai 30 08:14 2 -> /home/blau/exemplo.txt #+end_example Aqui, a listagem em =stdout= foi impressa no terminal e a mensagem de erro foi escrita no arquivo: #+begin_example :~$ cat exemplo.txt ls: não foi possível acessar 'banana.txt': Arquivo ou diretório inexistente #+end_example Na listagem impressa, aliás, nós também podemos ver que o descritor de arquivo =2= foi redirecionado para o arquivo. A entrada padrão também pode ser redirecionada para a leitura de um arquivo, mesmo que o programa não o utilize. Isso é feito com o operador de redirecionamento de leitura (=<=): #+begin_example $ ls -l /proc/self/fd < exemplo.txt total 0 lr-x------ 1 blau blau 64 mai 30 08:20 3 -> /proc/268655/fd lr-x------ 1 blau blau 64 mai 30 08:20 0 -> /home/blau/exemplo.txt lrwx------ 1 blau blau 64 mai 30 08:20 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 08:20 2 -> /dev/pts/0 #+end_example Veja que o descritor de arquivo =0= estava ligado a =exemplo.txt= durante a execução de =ls=. ** Mecanismo de pipe Enquanto o mecanismo de redirecionamento conecta processos a arquivos, o mecanismo de /pipe/ conecta o fluxo padrão de saída de um processo ao fluxo padrão de entrada de outro processo através de um arquivo do tipo /pipe/. #+begin_example ┌────────────┐ STDOUT ┌──────┐ STDIN ┌────────────┐ │ PROCESSO 1 ├─────────▶│ PIPE ├────────▶│ PROCESSO 2 │ └────────────┘ └──────┘ └────────────┘ #+end_example No shell, o mecanismo é implementado com o operador de pipe (=|=) e expressa perfeitamente, na forma de interface com o usuário, todos os princípios da Filosofia Unix. Programas como =ls= e =cat=, por exemplo, são especializados em fazer muito bem apenas uma coisa (listar diretórios e concatenar conteúdos de arquivos), e ambos são capazes de trabalhar juntos através de um fluxo de texto canalizado pelo mecanismo do /pipe/: #+begin_example :~$ ls -l /proc/self/fd /proc/$(pidof cat)/fd | cat /proc/270862/fd: total 0 lr-x------ 1 blau blau 64 mai 30 08:48 0 -> pipe:[1689012] lrwx------ 1 blau blau 64 mai 30 08:48 1 -> /dev/pts/0 lrwx------ 1 blau blau 64 mai 30 08:48 2 -> /dev/pts/0 /proc/self/fd: total 0 lr-x------ 1 blau blau 64 mai 30 08:48 3 -> /proc/270861/fd lrwx------ 1 blau blau 64 mai 30 08:48 0 -> /dev/pts/0 l-wx------ 1 blau blau 64 mai 30 08:48 1 -> pipe:[1689012] lrwx------ 1 blau blau 64 mai 30 08:48 2 -> /dev/pts/0 #+end_example Neste exemplo, o =ls= foi executado para listar o conteúdo de seu diretório =fd= e o diretório do processo do =cat= que, por estar encadeado por pipe na linha do comando, foi executado simultaneamente e seu PID foi obtido com o utilitário =pidof=. Como resultado, nós temos os dois diretórios, onde podemos ver que a entrada padrão do processo do =cat= (=FD0=) estava ligada ao mesmo arquivo pipe conectado à saída padrão do =ls= (=FD1=). ** Captura da entrada padrão Uma terceira forma de manipulação de fluxos de dados,é a possibilidade de pausar a execução do shell para ler a entrada padrão e armazenar os dados lidos em uma ou mais variáveis. Isso é feito por meio do comando =read=, implementado como um comando interno do shell: #+begin_example read [-r] VAR [VARS ...] # Sintaxe POSIX read [OPÇÕES] [VAR ...] # Sintaxe no Bash #+end_example #+begin_quote Nas versões POSIX, o =read= lê apenas a entrada padrão, mas o Bash inclui uma opção para a leitura de qualquer descritor de arquivo (opção =-u=). #+end_quote Com o =read=, é possível receber dados interativamente, ler uma linha de um arquivo por redirecionamento de leitura ou ler uma linha da saída de outro comando encadeado por pipe, mas em circunstâncias bem distintas: | Critério | Leitura interativa | Redirecionamento | Pipe | |-------------------+--------------------+------------------+--------------| | *Origem da entrada* | Terminal | Arquivo | Arquivo pipe | | *Fim da leitura* | =Enter= | =\n= ou =EOF= | =\n= ou =EOF= | | *Interrupção* | =Ctrl+D= | Não | Não | | *Processo* | Shell corrente | Shell corrente | /Subshell/ | | *Dados capturados* | Disponíveis | Disponíveis | Inexistentes | *** Origem da entrada Enquanto pipes e redirecionamentos alteram a ligação com o descritor de arquivo =0= (=stdin=), a leitura interativa utiliza a ligação original com o terminal associado ao processo corrente do shell. Em scripts, isso pode ser detectado através de vários testes, como: #+begin_example test -t 0 # Comando 'test' (POSIX e Bash) [ -t 0 ] # Comando '[' (POSIX e Bash) [[ -t 0 ]] # Comando composto '[[ ... ]]', do Bash #+end_example Nos três casos, os comandos terminam com /sucesso/ (estado =0=) se o descritor de arquivo =0= estiver ligado a um terminal. *** Fim da leitura O comando interno =read= lê apenas uma linha de dados, o que geralmente é delimitado pelo caractere de nova linha (=\n= na notação ANSI-C ou =0x0a= na tabela ASCII) ou quando não houver mais caracteres para ler (fim do arquivo). Contudo, em leituras não interativas, o Bash possibilita a alteração do caractere delimitador de linha =-d=. Numa leitura interativa, onde a linha é digitada pelo usuário no terminal, o fim da leitura acontece quando o terminal recebe o comando para enviar os caracteres lidos, o que é feito com a tecla =Enter=. *Notas:* - Por padrão, o delimitador de fim de linha é descartado. - O Bash também implementa a possibilidade de finalizar o comando após a leitura de um dado número de caracteres (opções =-n= e =-N=). *** Interrupção da leitura No modo interativo, a leitura da entrada padrão pode ser cancelada com a combinação de teclas =Ctrl+D=, que envia para o terminal o caractere ASCII =EOT= (fim de transmissão). Já em pipes e redirecionamentos, a leitura não pode ser cancelada, mas o término dos processos envolvidos pode ser forçado, por exemplo, com a combinação de teclas =Ctrl+C=, que envia para eles o sinal =SIGINT=. *** Processos envolvidos na leitura Como o =read= é um comando interno do shell, sua execução não dá início a um novo processo. Isso vale para seu uso em comandos simples, mesmo que a leitura seja feita por um redirecionamento de leitura. Entretanto, se o =read= estiver encadeado por pipe com outros comandos, haverá o início de um novo processo do shell para executá-lo (um /subshell/), o que traz consequências importantes quanto à disponibilidade dos dados capturados. *** Disponibilidade dos dados capturados A linha lida pelo =read= é separada em campos conforme os caracteres definidos na variável interna =IFS=. Por padrão, esses caracteres são o espaço e a tabulação -- o caractere de nova de linha também está em =IFS=, mas é utilizado como delimitador de linha. O que determina a quantidade de campos é a quantidade de argumentos passados para o =read= como nomes de variáveis, por exemplo: #+begin_example read # Toda a linha será atribuída à variável interna 'REPLY' read var # Toda a linha será atribuída à variável 'var' read a b c # 'a' recebe o primeiro campo, 'b' o segundo e 'c' o restante da linha #+end_example De qualquer forma, todos os dados capturados serão atribuídos a variáveis de escopo local: ou seja, disponíveis apenas para o processo do shell em que o =read= for executado. Sendo assim, quando o =read= é executado em um pipe, todas as variáveis definidas serão perdidas quando o processo do subshell em que ele for executado terminar. * Uso dos fluxos de dados padrão em programas Em baixo nível, os descritores de arquivos padrão são utilizados diretamente como argumentos de chamadas de sistema para leitura ou escrita no terminal associado à execução do processo do programa, como as chamadas =read= e =write=, quando passamos os descritores =0= e =1=, respectivamente, como argumentos. Também existem chamadas de sistema que possibilitam a escrita de programas que redirecionam os fluxos padrão para outros arquivos ou para pipes, como é o caso da chamada =dup= quando utilizada em conjunto com a chamada =open= (ou a chamada =pipe=) para trocar o descritor associado ao arquivo aberto por um dos descritores de arquivos padrão. ** Demonstrando um redirecionamento interno Por exemplo, se eu quiser que a saída do meu programa (=salve-redir.asm=) seja escrita em um arquivo, e não no terminal, eu posso redirecionar o descritor de arquivo =1= com a chamada de sistema =dup2= após criar o arquivo de destino. #+begin_quote Obviamente, essa é a demonstração de um redirecionamento para escrita em =stdout= tal como é feita pelo shell: nós não precisamos redirecionar nada nos nossos programas para escrever em arquivos. #+end_quote Arquivo: [[exemplos/08/salve-redir.asm][salve-redir.asm]] #+begin_src asm :tangle exemplos/08/salve-redir.asm ; Chamadas de sistema... %define SYS_WRITE 1 %define SYS_OPEN 2 %define SYS_CLOSE 3 %define SYS_DUP2 33 %define SYS_EXIT 60 ; Flags da chamada SYS_OPEN... %define O_WRONLY 0x1 ; apenas para escrita %define O_CREAT 0x40 ; cria se não existir %define O_TRUNC 0x200 ; trunca conteúdo existente ; Permissões do arquivo criado... %define F_MODE 0o644 ; Descritores de arquivos... %define STDOUT 1 section .rodata file db "mensagem.txt", 0 ; arquivo de destino msg db "Salve, simpatia!", 10 ; Mensagem len equ $ - msg ; Tamanho da mensagem section .text global _start _start: ; open (ARQUIVO, FLAGS, MODO) mov rax, SYS_OPEN mov rdi, file mov rsi, O_WRONLY | O_CREAT | O_TRUNC mov rdx, F_MODE syscall mov r12, rax ; salvar descritor de arquivo retornado ; dup2(ANTIGO_FD, NOVO_FD) <-- redirecionamento mov rax, SYS_DUP2 mov rdi, r12 mov rsi, STDOUT syscall ; write(FD, STRING, SIZE) mov rax, SYS_WRITE mov rdi, STDOUT mov rsi, msg mov rdx, len syscall ; close(FD) mov rax, SYS_CLOSE mov rdi, r12 syscall ; exit(STATUS) mov rax, SYS_EXIT xor rdi, rdi syscall #+end_src Montagem e execução: #+begin_example :~$ nasm -f elf64 salve-redir.asm :~$ ld -o salve-redir salve-redir.o :~$ ./salve-redir :~$ cat mensagem.txt Salve, simpatia! #+end_example ** Demonstrando um pipe interno O programa abaixo (=salve-pipe.asm=) demonstra o encadeamento por pipe entre ele mesmo e o utilitário =cat=, também utilizando os descritores de arquivos padrão, como o shell faria. A ideia é emular o comportamento do comando: #+begin_example echo 'Salve, simpatia!' | cat #+end_example Arquivo: [[exemplos/08/salve-pipe.asm][salve-pipe.asm]] #+begin_src asm :tangle exemplos/08/salve-pipe.asm ; ---------------------------------------------------------- ; Arquivo : salve-pipe.asm ; Reproduz: echo 'Salve, simpatia!' | cat ; Montagem: nasm -f elf64 salve-pipe.asm ; Ligação : ld -o salve-pipe salve-pipe.o ; ---------------------------------------------------------- ; Chamadas de sistema... ; ---------------------------------------------------------- %define SYS_WRITE 1 %define SYS_CLOSE 3 %define SYS_PIPE 22 %define SYS_DUP2 33 %define SYS_FORK 57 %define SYS_EXECVE 59 %define SYS_EXIT 60 %define SYS_WAIT 61 ; ---------------------------------------------------------- ; Parâmetros de SYS_WAIT... ; ---------------------------------------------------------- %define W_CHILD -1 ; filho de qualquer PID %define W_STATUS 0 ; ignorar o estado de término (NULL) %define W_OPTIONS 0 ; sem opções ; ---------------------------------------------------------- ; Descritores de arquivos padrão... ; ---------------------------------------------------------- %define STDIN_FD 0 %define STDOUT_FD 1 ; ---------------------------------------------------------- ; Estados de término... ; ---------------------------------------------------------- %define EXIT_SUCCESS 0 %define EXIT_ERROR 1 ; ---------------------------------------------------------- section .rodata ; ---------------------------------------------------------- ; Mensagem... msg db "Salve, simpatia!", 10 len equ $ - msg ; Argumentos de execve... comm db "/bin/cat", 0 argv dq comm, 0 envp dq 0 ; ---------------------------------------------------------- section .bss ; ---------------------------------------------------------- read_end resd 1 ; Descritor da ponta de leitura do pipe write_end resd 1 ; Descritor da ponta de escrita do pipe ; ---------------------------------------------------------- section .text ; ---------------------------------------------------------- global _start _start: ; pipe(int pipefd[]) ; retorna: pipefd[0] => read_end; pipefd[1] => write_end mov rax, SYS_PIPE mov rdi, read_end ; Retorno vai avançar em write_end syscall .parada1: ; examinar FDs retornados! ; fork() mov rax, SYS_FORK syscall test rax, rax ; rax = 0 -> processo filho jz .filho ; pula a execução do código do pai ; ---------------------------------------------------------- ; Código executado apenas no processo pai... ; ---------------------------------------------------------- .pai: ; ---------------------------------------------------------- ; dup2(write_end, STDOUT) mov rax, SYS_DUP2 mov edi, [write_end] ; FD original: write_end mov rsi, STDOUT_FD ; novo descritor de escrita syscall ; Fechar write_end e read_end... call _close_all .parada2: ;examinar /proc//fd! ; write(STDOUT, msg, len) mov rax, SYS_WRITE mov rdi, STDOUT_FD mov rsi, msg mov rdx, len syscall ; wait4(-1, NULL, 0) mov rax, SYS_WAIT mov rdi, W_CHILD mov rsi, W_STATUS mov rdx, W_OPTIONS ; exit(0) mov rax, SYS_EXIT mov rdi, EXIT_SUCCESS ; sai com sucesso se chegar aqui syscall ; ---------------------------------------------------------- ; Código executado apenas no processo filho... ; ---------------------------------------------------------- .filho: ; ---------------------------------------------------------- ; dup2(read_end, STDIN) mov rax, SYS_DUP2 mov edi, [read_end] ; descritor de leitura no pipe mov rsi, STDIN_FD ; novo descritor de leitura syscall ; Fechar write_end e read_end... call _close_all ; execve("/bin/cat", ["/bin/cat", NULL], [NULL]) mov rax, SYS_EXECVE mov rdi, comm mov rsi, argv mov rdx, envp syscall ; exit(1) mov rax, SYS_EXIT mov rdi, EXIT_ERROR ; sai com erro se execve falhar syscall ; ---------------------------------------------------------- ; Sub de fechamento dos descritores originais do pipe... ; ---------------------------------------------------------- _close_all: ; ---------------------------------------------------------- ; close(read_end) mov rax, SYS_CLOSE mov rdi, [read_end] syscall ; close(write_end) mov rax, SYS_CLOSE mov rdi, [write_end] syscall ret #+end_src Montagem e execução: #+begin_example :~$ nasm -g -felf64 salve-pipe.asm :~$ ld -o salve-pipe salve-pipe.o :~$ ./salve-pipe Salve, simpatia! #+end_example Não há como observar o que o programa faz apenas pelo seu resultado, então vamos usar o =gdb= para analisar seus efeitos em dois pontos-chave (repare que montamos o programa com a opção =-g=): - Após a criação do pipe (=.parada1:=); - Antes da impressão da mensagem (=.parada2:=). Nossos objetivos serão: - Observar os descritores de arquivos retornados para as pontas de leitura e escrita do pipe; - Observar o diretório =/proc//fd= do processo pai após a criação do pipe; - Observar o diretório =/proc//fd= do processo pai antes da impressão da mensagem; - Observar o diretório =/proc//fd= do processo do =cat= antes da impressão da mensagem. *Abertura do programa no GDB:* #+begin_example :~$ gdb ./salve-pipe Reading symbols from ./salve-pipe... (gdb) #+end_example *Definição dos pontos de parada e execução:* #+begin_example (gdb) b _start.parada1 Breakpoint 1 at 0x401011: file salve-pipe.asm, line 64. (gdb) b _start.parada2 Breakpoint 2 at 0x401035: file salve-pipe.asm, line 86. (gdb) r Starting program: /home/blau/git/pbn/curso/exemplos/08/salve-pipe Breakpoint 1, _start.parada1 () at salve-pipe.asm:64 64 mov rax, SYS_FORK (gdb) #+end_example Neste ponto, a criação do pipe acabou de ser executada: #+begin_src asm mov rax, SYS_PIPE mov rdi, read_end ; Retorno vai avançar em write_end syscall #+end_src Quando a chamada =pipe= é executada, um arquivo do tipo pipe é criado e dois descritores de arquivos são associados a ele no processo: #+begin_example ┌──────────┐ │ ├─ 0 │ ├─ 1 │ PROCESSO ├─ 2 ┌──────┐ │ │◀─── READ_END ───┤ │ │ │ │ PIPE │ │ ├─── WRITE_END ──▶│ │ └──────────┘ └──────┘ #+end_example Os números dos descritores de arquivos são dois inteiros (4 bytes cada) que, no nosso programa, são escritos a partir do endereço representado pelo rótulo =read_end=. Sendo assim, vamos examinar seus valores: #+begin_example (gdb) x /1gx &read_end 0x403034 : 0x0000000400000003 (gdb) x /1wx &read_end 0x403034 : 0x00000003 (gdb) x /1wx &write_end 0x403038 : 0x00000004 #+end_example Isso mostra que os descritores de arquivos do pipe são =3= (ponta de leitura) e =4= (ponta de escrita). Sabendo o PID do programa, nós podemos listar o conteúdo de seu diretório =fd= em outro terminal: #+begin_example :~$ pidof salve-pipe 547187 :~$ ls -l /proc/547187/fd total 0 lrwx------ 1 blau blau 64 jun 1 10:03 0 -> /dev/pts/1 lrwx------ 1 blau blau 64 jun 1 10:03 1 -> /dev/pts/1 lrwx------ 1 blau blau 64 jun 1 10:03 2 -> /dev/pts/1 lr-x------ 1 blau blau 64 jun 1 10:03 3 -> pipe:[3331719] l-wx------ 1 blau blau 64 jun 1 10:03 4 -> pipe:[3331719] #+end_example #+begin_quote Observe as permissões =r= e =w= nos dois novos descritores de arquivos. #+end_quote A partir daqui, o programa deve clonar o processo pai (chamada =fork=) em um novo processo filho, em seguida... - No processo pai: - Mover o descritor de escrita do pipe para a saída padrão; - Escrever a mensagem no pipe; - Aguardar o teŕmino do processo filho. - No processo filho: - Mover o descritor de leitura do pipe para a entrada padrão; - Executar o utilitário =cat= para ler o pipe. O efeito sobre os descritores de arquivos pode ser visto no próximo ponto de parada, definido para antes da impressão da mensagem: #+begin_example (gdb) c Continuing. [Detaching after fork from child process 549470] Breakpoint 2, _start.parada2 () at salve-pipe.asm:86 86 mov rax, SYS_WRITE #+end_example No outro terminal, nós vamos repetir a listagem de =fd=: #+begin_example :~$ ls -l /proc/547187/fd total 0 lrwx------ 1 blau blau 64 jun 1 10:03 0 -> /dev/pts/1 l-wx------ 1 blau blau 64 jun 1 10:03 1 -> pipe:[3331719] lrwx------ 1 blau blau 64 jun 1 10:03 2 -> /dev/pts/1 #+end_example Como podemos ver no processo pai, os descritores =3= e =4= não existem mais e o pipe está conectado apenas ao descritor de arquivos =1= (=stdout=). Para ver como estão os descritores do processo do =cat=, vamos utilizar =pidof= na própria linha do comando: #+begin_example :~$ ls -l /proc/$(pidof cat)/fd total 0 lr-x------ 1 blau blau 64 jun 1 10:36 0 -> 'pipe:[3331719]' lrwx------ 1 blau blau 64 jun 1 10:36 1 -> /dev/pts/1 lrwx------ 1 blau blau 64 jun 1 10:36 2 -> /dev/pts/1 #+end_example O que demonstra que o pipe está ligado ao descritor de arquivo =0= (=stdin=) e o nosso programa conseguiu reproduzir perfeitamente o funcionamento do mecanismo de pipe no shell. ** Demonstrando a leitura da entrada padrão O kernel fornece uma chamada de sistema para leitura da entrada padrão ou de descritores de arquivos: a chamada =read=. Assim como o comando de mesmo nome no shell, ao atuar sobre =stdin=, essa chamada pode receber dados de forma interativa, ler arquivos por redirecionamento ou a saída de outro comando encadeado por pipe. Contudo, diferente do comando, a chamada de sistema não se limita à leitura de uma linha. Em vez disso, ela tenta ler uma quantidade fixa de bytes e escreve os dados lidos no endereço de memória especificado. Interativamente, a leitura só é iniciada a partir do momento que o terminal envia os dados digitados para o processo -- geralmente, quando o usuário tecla =Enter=. *** Uso da chamada de sistema 'read' Na arquitetura x86_64, a chamada de sistema =read= é utilizada da seguinte forma para ler a entrada padrão: #+begin_src asm mov rax, 0 ; número da chamada de sistema read mov rdi, FD ; Descritor de arquivo (0 = entrada padrão) mov rsi, BUF ; O endereço do buffer em que os bytes serão escritos mov rdx, COUNT ; A quantidade de bytes a ler syscall ; executa a chamada de sistema #+end_src O retorno, passado por =rax=, é a quantidade de bytes lidos ou =-1= em caso de erro. *** Exemplo de uso em Assembly O exemplo abaixo (=salve-read.asm=) demonstra como podemos receber dados pela entrada padrão utilizando a chamada de sistema =read=. Arquivo: [[exemplos/08/salve-read.asm][salve-read.asm]] #+begin_src asm ; ---------------------------------------------------------- ; Arquivo : salve-read.asm ; Montagem: nasm -f elf64 salve-read.asm ; Ligação : ld -o salve-read salve-read.o ; ---------------------------------------------------------- ; Chamadas de sistema ; ---------------------------------------------------------- %define SYS_READ 0 %define SYS_WRITE 1 %define SYS_EXIT 60 ; ---------------------------------------------------------- ; Descritores de arquivos padrão ; ---------------------------------------------------------- %define STDIN_FD 0 %define STDOUT_FD 1 ; ---------------------------------------------------------- ; Estados de término ; ---------------------------------------------------------- %define EXIT_SUCCESS 0 ; ---------------------------------------------------------- ; Constantes simbólicas ; ---------------------------------------------------------- %define BUF_SIZE 256 ; ---------------------------------------------------------- section .rodata ; ---------------------------------------------------------- msg db `Salve, simpatia!\nQual é a sua graça?\n` msg_len equ $ - msg resp db "Falaê, " resp_len equ $ - resp tail db `!\n` ; ---------------------------------------------------------- section .bss ; ---------------------------------------------------------- buf resb BUF_SIZE ; Buffer de leitura count resd 1 ; retorno de read (int) ; ---------------------------------------------------------- section .text ; ---------------------------------------------------------- global _start _start: ; Imprime a mensagem inicial mov rsi, msg mov rdx, msg_len call _print ; Aguarda os dados da entrada padrão mov rax, SYS_READ mov rdi, STDIN_FD mov rsi, buf mov rdx, BUF_SIZE syscall ; Salva retorno da chamada (bytes lidos) mov [count], eax ; Imprime prefixo da resposta mov rsi, resp mov rdx, resp_len call _print ; Imprime resposta mov rsi, buf mov rdx, [count] ; total de bytes lidos dec rdx ; desconta \n call _print ; Imprime final da resposta mov rsi, tail mov rdx, 2 ; !\n = 2 bytes call _print ; Termina o programa mov rax, SYS_EXIT mov rdi, EXIT_SUCCESS syscall ; ---------------------------------------------------------- ; Sub-rotinas... ; ---------------------------------------------------------- _print: ; ---------------------------------------------------------- mov rax, SYS_WRITE mov rdi, STDOUT_FD syscall ret #+end_src *Montagem e execução:* #+begin_example :~$ nasm -f elf64 salve-read.asm :~$ ld -o salve-read salve-read.o :~$ ./salve-read Salve, simpatia! Qual é a sua graça? [Espera a digitação] #+end_example Digitando, por exemplo, meu nome: #+begin_example Blau Falaê, Blau! #+end_example *** Testes com pipe e redirecionamento O nosso exemplo também pode ler a entrada padrão se ela estiver ligada a um arquivo, por redirecionamento, ou à ponta de leitura de um pipe, como veremos nos próximos testes. *Leitura por pipe:* #+begin_example :~$ echo Fulano de Tal | ./salve-read Salve, simpatia! Qual é a sua graça? Falaê, Fulano de Tal! #+end_example *Leitura de um arquivo:* #+begin_example :~$ ./salve-read < /proc/sys/kernel/ostype Salve, simpatia! Qual é a sua graça? Falaê, Linux! #+end_example *Leitura em uma /here string/ (Bash):* #+begin_example :~$ ./salve-read <<< 'Teste 123' Salve, simpatia! Qual é a sua graça? Falaê, Teste 123! #+end_example *Leitura de várias linhas (/here doc/):* #+begin_example :~$ ./salve-read << FIM > Linha 1 > Linha 2 > Linha 3 > FIM Salve, simpatia! Qual é a sua graça? Falaê, Linha 1 Linha 2 Linha 3! #+end_example ** Identificando a origem do fluxo de leitura Para determinar se a entrada padrão está ligada ou não a um terminal, nós podemos utilizar a chamada de sistema =ioctl= (identificador =16=), que manipula diversos parâmetros internos de dispositivos especiais, como os terminais. *Argumentos da chamada de sistema =ioctl=:* #+begin_src asm mov rax, 16 ; número da chamada de sistema ioctl mov rdi, FD ; descritor de arquivo (0 = stdin) mov rsi, OP ; código da operação (TCGETS = 0x5401) mov rdx, BUF ; NULL ou buffer para receber os parâmetros solicitados syscall #+end_src O código de operação que nos interessa é =TCGETS= (=0x5401=), que é a solicitação das configurações do terminal associado ao descritor de arquivo em =rdi=. Se não houver a ligação com um terminal, a chamada termina com erro =ENOTTY= (/não é um TTY/) e retorna o inteiro =-1= em =rax=. Como só queremos testar o retorno da chamada =ioctl=, nós não precisamos de um buffer e podemos passar o valor =0= (=NULL=) para =rdx=. *Exemplo genérico de teste:* #+begin_src asm mov rax, 16 ; chamada ioctl mov rdi, 0 ; stdin mov rsi, 0x5401 ; TCGETS xor rdx, rdx ; 0 = NULL syscall cmp rax, 0 jl .noy_a_tty ; rax < 0 -> não é um terminal .is_a_tty: ; rotina se stdin for um terminal... ; ... jmp .end: .not_a_tty: ; rotina se stdin não for um terminal... ; ... .end: ; Rotinha de término... #+end_src * Criação e manipulação de fluxos de dados Criar um novo fluxo de dados tem o significado de estabelecer uma nova conexão entre um processo e um arquivo. Em alto nível, com C/C++ por exemplo, isso é feito através das abstrações da =glibc=, o que pode resultar na ligação com arquivos via /streams/ (por exemplo, com =fopen=) ou via descritores de arquivos (por exemplo, com =pipe=). Exemplos: | Função | Descrição | |-----------------+--------------------------------------------------| | =fopen= | Abre um arquivo comum | | =fclose= | Fecha um /stream/ | | =freopen= | Redireciona um =FILE *= existente | | =fdopen= | Associa um descritor de arquivo a um =FILE *= | | =popen= | Executa um comando e abre um pipe unidirecional | | =fmemopen= | Cria um =FILE *= associado a uma região da memória | | =open_memstream= | Cria um =FILE *= de escrita que escreve em memória | | =open_wmemstream= | Versão wide-char de =open_memstream= | Já em baixo nível, como vimos nos nossos exemplos, a criação de novos fluxos de dados é feita pelas chamadas de sistema do kernel e sempre resulta na ligação com arquivos via descritores de arquivos. Exemplos (Linux x86_64): | Nº | Syscall | Descrição | |-----+------------+----------------------------------------------------| | 2 | =open= | Abre um arquivo e retorna um =file descriptor= | | 3 | =close= | Fecha um descritor de arquivo | | 85 | =creat= | Cria/abre arquivo para escrita | | 41 | =dup= | Duplica um descritor de arquivo | | 33 | =dup2= | Duplica descritor para um valor específico | | 292 | =dup3= | Como =dup2=, mas com flags | | 22 | =pipe= | Cria um pipe anônimo (=int pipefd[2]=) | | 293 | =pipe2= | Versão de =pipe= com flags adicionais | | 41 | =socket= | Cria endpoint de comunicação (AF_INET, AF_UNIX...) | | 53 | =socketpair= | Cria par de sockets conectados | * Armazenamento temporário (/bufferização/) Bufferização (*buffering*) é a técnica de usar áreas intermediárias de memória chamadas *buffers* para armazenar temporariamente dados durante operações de entrada e saída, principalmente com o propósito de: - Reduzir o número de chamadas de sistema. - Adaptar diferenças de velocidade entre a produção e o consumo de dados. - Agrupar dados em blocos maiores, aumentando a eficiência da comunicação com dispositivos ou arquivos. ** Bufferização em alto nível No GNU/Linux, a =glibc= implementa três modos de bufferização: | Tipo | Descrição | |------------------------+----------------------------------------------------------------| | *Sem bufferização* | Dados são enviados diretamente, sem armazenar em buffer. | | *Bufferização por linha* | Dados são enviados quando uma nova linha é detectada (=\n=). | | *Bufferização total* | Dados são armazenados até o buffer ficar cheio ou ser liberado | Os três fluxos padrão estão associados a buffers internos mantidos pela =glibc=: - *stdin*: bufferizado totalmente. - *stdout*: bufferizado por linha se conectado a um terminal; caso contrário, totalmente. - *stderr*: sem bufferização. Esses buffers fazem parte da estrutura =FILE=, da biblioteca C padrão, que aloca uma região de memória para guardar dados temporariamente. ** Bufferização no nível do kernel Além da =glibc=, o kernel Linux também implementa buffers internos em diversos contextos: | Tipo de buffer | Onde ocorre | Finalidade principal | |---------------------+--------------------------+-----------------------------------------------| | Cachê de página | Sistema de arquivos | Acelerar leitura/escrita em disco | | Buffer de pipe | IPC (pipe) | Comunicação entre processos | | Buffer de socket | Rede (TCP/UDP) | Armazenar pacotes antes/depois da transmissão | | Disciplina de linha | Dispositivos de terminal | Controlar entrada interativa | | Buffer de blocos | Dispositivos de bloco | Otimização de leitura/escrita de disco | *** Cachê de página O subsistema de arquivos do kernel utiliza cachês de página (/page cache/) para manter dados recentemente lidos ou escritos na memória RAM. Isso evita acessos frequentes ao disco, que são muito mais lentos. Quando um processo executa =read=, o kernel verifica se os dados já estão no cache; quando um processo executa =write=, os dados vão para o cache e são escritos no disco posteriormente (/write-back/). #+begin_quote Esse mecanismo é transparente para o usuário e só afeta o desempenho. #+end_quote *** Buffers de pipes (IPC) O kernel mantém buffers internos para comunicação entre processos (IPC) via pipes, que possuem um buffer circular (/ring buffer/) para armazenar dados de forma contínua em um espaço fixo de memória (tipicamente, 64kB em sistemas modernos). Ao escrever num pipe, os dados vão para esse buffer e são consumidos quando acontece a leitura. Se o buffer estiver cheio, a escrita é bloqueada; se estiver vazio, a leitura é bloqueada. #+begin_quote Esta condição padrão é chamada de /modo bloqueante/, mas também é possível abrir arquivos em /modo não bloqueante/ se estivermos lidando com eventos simultâneos, escrevendo programas que não podem parar esperando operações de entrada e saída ou se precisarmos implementar um controle fino sobre o fluxo de dados. #+end_quote *** Buffers de sockets Comunicações via /sockets/ também contam com buffers internos no kernel. Cada socket possui um buffer de envio (/send buffer/) e um recepção (/receive buffer/) para armazenar dados enquanto o pacote é processado, retransmitido ou recebido pela outra extremidade. #+begin_quote O tamanho dos buffers pode ser consultado com a chamada de sistema =getsockopt= ou definido com a chamada =setsockopt=. #+end_quote *** Buffers de terminais (TTY) Os dispositivos de terminal possuem buffers no kernel que são manipulados pelo driver de TTY. No /modo canônico/ (também chamado de /modo linha/), os dados são acumulados até que o usuário tecle =Enter=. Além disso, o terminal é responsável pela edição a linha antes que ela seja entregue ao processo. #+begin_quote Esse buffer faz parte da /disciplina de linha/ do kernel, um componente que fica entre o driver do terminal e o processo do usuário para interpretar e gerenciar entradas e saídas de texto. #+end_quote As principais características do modo canônico, que é o modo padrão de terminais interativos, são: - Entrada bufferizada por linha. - Possibilidade de edição das linhas antes do envio. - Interpretação de caracteres de controle. No modo não canônico (modo /bruto/ ou /"caractere a caractere"/), a entrada não é bufferizada e, portanto, cada caractere digitado é disponibilizado imediatamente para o programa. Isso é muito utilizado por programas com interfaces em tela cheia (como o editor Vim, por exemplo), multiplexadores de terminal (como o Tmux e o GNU Screen) e pseudo terminais remotos (como no SSH). *** Buffers de blocos (bloco de disco) Dispositivos de bloco (como discos) usam buffers no kernel para: - Agrupar operações de escrita. - Reordenar acessos. - Sincronizar acesso com caches do sistema de arquivos. ** Bufferização em Assembly (x86_64) Em Assembly, os buffers são criados manualmente com a reserva de espaço na seção =.bss= (=resb=, =resq=, etc), utilizando a pilha ou alocando espaços dinamicamente, por exemplo, com as chamadas de sistema =brk= e =mmap=. No último exemplo (=salve-read.asm=), foi o que fizemos quando reservamos 256 bytes (valor de =BUF_SIZE=) em =.bss= para armazenar os caracteres lidos pela chamada =read=: #+begin_src asm ; ---------------------------------------------------------- section .bss ; ---------------------------------------------------------- buf resb BUF_SIZE ; Buffer de leitura count resd 1 ; retorno de read (int) #+end_src Essa mesma técnica é aplicável a programas em C, geralmente utilizando vetores de caracteres ou alocando espaço no /heap/ com funções como =malloc=, da biblioteca padrão. * Exercícios propostos 1. Altere o tamanho do buffer do programa =salve-read.asm= para =10= (bytes), observe seu novo funcionamento e responda: - O que acontece quando digitamos mais de 10 caracteres? - Como você explica o que aconteceu? 2. Ainda usando =salve-read.asm= como base, faça com que o programa só funcione no modo interativo, imprimindo uma mensagem por =stderr= e terminando com estado =1= caso esteja num pipe ou com a entrada padrão redirecionada para um arquivo. 3. Pesquise e escreva um programa em C que reproduza o funcionamento do exemplo =salve-pipe.asm=. 4. Pesquise e escreva programas em C que reproduzam o funcionamento do exemplo =salve-read.asm= e de todas as alterações feitas nos exercícios 1 e 2. * Tabela das chamadas de sistema utilizadas | Chamada | #ID | Propósito | rax | rdi | rsi | rdx | |---------+-----+-------------------------------------+-----+----------------------+----------------+-------------| | =read= | 0 | Ler um descritor de arquivo | 0 | fd (ex: STDIN=0) | buffer | quantidade | | =write= | 1 | Escrever em um descritor de arquivo | 1 | fd (ex: STDOUT=1) | buffer | tamanho | | =open= | 2 | Abre um arquivo | 2 | nome do arquivo | flags | modo | | =close= | 3 | Fechar descritor de arquivo | 3 | fd | - | - | | =ioctl= | 16 | Obter parâmetros de dispositivos | 16 | fd | operação | buffer | | =pipe= | 22 | Criar pipe (par de descritores) | 22 | ponteiro pipefd[2] | - | - | | =dup2= | 33 | Duplicar descritor de arquivo | 33 | oldfd | newfd | - | | =fork= | 57 | Criar novo processo | 57 | - | - | - | | =execve= | 59 | Executar novo programa | 59 | caminho ("/bin/cat") | argv | envp | | =exit= | 60 | Encerrar o processo | 60 | código de saída | - | - | | =wait4= | 61 | Esperar término de processo filho | 61 | pid (-1 = qualquer) | statloc = NULL | options = 0 | Retorno em =rax=. * Referências - =man 2 read= - =man 2 write= - =man 2 open= - =man 2 close= - =man 2 ioctl= - =man 2 pipe= - =man 2 dup2= - =man 2 fork= - =man 2 execve= - =man 2 wait4= - =man 2 waitpid= - =man 3 stdin= - =man 5 proc_pid_fd= - [[https://en.wikipedia.org/wiki/Pipeline_(Unix)][Wikipedia: Pipeline (Unix)]]