Unistmo Ing. en Computación Sistemas Operativos Sistemas Operativos Problemas de comunicación y sincronización (Lectores-Escritores). En el problema de los lectores-escritores existe un determinado objeto que va a ser utilizado y compartido por una serie de procesos concurrentes. Algunos procesos solo acceden al objeto sin modificarlo (lectura) y algunos acceden para modificarlo (escritura), lo que lleva a algunas restricciones: 1.- Solo se permite que un escritor tenga acceso al mismo tiempo. 2.- Se permite cualquier número de lectores accediendo al mismo tiempo. En el presente documento se explica como crear una aplicación gráfica que utiliza una tubería sin nombre como mecanismo de sincronización para simular el problema de los Lectores-Escritores. Primero crearemos la ventana principal utilizando la aplicación Glade-2 mediante los siguientes pasos: 1.- Generamos un Nuevo Proyecto GTK + 2.- De la Paleta de Widgets seleccionamos Window 3.- En la ventana de propiedades del widget window configuramos lo siguiente: a) Nombre: window b) Título: Lectores-Escritores c) Ancho y largo de 250 d) Añadimos la señal destroy 4.- Adicionamos y configuramos widgets de la siguiente forma: a) Adicionamos una caja vertical con dos renglones a window b) En el primer renglón adicionamos una barra de herramientas con dos botones c) El primer botón será para el cuadro Acerca de (usar botón de stock y añadir la señal clicked) d) El segundo botón será para salir (usar botón de stock y añadir la señal clicked) e) En el segundo renglón adicionamos una área de dibujo (Drawing Area), cambiar el nombre a drawingarea y añadir las señales de configure_event y expose_event a este widget 5.- Guardar como LectoresEscritores y construir 6.- Entramos a la terminal y nos cambiamos a la ruta del proyecto cd Projects/LectoresEscritores/ 7.- Ejecutamos la autogeneración y compilamos ./autogen.sh make 8.- Nos cambiamos al directorio de los fuentes y editamos el archivo callbacks.c cd src/ gedit callbacks.c 9.- El código de la retrollamada on_window_destroy deberá quedar como sigue: void on_window_destroy { (GtkObject gpointer *object, user_data) gtk_main_quit(); } Esto hará que se salga del bucle de procesamiento de las señales y se termine la aplicación. M. en C. J. Jesús Arellano Pimentel Abril 2011 Unistmo Ing. en Computación Sistemas Operativos 10.- El código para la retrollamada on_toolbuttonSalir_clicked deberá quedar como sigue: void on_toolbuttonSalir_clicked { (GtkToolButton gpointer } *toolbutton, user_data) on_window_destroy((GtkObject *)toolbutton, user_data); Simplemente se trata de llamar a la función editada en el paso anterior (nueve). 11.- Codificamos las retrollamadas configure_event y expose_event para el área de dibujo de la siguiente manera: gboolean on_drawingarea_configure_event { (GtkWidget *widget, GdkEventConfigure *event, gpointer user_data) if(pixmap != NULL) gdk_pixmap_unref(pixmap); pixmap = gdk_pixmap_new(widget->window, widget->allocation.width, widget->allocation.height, -1); trazar(widget); } return TRUE; gboolean on_drawingarea_expose_event { (GtkWidget GdkEventExpose gpointer *widget, *event, user_data) gdk_draw_pixmap(widget->window, widget->style->fg_gc[GTK_WIDGET_STATE(widget)], pixmap, event->area.x, event->area.y, event->area.x, event->area.y, event->area.width, event->area.height); return FALSE; } Observe que en código anterior se utiliza una variable de nombre pixmap y un procedimiento llamado trazar, la creación de estos dos elementos se detallan en los siguientes pasos. 12.- Declaramos la variable global pixmap después de la inclusión de librerías GdkDrawable * pixmap = NULL; Esta variable es prácticamente el lienzo sobre el cual vamos a dibujar. 13.- Implementamos, abajo de la declaración de la variable anterior, el procedimiento trazar int trazar(GtkWidget * widget) { GdkRectangle update_rect; GtkWidget *where_to_draw = lookup_widget(widget,"drawingarea"); GdkFont *font = NULL; M. en C. J. Jesús Arellano Pimentel Abril 2011 Unistmo Ing. en Computación Sistemas Operativos if(font == NULL) if((font = gdk_font_load("fixed"))==NULL) return FALSE; gdk_draw_rectangle(pixmap, widget->style->white_gc, TRUE, 0, 0, where_to_draw->allocation.width, where_to_draw->allocation.height); update_rect.x = 0; update_rect.y = 0; update_rect.width = where_to_draw->allocation.width; update_rect.height = where_to_draw->allocation.height; gtk_widget_draw(where_to_draw, &update_rect); return TRUE; } Este procedimiento solo “limpia” el área de dibujo con un rectángulo relleno de color blanco toda el área cliente de la ventana. 14.- En este punto podemos guardar los cambios al archivo callbacks.c y compilar para verificar que no existen errores de sintaxis, desde la terminal se debe ejecutar la siguiente sentencia: make 15.- Ahora adicionamos el cuadro de dialogo Acerca de. Para esto, presionamos el botón de los widgets adicionales de la paleta de widgets del Glade2, damos clic en el widget correspondiente al cuadro de dialogo Acerca de (About Dialog). Configuramos las siguientes propiedades: a) Lo nombramos aboutdialog b) Como comentario escribimos “Simulación del problema de lectores-escritores” c) En autores le ponemos nuestro nombre :). d) Habilitamos la propiedad de auto-destruir e) Le adicionamos la señal de destroy 16.- Guardamos, construimos y compilamos 17.- Editamos nuevamente el archivo callbacks.c adicionando la siguiente variable global: GtkWidget * acercade = NULL; 18.- El código de la retrollamada on_toolbuttonAcercade_clicked debe quedar de la siguiente forma: void on_toolbuttonAcercade_clicked { (GtkToolButton gpointer *toolbutton, user_data) if(acercade == NULL){ acercade = create_aboutdialog(); } gtk_widget_show(acercade); } Esta función simplemente muestra el widget del cuadro de dialogo Acerca de, siempre y cuando exista un cuadro de dialogo creado. 19.- El código de la retrollamada on_aboutdialog_destroy debe quedar de la siguiente forma: M. en C. J. Jesús Arellano Pimentel Abril 2011 Unistmo Ing. en Computación void on_aboutdialog_destroy { } (GtkObject gpointer Sistemas Operativos *object, user_data) acercade = NULL; 20.- Guardamos, compilamos y probamos make ./lectoresescritores Hecho lo anterior, se tiene lista la interfaz gráfica de usuario para implementar el problema de los lectores-escritores. Parra este propósito se deberán realizar los siguientes pasos: 1.- Editar el archivo main.c. gedit main.c 2.- Añadir el siguiente código después de la inclusión de librerías: #include <stdlib.h> #define ESCRITOR #define LECTOR 1 2 extern int trazar(GtkWidget *); int tuberia[2]; /*mecanismo de sincronización int numproc = 5; /*número de procesos a ejecutar int rol = LECTOR; /*rol del proceso en ejecución char c; /*dato testigo gchar szRol[64]= "Iniciando ..."; */ */ */ */ Estamos declarando la tubería (mecanismo de sincronización) a utilizar y cuantos procesos se ejecutarán simultáneamente, así como el rol inicial que tendrán todos ellos. 3.- Los procesos estarán cambiando de rol para competir por la escritura, con la restricción de que solo uno de ellos puede escribir a la vez. Para cambiarse de rol y permitir un solo escritor a la vez la siguiente función puede ayudar: int cambiarrol(void *w) { int ale; trazar((GtkWidget *)w); if(rol == ESCRITOR) { read(tuberia[0], &c, 1); sleep(3); write(tuberia[1], &c, 1); } ale = 1+(int) (10.0*rand()/(RAND_MAX+1.0)); if(ale <= 9){ rol = LECTOR; sprintf(szRol,"Lector Activo %d",ale); } else{ rol = ESCRITOR; M. en C. J. Jesús Arellano Pimentel Abril 2011 Unistmo Ing. en Computación } Sistemas Operativos sprintf(szRol,"Escritor %d",ale); return TRUE; } Los lectores siempre estarán activos, pero un escritor deberá obtener el dato testigo primero antes de escribir. Aleatoriamente se decide si un proceso es escritor o lector, puede observarse en el código que si el número aleatorio generado es menor a 9 el proceso será lector, pero si el número aleatorio es 10 será escritor. 4.- El siguiente paso es modificar la función main. Habrá que agregar la declaración de las siguientes variables: pid_t pid; int i; 5.- En la misma función main se deberá crear la tubería, escribir el dato testigo y crear la cantidad de procesos deseados. Esto se logra mediante el siguiente segmento de código después del bloque de compilación condicional: if (pipe(tuberia) == -1) { perror("[pipe]"); return -1; } write(tuberia[1], &c, 1); for(i = 0; i < numproc; i++){ pid = fork(); if(pid == -1){ printf("Error al crear el hijo"); break; } if(pid != 0){ break; } } 6.- Ahora debemos programar un temporizador para que los procesos cambien de rol cada cierto tiempo. Esto también es dentro de la función main, después de haber creado y mostrado la ventana principal mediante la siguiente línea de código: g_timeout_add(1000, cambiarrol, (void *)window); 7.- Finalmente se deberá cerrar la tubería antes del return 0; de la función main de la siguiente forma: close(tuberia[0]); close(tuberia[1]); 8.- Para poder visualizar el rol del proceso es necesario modificar la función trazar del archivo callbacks.c adicionando la siguiente línea de código después de dibujar el rectángulo blanco: gdk_draw_string(pixmap, font, widget->style->black_gc, 10, 15, szRol); 9.- Sin embargo la variable global szRol esta definida en el archivo main.c, por lo que se deberá declarar como variable externa en el archivo callbacks.c. extern gchar szRol[64]; 10.- Guardar los cambios en los archivos main.c y callbacks.c, compilar y probar desde la terminal. make ./lectoresescritores & M. en C. J. Jesús Arellano Pimentel Abril 2011 Unistmo M. en C. J. Jesús Arellano Pimentel Ing. en Computación Sistemas Operativos Abril 2011