#+title: Curso Básico da Linguagem C #+subtitle: Aula 14: Abertura de arquivos para leitura #+author: Blau Araujo #+startup: show2levels #+options: toc:3 * Aula 14: Abertura de arquivos para leitura [[https://www.youtube.com/watch?v=uh3UdYyzXRM][Vídeo desta aula]] Em sistemas /Unix-like/, um /arquivo/ é qualquer abstração de recursos de entrada e saída representada como /fluxos de dados/ (/streams/) acessados por meio de operações padronizadas, independentemente de seu conteúdo ou finalidade. Neste sentido, existem sete tipos de arquivos: - Diretórios - Arquivos regulares (ou comuns) - Ligações simbólicas - Dispositivos especiais caractere - Dispositivos especiais de bloco - Arquivos de pipe (ou FIFO) - Sockets Quando queremos acessar um arquivo, seja de que tipo for, nós precisamos que o sistema disponibilize vários recursos, como: - Um /descritor de arquivo/ (/file descriptor/): um número inteiro que identifica de forma única o arquivo aberto no contexto do processo. - Uma tabela de descritores do processo: uma estrutura que associa os descritores de arquivos a entradas na tabela global de arquivos abertos. - A tabela global de arquivos abertos (/file table/): que armazena informações como a posição atual de leitura/gravação e as flags de acesso. - A tabela de /inodes/: que contém os metadados do arquivo, como permissões, o "dono" do arquivo, o tipo do arquivo e o ponteiro para os dados no disco. - Os /buffers/ do sistema: usados para otimizar acesso ao conteúdo do arquivo acumulando dados temporariamente na memória. Na linguagem C, tudo isso é abstraído como o tipo =FILE *=, que é um ponteiro para a estrutura de dados (uma /struct/) disponibilizada para cada operação de acesso ao /fluxo de dados/ (/stream/) que irá representar o arquivo. Resumindo: - No sistema, arquivos são abstrações de recursos de entrada e saída representados como /fluxos de dados/; - Na linguagem C, os fluxos de dados, bem como os recursos que eles representam, são abstraídos com o tipo =FILE *= (ponteiro para =FILE=). #+begin_quote O tipo =FILE *= é definido na biblioteca padrão (cabeçalho =stdio.h=). #+end_quote ** Diferença entre streams e descritores de arquivos *** Descritores de arquivos (tipo =int=) - São expressos como inteiros (tipo =int=). - Visíveis em =/proc/PID/fd=. - Identificam arquivos em chamadas de sistema (=open=, =close=, =read=, =write=, etc). - Representam diretamente uma entrada na tabela de arquivos abertos do processo. *** Streams (tipo ponteiro para =FILE=) - São abstrações de todos os recursos abertos para operações com arquivos. - Utilizados em funções da biblioteca padrão (=fopen=, =fclose=, =fgets=, =fprintf=, etc). - Utilizam descritores de arquivos para interagir com o sistema. - Envolvem acumulação (/buffering/) na memória do processo. ** Procedimento geral de acesso a arquivos para leitura Geralmente, as operações com arquivos envolvem três etapas: abertura, processamento e fechamento. *** Abertura - Disponibilização dos recursos para acesso ao arquivo. - Definição do tipo de operação (escrita, /append/ e/ou leitura). - Com a chamada de sistema =open=, recebemos um descritor de arquivos. - Com a função =fopen=, recebemos um ponteiro para =FILE= (/stream/). - Com =fopen=, um ponteiro nulo (=NULL=) será retornado no caso de erros. - Com =open=, o retorno será =-1= no caso de erros. - Com ambas as funções, a variável =errno= receberá a identificação do erro. #+begin_quote Neste curso, nós utilizaremos apenas a função =fopen=. #+end_quote Exemplo: #+begin_src c char *file = "arquivo.txt"; FILE *stream = fopen(file, "r"); #+end_src Aqui, o arquivo =arquivo.txt=, se existir no diretório corrente, será aberto para leitura (modo de abertura ="r"=). Se tudo correr bem, nós teremos o fluxo de dados =stream= para manipular o arquivo. Para tratar eventuais erros, nós podemos fazer algo assim: #+begin_src c char *file = "arquivo.txt"; FILE *stream = fopen(file, "r"); if (!stream) { perror("Erro ao abrir o arquivo"); return EXIT_FAILURE; } #+end_src #+begin_quote A função =perror= imprime, na saída padrão de erros (=stderr=), a descrição do último erro atribuído à variável =errno=. A string, passada como argumento, será utilizada como prefixo da mensagem impressa. #+end_quote *** Processamento É quando nós realizamos alguma operação com o arquivo, como ler seu conteúdo, escrever algo nele, obter seu tamanho em bytes, reposicionar seu ponteiro interno, etc. Por exemplo, nós podemos abrir o arquivo =/etc/shells= para ler e imprimir todo seu conteúdo: #+begin_src c char *file = "/etc/shells"; // Abertura do arquivo... FILE *stream = fopen(file, "r"); if (!stream) { perror("Erro ao abrir o arquivo"); return EXIT_FAILURE; } // Processamento... char line[BUFSIZ]; // Buffer para acumular cada linha lida while (fgets(line, BUFSIZ, stream)) { printf("%s", line); } #+end_src Aqui, =fgets= lerá o conteúdo do arquivo associado a =stream= (=/etc/shells=) até encontrar uma quebra de linha ou chagar a =BUFSIZ-1= bytes. #+begin_quote A macro =BUFSIZ= é definida em =stdio.h= e expande o valor =8192= (8kB). #+end_quote De um modo ou de outro, a estrutura de dados associada a =stream= registrará a quantidade de bytes que já foram lidos para que, no caso de um acesso subsequente, a leitura continue a partir do byte seguinte -- isso é o que chamamos de /ponteiro interno do arquivo/. #+begin_quote O ponteiro interno do arquivo pode ser obtido e manipulado com as funções =fseek=, =ftell= e =rewind=. #+end_quote Consequentemente, enquanto =fgets= não retornar =NULL= (no caso de um erro ou do fim do arquivo), o conteúdo do buffer =line= será impresso e o arquivo será lido novamente a partir da nova posição de seu ponteiro interno. *** Fechamento - Liberação dos recursos disponibilizados na abertura. - Tratamento adequado para os dados pendentes. - Possibilita a verificação de erros de fechamento. - Com =close= ou =fclose=, o retorno será =0= em caso de sucesso. - Com =close=, o retorno será =-1= em caso de erro. - Com =fclose=, o retorno será =EOF= em caso de erro (que também tem valor =-1=). - Com ambas as funções, a variável =errno= receberá a identificação do erro. #+begin_quote Os arquivos abertos são fechados com o fim do processo, mas é sempre preferível fechá-los explicitamente para garantir a integridade dos dados e evitar vulnerabilidades (como o vazamento de recursos). #+end_quote Para fechar o arquivo do nosso exemplo: #+begin_src c char *file = "/etc/shells"; // Abertura do arquivo... FILE *stream = fopen(file, "r"); if (!stream) { perror("Erro ao abrir o arquivo"); return EXIT_FAILURE; } // Processamento... char line[BUFSIZ]; // Buffer para acumular cada linha lida while (fgets(line, BUFSIZ, stream)) { printf("%s", line); } // Fechamento do arquivo... fclose(stream); #+end_src