#+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)]]
