Herramientas de usuario

Herramientas del sitio


unix-es

Manejo de Archivos en Unix

Unix provee una interfaz uniforme, en que todos los dispositivos se ven como archivos. Por ejemplo:

/home/lmateu/datos.txt
/bin/ls
/dev/tty1

El primero corresponde a un archivo normal en donde se almacena algún tipo de datos. El segundo es el archivo binario ejecutable del comando ls. El tercero no es realmente un archivo en disco, si no que representa un terminal conectado a través de una puerta serial. Escribir un string en /dev/tty1 significa hacer que se escriba ese string en la pantalla de ese terminal. Leer de /dev/tty1 significa esperar a que el usuario de ese terminal ingrese algo en el teclado y presione la tecla enter. De la misma forma existen pseudo archivos en el directorio /dev que representan los discos, el CD, el mouse, etc.

Al abrir un archivo en Unix, se recibe un número entero pequeño mayor o igual que cero, que se llama un file descriptor (fd). Nótese que esto es distinto de lo que ocurre al abrir un archivo en la biblioteca standard de C, en que se recibe un file pointer.

Este fd identifica al archivo abierto para todas la operaciones (leer, escribir, etc.).

Todo programa comienza con tres fds ya abiertos:

fd función
0 entrada estándar
1 salida estándar
2 salida estándar de errores

Lectura/escritura de archivos

Las operaciones básicas de entrada/salida son:

char *buf;
int fd;
size_t nbytes;
ssize_t leidos, escritos;
leidos = read(fd, buf, nbytes);

escritos = write(fd, buf, nbytes);

Cada llamada retorna el número de bytes transferidos. Aunque la operación read usualmente entrega la cantidad de bytes pedidos, por ejemplo cuando se lee desde un archivo, también puede entregar menos de lo pedido, por ejemplo cuando se lee del terminal. En el caso del write es un error que éste no sea igual al número pedido. En caso de fin de archivo se retorna un cero y, en caso de error, un -1.

Ejemplo: copiar

/* Copia stdin -> stdout */
#include <stdio.h>
#include <unistd.h>

#define BUFSIZE 8192

int main() {
  char buf[BUFSIZE];
  ssize_t n;

  while((n= read(0, buf, BUFSIZE))>0)
    write(1, buf, n);

  return 0;
}

La diferencia de esta versión con una que se hizo previamente y que recurría a fread y fwrite es que ahora se usan las funciones read y write de la API de Unix. Estos significa que no todas las implementaciones de C proveen read y write, pero todas deben ofrecer fread y fwrite.

Esta nueva versión puede ser más o menos rápida que la de fread y fwrite según el tamaño del buffer. Si BUFSIZE es 1, será terriblemente más lenta por el número de llamadas a Unix. Las llamadas a Unix son caras porque llevan asociadas una interrupción. Por otra parte si BUFSIZE es 8192, será igualmente rápida. De hecho en un sistema Unix fread y fwrite típicamente se implementan en base a read y write usando un buffer de 8192 bytes. Sin embargo para mayor eficiencia se puede usar un buffer de un megabyte logrando así la mayor tasa de transferencia entre el disco y la memoria del computador.

Ejemplo: migetchar

Las funciones read y write también se pueden usar para construir funciones de más alto nivel, como por ejemplo nuestro propio getchar:

#include <stdio.h>
#include <unistd.h>
      
int migetchar() {
  char c;   
      
  return (read(0, &c, 1)==1? c : EOF);
}         
      
int main() {
  int c;    
      
  while( (c= migetchar()) != EOF)
    putchar(c); 
  
  return 0;
}         

Como se dijo previamente, leer de a un bytes es ineficiente. La siguiente versión lee de a 8192 bytes:

#include <stdio.h>
#include <unistd.h>

int migetchar() {
  static char buf[BUFSIZ]; /* Definido en stdio.h.  Típicamente 8192 */
  static char *p=buf;
  static ssize_t n=0;

  if(n==0) { /* buffer vacio */
    n=read(0,buf, sizeof buf);
    p=buf;
  }

  return (--n>=0? *p++ : EOF);
}

int main() {
  int c;

  while( (c= migetchar()) != EOF)
    putchar(c);
  return 0;
}

Abrir y cerrar archivos

Para abrir un archivo se usa open:

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

fd= open(path, flags, perms);
/* flags puede ser O_RDONLY, O_WRONLY, O_RDWR */
/* Además se le puede superponer los flags siguientes:
   O_APPEND      ir al final antes de cada write
   O_CREAT       crea el archivo si no existe
   O_EXCL        junto con O_CREAT, falla si el archivo ya existe
   O_TRUNC       trunca el archivo a largo cero

   El parámetro perms se usa solo junto con O_CREAT
 */

para cerrar un archivo se usa close:

close(fd);

La llamada:

fd= creat(path, perms);

crea el archivo con los permisos indicados y equivale a:

fd= open(path, O_WRONLY|O_CREAT|O_TRUNC, perms);

Permisos

Se identifican 3 categorías de usuarios: el propietario (owner), el grupo (group) y otro (other). Para cada categoría existen 3 bits que indican si los usuarios en esa categoría pueden leer, escribir o ejecutar.

owner group other

r w x r w x r w x

Típicamente los permisos se expresan en octal porque un dígito octal corresponde exactamente a los 3 bits para un tipo de usuario. Ejemplos:

0755   rwx para owner, rx para los demás
0666   rw para todos

Ejemplo: copia de archivos

/* Uso: micp from to */
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

#define SIZE 8192

int main(int argc, char *argv[]) {
  int in, out;
  ssize_t n;
  char buf[SIZE];
  
  if (argc!=3) {
    fprintf(stderr, "Use: %s <from> <to>\n", argv[0]);
    exit(1);
  }
  
  if ((in= open(argv[1], O_RDONLY))==-1) {
    perror(argv[1]);
    exit(2);
  }
  
  if ((out= creat(argv[2], 0666))==-1) {
    perror(argv[2]);
    exit(3);
  }
  
  /* usando stats se pueden mantener los permisos */
  
  while ((n= read(in, buf, SIZE))>0) {
    if (write(out, buf, n)!=n) {
      perror(argv[2]);
      exit(4);
    }
  }
  
  if (n<0) {
    perror(argv[1]);
    exit(5);
  }
  
  close(in);
  close(out);
  
  return 0;
}

Cambiar los permisos de un archivo

#include <sys/stat.h>
char *path;
mod_t mode;

chmod(path, mode);

Borrar un archivo

Se usa la funcion unlink:

#include <unistd.h>
char *path;
unlink(path);

Cambiar el nombre de un archivo

Se usa la funcion rename:

#include <stdio.h>
char *oldpath, *newpath;
rename(newpath, oldpath);

Se usa la función link:

#include <unistd.h>
char *path, *newpath;
link(path, newpath);

Se usa para crear un nombre sinónimo de un archivo existente. También se puede crear un link desde el shell de comandos:

% ln datos.txt datos2.txt

En este caso datos.txt y datos2.txt se refieren al mismo archivo. Si se edita datos.txt y se introduce un cambio, datos2.txt va a reflejar ese cambio. Si se borra datos.txt, el archivo datos2.txt sigue siendo válido.

También se puede crear un link duro en un directorio distinto del directorio en donde aparece el nombre original del archivo. La única restricción es que ambos nombres se deben encontrar en directorios de la misma partición de disco.

Se usa la función symlink para crear un nombre sinónimo de un archivo existente:

#include <unistd.h>
char *path, *newpath;
symlink(path, newpath);

También se puede crear un link simbólico desde el shell de comandos:

% ln -s datos.txt datos2.txt

En este caso datos2.txt es un nombre alternativo para datos.txt. ¡Pero cuidado! La desventaja de los links simbólicos (con respecto a los links duros) es que si se borra datos.txt, datos2.txt ya no es válido.

La ventaja con respecto a los links duros es que sí se pueden crear links simbólicos con el nuevo nombre en una partición distinta de la partición en donde se ubica el nombre original.

Acceso directo

Se usa la función lseek:

#include <sys/types.h>
#include <unistd.h>

int fd;
off_t offset;
int how; /* normalmente SEEK_SET (posición absoluta), pero también
          * puede ser SEEK_CUR (relativo) o SEEK_END (fin archivo).
          */
lseek(fd, offset, how);

Ejemplos:

lseek(fd, 0L, SEEK_END);  /* para append */
       
lseek(fd, 0L, SEEK_SET);  /* rewind */

La función lseek retorna la nueva posición dentro del archivo, o -1 si hubo un error.

Manejo de Directorios

Los directorios son archivos especiales que contienen listas de archivos en su interior. Para su manejo existen funciones especiales de la biblioteca estándar de C (sección 3):

#include <sys/types.h>
#include <dirent.h>

/* Esto define un "struct dirent" que incluye un
 *   char d_name[NAME_MAX];
 */

DIR *dirp;
dirp= opendir(dirname); /* NULL en caso de error */
struct dirent *d;

d= readdir(dirp); /* entrega siguiente nombre de archivo
                     en d->d_name, retorna NULL al final */

closedir(dirp);
rewinddir(dirp); /* vuelve al inicio */

buf= getcwd(buf, size); /* guarda en buf pathname absoluto del
                          directorio actual, retorna NULL en caso de error */
chdir(name);    /* cambia el directorio actual.  Retorna !=0 en caso de error. */

Llamadas al sistema (sección 2):

#include <unistd.h>

status= chdir(path); /* 0 => OK, -1 => error */
mkdir(path, perms);
rmdir(path);
link(oldpath, newpath); /* no funciona para directorios */
unlink(path);
rename(oldpath, newpath);

Características de un archivo

Para averiguar atributos de un archivo, se usa la llamada a sistema stat:

stat(path, buf); /* buf es de tipo "struct stat *" */

La estructura stat tiene campos tales como:

st_mode             modo (=perms)
st_ino              inode
st_dev              device
st_nlink            número de links
st_uid              user id del owner
st_gid              grupo
st_size             tamaño del archivo en bytes
st_atime            dia y hora del último acceso
st_ctime            día y hora de creación
st_mtime            día y hora de la última modificación

Hay macros para “testear” el modo:

S_ISDIR             directorio?
S_ISCHR             char special device?
S_ISBLK             block special device?
S_ISREG             regular file?
S_ISFIFO            named pipe?
S_ISLNK             link simbólico?

Esta función es usada por el comando ls -l para averiguar las características de un archivo:

 $ ls -l index.html
 -rw-r--r-- 1 lmateu inv 3851 Aug 16  2007 index.html

La mayoría de estos atributos se pueden cambiar con llamadas al sistema, como por ejemplo:

chmod(path, perms);
chown(path, owner, group);
utime(path, ...);

La función utime (ver manual) permite “falsificar” el “time stamp” de un archivo. Esto lo usa el comando tar para restaurar la fecha y hora de creación y modificación de los archivos.

Ejemplo 1: Determinar si 2 nombres de archivos corresponden al mismo archivo

Dada la existencia de los links duros, el nombre de un archivo no basta para identificarlo, porque 2 nombres distintos pueden referirse al mismo archivo. La verdadera identidad de un archivo consiste en la identificación de la partición de disco en donde se ubica (campo st_dev) y el inodo asignado (campo st_ino). Usando estos 2 campos se puede programar un comando que determina si dos nombres están asociados al mismo archivo. Llamaremos a este comando linked:

Ejemplo de uso:

  % cp a.txt b.txt
  % ln a.txt c.txt
  % linked a.txt b.txt
  not linked
  % linked a.txt c.txt
  linked
  %

Esta es la implementación usando stat:

  /* linked.c */
  #include <stdio.h>
  #include <sys/stat.h>
  #include <stdlib.h>
  #include <unistd.h>

  int main(int argc, char **argv) {
    struct stat st1, st2;
    if (stat(argv[1], &st1)!=0) {
      fprintf(stderr, "can't stat %s\n", argv[1]);
      exit(1);
    }
    if (stat(argv[2], &st2)!=0) {
      fprintf(stderr, "can't stat %s\n", argv[2]);
      exit(1);
    }

    if (st1.st_dev==st2.st_dev && st1.st_ino==st2.st_ino)
      printf("linked\n");
    else
      printf("not linked\n");
    return 0;
  }

Si 2 nombres de archivos están en directorios de particiones distintas, entonces no pueden ser el mismo archivo. La identificación de una partición se almacena en st_dev. Si están en la misma partición y corresponden al mismo archivo entonces tendrán el mismo valor en st_ino.

Ejemplo 2: Listar directorios recursivamente

#include <stdio.h>
#include <stdarg.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
       
#define MAX_PATH 1000

void listdir(int indent, char *name) {
  DIR *current_dir;
  struct dirent *this_entry;
  char cwd[MAX_PATH+1];
  int i;  
  struct stat status;
      
  /* imprimimos directorio actual */
  for (i= 1; i<=indent; i++)
    printf("    ");
  printf("%s\n", name);
      
  if (stat(name, &status)!=0)
    error("Can't stat %s", name);
  if(!S_ISDIR(status.st_mode))
    return;   
      
  /* abrimos el directorio para lectura */
  if ((current_dir= opendir(name))==NULL)
    error("Can't open directory %s", name);
      
  if (getcwd(cwd, MAX_PATH+1)==NULL)
    error("Can't get cwd");
  if (chdir(name)!=0)
    error("Can't chdir");
      
  while ((this_entry= readdir(current_dir))!=NULL) {
    if (strcmp(this_entry->d_name, ".")==0 || 
        strcmp(this_entry->d_name, "..")==0)
      continue;   
      
    listdir(indent+1, this_entry->d_name);
  }         
      
  closedir(current_dir);
  chdir(cwd);
}       

int main() {
  listdir(0, ".");
  return 0;
}

Ejercicio

  1. Modifique el programa de más arriba de modo que entregue el tamaño del archivo. Use stat para ello.
  2. Modifique el programa mi_cp para que no haga la copia cuando el archivo de destino ya existía.
unix-es.txt · Última modificación: 2016/11/15 09:15 por lmateu