Apellidos Nombre Universidad de Alcalá. Escuela Politécnica. Dpto. Automática. 22-02-2003 Examen de Laboratorio de Informática Industrial II El examen puntúa de 0 a 4. Ponga apellidos y nombre en mayúsculas en las hojas. Preguntas breves Responda en el espacio reservado a las cuestiones planteadas. Sea breve y conciso. 0,125 puntos por pregunta. 1) ¿Cuál es la diferencia entre una cabecera de C y una biblioteca? Indicar de qué se componen y para qué sirven. Las cabeceras son archivos de texto con código C que contienen principalmente prototipos de funciones, declaraciones de variables y estructuras, macros y funciones en línea. Sirven para que el compilador conozca el significado de los nombres de las palabras no reservadas que use el programador, sobre todo el tipo de datos. Las bibliotecas son archivos compuestos por varios módulos de código compilado (habitualmente código máquina). Sirven para incorporar código ya generado y depurado a nuestra aplicación sin necesidad de rescribirlo. 2) ¿Cómo se le indica a un compilador de C que emplee una biblioteca concreta? Mediante opciones del compilador. En el caso de gcc, se usaría el modificador –l seguido por el nombre efectivo de la biblioteca. Por ejemplo, para incluir libpthreads.a, usaríamos: $ gcc –lpthreads … 3) ¿Qué es y para qué sirve el preprocesador de C? Es el programa encargado de resolver las directivas del preprocesador de los archivos fuente de C. Estas son todas las líneas que comienzan por # e indican inclusiones, macros, y compilación condicional, principalmente. El preprocesador genera un archivo de código C “limpio” que no contiene tales líneas, que será pasado al núcleo del compilador. 4) ¿Cuál es la misión principal del enlazador? Crear un programa ejecutable (o una biblioteca de enlace dinámico), a partir de las funciones que busca en los archivos de código objeto y bibliotecas que se le indiquen. 5) ¿Para qué se utilizan las llamadas al sistema? Para comunicar a las aplicaciones con el núcleo del Sistema Operativo. De esta forma entre otras cosas pueden hacer las necesarias peticiones de recursos para su funcionamiento. 6) Llamar a printf ¿Implica hacer una llamada al sistema? En caso afirmativo ¿Cuál podría ser? Justificar la respuesta. 1 Sí, ya que accede al dispositivo de salida estándar para enviar texto. La escritura en dispositivos se realiza por medio de la llamada al sistema write, y en este caso usando el descriptor del flujo de salida estándar stdout 1. 7) La siguiente pregunta vale por dos: 8) ¿Qué archivo se pasará internamente al compilador si usamos el código fuente siguiente? (El archivo resultante de aplicar el modificador –E a gcc) #define NDEBUG #define INFO int Divide(int a, int b) { #ifndef NDEBUG if(b == 0) { perror("El divisor es 0!!!\n"); return 0; } #endif #ifdef INFO printf("El resultado de la división es %d\n", a / b); #endif return a / b; } int Divide(int a, int b) { printf("El resultado de la división es %d\n", a / b); return a / b; } 9) En la compilación de programas… ¿Qué utilidad encuentra en habilitar la compatibilidad estricta con ISO C? Aumenta la portabilidad ya que permite compilarla en cualquier sistema que disponga de un compilador que cumpla con esta norma (que es la referencia). 10) Pregunta de observación: ¿Cuál es la finalidad de las advertencias (warnings) que muestran los compiladores? ¿Cuál es la diferencia con los errores? Los warnings se tratan de construcciones que el compilador acepta pero advierten de potenciales errores que podrían aparecer bien en tiempo de ejecución o bien en fases futuras de la compilación. El error se produce porque el compilador ha encontrado un código que no puede compilar al no adaptarse a sus reglas internas, por lo que la existencia de errores impide la generación del archivo compilado. 11) En la depuración de programas… ¿Qué es un breakpoint? ¿Qué es un watchpoint? Aclarar las diferencias entre ambos. El breakpoint o punto de interrupción es un punto del código en donde el programa en depuración se detendrá para permitir observar detenidamente el estado de la aplicación, avanzar detenidamente, etc. El watchpoint o punto de comprobación es una parada de la ejecución condicionada a que se verifique cierta expresión del programa. Por ejemplo “detener cuando la variable num sea mayor de 10”. 2 12) ¿Qué dos opciones de ejecución paso a paso nos brindan los depuradores? Explicar brevemente. Línea a línea del programa entrando en subrutinas de las que se disponga de información de depuración. Una vez que se ha entrado se sigue con la ejecución línea a línea. Línea a línea del programa considerando las subrutinas como una línea más. En este caso las subrutinas se ejecutan de una vez. 13) ¿Qué utilidad tienen los descriptores de fichero en Unix? Son los identificadores de archivo que utilizamos en algunas llamadas al sistema y llamadas a biblioteca para referenciarlos inequívocamente y poder realizar operaciones como leer o escribir. En Unix son números enteros positivos. 14) ¿Qué tipo de ficheros se almacenan en /dev? Archivos de dispositivo que contienen el nombre lógico de la mayoría de los controladores de dispositivo del sistema. Esta forma permite tratar a los dispositivos como si se trataran de archivos, por lo que se puede utiliza la misma interfaz básica (open, close, read, write,…). 15) ¿Qué quiere decir “redirigir la salida estándar”? Hacer que el archivo de salida estándar sea otro diferente al original stdout. La salida es habitualmente la consola de texto desde donde comenzó la aplicación, con la redirección podemos hacer que la salida vaya a cualquier otro archivo como otra consola, u otros ficheros de disco. 16) ¿Cuáles son las ventajas y los inconvenientes de utilizar llamadas al sistema directamente? Citar al menos una de cada. Ventajas: Escasa sobrecarga, tamaño, y velocidad. Además se permite acceder a funciones avanzadas no disponibles en bibliotecas. Desventajas: Poca portabilidad, dificultad en la escritura de los programas. 3 Completar el siguiente programa respondiendo sólo dentro de las cajas: (2 puntos) • A continuación aparece un programa en el que un hilo Servidor (productor) deposita datos en la variable global g_ficha para que otros hilos Cliente la procesen. • Cuando el Servidor genere un nuevo dato se quedará esperando a que un Cliente lo consuma. Una vez que esto suceda, podrá generar otro dato. • Sólo un cliente puede acceder a g_ficha simultáneamente, después de lo cual se considera que la variable es inválida hasta que el Servidor haya generado otro dato. • Cada vez que el usuario teclea [C] se crea un nuevo cliente que hará hasta 20 intentos en diferentes instantes por ser el que consuma el dato. Si consiguiera acceder en alguna ocasión al dato, no debería volver a hacer más intentos, terminando su ejecución. • Aunque el dato al que los clientes quieren acceder estuviera siendo utilizado por otro cliente o fuera inválido, el hilo no se bloquearía. • Si el usuario presiona la tecla [Q], cuando se consuma el último dato generado la aplicación terminará. • • • No todas las cajas han de ser rellenadas. Lea primero el código tal cual está. Una vez comprendido puede empezar a plantear la solución. Se puede resolver con tan solo dos mútex. Literales a usar: pthread_mutex_t PTHREAD_MUTEX_INITIALIZER EBUSY int pthread_mutex_init (pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); int pthread_mutex_destroy (pthread_mutex_t *mutex); int pthread_mutex_lock (pthread_mutex_t *mutex); int pthread_mutex_unlock (pthread_mutex_t *mutex); int pthread_mutex_trylock (pthread_mutex_t *mutex); 4 #include <pthread.h> #include <stdlib.h> #include <stdio.h> struct sFicha { char sexo; int num; }; void* HiloCliente(void *arg); void* HiloServidor(void *arg); // Declarar las variables que sean necesarias int g_salir = 0; struct sFicha g_ficha; pthread_mutex_t exmut_1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t exmut_2 = PTHREAD_MUTEX_INITIALIZER; int main() { // Declarar las variables que sean necesarias char c = 0; pthread_t thServidor = 0, thID = 0; // Introducir el código que sea necesario pthread_mutex_lock(&exmut_1); pthread_mutex_lock(&exmut_2); printf("Introduzca 'C' para crear un nuevo hilo consumidor.\n"); pthread_create(&thServidor, NULL, HiloServidor, NULL); do { c = getchar(); if (c == 'c' || c == 'C') { // Crear un nuevo hilo de tipo Cliente pthread_create(&thID, NULL, HiloCliente, NULL); } } while (c != 'q' && c != 'Q'); g_salir = 1; // Sólo se debería salir cuando el último dato generado haya sido procesado pthread_mutex_lock(&exmut_1); pthread_mutex_unlock(&exmut_2); pthread_join(thServidor, (void**) NULL); return 0; } 5 void* HiloCliente(void *arg) { int cont; pthread_detach(pthread_self()); printf("Se ha creado un nuevo hilo con ID=%d\n", pthread_self()); for (cont = 0; cont < 20; cont++) { // Simular que el cliente está haciendo procesamiento usleep(rand() % 4000000 + 500000); // Intentar acceder al recurso if (pthread_mutex_trylock(&exmut_1) == 0) { // En este caso simplificado el acceso consiste en mostrar la información printf("El hilo %d ha recibido la información:\n\t'%c'\t%d\n", pthread_self(), g_ficha.sexo, g_ficha.num); // Si se ha conseguido acceder, hay que salir del bucle pthread_mutex_unlock(&exmut_2); } return 0; } void* HiloServidor(void *arg) { struct sFicha ficha; while(g_salir == 0) { // Simulación de que el servidor tarde en generar un nuevo dato usleep(rand() % 4000000 + 500000); ficha.sexo = rand() & 1 ? 'H' : 'M'; ficha.num = rand(); // g_ficha es un recurso compartido g_ficha = ficha; pthread_mutex_unlock(&exmut_1); pthread_mutex_lock(&exmut_2); } return 0; } 6 #include <pthread.h> #include <stdlib.h> #include <stdio.h> struct sFicha { char sexo; int num; }; void* HiloCliente(void *arg); void* HiloServidor(void *arg); // Declarar las variables que sean necesarias int g_salir = 0; pthread_mutex_t exmut_1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t exmut_2 = PTHREAD_MUTEX_INITIALIZER; struct sFicha g_ficha; int main() { // Declarar las variables que sean necesarias char c = 0; pthread_t thServidor = 0, thID = 0; // Introducir el código que sea necesario printf("Introduzca 'C' para crear un nuevo hilo consumidor.\n"); pthread_mutex_lock(&exmut_1); pthread_mutex_lock(&exmut_2); pthread_create(&thServidor, NULL, HiloServidor, NULL); do { c = getchar(); if (c == 'c' || c == 'C') { // Crear un nuevo hilo de tipo Cliente pthread_create(&thID, NULL, HiloCliente, NULL); } } while (c != 'q' && c != 'Q'); g_salir = 1; pthread_mutex_lock(&exmut_1); pthread_mutex_unlock(&exmut_2); pthread_join(thServidor, (void**) NULL); return 0; } 7 void* HiloCliente(void *arg) { int cont; pthread_detach(pthread_self()); printf("Se ha creado un nuevo hilo con ID=%d\n", pthread_self()); for (cont = 0; cont < 20; cont++) { // Simular que el cliente está haciendo procesamiento usleep(rand() % 4000000 + 500000); // Intentar acceder al recurso if (pthread_mutex_trylock(&exmut_1) == 0) { printf("El hilo %d ha recibido la información:\n\t'%c'\t%d\n", pthread_self(), g_ficha.sexo, g_ficha.num); pthread_mutex_unlock(&exmut_2); break; } } return 0; } void* HiloServidor(void *arg) { struct sFicha ficha; while(g_salir == 0) { // Generar un nuevo dato usleep(rand() % 4000000 + 500000); ficha.sexo = rand() & 1 ? 'H' : 'M'; ficha.num = rand(); g_ficha = ficha; pthread_mutex_unlock(&exmut_1); pthread_mutex_lock(&exmut_2); } return 0; } 8