conteúdo da aula 10

This commit is contained in:
Blau Araujo 2025-04-23 10:28:56 -03:00
parent 559168f9d5
commit d7a8eab571

408
aulas/10-dataio/README.org Normal file
View file

@ -0,0 +1,408 @@
#+title: Curso Básico da Linguagem C
#+subtitle: Aula 10: Entrada e saída de dados
#+author: Blau Araujo
#+startup: show2levels
#+options: toc:3
* Aula 10: Entrada e saída de dados
[[https://youtu.be/b6cbnZlY328][Vídeo desta aula]]
** Tabela de descritores de arquivos
Um /fluxo de dados/ (ou /stream/) é a abstração, em software, do transito de
dados entre dispositivos de hardware e processos no espaço de usuário.
Por exemplo:
- 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.
No 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/.
Ao serem iniciados, os processos herdam vários dados de seus respectivos
processos pais, entre eles, uma cópia da /tabela de descritores de arquivos/
representando, no mínimo, três fluxos de dados padrão.
*** Fluxos de dados padrão
Por padrão, todo processo inicia com acesso a, pelo meno, três dispositivos
ligados a um terminal:
- Entrada padrão (=/dev/stdin=): leitura da digitação no terminal;
- Saída padrão (=/dev/stdout=): exibição de mensagens no terminal;
- Saída padrão de erros (=/dev/stderr=): exibição de mensagens de erros no terminal.
Cada um desses dispositivos padrão é disponibilizado para cada processo,
individualmente, através de uma entrada na sua tabela de descritores de
arquivos:
- =stdin=: descritor de arquivos =0=;
- =stdout=: descritor de arquivos =1=;
- =stderr=: descritor de arquivos =2=.
*** Uma nota sobre dispositivos
Sistemas parecidos com o UNIX geralmente 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 dizemos algo como: /"dispositivo de terminal"/, ou /"dispositivo
padrão de entrada"/ (ou de saída), nós estamos falando de um desses tipos de
arquivos.
*** Representação em /proc
Como vimos na [[https:../08-processos#headline-8][aula 8]], o kernel disponibiliza todas as informações sobre todos
os processos em execução na forma de um sistema de arquivos virtual (=procfs=)
montado no diretório =/proc=. Nele, cada processo terá um subdiretório nomeado
segundo seu número de identificação (PID) e, neste subdiretório, existe um
outro subdiretório com as ligações simbólicas entre os descritores de arquivos
do processo e os arquivos e dispositivos a que ele tem acesso: é o diretório
=/proc/PID/fd=.
Se listarmos, por exemplo, o diretório =fd= do processo do shell, nós veremos
algo assim:
#+begin_example
$ ls -l /proc/$$/fd
total 0
lrwx------ 1 blau blau 64 abr 23 08:09 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:09 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:09 2 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:09 255 -> /dev/pts/1
#+end_example
#+begin_quote
Aqui, =$$= é a expansão do parâmetro especial do shell que contém o número
do seu processo.
#+end_quote
Observe que os três descritores de arquivos padrão (=0=, =1= e =2=) estão
presentes como ligações simbólicas (outro tipo de arquivo) para o mesmo
dispositivo de terminal (=/dev/pts/1=).
*** Um exemplo em C
Com a função =system=, declarada no cabeçalho =stdlib.h=, nós podemos executar
o utilitário =ls= para listar os descritores de arquivos atribuídos ao processo
iniciado para a execução do nosso programa (=fdlist.c=):
#+begin_src c
#include <stdlib.h>
int main(void) {
system("ls -l /proc/$(pidof a.out)/fd");
return 0;
}
#+end_src
#+begin_quote
O utilitário =pidof= exibe o número do PID de um processo a partir do
nome do programa que o iniciou passado como argumento: no caso, o nosso
=a.out=.
#+end_quote
Compilando e executando:
#+begin_example
:~$ gcc -Wall fdlist.c
:~$ ./a.out
total 0
lrwx------ 1 blau blau 64 abr 23 08:16 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:16 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:16 2 -> /dev/pts/1
#+end_example
Repare que, como o processo foi iniciado pela mesma sessão do shell do
exemplo anterior, nós temos os três descritores de arquivos padrão ligados
ao mesmo terminal de antes (=/dev/pts/1=). Isso aconteceu porque esses
fluxos de dados foram herdados do processo pai (o processo do shell).
** Redirecionamentos e pipes
Também na [[https://bolha.dev/blau_araujo/cblc/src/branch/main/aulas/08-processos#headline-6][aula 8]], nós vimos que as linhas de comandos simples podem conter
operadores de redirecionamento de fluxos de dados de/para arquivos, ou
ainda, poderiam ser encadeados com outros comandos simples através do
operador de /pipe/.
*** Redirecionamentos
Quando um fluxo de dados é redirecionado, por exemplo, para ler um arquivo,
o que é feito com o operador de redirecionamento de leitura (=<=), a ligação
do descritor de arquivos =0= (=stdin=) passa a ser feita com o arquivo.
Por exemplo, se eu tiver um arquivo chamado =lista.txt= e redirecionar seu
conteúdo para leitura pelo programa do exemplo anterior, o resultado seria
esse:
#+begin_example
:~$ ./a.out < lista.txt
total 0
lr-x------ 1 blau blau 64 abr 23 08:47 0 -> /home/blau/lista.txt
lrwx------ 1 blau blau 64 abr 23 08:47 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 08:47 2 -> /dev/pts/1
#+end_example
Já se eu quisesse escrever a saída do programa em um arquivo, bastaria
fazer um redirecionamento de escrita (operadores =>= ou =>>=):
#+begin_example
:~$ ./a.out > lista.txt
:~$ cat lista.txt
total 0
lrwx------ 1 blau blau 64 abr 23 08:49 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 08:49 1 -> /home/blau/lista.txt
lrwx------ 1 blau blau 64 abr 23 08:49 2 -> /dev/pts/1
#+end_example
Como eu redirecionei a saída padrão do programa para um arquivo, nada foi
mostrado no terminal e, por isso, eu exibi o conteúdo do arquivo com o
utilitário =cat=.
*** Pipes
Com os pipes é semelhante, mas nós teríamos uma ligação com outro processo
interfaceada por um arquivo do tipo /pipe/ criado pelo sistema.
Por exemplo, eu posso encadear o nosso programa de exemplo em um pipe com
o utilitário =cat=:
#+begin_example
$ ./a.out | cat
total 0
lrwx------ 1 blau blau 64 abr 23 08:56 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 08:56 1 -> pipe:[2551457]
lrwx------ 1 blau blau 64 abr 23 08:56 2 -> /dev/pts/1
#+end_example
Aqui, a saída do nosso programa (descritor de arquivos =1=) está ligada
ao arquivo =pipe:[2551457]= para escrita. Ao mesmo tempo, o processo do
=cat= teria a sua entrada padrão (descritor de arquivos =0=) ligada ao mesmo
arquivo para leitura.
Eu posso até modificar meu programa para exibir os diretórios =fd= dos dois
processos:
#+begin_src c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
puts("Processo do programa:");
fflush(stdout);
system("ls -l /proc/$(pidof a.out)/fd");
puts("\nProcesso do cat:");
fflush(stdout);
system("ls -l /proc/$(pidof cat)/fd");
return 0;
}
#+end_src
Compilando e executando com o pipe novamente:
#+begin_example
$ gcc -Wall fdlist.c
blau@xeon:[~/git/cblc/aulas/10-dataio] (main)
$ ./a.out | cat
Processo do programa:
total 0
lrwx------ 1 blau blau 64 abr 23 09:06 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 09:06 1 -> pipe:[2550676]
lrwx------ 1 blau blau 64 abr 23 09:06 2 -> /dev/pts/1
Processo do cat:
total 0
lr-x------ 1 blau blau 64 abr 23 09:06 0 -> pipe:[2550676]
lrwx------ 1 blau blau 64 abr 23 09:06 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:06 2 -> /dev/pts/1
#+end_example
** Acumuladores (buffers)
Na última versão do exemplo, eu precisei chamar a função =fflush(stdout)=,
e veja o que aconteceria sem ela:
#+begin_src c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
puts("Processo do programa:");
system("ls -l /proc/$(pidof a.out)/fd");
puts("\nProcesso do cat:");
system("ls -l /proc/$(pidof cat)/fd");
return 0;
}
#+end_src
Compilando e executando:
#+begin_example
:~$ gcc -Wall fdlist.c
:~$ ./a.out | cat
total 0
lrwx------ 1 blau blau 64 abr 23 09:12 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 09:12 1 -> pipe:[2569607]
lrwx------ 1 blau blau 64 abr 23 09:12 2 -> /dev/pts/1
total 0
lr-x------ 1 blau blau 64 abr 23 09:12 0 -> pipe:[2569607]
lrwx------ 1 blau blau 64 abr 23 09:12 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:12 2 -> /dev/pts/1
Processo do programa:
Processo do cat:
#+end_example
Notou que as linhas impressas pela função =puts= só foram exibidas depois
das listagens feitas pela função =system=?
Isso aconteceu porque, *em um pipe*, a saída do programa é /totalmente bufferizada/,
ou seja, os dados impressos por funções como =puts= e =printf= são acumuladas
no /buffer de saída/ do processo, mas não podem ser despejadas imediatamente
na saída padrão até que outros subprocessos o façam. Como nós temos as
chamadas da função =system=, que iniciam subprocessos para executar comandos,
as listagens dos diretórios são despejadas e só depois as linhas das chamadas
de =puts= são despejadas.
Isso não aconteceria se não houvesse um pipe:
#+begin_src c
#include <stdio.h>
#include <stdlib.h>
int main(void) {
puts("Processo do programa:");
system("ls -l /proc/$(pidof a.out)/fd");
puts("\nProcesso do cat:");
system("ls -l /proc/$(pidof cat)/fd");
return 0;
}
#+end_src
Compilando e executando:
#+begin_example
:~$ gcc -Wall fdlist.c
:~$ ./a.out
Processo do programa:
total 0
lrwx------ 1 blau blau 64 abr 23 09:21 0 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:21 1 -> /dev/pts/1
lrwx------ 1 blau blau 64 abr 23 09:21 2 -> /dev/pts/1
Processo do cat:
ls: não foi possível acessar '/proc//fd': Arquivo ou diretório inexistente
#+end_example
Ignorando o erro (o =cat= não estava em execução), veja que as linhas foram
impressas na ordem em que apareciam no programa, sem a função =fflush=. Isso
porque, sem pipes ou redirecionamentos ocupando a saída padrão, o /buffer de
saída/ do programa é liberado /linha a linha/ (na ocorrência do caractere de
quebra de linha =\n=). Mas, com um redirecionamento, por exemplo...
#+begin_example
:~$ ./a.out > lista.txt
ls: não foi possível acessar '/proc//fd': Arquivo ou diretório inexistente
:~$ cat lista.txt
total 0
lrwx------ 1 blau blau 64 abr 23 09:38 0 -> /dev/pts/1
l-wx------ 1 blau blau 64 abr 23 09:38 1 -> /home/blau/lista.txt
lrwx------ 1 blau blau 64 abr 23 09:38 2 -> /dev/pts/1
Processo do programa:
Processo do cat:
#+end_example
#+begin_quote
*Nota:* veja que a mensagem de erro não foi escrita no arquivo, mas foi exibida
no terminal, porque ela foi enviada através da /saída padrão de erros/ (=stderr=).
#+end_quote
O que a função =fflush= faz, com o fluxo de dados =stdout= como argumento, é
forçar a descarga do que houver no buffer de saída do programa, independente
de haver pipes ou redirecionamentos.
*** Buffers de entrada e saída do programa
Voltando um pouco o assunto, algumas funções da =libc/glibc= alocarão
algum espaço no /heap/ do processo do programa para acumular dados recebidos
pela entrada padrão ou destinados à saída padrão.
Por exemplo:
- Produzem um buffer de saída: funções =puts=, =printf=, etc;
- Produzem um buffer de entrada: funções =scanf=, =fgets=, etc.
*** Buffer de saída do programa
No caso do buffer de saída, como vimos, o tipo de acumulação é determinado,
entre outras coisas, pela eventualidade de algum pipe ou redirecionamento:
- *Bufferização linha a linha:* programa executado sem pipes ou redirecionamentos.
- *Bufferização total:* programa executado em um pipe ou com redirecionamentos.
- *Sem bufferização:* com chamadas de sistema (ex: =write=) ou quando configurado
explicitamente.
*** Buffer de entrada do programa
De certo modo, a acumulação de dados lidos de arquivos ou do terminal são
mais simples, ou melhor, não estão sujeitos a tantas condições como o
buffer de saída, mas também têm as suas particularidades.
Com a funções como =scanf= e =fgets=, a entrada é bufferizada e todos os bytes
recebidos são acumulados. No entanto, caberá às características de cada
função determinar como esses dados serão consumidos no programa.
Por exemplo, se utilizarmos as funções =scanf= ou =fgets= para ler algo digitado
no terminal (entrada interativa), pode ser que, de tudo que for digitado,
apenas uma parte seja consumida, fazendo com que reste alguma coisa no
buffer de entrada. Os dados restantes poderão vir a ser consumidos nas
chamadas subsequentes dessas funções, com resultados bastante inconvenientes!
Como a =glibc= não possui uma função para consumir os dados restantes no
buffer de entrada após uma leitura, é comum utilizarmos algo assim depois
da chamada de uma função com entrada bufferizada (se a leitura for da
entrada padrão):
#+begin_src c
int ch;
while ((ch = getchar()) != '\n' && ch != EOF);
#+end_src
#+begin_quote
Nas próximas aulas, nós falaremos das particularidades da leitura da
entrada padrão com as funções =scanf= e =fgets=.
#+end_quote
*** Entrada padrão não bufferizada
Quando a entrada padrão não é bufferizada, nós ficamos sujeitos às regras
de bufferização do lado do sistema: em especial, o buffer do dispositivo
de terminal.
Por exemplo, numa leitura interativa com a chamada de sistema =read=, onde nós
limitamos a quantidade de bytes que serão consumidos, pode restar algo do que
foi digitado no buffer do terminal e, se o nosso programa não consumir esses
bytes excedentes, eles serão descarregados na saída padrão pelo próprio terminal,
quando o processo do nosso programa terminar, e serão lidos pelo shell como
se fosse um comando.
Nas próximas três aulas, nós vamos explorar a leitura interativa de dados
digitados no terminal e todas essas particularidades serão demonstradas.