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.
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.
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.
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.
En el apartado 5.4 se describen detalladamente las interfaces de todos los módulos.
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.
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.
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é.
· 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é.
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.
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.
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.
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.
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.
- ... -