1351 lines
50 KiB
Org Mode
1351 lines
50 KiB
Org Mode
|
#+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)]]
|