7

I/O de baixo-nível em ficheiros

 

 

 

 

Sumário:

 

  • Introdução
  • Funções de entrada/saída de baixo-nível

 

 

 

 

 

Referência bibliográfica:W. Stevens. Advanced Programming in the UNIX Environment. Addison-Wesley, 1992 (cap.3)

 

Introdução

 

 

 

Este capítulo. Objetivos:

·        Descrição das funções I/O de baixo nível (system calls)

·        Descrição das funções sem entreposição (unbuffered I/O) por contraste com as funções I/O com entreposição (buffered I/O) já estudadas em semestres anteriores. Veja-se capítulo anterior para uma breve revisão.

·        Cada read ou write invoca uma chamada ao sistema no kernel.

·        Mostrar como os ficheiros são partilhados entre vários processos e as estruturas de dados envolvidas no kernel.

·        Usar MANUAL  man para obter informação.

  Secção 1 (bash Shell commands)

  Secção 2 (System Calls)

  Secção 3 (C standard library)

 

 

 

 

 


Descritores de ficheiros

 

A entrada/saída de baixo-nível é feita sem entreposição (i.e. unbuffered). Isto significa que qualquer operação I/O a um ficheiro é feita diretamente. Equivalentemente, é escrito ou lido um bloco de bytes. Portanto, o ficheiro é considerado como um ficheiro binário (sem formatação), e não de texto (onde os bytes são interpretados em algum sistema de codificação, p.ex ASCII). Para o “kernel”, todos os ficheiros abertos são identificados por descritores. Um descritor é simplesmente um inteiro não-negativo. Por exemplo, quando se abre um ficheiro ou se cria um novo, o kernel devolve um descritor desse ficheiro ao processo em causa. Cada processo tem portanto uma tabela (vector) de descritores de ficheiros

 

Por convenção, o descritor 0 identifica a entrada estandardizada (standard input), o descritor 1 identifica a saída estandardizada (standard output) e o descritor 2 identifica o erro estandardizado (standard error).

Estes descritores 0,1 e 2 podem ser substituídos pelas constantes simbólicas STDIN_FILENO, STDOUT_FILENO e STDERR_FILENO, respectivamente, em aplicações POSIX.

 

Funções I/O de baixo nível

 

Função open   A função para abrir um ficheiro é a seguinte:

 

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

 

int open(char *filename, int access);

Retorna: descritor se OK, -1 em caso de erro

 

O argumento access descreve o tipo de acesso (ver <fcntl.h>); por exemplo, o valor O_RDONLY (open read only = abertura em modo apenas de leitura), O_APPEND, O_CREAT, O_EXCL, O_RDWR, O_WRONLY, etc. Estes acessos podem ser combinados através de operadores lógicos. Para mais informação, veja-se o manual on-line (man 2 open).

 

Função creat    A função para criar um ficheiro é a seguinte:

 

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

 

int creat(const char *filename, int perms);

Retorna: descritor aberto para escrita se OK, -1 em caso de erro

 

O argumento perms contém as bandeiras (flags) de permissão. Para mais informação: (man 2 creat)

 

Função close    A função para fechar um ficheiro é a seguinte:

 

#include <unistd.h>

 

int close(int filedes);

Retorna: 0 se OK, -1 em caso de erro

 

Quando um processo termina, todos os ficheiros abertos são automaticamente fechados pelo kernel, não sendo necessário fechá-los expressamente. Para mais informação, veja-se o manual on-line (man close).

Creat - Permission flags

As constantes de permissão estão no ficheiro de cabeçalho <sys/stat.h>. Podem consultar o ficheiro stat.h no (por defeito) directório /usr/include/sys. As flags de permissão podem ser combinadas com o operador binário OR (|). Alguns dos constantes são dados na tabela em baixo.

 

S_IRUSR

User read permission

S_IWUSR

User write permission

S_IXUSR

User execute permission

S_IRGRP

Group read permission

S_IWGRP

Group write permission

S_IXUSR

Group execute permission

S_IROTH

Other read permission

S_IWOTH

Other write permission

S_IXUSR

Other execute permission

 

Uma utilização típica é de definir um constante que represente a criação dum ficheiro com permissões de leitura e escrita para o próprio que será usado depois numa chamada a função creat.

              

               #define  COMMON_FILE_MODE  ( S_IRUSR | S_IWUSR )

creat("novoF.bin", COMMON_FILE_MODE  );

 

Função read  Esta função permite ler a partir dum ficheiro aberto. Assim:

 

#include <unistd.h>

 

ssize_t read(int filedes, void *buff, size_t nbytes);

Retorna: número de bytes lidos, 0 se EOF, -1 em caso de erro

 

Se a leitura é feita com sucesso, a função devolve o número de bytes lidos. Se o fim do ficheiro é encontrado, a função devolve o valor 0 e em caso de erro o valor de -1.

Há vários casos em que o número de bytes lidos poderá ser inferior à quantidade pedida em nbytes:

§  Se o EOF é atingido antes de atingir o número de bytes pedidos. Por exemplo, se houver só mais 30 bytes para ler quando tinham sido solicitados 100, a função read só devolve aqueles 30 bytes.

§  Quando se lê a partir dum terminal, só se lê normalmente uma linha de cada vez.

§  Quando se lê a partir duma rede, o tamanho do buffer de rede pode ser menor que a quantidade de bytes pretendida.

§  Nalguns dispositivos baseados em registos (record-oriented devices), tais como os de fita magnética, só retornam um registo de x bytes de cada vez.

 

Para mais informação, veja-se o manual on-line (man 2 read).

 

Função write  Esta função permite escrever para um ficheiro aberto. Assim:

 

#include <unistd.h>

ssize_t write(int filedes, const void *buff, size_t nbytes);

Retorna: número de bytes escritos se OK, -1 em caso de erro

 

O valor devolvido pela função é usualmente igual ao valor do argumento nbytes; caso contrário, é porque ocorreu um erro, por exemplo devido a ter esgotado a capacidade física onde reside o ficheiro, o disco rígido por exemplo, ou o tamanho máximo admissível para um ficheiro dum dado processo.


Exemplos e Exercícios

Exemplo 7.1:

 

O programa em baixo ilustra a criação dum ficheiro e a escrita neste ficheiro dum vector de dez inteiros.

 

Ler, Escrever, Compilar e Executar o programa.

 

/* ex71.c : compilar cc -Wall -c ex71.c -o ex71 */

#include <sys/stat.h>

#include <unistd.h>

#include <fcntl.h>

int main()

{

  int  fd, i, vec[10];

 

  for (i=0;i <10; i++)

           vec[i]=i+512;

 

  fd = creat("test.bin", S_IRUSR| S_IWUSR ) ;

 

  write(fd, vec , sizeof(int)*10);

 

  return (0);

}

 

Ver os detalhes do ficheiro, em particular o tamanho e tipo,  criado com os comandos:

·        ls -l test.bin

·        file test.bin

·        cat test.bin (para tentar visualizar o ficheiro)

 

Experimentar com o comando  Octal Dump”,  od test.bin”.  Experimentar usar

·        a opção –i do “octal dump para visualizar grupos de 4 bytes como inteiros

·        od -s” para visual como “short ints” e

·        od -b” para para cada byte (nota por exemplo que 513 = 1 + 2*256)

·        ou qualquer combinação, por exemplo “od -bi test.bin” ou “od -bc ex71.c | head +2”

 

Exercício 7.1:

 

(i) Escreva um programa (em C) para ler todo o conteúdo do ficheiro “test.bin” criado no exemplo 7.1 para um vector usando as funções de open() e read() e depois imprimir no ecrã os valores dos inteiros lidos (512 513 ….521 etc.) usando printf()

 

Vai precisar da função open() com opção O_RDONLY e a função read()

 

(ii) Altere o programa para usar “shorts” em vez de “ints” e ver o resultado !

 

 

(iii) Experimente a utilização do programa strace (system call trace) na execução dos dois programas.

 

·        O comando é por exemplo :  $ strace ./ex71

·        Deverá tentar perceber porque o creat() devolve o descritor de ficheiro com valor de três (3)

 

 


Exemplo 7.2:

O programa seguinte ilustra a cópia da entrada estandardizada (teclado) para a saída estandardizada (ecrã). Compilar e Executar o programa.

 

/* ex72.c : compilar cc -Wall -c ex72.c -o ex72 */

#include <stdio.h>

#include <sys/types.h>

#include <unistd.h>

#include <errno.h>

#define BUFFSIZE 128

void ioCopy (int IN, int OUT);

int main(){

    ioCopy (STDIN_FILENO, STDOUT_FILENO);   // 0 , 1

    return(0);

}

void ioCopy (int IN, int OUT)  //no error reporting
{
 

   int n;

   char buf[BUFFSIZE];

   while ( ( n = read (IN, buf, BUFFSIZE)) > 0)

   {  

      if (write (OUT, buf, n) != n)

         perror("Erro de escrita!\n");

   }

   if (n < 0)

     perror("Erro de leitura!\n");

}

 

·        Compilar  cc  –Wall exemplo72.c –o exemplo72

·        Executar usando o teclado como input     exemplo72     (lembrar: ctrl-d para EOF)

·        Executar usando redireccionamento        exemplo72 < exemplo72.c

 

Exercício 7.2:

 

Baseado no programa anterior escreva um programa principal para copiar o conteúdo dum ficheiro para outro usando I/O de baixo nível. Os nomes dos dois ficheiros são fornecidos como argumentos do programa. A cópia do ficheiro é feita por blocos de 128 bytes.

 

            Por exemplo ./exercicio72   exercicio72.c   backup.c

 

Deverá verificar todos os casos de erro e produzir mensagens de error apropriado

Dica: Utilizar a função perror().

 

Um esboço do programa é dado em baixo

 

main( int argc, char *argv[] )

{

    int fdIn = open( argv[1]  ..);

    if erro ..

    int fdOut = creat( argv[2] ..… )

    if erro …

    ioCopy( fdIn, fdOut);

}

 


Função lseek

 

Esta função permite alterar a posição relativa (offset), a partir da qual são feitas outras operações sobre um ficheiro. Assim:

 

#include <sys/types.h>

#include <unistd.h>

 

off_t lseek(int filedes, off_t offset, int whence);

Retorna: novo file offset se OK, -1 em caso de erro

 

Qualquer ficheiro aberto tem um offset associado. Um offset é um inteiro não-negativo que mede o número de bytes a partir dum ponto do ficheiro. As operações de leitura e escrita num ficheiro são normalmente feitas no offset corrente do ficheiro, o que faz com que o offset seja incrementado pelo número de bytes lidos ou escritos. Por defeito, este offset é inicializado a 0 quando um ficheiro é aberto, a não ser que a opção O_APPEND seja especificada. A interpretação do offset depende do valor do argumento whence:

§  Se whence==SEEK_SET, então o offset é igual ao número de bytes em offset contados a partir do início do ficheiro.

§  Se whence==SEEK_CUR, então o offset é igual ao seu valor corrente acrescentado do valor em offset. O valor do argumento offset pode ser positivo ou negativo.

§  Se whence==SEEK_END, então o offset é igual ao tamanho do ficheiro acrescentado do valor em offset. O valor do argumento offset pode ser positivo ou negativo.

 

Para mais informação, veja-se o manual on-line (man lseek).

 

Exemplo 7.3:

 

O programa abaixo ilustra a utilização da função lseek() para ler os últimos dois valores do vector escrito no exemplo 7.1

 

int main(void)

{

        int fd, vec[2];

        fd = open("test.bin", O_RDONLY ) ;

        lseek(fd, sizeof(int)*8,SEEK_SET)

        read(fd, &vec[0] , sizeof(int)*2);

        printf("Ultimos valores do vector %d %d\n", vec[0],vec[1]);

        return (0);

}

 


Exemplo 7.4:

 

O programa abaixo ilustra a utilização da função lseek() e como se pode criar um ficheiro com um buraco, isto é, sem dados pelo meio. De facto, o offset dum ficheiro pode ser maior do que o tamanho actual do ficheiro. Quaisquer bytes num ficheiro que não tenham sido escritos são lidos como 0.

 

#include <sys/types.h>  <sys/stat.h> <fcntl.h> <unistd.h> <stdio.h>

 

#define FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) //permissões típicos

 

char buf1[] = "abcdef", buf2[] = "ABCDEF";

 

int main()

{

        int fd;

        if ((fd = creat("file.hole", FILE_MODE)) < 0)

                printf("Erro na criacao de ficheiro!\n");

 

        if (write(fd, buf1, 10) != 10)

                fprintf(stderr,"Erro de escrita em buf1!\n");

        // offset now = 10

 

        if (lseek(fd, 40, SEEK_SET) == -1)

                fprintf(stderr,"Erro no posicionamento!\n");

        // offset now = 40

 

        if (write(fd, buf2, 10) != 10)

                fprintf(stderr,"Erro de escrita em buf2!\n");

        // offset now = 50

 

        return(0);

}

 

Compilar e Analisar os avisos do compilador:   bash$ cc -Wall exemplo7-4.c

 

A execução do programa fornece o seguinte:

 

bash$ ./a.out

bash$ ls -l file.hole

-rw-r--r--    1 a15583   alunos         50 Mar 30 18:14 file.hole

 

bash$ cat file.hole

bash$ od -c file.hole

0000000   a   b   c   d   e   f  \0   A   B   C  \0  \0  \0  \0  \0  \0

0000020  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0  \0

0000040  \0  \0  \0  \0  \0  \0  \0  \0   A   B   C   D   E   F  \0  \0

0000060  \0  \0

 

O comando od permite ver o conteúdo do ficheiro file.hole. A flag –c serve para escrever o conteúdo do ficheiro em caracteres. Podemos ver que 30 bytes não foram escritos no meio do ficheiro, sendo vistos a 0. Os 7 dígitos no início de cada linha do ouput do od é o offset em octal p.ex 0000020 -> byte position 16

 

Nota que neste caso as variáveis buf1 e buf2 estavam contiguas (lado ao lado) em memoria. Desta maneira a escrever 10 bytes a partir do buf1 também escreveremos 3 bytes (ABC) do buf2.

Isto, variáveis contiguas, pode não ser o caso quando execute o programa.


Exercícios

 

Exercício 7.3: Fácil

 

Escreva um programa para pedir o índice do inteiro a ler que está no ficheiro “test.bin” criado no exemplo 7.1 e depois imprimir no ecrã só o valor neste índice.

Deverá utilizar a função lseek()  para posicionar o ficheiro no sitio correto e a função read() para ler apenas um inteiro (4 bytes) e a função printf() para imprimir o valor formatado no ecrã. Pode fazer em ciclo até que o utilizador introduza um indice negativo.

 

Exemplo de Execução

 

Introduza um índice :  1

Valor no índice 1 é 513

Introduza um índice :  4

Valor no índice 1 é 516

Introduza um índice :  -1

Terminação

 

Exercício 7.4: Fácil

 

Escreva um programa que utilize I/O de baixo-nível que determine o número de linhas dum ficheiro de texto.

 

Exercício 7.5: Difícil

 

Escreva um programa que utilize I/O de baixo-nível para imprimir as últimas n>0 linhas dum ficheiro de texto – numa maneira eficiente.

 

Uma solução possível implica saltar para o fim do ficheiro com lseek e a recuar K bytes. A seguir ler os K bytes e indo para traz tentar identificar a posição da nsima ‘\n’

 

 

Exercício 7.6: Difícil

 

Escreva um programa eficiente que utilize I/O de baixo-nível para comparar dois ficheiros e que escreva no ecrã as linhas que são diferentes.

 

Uma solução possível é ler um bloco de Max_Linha bytes (255) de cada ficheiro para um buffer, identificar os fins de linha (e reposicionar o file offset com lseek) e imprimir para a ecrã as linhas se forme diferentes.

 

Faça uma solução (esta versão será muito mais simples) usando a biblioteca do alto nível, fgets(..)

 

Exercício 7.7:

 

Experimente a utilização do programa strace (system call trace) na execução dos programas.