cblc/aulas/13-read/README.org
2025-04-30 14:47:32 -03:00

13 KiB
Raw Blame History

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:

  • 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 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').

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

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 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 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);
    }
    ...
}