13 KiB
Curso Básico da Linguagem C
Aula 13: Leitura da entrada padrão com 'read'
A função read
é um wrapper (uma "embalagem") para simplificar o uso da chamada
de sistema read
. Chamadas de sistema são funções internas que o kernel
implementa para que os programas (executados no espaço de usuário) tenham acesso
a serviços e funcionalidades do sistema (disponíveis no espaço do kernel).
Entre essas funcionalidades, estão:
- Acesso a arquivos para leitura e escrita;
- Alocação de espaço em memória;
- Criação de processos;
- Término de programas.
Nossos programas em C não interagem diretamente com o hardware ou com recursos protegidos do sistema. Em vez disso, eles fazem chamadas de sistema para "pedir" que o kernel realize essas operações por ele. Sendo assim, fica fácil concluir que todas as funções da biblioteca C padrão que nós utilizamos até agora, em algum momento, fazem chamadas de sistema.
Por exemplo, a função fgets
utiliza a chamada de sistema read
para preencher o
buffer de entrada do processo e, em seguida, copiar parte desse conteúdo para o
buffer definido no programa para receber uma string. Se quisermos utilizar
a chamada de sistema read
para escrever bytes diretamente no buffer de destino,
evitando o buffer de entrada do processo, nós podemos utilizar a função read
da glibc
, que é uma interface (um wrapper, ou "embalagem") para a chamada de
sistema propriamente dita.
As man pages das interfaces para as chamadas de sistema implementadas na
glibc
podem ser encontradas na seção 2 (ex.:man 2 read
).
A função 'read'
A função read
é declarada no cabeçalho unistd.h
da seguinte forma:
ssize_t read(int fd, void buf[.count], size_t count);
Onde:
int fd
- O número de um descritor de arquivos aberto para leitura;void buf[.count]
- O endereço do buffer de destino dos bytes lidos;size_t count
- A quantidade máxima de bytes a serem lidos.
O tipo
ssize_t
é um apelido paralong
esize_t
paraunsigned long
.
Por exemplo:
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
read(STDIN_FILENO, buf, BUFMAX);
...
return 0;
}
Isso fará com que até 10 bytes (BUFMAX
) recebidos pela entrada padrão
(descritor de arquivos 0
, valor expandido pela macro STDIN_FILENO
) sejam
escritos na memória a partir do endereço de buf
. Mas, diferente de fgets
e
scanf
, não haverá a inclusão do terminador nulo ('\0'
).
Incluindo o terminador nulo
Se quisermos que o conteúdo do buffer seja uma string, nós teremos que
implementar uma forma de acrescentar o terminador nulo – por exemplo,
utilizando a quantidade de bytes lidos, que é retornada pela função read
em caso de sucesso:
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
ssize_t count = 0;
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) > 0) {
buf[count] = '\0';
}
/* ... */
return 0;
}
O retorno
0
indica o fim do arquivo e-1
indica um erro, cujo identificador é atribuído à variávelerrno
e pode ser exibido com a funçãoperror
.
A solução do exemplo faz com que read
se comporte quase como a função
fgets
, exceto por não criar um buffer de entrada no heap do processo:
- A leitura é feita até
BUFMAX - 1
; - O caractere seguinte no buffer (
buf[count]
) recebe'\0'
; - O caractere
'\n'
será preservado se estiver no limite de leitura.
Para demonstrar, digamos que o usuário digitou 12345
e teclou Enter
. O valor
de count
será 6
e os bytes em buf
serão:
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | lixo | lixo | lixo | lixo | +------+------+------+------+------+------+------+------+------+------+
Depois de alterado o byte em buf[count]
(buf[6]
), nós teremos:
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | 0x00 | lixo | lixo | lixo | +------+------+------+------+------+------+------+------+------+------+
Se for digitado 1234567890
, nós teremos inicialmente:
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | lixo | +------+------+------+------+------+------+------+------+------+------+
Os caracteres 0x30
(0
) e 0x0a
('\n'
) não serão lidos, o valor de count
será 9
e buf[9]
receberá o terminador nulo:
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9] +------+------+------+------+------+------+------+------+------+------+ buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 | +------+------+------+------+------+------+------+------+------+------+
Quando não converter os bytes lidos em string
O tratamento anterior só faz sentido quando queremos utilizar a entrada como uma string. Mas nada disso será necessário, nem será adequado, se estivermos lendo…
- Dados binários (arquivos binários em geral);
- Dados em blocos fixos (setores de disco, pacotes de rede, etc);
- Transferências de dados entre processos;
- Dados para cálculos de checksum e hash;
- Dados para compressão ou criptografia… Entre outras tantas situações.
O buffer do terminal
Quando um programa com a função read
é executado interativamente, os dados
digitados são inicialmente armazenados em um buffer gerenciado pelo terminal.
Ao chamar a função read
, o programa consome parte desses dados da entrada padrão
e os bytes que não forem consumidos permanecerão no buffer do terminal.
Esses dados residuais não são descartados e permanecem disponíveis para futuras chamadas de leitura – inclusive por processos subsequentes, caso o processo atual termine. Isso pode causar efeitos inesperados em programas interativos executados na sequência (como o próprio shell, por exemplo), pois os caracteres digitados anteriormente, mas ainda não consumidos, serão lidos automaticamente.
Veja este exemplo:
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
ssize_t count = 0;
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1;
}
// Tratamento do terminador nulo...
buf[count] = '\0';
printf("%s\n", buf);
return 0;
}
Compilando e executando:
:~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$ 012345 bash: 012345: comando não encontrado
A função read
consumiu apenas os 9 primeiros caracteres digitados, os 6
caracteres restantes continuaram no buffer do terminal e foram lidos pelo
processo do shell (interativo) que tentou executá-los como um comando.
A solução para isso é simples (e já conhecida por nós): consumir todos os bytes restantes na entrada padrão:
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
ssize_t count = 0;
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1;
}
// Tratamento do terminador nulo...
buf[count] = '\0';
// Tratamento dos caracteres restantes no buffer do terminal...
char c;
while((c = getchar()) != '\n' && c != EOF);
printf("%s\n", buf);
return 0;
}
Compilando e executando novamente:
:~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$
Definindo um prompt
O nosso programa precisa de um prompt, então vamos implementá-lo:
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
ssize_t count = 0;
// Prompt...
printf("Digite algo: ");
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1;
}
// Tratamento do terminador nulo...
buf[count] = '\0';
// Tratamento dos caracteres restantes no buffer do terminal...
char c;
while((c = getchar()) != '\n' && c != EOF);
printf("%s\n", buf);
return 0;
}
Agora, nós temos a impressão da string "Digite algo: "
antes da leitura
do terminal. Entretanto, ao compilar e executar o programa, nada é exibido
até nós teclarmos Enter
:
:~$ gcc -Wall teste.c :~$ ./a.out 1234567890 Digite algo: 123456789 :~$
Isso acontece porque printf
utiliza o buffer de saída do programa (criado
pela biblioteca padrão) e, por padrão, os dados na saída podem ser:
- Totalmente bufferizados: quando o programa escreve em arquivos ou pipes;
- Bufferizados por linha: quando a escrita é em um terminal.
Como estamos escrevendo no terminal, a saída de printf
é bufferizada por
linha e só é liberada quando o buffer de saída fica cheio (BUFSIZ = 8192 bytes
),
quando recebe o caractere de quebra de linha ('\n'
) ou quando é esvaziada
explicitamente (por exemplo, com fflush(stdout)
).
No caso do exemplo, a saída não foi descarregada porque a string na chamada
de printf
não tem uma quebra de linha (exatamente como nós queremos). Uma
solução simples, porém, é descarregar o buffer explicitamente:
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
ssize_t count = 0;
// Prompt...
printf("Digite algo: ");
// Descarga do buffer de saída...
fflush(stdout);
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1;
}
// Tratamento do terminador nulo...
buf[count] = '\0';
// Tratamento dos caracteres restantes no buffer do terminal...
char c;
while((c = getchar()) != '\n' && c != EOF);
printf("%s\n", buf);
return 0;
}
Compilando e testando:
:~$ gcc -Wall teste.c :~$ ./a.out Digite algo: 1234567890 123456789 :~$
Ainda temos um problema!
Acontece, porém, que os nossos testes estão mascarando outro problema.
Observe o que acontece quando digitamos menos caracteres do que a função
read
espera ler:
:~$ gcc -Wall teste.c :~$ ./a.out Digite algo: 12345 12345 :~$
A segunda linha em branco era esperada porque a quebra de linha da minha
digitação foi preservada, mas a primeira linha em branco aconteceu
porque eu tive que teclar Enter
duas vezes!
Isso aconteceu por causa da forma como estamos descarregando o restante do buffer de entrada do terminal:
// Tratamento dos caracteres restantes no buffer do terminal...
char c;
while((c = getchar()) != '\n' && c != EOF);
Quando digitamos menos caracteres do que o read
espera ler, não há sobras
no buffer do terminal e não há quebras de linha nem uma indicação de fim de
arquivo (a menos que o usuário tecle Ctrl+D
). Por isso, a limpeza do buffer
deve ser condicionada de uma das seguintes formas:
- Ou verificamos se o penúltimo caractere no buffer (
buf[count - 1]
) é diferente de'\n'
, o que indicaria que o buffer não foi consumido completamente; - Ou verificamos se o buffer está vazio ou não com a chamada de sistema
ioctl
.
Experimente as duas soluções e escolha a sua preferida.
Solução com o conteúdo de buf[count - 1]
// Verificar se '\n' faz parte da string...
if (buf[count - 1] != '\n') {
// Tratamento dos caracteres restantes no buffer do terminal...
char c;
while((c = getchar()) != '\n' && c != EOF);
}
Solução com ioctl
A chamada de sistema ioctl
, no cabeçalho sys/ioctl.h
, manipula parâmetros
de dispositivos especiais (como terminais, por exemplo). Seus argumentos
são:
- O descritor de arquivos do dispositivo (ex.:
0
ouSTDIN_FILENO
); - O número da operação com o dispositivo (ex.:
FIONREAD
, quantidade de bytes disponíveis para leitura imediata); - Um ponteiro para o endereço que vai receber a informação obtida.
Sendo assim, nós poderíamos utilizá-la desta forma para determinar se
o buffer do terminal contém alguma coisa (0
indica buffer vazio):
#include <sys/ioctl.h>
...
int main(void) {
...
// Verificar se o buffer está vazio...
int r = 0;
if (ioctl(STDIN_FILENO, FIONREAD, &r) != 0) {
// Tratamento dos caracteres restantes no buffer do terminal...
char c;
while((c = getchar()) != '\n' && c != EOF);
}
...
}