E

sta práctica pretende acercar al alumno a la programación modular en lenguaje C, para que se adiestre o descubra esta metodología de programación y se familiarice con elementos típicos de la programación de sistemas. La primera parte de este documento introduce al lector a las ventajas de la programación modular en general y muestra cómo desarrollar esta técnica dentro del lenguaje C. Se explica también la herramienta make del entorno UNIX. La segunda parte es un ejercicio que se propone al alumno, para que desarrolle en grupo una pequeña aplicación modular de la que se da el diseño ya preparado.

1. Actividad práctica

A continuación se propone un ejercicio práctico donde el alumno aplicará los fundamentos y herramientas de programación modular descritas en este texto. Se ha de desarrollar un programa simple que manipule registros de datos almacenados en disco, para lo cual se habrá de construir una aplicación en C con cuatro módulos .c y sus correspondientes cabeceras .h. Además, se elaborará un fichero Makefile para el mantenimiento del programa.

Las siguientes páginas describen con detalle el diseño de la aplicación. El alumno sólo tendrá que implementar los servicios descritos. Aunque el alumno no haya de diseñar en esta práctica, es muy conveniente que perciba cómo se ha estructurado la aplicación, para que en futuras actividades sea capaz de hacerlo por su cuenta y además elaborar la documentación de su software, de lo cual estas páginas son un ejemplo.

1.1. Módulos del programa

En la aplicación se trabajará con ficheros consistentes cada uno en una serie registros de tamaño fijo. Las lecturas y escrituras de registros se realizarán por medio de funciones de un módulo llamado bdatos.c. Otro módulo de nombre principal.c utilizará estas funciones, ofreciendo al usuario un menú para que pueda realizar las distintas operaciones posibles.

Se mantendrá una caché de registros en memoria principal, de manera que cuando se solicite trabajar con un registro, primero se compruebe que se encuentra en la caché, para no acceder al disco en ese caso.

Un módulo llamado cache.c gestionará el conjunto de registros de la caché. Las lecturas y escrituras en disco serán servidas por el módulo disco.c. El módulo bdatos.c empleará las funciones públicas de cache.c y disco.c para cumplir con su misión.

Salvo principal.c, todos los módulos tendrán su fichero cabecera donde se declararán sus interfaces respectivas. Con todo ello, la aplicación completa estará compuesta de estos ficheros:

disco.c, disco.h              Acceso a registros en disco en bajo nivel        
cache.c, cache.h              Gestión de la caché de registros                 
bdatos.c, bdatos.h            Funciones de manejo de registros                 
dato.h                        Tipo de datos común (ver más adelante)           
principal.c                   Programa principal (main)                        
Makefile                      Fichero de mantenimiento para make               

La estructura de la aplicación puede representarse con este diagrama, en el que los módulos aparecen como cuadros. Las flechas indican las relaciones de clientela que existen entre los módulos: por ejemplo, que bdatos.c utiliza los servicios de disco.c.

1.2. Estructuras de datos

Los ficheros de datos contendrán un número variable de registros. Cada registro es una cadena de caracteres. Todos los registros de un mismo fichero tendrán la misma longitud, pero los registros de ficheros diferentes podrán tener longitudes distintas.

Los registros se numeran de 1 (uno) en adelante.

Un fichero de datos tendrá la siguiente estructura:

· Cabecera al inicio del fichero que describirá las características internas del fichero. Constará de estos dos campos enteros:
- Número de registros en el fichero
- Tamaño en bytes de cada registro

· Zona de registros, constituida por un número de registros de tamaño fijo, cumpliendo lo especificado en la cabecera.

Tipo para los registros

Para manejar en memoria principal los registros, se dispondrá de un tipo de datos estructurado, llamado struct Dato, según se define a continuación:

struct Dato

{

int nreg; /* Número de registro */

char* registro; /* Puntero a la cadena de caracteres */

};

Este tipo de datos será usado en bdatos.c y en cache.c. Por ello ha de crearse un fichero cabecera, llamado dato.h, en el que se declare esta estructura.

1.3. Descripción del funcionamiento de la aplicación

En este apartado se dará una explicación general del funcionamiento de cada uno de los módulos y cómo se relacionan entre sí. También se darán algunas recomendaciones o indicaciones acerca de la implementación.

En el apartado 5.4 se describen detalladamente las interfaces de todos los módulos.

Módulo disco

En este módulo se implementarán las funciones básicas para l

int crea_fich ( const char* nombre, int lreg, int nreg );

int activa_fich ( const char* nombre, struct Cabecera* cabec );

int accede_fich ( int fich, int nreg, char* registro, int clase_acceso );

int desactiva_fich ( int fich );

crea_fich construirá un fichero de datos con el número y tipo de registros que se le solicite.

Los ficheros de datos han de abrirse con activa_fich antes de usarse. Devuelve un descriptor de fichero imprescindible para operar con él. Cuando se deja de trabajar con un fichero, se llama a desactiva_fich.

Se lee o escribe información con accede_fich, a la que se le pasa como parámetro un descriptor de fichero previamente abierto.

activa_fich leerá la cabecera del fichero, con el fin de conocer cuántos registros hay y de qué tamaño son.

Para trabajar con el disco, se utilizarán las funciones de bajo nivel del UNIX: creat, open, close, read, write y lseek. En los manuales del laboratorio hay información sobre estos servicios.

Módulo cache

La caché de registros se manipulará como una cola en la que se insertan registros de tipo struct Dato, para luego buscarlos por número de registro. Las funciones que gestionan la caché serán estas:

int inicia_cache ( int maxreg );

int ingresa_cache ( const struct Dato* registro );

int busca_cache ( int nreg, struct Dato* registro );

int cache_llena (void);

int extrae_cache ( struct Dato* registro );

La caché tendrá un tamaño predeterminado, que se fijará con una llamada a inicia_cache. Inicialmente, la caché está vacía. Puede ingresarse un registro nuevo con ingresa_cache, con lo que la caché irá llenándose. Para buscar un registro se utiliza busca_cache, que busca el registro nreg en la caché y lo deposita en registro si lo encuentra.

La caché puede llenarse de registros. En ese caso, la función ingresa_cache dará un error. Para detectar si la caché aún tiene espacios se utiliza la función cache_llena. Si la caché está llena, pero de todas formas se quiere ingresar un registro, hay que eliminar alguno de los registros que ya están presentes, de lo que se encarga extrae_cache. Esta función devuelve algún registro de la caché y lo elimina de ella.

Hay que resolver qué registro de los presentes en la caché se selecciona como víctima para su desalojo de la caché cuando se invoca extrae_cache. Eso queda a criterio del alumno; basta con adoptar una política FIFO.

La estructura interna de la caché también queda a discreción del alumno, siempre que cumpla con el funcionamiento deseado. Algunas posibilidades son: vector dinámico, lista encadenada o tabla hash.

Módulo bdatos

Este módulo será el que integrará los servicios de disco.c y cache.c para ofrecer una interfaz cómoda y potente de trabajo con los ficheros de datos.

int crea_bdatos ( const char* fichero, int lreg, int nreg );

int abre_bdatos ( const char* fichero );

int lee_registro ( int nreg, struct Dato* registro );

int escribe_registro ( int nreg, struct Dato* registro );

int cierra_bdatos (void);

El fichero de datos se manipulará abriéndolo primero con abre_bdatos; luego se podrán leer y escribir registros con lee_registro y escribe_registro. Finalmente, habrá que cerrar el fichero con cierra_bdatos.

La función crea_bdatos simplemente invoca a crea_fich y tiene su mismo significado.

Sólo puede haber un fichero de datos abierto en cada momento. Obsérvese que se guarda de forma transparente al usuario el descriptor de fichero que se abre.

El acceso a un registro, tanto en lee_registro como en escribe_registro, se hará como sigue: primero se busca el registro en la caché. Si se encuentra, se trabaja con el dato en memoria. De lo contrario, se accede al disco con accede_fich y se trae el registro, el cual se ingresa en la caché.

La caché puede llenarse y ello implica la invocación a extrae_cache. Como el elemento extraído puede haber sido modificado, habrá que guardarlo en el disco.

La llamada a cierra_bdatos no sólo cierra el fichero actual con desactiva_fich, sino que también salva en disco todos los elementos de la caché mediante repetidas llamadas a extrae_cache. De lo contrario se podría perder la información escrita en la caché.

Módulo principal

El módulo principal contendrá la función main, en la que el usuario podrá seleccionar distintas operaciones con ficheros de datos:

· Crear un fichero nuevo y abrirlo

· Abrir un fichero existente

· Leer un registro determinado del fichero actual

· Escribir un registro determinado del fichero actual

· Cerrar el fichero actual

Todas estas funciones se realizarán a través del módulo bdatos.c. No se admite que el módulo principal utilice directamente servicios de cache.c, de disco.c, ni que se trabaje directamente con las funciones de UNIX para ficheros.

Cuando se lea un registro, se imprimirán por pantalla los caracteres leídos. En el caso de la escritura, se pedirá al usuario que entre un texto, que será el que se escriba en el registro deseado.

El programa deberá informar adecuadamente de cualquier error que se produzca. Por ejemplo, intentar abrir un fichero no existente, leer un registro no existente, carecer de memoria suficiente, etc.

Pueden añadirse más operaciones, con el fin de evaluar la tasa de aciertos en la caché.

1.4. Interfaces de los módulos

En esta sección se describen los prototipos de las funciones públicas de los distintos módulos de la aplicación, junto con su funcionamiento. Estas funciones han de figurar en los ficheros cabeceras de los módulos.

Módulo disco.c

Nombre

struct Cabecera - estructura para la cabecera del fichero

Sintaxis

struct Cabecera

{

int nreg; /* Número de registros */

int lreg; /* Longitud en bytes de cada registro */

};

Nombre

crea_fich - crea y pone a punto un fichero de datos

Sintaxis

int crea_fich ( const char* nombre, int lreg, int nreg );

Descripción

Crea un fichero de datos con la estructura descrita en la página 4. nombre es un puntero a la cadena que contiene la ruta del fichero. lreg es la longitud en bytes de los registros del fichero. nreg es el número de registros del fichero.

La función devuelve un 0 si se realiza con éxito y -1 si se produce un error.

Recomendaciones

Se sugiere que se dé a cada registro un valor característico con objeto de que posteriormente se pueda comprobar la implementación correcta de las funciones de acceso.

Esta función habrá de invocar a la función creat del UNIX.

Nombre

activa_fich - Activa (abre) un fichero

Sintaxis

int activa_fich ( const char* nombre, struct Cabecera *cabec );

Descripción

Abre un fichero y a continuación lee la cabecera de éste para poder acceder a sus registros posteriormente, por tanto se debe usar antes de acceder a un fichero.

nomb puntero a la cadena que contiene la ruta del fichero para abrir

cabec puntero a una estructura de tipo Cabecera en donde la función depositará las características del fichero abierto (tamaño de los registros y número de ellos).

Si la función se ejecuta con éxito entonces retorna el descriptor del fichero abierto (valor entero positivo). Si se produce un error devuelve un -1.

Recomendaciones

Esta función tendrá que invocar a la función open del UNIX.

Nombre

accede_fich - lee o escribe registros en un fichero

Sintaxis

int accede_fich ( int fd, int nreg, char *bufer, int clase )

Descripción

Accede a un registro de un fichero abierto previamente por activa_fich.

fd descriptor válido de fichero devuelto por activa_fich.

reg número de registro a acceder.

bufer puntero al buffer desde donde o a donde se realizará la transferencia.

clase indicador del tipo de acceso (LECTURA o ESCRITURA).

Si la función devuelve un entero positivo, se interpretará como el número de bytes del registro transferidos. Si el retorno es -1, entonces es que se ha producido un error y no se ha transferico ningún byte.

Nombre

desactiva_fich - Cierra un fichero

Sintaxis

int desactiva_fich ( int fd );

Descripción

fd es el descriptor válido del fichero para cerrar

Retorna 0 si se ha ejecutado con éxito y -1 si un error ha ocurrido.

Módulo cache.c

Nombre

inicia_cache - pone a punto la caché

Sintaxis

int inicia_cache ( int maxreg );

Descripción

Habilita las estructuras necesarias para albergar maxreg registros. maxreg debería ser mayor que cero.

Devuelve un 0 si todo ha ido bien y un -1 si hay error. Puede darse error por memoria insuficiente o valor incorrecto de maxreg.

Nombre

ingresa_cache - ingresa un registro en la caché

Sintaxis

int ingresa_cache ( const struct Dato* registro );

Descripción

Ingresa un registro en la caché. registro es un puntero al dato que se va a ingresar.

Devuelve un 0 si todo ha ido bien y un -1 si registro es un puntero nulo o si la caché está llena.

Nombre

busca_cache - busca un registro dentro de la caché

Sintaxis

int busca_cache ( int nreg, struct Dato* registro );

Descripción

Busca el registro número nreg dentro de la caché. registro es un puntero a la zona de memoria donde se copiará el elemento.

Devuelve un cero si han ido bien las cosas; -1 en otro caso.

Nombre

cache_llena - devuelve el estado de la caché

Sintaxis

int cache_llena (void);

Descripción

Devuelve un 0 si la caché tiene huecos disponibles, y un valor no nulo si está llena por completo.

Nombre

extrae_cache - elimina un elemento de la caché

Sintaxis

int extrae_cache ( struct Dato* registro );

Descripción

Elimina un elemento de la caché y lo deposita en registro. Devuelve un 0 si la ejecución ha sido satisfactoria y un -1 si ha habido problemas.

Módulo bdatos.c

Nombre

crea_bdatos - crea un fichero de datos

Sintaxis

int crea_bdatos ( const char* nombre, int lreg, int nreg );

Descripción

Crea un fichero de datos con la estructura descrita en la página 4. Su funcionamiento es idéntico a crea_fich, descrita en el módulo disco.c

Recomendaciones

Ha de implementarse como una simple llamada a crea_fich

Nombre

abre_bdatos - abre un fichero de datos con caché

Sintaxis

int abre_bdatos ( const char* nombre );

Descripción

Abre un fichero de datos cuya ruta viene dada en nombre. La caché se vacía y pone a punto con espacio para un 20% del número de elementos del fichero.

Devuelve 0 si ha tenido éxito y -1 en caso de error. Algunas condiciones de error son:

· Ya existía un fichero de datos abierto

· El fichero de datos no existe

· Error con la caché

Nombre

lee_registro - lee un registro de un fichero de datos

Sintaxis

int lee_registro ( int nreg, struct Dato* registro );

Descripción

Lee el registro número nreg del fichero de datos previamente abierto. Lo deposita en la zona de memoria apuntada por registro.

Si el registro se encuentra en la caché, se accede a ésta; si no, se lee el dato directamente del fichero y se guarda en la caché. Si hubo que desalojar algún elemento de la caché, éste se escribe en el fichero.

La función devuelve un cero si ha ido bien y un -1 en caso contrario. Algunas condiciones de error son: fichero de datos no abierto, error en la caché, número de registro inválido, etc.

Nombre

escribe_registro - escribe un registro en un fichero de datos

Sintaxis

int escribe_registro ( int nreg, struct Dato* registro );

Descripción

Escribe el registro número nreg del fichero de datos previamente abierto. Los datos para escribir se encuentran en la zona de memoria apuntada por registro.

El registro siempre se escribe en la caché. Si hay que desalojar algún elemento de la caché, éste se escribe en el fichero.

La función devuelve un cero si ha ido bien y un -1 en caso contrario. Algunas condiciones de error son: fichero de datos no abierto, error en la caché, número de registro inválido, etc.

Nombre

cierra_bdatos - cierra el fichero de datos actual

Sintaxis

int cierra_bdatos (void);

Descripción

Cierra el fichero de datos actualmente abierto. Los datos presentes en la caché son escritos en el fichero antes de cerrarlo.

Devuelve 0 si todo ha ido bien; -1 en caso contrario.

Recomendaciones

Los datos de la caché se irán leyendo con extrae_cache.

1.5. Tratamiento de errores

Los errores generados en una aplicación UNIX suelen dividirse en dos categorías:

Errores producidos en las funciones del sistema. Son los que se dan en todas aquellas funciones del sistema que han sido invocadas durante la ejecución de la aplicación. UNIX dispone para el tratamiento de estos errores de un fichero cabecera llamado errno.h, en el que están definidos todos los errores que pueden ocurrir en cualquier función del sistema.

La variable externa errno, que debe ser declarada en el programa, contiene el código del error que se acaba de producir.

La función perror() imprime por pantalla un mensaje de error acorde con el contenido de errno, habitualmente en inglés.

Errores específicos de la aplicación. Son los que se producen cuando se viola alguna restricción que ha sido impuesta implícita o explícitamente en las especificaciones del programa a desarrollar. En la aplicación propuesta, un ejemplo de este tipo de errores ocurriría cuando se pretende acceder a un registro fuera del rango del fichero actual.

El tratamiento de este tipo de errores debe contemplarse en cualquier aplicación y se debe hacer de forma que cuando se produzcan se puedan detectar inmediatamente y además conocer sus orígenes. Una manera de llevar a cabo este tratamiento es desarrollar una función genérica de tratamiento de errores y el uso de un fichero cabecera en donde se definan los códigos de errores.

Queda como opción el desarrollar un módulo error.h y error.c donde se tramiten los errores.

1.6. Planificación del trabajo

Esta aplicación es sencilla y se puede construir en menos de diez horas de trabajo, una vez leído este documento. Aunque puede realizarse de forma individual, resulta más productivo hacerlo en grupo con otra persona, para experimentar la separación del trabajo.

Cada miembro del grupo de prácticas desarrollará dos de los cuatro módulos, con la restricción de que una misma persona no podrá desarrollar disco.c y cache.c a la vez. El principal motivo es que son los módulos que ocuparán más tiempo o dificultad.

El trabajo se hará simultáneamente. Como durante la elaboración de los módulos no se podrá contar con el par de módulos del compañero, habrá que verificar el funcionamiento del código escrito usando prototipos "tontos" de los otros módulos.

Una vez que se disponga de los cuatro módulos, se integrarán en la aplicación definitiva. Se elaborará un informe explicativo del trabajo realizado y se comunicará al profesor encargado de tutorizar la práctica para su evaluación.

- ... -