cblc/aulas/13-read
2025-05-01 11:36:05 -03:00
..
README.org conteúdo da aula 13 2025-05-01 11:36:05 -03:00

Curso Básico da Linguagem C

Aula 13: Leitura da entrada padrão com 'read'

Vídeo desta aula

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 para long e size_t para unsigned 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ável errno e pode ser exibido com a função perror.

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 quanto STDIN_FILENO dependem da inclusão de unistd.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 ou STDIN_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 (como fgets e scanf) é que são implementadas com mecanismos para descargar o buffer de saída automaticamente.