PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

SEGUNDA SESIÓN


  1. Uso de búferes para pasar o recibir información.

    Muchas llamadas al sistema requieren que se le pase o devuelven un bloque de datos. Habitualmente, estos bloques de datos se declaran en los prototipos de las llamadas al sistema como punteros a carácter (char *).

    En todos los casos, es necesario no sólo pasar el puntero a la función, sino que este puntero señale a una zona de la memoria válida. Tenemos varias opciones para conseguirlo:



    Las posibilidades para usar el búfer son dos:
    1. Pasar datos a la llamada al sistema:

      Un ejemplo lo tenemos en la llamada al sistema write, cuyo prototipo es:
      ssize_t write(int fildes, const void *buf, ssize_t nbyte); 
      El segundo parámetro es un puntero a la zona de memoria donde se encuentran los datos que queremos escribir en un fichero y el tercer parámetro es el número de bytes que queremos escribir. El tercer parámetro es necesario porque el puntero sólo contiene la dirección de memoria donde se almacenan los datos, no la longitud. Para comunicar la longitud de los datos a la función, se usa nbyte. Nótese que el tipo es void *. Esto indica que el compilador admitirá cualquier tipo de puntero ahí. Además viene precedido por la palabra clave const, que indica que la función no modificará los datos que se le pasan en el búfer.

      Si la llamada al sistema tiene como parámetro una cadena de caracteres, no es necesario pasarle la longitud de los datos. La función la sabrá porque la cadena de caracteres acaba cuando encuentre el carácter 0 ('\0'). Ejemplo de función con cadena de caracteres como parámetro:
      int unlink(const char *path); 
    2. Recibir datos de la llamada al sistema:

      Las llamadas al sistema que devuelven datos en un búfer que nosotros le pasemos requiren que nosotros le especifiquemos una dirección de memoria mediante un puntero, le reservemos la memoria (bien implícita o explícitamente) y le indiquemos cuál es el tamaño de la zona que le hemos reservado. Esto es así para que la llamada no se pase escribiendo en el búfer y sobreescriba otras áreas de datos del programa. Un buen ejemplo de llamada al sistema que devuelve datos en un búfer previamente reservado es:
      ssize_t read(int fildes, void *buffer, size_t nbyte); 
      Obsérvese cómo el buffer ahora no es const como en write pues va a ser modificado. Como ejemplo de uso, para leer un máximo de 50 bytes del teclado, podíamos usar un fragmento como este:
      char buffer[50];
      [...]
      read(0, buffer, 50); 
      Si en lugar de este fragmento, usáramos este otro:
      char *buffer;
      [...]
      read(0, buffer, 50); 
      el compilador no se quejaría pues el tipo de los parámetros es el correcto, pero al ejecutarlo, la llamada al sistema escribiría lo leído en una zona de memoria prohibida y daría el error Bus error o el programa empezaría a comportarse de forma inestable e inesperada.

      Las llamadas al sistema que devuelven datos en un búfer pasado por el programador suelen devolver el tamaño de los datos devueltos, es decir, qué parte del búfer que le pasamos hay que considerar. Es el caso de la función read, que devuelve el número de bytes leídos del fichero y almacenados en el búfer.


  2. Tipos de datos específicos.

    Algunas llamadas al sistema usan tipos de datos específicos y que se salen de los habituales. Como ejemplo, tomemos la llamada read de nuevo:
    ssize_t read(int fildes, void *buffer, size_t nbyte); 
    Si leemos la página de manual de read, veremos que la función devuelve el -1 si hubo un error u otro entero mayor o igual que 0 si no lo hubo y que especifica el número de caracteres que se han devuelto en el búfer. ¿No es esto, al fin y al cabo, un entero? ¿Por qué el tipo devuelto por la función es ssize_t? ¿Qué tipo de dato se esconde detrás de él?

    Para responder a estas preguntas, tendremos que ir a los ficheros de cabecera de read. Partiendo de /usr/include/unistd.h, llegamos a /usr/include/sys/types.h, fichero donde se se encuentran definidos muchos tipos de dato especiales. En ese fichero se puede leer:
    [...]
    #  ifndef _SSIZE_T
    #    define _SSIZE_T
            typedef long ssize_t;
    #  endif /* _SSIZE_T */
    [...]       
    Así que ssize_t no es más que un long. ¿Por qué, entonces, no se define read de modo que devuelva un long? Alguna razón:

  3. Operaciones con campos de bits.

    Algunas llamadas al sistema, como es el caso de open, tienen muchas opciones no excluyentes de funcionamiento. open sirve para abrir un fichero. Pero ese fichero se puede abrir para lectura, para escritura, para añadir, para borrarlo y reescribirlo, etc. y estas opciones pueden no ser excluyentes, es decir, puedo especificar más de una a la vez.

    Las llamadas al sistema de este tipo suelen especificar las opciones que se desean mediante un campo de bits. Veamos el prototipo de open:
    int open(const char *path, int oflag, ... /* (mode_t mode) */ ); 
    Los puntos suspensivos y los paréntesis indican que el último parámetro es opcional. En el segundo parámetro (oflag), es donde se van a dar las opciones de apertura del fichero. Se trata de un entero que guarda en su interior un campo de bits. Un campo de bits es un entero que expone su significado cuando se expresa en binario. Cada dígito binario del campo expresará la presencia (1) o ausencia (0) de una propiedad. En el caso del campo oflag, el significado de cada bit es:

    Por ejemplo, si queremos abrir el fichero jarabes.txt para lectura y escritura y para añadir a lo que ya contiene, el campo de bits quedaría así:

    Esto nos da el número binario 1010, que en decimal es el 10. Por lo tanto, la orden open que habría que dar sería:
    open("jarabes.txt",10);
                
    Como este modo de trabajar es bastante complicado y el resultado resulta un poco críptico, en los ficheros de cabecera del compilador de C existen unas macros para ayudar en la especificación de las opciones de open. Algunas de estas macros y su valor en binario se dan en esta tabla:

    Macro Valor (binario)
    O_RDONLY 0
    O_WRONLY 1
    O_RDWR 10
    O_APPEND 1000
    O_CREAT 100000000
    O_TRUNC 1000000000
    O_EXCL 10000000000
    O_NONBLOCK 100000000000000

    Para poder especificar varias de estas macros en la llamada al sistema, se usa el operador OR entre ambas. Veamos que funciona con el ejemplo anterior:


    Por lo tanto, la orden anterior también se puede escribir en C como:
    open("jarabes.txt", O_RDWR | O_APPEND);
                


  4. Tabla de ficheros.

    Todos los procesos (programas en ejecución) de UNIX tienen una tabla con la información de los ficheros que tienen abiertos. Cuando el programa abre un fichero nuevo, el Sistema Operativo localiza una entrada libre de la tabla y deposita ahí la información del nuevo fichero. Al índice de la tabla donde se almacena la información se le denomina descriptor de fichero. Es el número que devuelve la llamada al sistema open y que servirá para referirnos al fichero en todas las operaciones que realicemos con él.

    Hay tres descriptores de fichero siempre abiertos en un programa en C al comienzo de su ejecución: el 0, el 1 y el 2. Corresponden a la entrada estándar, la salida estándar y el error estándar. Estarán, en condiciones normales conectados con el teclado, la pantalla y la pantalla, respectivamente:


    Para cambiar este comportamiento por defecto, existen los caracteres de redirección de la shell. Así, para que la salida estándar de la orden ls -l vaya al fichero listado.txt en lugar de a la pantalla, hay que teclear:
    ls -l > listado.txt 
    Se pueden encadenar comandos de modo que la salida estándar de uno vaya a la entrada estándar del siguiente mediante el carácter de tubería (|). Así, si queremos que la salida de la orden ls -l efectúe una pausa cada vez que pasa de página podemos combinarlo con la orden more así:
    ls -l | more


  5. Ejemplo: escritura de "Hola mundo" en un fichero mediante llamadas al sistema.

    #include <fcntl.h>
    #include <string.h>
    #include <unistd.h>
    [...]
    int fd;
    char mensaje[]="Hola, mundo\n";
    [...]
    fd=open("hola.txt",O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd==-1) /* Error */ ...
    [...]
    if (write(fd,mensaje,strlen(mensaje))==-1) /* Error */ ...
    [...]
    close(fd);
    [...]


  6. Punteros de acceso a los ficheros.

    Todos los ficheros abiertos tienen un indicador para saber por dónde se va en su lectura o escritura. Es el puntero de acceso al fichero. El tipo de este puntero es off_t, pero en realidad en un número entero. Este número indica cuál es la próxima posición del fichero que se va a leer o escribir. La primera posición del fichero tiene el valor 0, la última posición del fichero tiene un valor igual al tamaño del fichero. Para poder saber o modificar la posición actual de un fichero, se usa la llamada al sistema lseek, cuyo prototipo es:
    off_t lseek(int fildes, off_t desplazamiento, int dOnde); 
    Si queremos cambiar la posición del puntero de acceso, realizamos la llamada al sistema con el descriptor del fichero cuyo puntero queremos modificar. Para modificar el puntero se indica un desplazamiento (segundo parámetro) tanto positivo como negativo, con respecto a uno de tres puntos de referencia (tercer parámetro):
    Tercer parámetro Punto de referencia
    SEEK_SET Comienzo del fichero
    SEEK_CUR Punto actual
    SEEK_END Fin del fichero

    Algunos ejemplos:
    1. Situar el puntero en la posicion 1000:
      lseek(fd,1000,SEEK_SET); 
    2. Dejar el puntero como está:
      lseek(fd,0,SEEK_CUR); 
    3. Hacer retroceder el puntero tres posiciones:
      lseek(fd,-3,SEEK_CUR); 
    4. Situar el puntero al final del fichero:
      lseek(fd,0,SEEK_END); 

    Para saber la posición del puntero, basta con saber que la función devuelve la posición del puntero después de haberlo desplazado. Con esto podemos además saber la longitud de un fichero.

  7. Bloqueo de ficheros

    Bloquear el acceso a parte de un fichero consiste en evitar que otro proceso pueda acceder a esa parte del fichero mientras lo estemos usando. Para hacerlo, se ha de usar la llamada al sistema:
    #include <unistd.h>
    int lockf(int fildes, int funciOn, off_t tamaNo);
    
    El primer parámetro es un descriptor del fichero que queremos bloquear. funciOn puede ser F_LOCK o F_ULOCK para bloquear o desbloquear el fichero respectivamente. El bloqueo se produce desde la posición actual del puntero del fichero abarcando una longitud expresada por el parámetro tamaNo. Si tamaNo es cero, se bloquea/desbloquea hasta el final del fichero. También existe la posibilidad de consultar si tenemos acceso, sin bloquearse o bloqueándose si no tenemos acceso. Podéis consultar el manual.

    Supongamos que queremos bloquear el acceso a un fichero mientras escribimos. La secuencia sería: abrir el fichero, bloquearlo con lockf(fd,F_LOCK,0), escribir, mover el puntero al principio con lseek y desbloquearlo con lockf(fd,F_ULOCK,0). Fijaos bien en que hay que mover el puntero de nuevo al principio del fichero, pues se ha movido al realizar la escritura.

    Considerad también que estos bloqueos son bloqueos colaborativos. Es decir, si un proceso bloquea parte de un fichero, esto no impide que otro proceso abra el fichero, lea o escriba en dicha parte del fichero. Lo único que impide es que otro proceso pueda bloquear esa misma parte del fichero. Tenedlo en cuenta.

  8. Primera práctica.

    Esta es una práctica que no hay que entregar. El programa admitirá el nombre de un fichero como argumento. Abrirá el fichero, si puede, e irá imprimiendo por la salida estándar dicho fichero cambiando todas las 'a' por 'o'. Haced una comprobación no paranoica de errores (basta con usar bien perror). Solamente usad llamadas al sistema (no usar printf ni fopen) en la parte principal de la práctica.

    Soluciones y comentarios.

  9. Ficheros proyectados en memoria.

    Un fichero proyectado en memoria es, como su nombre indica, un fichero cuyo contenido aparece momentáneamente en una zona de memoria principal, de modo que podemos allí modificarlo. La llamada al sistema que permite hacer esto es:
    void *mmap(void *addr, size_t longitud, int prot, int flags, int fildes, off_t desplazamiento); 
    Lo mejor es dejar que el sistema operativo decida en qué posición de memoria virtual va a realizar la proyección indicando 0 como primer parámetro. La posición donde se realizará la proyección nos será devuelta por la función, si la llamada tiene éxito.

    El segundo parámetro es el tamaño de la zona que se va a proyectar. El tercero, indica el tipo de acceso que se va a realizar, que tiene que ser concordante con el modo en que se abrió el fichero. Véase el resumen de las llamadas de esta sesión.

    El cuarto, dejadlo siempre a MAP_SHARED. El quinto es un descriptor del fichero que queremos proyectar (lo conseguís con open. El último es el desplazamiento, con respecto al comienzo del fichero, donde empieza la zona que queréis proyectar.

    No hay que olvidar deshacer la proyección con munmap y cerrar el fichero cuando se acabe.

  10. Segunda práctica.

    Esta segunda práctica consiste en un programa que admite el nombre de un fichero por la línea de órdenes de la shell. Esta práctica no es para entregar. Efectúa una proyección en memoria de dicho fichero y sustituye todas sus 'a' por 'o'. Deshace la proyección y cierra el fichero. Comprobación de errores no paranoica. ¿Qué habría que usar para hacer esto si no se dispusiera de ficheros proyectados en memoria en el sistema operativo? ¿Qué ventajas suponen estos ficheros?

    Soluciones y comentarios.

  11. Devolución de datos en variables y estructuras.

    Algunas llamadas al sistema devuelven información en estructuras de C. Es el caso de la familia de llamadas stat, que dan información acerca de los ficheros. El prototipo de stat es el siguiente:
    int stat(const char *path, struct stat *buf); 
    Como ya dijimos en el apartado de búferes, la función requiere un puntero a una zona de memoria previamente reservada. Los pasos que hay que seguir para obtener datos de la llamada al sistema son:

    1. Declarar una variable del tipo de la estructura que requiere la función. Esto reservará automáticamente el espacio de memoria para almacenar los valores de la estructura.
      struct stat ss;     
    2. Pasarle a la función la dirección de memoria que se ha reservado para los datos de la estructura mediante el operador & de C:
      stat("MiKasa.txt",&ss); 
    3. Acceder a los datos de la estructura. Por ejemplo:
      printf("La longitud de \"MiKasa.txt\" es %ld.\n", ss.st_size); 

    Es completamente incorrecto lo siguiente y se producirá un acceso a memoria ilegal en tiempo de ejecución, aunque el compilador no proteste en tiempo de compilación:
    struct stat *ssp;
    [...]
    stat("MiKasa.txt",ssp);
    printf("La longitud de \"MiKasa.txt\" es %ld.\n", ssp->st_size); 
    Todo lo dicho también es de aplicación al caso de variables normales. El paso por referencia en C, ya que no existe per se, se consigue pasando un puntero por valor que apunte a la zona de la memoria donde se almacenan los datos de la variable.

  12. Getdirentries (obtención de los ficheros contenidos en un subdirectorio).


    Muchos programas pueden requerir saber el nombre de los ficheros que hay en un subdirectorio. Para ello, existe en HPUX la llamada al sistema getdirentries. En Linux, esta llamada no es compatible. El funcionamiento de la llamada es un tanto complejo, fundamentalmente debido a que el tamaño de los subdirectorio puede ser arbitrariamente grande y puede ser necesario llamarla en varias ocasiones hasta conseguir el nombre de todos los ficheros del subdirectorio.

    Para conseguir el nombre y número de inodo de los ficheros de un directorio hay que llamar repetidamente a getdirentries hasta que la función nos devuelva un 0, con lo que indica que ya nos ha pasado el nombre de todos los ficheros del directorio (puede también devolver -1, lo que indicaría un error).

    Por cada vez que llamamos a getdirentries, la función nos devolverá información de unos cuantos ficheros. Observemos el prototipo de la función:
    int getdirentries(int fildes, struct direct *buf, size_t nbytes, off_t *base_p);           
    Los parámetros de la llamada al sistema son:
    1. fildes será un descriptor de fichero del subdirectorio que queremos examinar. Lo obtendremos con open. Lo abriremos como si se tratase de un fichero normal, pues al fin y al cabo, un subdirectorio es un fichero normal, con un formato y un contenido específicos.
    2. buf, aunque parece una estructura donde la función devolverá algo, es una excepción a lo visto más arriba. Se trata de un búfer de caracteres cuyo tamaño ha de ser un múltiplo del tamaño del bloque del sistema de ficheros. Esta información se puede obtener haciendo un stat sobre el directorio.
    3. nbytes es el tamaño del búfer anterior, para que getdirentries no se pase escribiendo fuera del área reservada para dicho búfer.
    4. En base_p, la función devolverá la posición del puntero de fichero del directorio después de la llamada. Es un valor no muy relevante, pero aún así hay que proporcionar una variable de tipo off_t donde almacenar dicho valor.

    Veamos con un ejemplo cuál es la información que aparece en el búffer después de la llamada para el directorio siguiente (la función nos devolvería 29):
    <T>/usuarios/gyermo/PRIVADO/SO/PRACTS/CANCIÓN$ ls -ia | cat
    360000 ..
    158446 .
    158447 Enrique
    158448 Ana  


    Como consejo práctico, para obtener la información que contiene el buffer, que habremos declarado como char *, podemos usar un puntero auxiliar de tipo struct direct. Con ese puntero auxiliar podemos acceder una a una a cada entrada de directorio del búfer. Para incrementarlo y pasar a la siguiente, tenemos que fijarnos en el campo d_reclen, que viene expresado en bytes. Hay que tener cuidado en no incrementar el puntero auxiliar directamente, sino hacer una conversión a char * antes, para que los incrementos del puntero vengan expresados en bytes y no en unidades del tamaño de la estructura. La línea de código que pasa a la siguiente entrada sería algo así:
    struct direct *sdp;
    [...]
    sdp=(struct direct *) ((char *)sdp+sdp->d_reclen); 
    Se debe continuar leyendo del búfer hasta que se supere el tamaño de lo escrito por getdirentries que es el valor que nos devolvió la función.


    La llamada al sistema equivalente en Solaris a getdirentries se llama getdents. Su prototipo es:
    #include <sys/dirent.h>
    
    int getdents(int fildes, struct dirent *buf, size_t nbyte);
    Es completamente igual a su hermana de HPUX salvo que la función carace del último parámetro que, por otra parte, tampoco es que sirviera para mucho.
    También devuelve los mismos valores que getdirentries y con idéntico significado. La estructura struct dirent difiere, por otro lado, ligeramente de struct direct:
    struct dirent
       {ino_t           d_ino;
        off_t           d_off;
        unsigned short  d_reclen;
        char            d_name[1];
        };
    d_ino equivale a d_fileno. d_reclen y d_name se llaman igual en las dos. Y d_off tiene un significado no especificado que hay que respetar. El tamaño máximo de un nombre de fichero es MAXNAMLEN caracteres.


    En el caso del Linux que tenéis en clase, la llamada al sistema también es getdents. Podéis consultar su página de manual con man 2 getdents. Lo curioso del asunto es que en la presente versión de Linux no basta en incluir los ficheros de cabecera y hacer la llamada al sistema dentro de vuestro código. Hay que efectuar la llamada al sistema a través de la interfaz syscall, como se muestra en el código de ejemplo que sigue:
    #include <fcntl.h>        /* open */
    #include <sys/stat.h>     /* open */
    #include <sys/types.h>    /* open */
    #include <sys/syscall.h>  /* syscall */
    #include <unistd.h>       /* syscall,getdents */
    #include <stdlib.h>       /* malloc */
    #include <stdio.h>        /* printf */
    #include <linux/types.h>  /* getdents /*
    #include <linux/dirent.h> /* getdents /*
    #include <linux/unistd.h> /* getdents /*
    
    /* Macro para que syscall "parezca" getdents.        */
    /* NOtese que __NR_getdents es:                      */ 
    /*     subrayado, subrayado, NR, subrayado, getdents */
    #define getdents(a,b,c) syscall(__NR_getdents,a,b,c)
    
    int main(void)
      {int fd; int ret; struct dirent *sdp;
    
       sdp=malloc(2048); /* En realidad un mUltiplo del bloque */
       fd=open(".",O_RDONLY);
       if (fd==-1)
         {perror("open"); return 1;}
    
       ret=getdents(fd,sdp,2048);
       if (ret==-1)
         {perror("getdents"); return 1;}
    
       printf("%d %d %d %s\n",sdp->d_ino,sdp->d_off,sdp->d_reclen,sdp->d_name);
       return 0;
       }
    En el código se ha definido la macro getdents para que podáis usar la llamada como si realmente estuviera definida en vuestro código. Es posible que esta situación se corrija en otras distribuciones o en el futuro. Cualquier otra llamada al sistema puede ser invocada con la interfaz syscall, a bajo nivel.

  13. Tercera práctica.


    Esta es una práctica que no hay que entregar. Esta práctica vale doble en cuanto al mejor que la entregue. El programa que hay que realizar no admite ningún argumento por la línea de órdenes de la shell. El programa listará el nombre de todos los ficheros del directorio actual que no posean más de un enlace duro. Hacer una comprobación no paranoica de errores.

    Soluciones y comentarios.


    El programa que hay que realizar no admite ningun argumento por la línea de órdenes de la shell. El programa listará el nombre de todos los ficheros del directorio actual en los que haya trascurrido más de una semana desde que fueron modificados la última vez hasta que fueron accedidos por última vez. Hacer una comprobación no paranoica de errores.

    Soluciones y comentarios.

  14. Órdenes de la shell relacionadas.

    cat
    escribe en la salida estándar todo lo que le llegue por la entrada estándar
    mv
    mueve o cambia el nombre de un fichero o directorio
    ls
    da información de los ficheros dentro de un directorio
    mkdir
    crea un directorio nuevo
    rmdir
    borra un directorio vacío
    cd
    cambia el directorio de trabajo actual


  15. Funciones de biblioteca relacionadas.

    malloc
    reserva un área de memoria dinámica
    free
    libera un área de memoria dinámica
    fopen, fread, fwrite, fclose, fprintf, fscanf, fgets, rewind, fseek, ftell.
    funciones de alto nivel de manejo de ficheros
    opendir, readdir, telldir, seekdir, rewinddir, closedir
    funciones de alto nivel para ver el contenido de los directorios.


  16. LPEs.


© 2008 Guillermo González Talaván.