diff --git a/aulas/13-read/README.org b/aulas/13-read/README.org new file mode 100644 index 0000000..e802c54 --- /dev/null +++ b/aulas/13-read/README.org @@ -0,0 +1,457 @@ +#+title: Curso Básico da Linguagem C +#+subtitle: Aula 13: Leitura da entrada padrão com 'read' +#+author: Blau Araujo +#+startup: show2levels +#+options: toc:3 + +* Aula 13: Leitura da entrada padrão com 'read' + +[[https://youtu.be/bW3Xox6LP_U][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. + +#+begin_quote +As /man pages/ das interfaces para as chamadas de sistema implementadas na =glibc= +podem ser encontradas na seção 2 (ex.: =man 2 read=). +#+end_quote + +** A função 'read' + +A função =read= é declarada no cabeçalho =unistd.h= da seguinte forma: + +#+begin_src c +ssize_t read(int fd, void buf[.count], size_t count); +#+end_src + +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. + +#+begin_quote +O tipo =ssize_t= é um apelido para =long= e =size_t= para =unsigned long=. +#+end_quote + +Por exemplo: + +#+begin_src c +#include + +#define BUFMAX 10 + +int main(void) { + + char buf[BUFMAX]; + read(STDIN_FILENO, buf, BUFMAX); + + ... + + return 0; +} +#+end_src + +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: + +#+begin_src c +#include + +#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; +} +#+end_src + +#+begin_quote +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=. +#+end_quote + +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: + +#+begin_example + 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 | + +------+------+------+------+------+------+------+------+------+------+ +#+end_example + +Depois de alterado o byte em =buf[count]= (=buf[6]=), nós teremos: + +#+begin_example + 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 | + +------+------+------+------+------+------+------+------+------+------+ +#+end_example + +Se for digitado =1234567890=, nós teremos inicialmente: + +#+begin_example + 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 | + +------+------+------+------+------+------+------+------+------+------+ +#+end_example + +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: + +#+begin_example + 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 | + +------+------+------+------+------+------+------+------+------+------+ +#+end_example + +*** 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: + +#+begin_src c +#include +#include + +#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; +} +#+end_src + +Compilando e executando: + +#+begin_example +:~$ gcc -Wall teste.c +:~$ ./a.out +123456789012345 +123456789 +:~$ 012345 +bash: 012345: comando não encontrado +#+end_example + +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: + +#+begin_src c +#include +#include + +#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; +} +#+end_src + +Compilando e executando novamente: + +#+begin_example +:~$ gcc -Wall teste.c +:~$ ./a.out +123456789012345 +123456789 +:~$ +#+end_example + +** Definindo um prompt + +O nosso programa precisa de um prompt, então vamos implementá-lo: + +#+begin_src c +#include +#include + +#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; +} +#+end_src + +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=: + +#+begin_example +:~$ gcc -Wall teste.c +:~$ ./a.out +1234567890 +Digite algo: 123456789 +:~$ +#+end_example + +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: + +#+begin_src c +#include +#include + +#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; +} +#+end_src + +Compilando e testando: + +#+begin_example +:~$ gcc -Wall teste.c +:~$ ./a.out +Digite algo: 1234567890 +123456789 +:~$ +#+end_example + +** 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: + +#+begin_example +:~$ gcc -Wall teste.c +:~$ ./a.out +Digite algo: 12345 + +12345 + +:~$ +#+end_example + +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: + +#+begin_src c +// Tratamento dos caracteres restantes no buffer do terminal... +char c; +while((c = getchar()) != '\n' && c != EOF); +#+end_src + +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=. + +**** Solução com o conteúdo de =buf[count - 1]= + +#+begin_src c +// 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); +} +#+end_src + +**** 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): + +#+begin_src c +#include + +... + +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); + } + ... +} +#+end_src + +#+begin_quote +Experimente as duas soluções e escolha a sua preferida. +#+end_quote +