mirror of
https://gitlab.com/blau_araujo/cblc.git
synced 2025-05-09 18:16:37 -03:00
409 lines
14 KiB
Org Mode
409 lines
14 KiB
Org Mode
|
#+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.
|
||
|
|
||
|
|