17 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 o acesso a arquivos para leitura e escrita, a alocação de espaço em memória, a criação de processos, o término de programas e muitas outras. Portanto, 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 eles.
Isso também significa que todas as funções da biblioteca C padrão que nós
utilizamos até agora, em algum momento, também fazem chamadas de sistema. Por
exemplo, a função fgets
utiliza a chamada de sistema read
para preencher o
buffer de entrada do programa e, em seguida, copiar parte desse conteúdo para um
determinado endereço na memória: o que também envolve algumas outras chamadas de
sistema.
Se quisermos utilizar a chamada de sistema read
para escrever bytes diretamente
no endereço de destino, evitando o buffer de entrada do processo, nós podemos
utilizar a função read
da glibc
, que é uma interface para a chamada de sistema
read
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'
). Se quisermos
garantir um byte para receber o terminador, nós precisamos fazer como na
função fgets
: ler count - 1
bytes…
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
read(STDIN_FILENO, buf, BUFMAX - 1);
...
return 0;
}
Conversão para string incluindo o terminador nulo
Para converter o conteúdo do buffer em uma string, nós temos que
implementar uma forma de acrescentar o terminador nulo – por exemplo,
utilizando a quantidade de bytes efetivamente 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 bytes = 0;
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}
// Tratamento do terminador nulo...
buf[bytes] = '\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
tenha um comportamento semelhante ao
de fgets
:
- A leitura é feita até
BUFMAX - 1
; - O caractere seguinte no buffer (
buf[bytes]
) 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 bytes
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[bytes]
(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 bytes
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 para receber dados digitados
em um terminal, a digitação é acumulada em um buffer gerenciado pelo terminal
e só será enviada para o processo do programa quando o usuário teclar Enter
.
No programa, a função read
consome parte desses dados e os bytes que não
forem consumidos permanecerão no buffer do terminal.
Os dados residuais não são descartados e permanecem disponíveis para futuras chamadas de leitura (inclusive por processos subsequentes, caso o processo do programa termine) ou até o terminal ser fechado. Isso pode causar efeitos inesperados em programas interativos executados na sequência (como o próprio shell, por exemplo), pois o conteúdo remanescente no buffer do terminal será lido automaticamente.
Veja este exemplo:
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
ssize_t bytes = 0;
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}
// Tratamento do terminador nulo...
buf[bytes] = '\0';
printf("%s\n", buf);
return 0;
}
Compilando e executando:
:~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$ 012345 <-- O Bash tentou interpretar e executar o conteúdo do buffer! bash: 012345: comando não encontrado
A função read
consumiu apenas os 9 primeiros caracteres digitados, mas os 6
caracteres restantes continuaram no buffer do terminal e foram lidos e
interpretados pelo processo do shell (interativo) como a linha de um comando.
Descarga do buffer do terminal
A solução para isso é simples e já é velha conhecida: consumir todos os bytes restantes na entrada padrão, por exemplo, com a função que nós criamos em outras aulas…
void flush_stdin(void) {
char c;
while((c = getchar()) != '\n' && c != EOF);
}
Mas esta função utiliza getchar
que, assim como fgets
e scanf
, fará a
leitura do buffer de entrada criado no processo pela glibc
. Certamente,
o conteúdo do buffer do terminal será copiado para o buffer de entrada do
programa. Porém, sem garantias de que haverá algo no buffer do terminal,
pode acontecer que a nossa função fique presa em getchar
até que o usuário
tecle Enter
novamente, enviando a quebra de linha que o loop while
espera
para terminar.
Por exemplo, utilizando a função flush_stdin
, o nosso exemplo funcionaria
corretamente quando houvesse mais caracteres digitados do que o read
pode consumir:
:~$ gcc -Wall teste.c :~$ ./a.out 123456789012345 123456789 :~$
Contudo, digitando menos caracteres do que o limite, nós teríamos que
teclar Enter
duas vezes: a primeira para a leitura de read
e a segunda
para getchar
…
:~$ gcc -Wall teste.c :~$ ./a.out 12345 <--- Aguardando um segundo ENTER 12345 <--- Quebra de linha na string impressa :~$
A segunda linha em branco era esperada porque nós não fizemos nada para
remover a quebra de linha lida por read
, mas a primeira aconteceu porque
eu tive que teclar Enter
mais uma vez para que getchar
tivesse algo para
ler.
Implementação de flush_stdin
com a chamada read
Antes de passarmos à solução da descarga do buffer do terminal, vamos
aproveitar que estamos lidando com chamadas de sistema para implementar
uma versão da função flush_stdin
com read
, em vez de getchar
:
void flush_tty_buffer(void) {
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n');
}
Importante! Tanto
read
quantoSTDIN_FILENO
dependem da inclusão deunistd.h
.
O princípio é o mesmo, só que, desta vez, o loop vai continuar enquanto
read
retornar que leu 1 byte e este byte for diferente de '\n'
. A outra
diferença é que, em vez de ler o buffer do programa, a nossa nova função
lerá diretamente o buffer do terminal.
Esta função não poderia ser utilizada se os bytes remanescentes estivessem no buffer de entrada do programa!
Mesmo assim, o problema continua: se o buffer do terminal estiver vazio,
read
ficará esperando o recebimento de uma linha, o que só acontecerá
quando nós digitarmos novamente um Enter
.
Descarga condicional do buffer do terminal
Se a causa do problema é a possibilidade do buffer do terminal estar vazio,
basta condicionar a chamada de flush_stdin
, ou de flush_tty_buffer
, à
verificação desta condição, o que pode ser avaliado de, pelo menos, duas
formas: utilizando os bytes lidos e a sua quantidade ou verificando o
estado do buffer do terminal.
Solução 1: Teste da presença de '\n'
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 | 0x0A | 0X00 | +------+------+------+------+------+------+------+------+------+------+
Aqui, a digitação foi 12345678\n
(exatamente BUFMAX - 1
bytes) e, mesmo que
fosse digitada qualquer quantidade de bytes menor que essa, o buffer do
terminal estaria vazio e o caractere '\n'
estaria presente na string. Logo,
nós poderíamos utilizar a seguinte condição:
if (buf[bytes - 1] != '\n') {
flush_stdout(); // Ou flush_tty_buffer
}
É uma solução interessante porque, além de simples, ela já verifica a existência da quebra de linha na string e nós poderíamos aproveitar, se fosse o caso, para removê-la:
if (buf[bytes - 1] == '\n') {
buf[bytes - 1] = '\0'; // Remoção da quabra de linha
} else {
flush_stdout(); // Ou flush_tty_buffer
}
Solução 2: verificação do estado do buffer do terminal
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 criar uma função para retornar 0
, no caso de
buffer vazio, ou a quantidade de bytes remanescentes no buffer do terminal:
#include <sys/ioctl.h>
...
int is_tty_empty(void) {
int r = 0;
return (ioctl(STDIN_FILENO, FIONREAD, &r) == 0);
}
Portanto, nossa função retornaria 1
(verdadeiro), se o buffer do terminal
estivesse vazio, ou 0
(falso), se não estivesse.
A ideia de criar uma função, em vez de utilizar
ioctl
diretamente, tem a ver com a atribuição de um valor semântico ao código.
No nosso exemplo, ela poderia ser utilizada desta maneira:
if (!is_tty_empty()) {
flush_stdout(); // Ou flush_tty_buffer
}
Qual solução utilizar?
Não existe uma resposta certa neste caso: tudo depende do contexto do
programa. Se eu estivesse buscando uma solução mais robusta e aplicável
em diversas situações, eu escolheria a segunda (ioctl
), mas escolheria
a primeira em contextos menos rigorosos, onde a quebra de linha tivesse
que ser detectada e removida da string de qualquer forma.
Eu vou optar pela primeira solução desta vez, então o nosso código ficará assim:
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
void flush_tty_buffer(void) {
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n');
}
int main(void) {
char buf[BUFMAX];
ssize_t bytes = 0;
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}
// Tratamento do terminador nulo...
buf[bytes] = '\0';
if (buf[bytes - 1] == '\n') {
// Remoção condicional da quebra de linha...
buf[bytes - 1] = '\0';
} else {
// Esvaziamento condicional do buffer do terminal...
flush_tty_buffer(); // Ou flush_stdin
}
printf("%s\n", buf);
return 0;
}
Compilando e testando…
:~$ gcc -Wall teste.c :~$ ./a.out 12345 <-- 6 bytes digitados (buffer tty vazio) 12345 <-- Quebra de linha removida :~$ ./a.out 12345678 <-- 9 bytes digitados (buffer vazio) 12345678 <-- Quebra de linha removida :~$ ./a.out 123456789012345 <-- 16 bytes digitados (7 bytes no buffer tty) 123456789 <-- Não havia quebra de linha para remover :~$
Definindo um prompt
O nosso programa precisa de um prompt, então vamos implementá-lo:
...
char buf[BUFMAX];
ssize_t bytes = 0;
printf("Digite algo: "); // Nosso prompt!
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}
...
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 <-- O prompt só foi impresso aqui! :~$
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 (geralmente,
8192 bytes
), quando recebe o caractere de quebra de linha ('\n'
) ou quando
é esvaziada explicitamente.
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 com a função
fflush
:
...
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;
}
...
Compilando e testando:
:~$ gcc -Wall teste.c :~$ ./a.out Digite algo: 1234567890 123456789 :~$
O problema da bufferização por linha não é exatamente causado pelo uso da chamada de sistema
read
. Na verdade, as funções de mais alto nível (comofgets
escanf
) é que são implementadas com mecanismos para descargar o buffer de saída automaticamente.