pbn/curso/aula-08.org

1351 lines
50 KiB
Org Mode
Raw Permalink Normal View History

2025-06-02 11:48:54 -03:00
#+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/<pid>/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/<pid>/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/<pid>/fd= do processo pai após a criação do pipe;
- Observar o diretório =/proc/<pid>/fd= do processo pai antes da impressão da mensagem;
- Observar o diretório =/proc/<pid>/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 <read_end>: 0x0000000400000003
(gdb) x /1wx &read_end
0x403034 <read_end>: 0x00000003
(gdb) x /1wx &write_end
0x403038 <write_end>: 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)]]