Práctica 2: Realización de una Alarma Temporizada - Diseño de Sistemas Operativos - U.L.P.G.C.
←
→
Transcripción del contenido de la página
Si su navegador no muestra la página correctamente, lea el contenido de la página a continuación
Práctica 2: Realización de una Alarma Temporizada Diseño de Sistemas Operativos – U.L.P.G.C. David Jesús Horat Flotats Enrique Fernández Perdomo
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Índice I. Explicación del Programa desarrollado.................................................................... 2 Programa Principal........................................................................................................................ 2 Variables Tarea que realiza Parámetros Movimiento de la bola Señales........................................................................................................................................... 9 Alarma (en Tiempo Real) Interrupción Lanzamiento de Procesos.............................................................................................................12 Uso de fork Uso de exec Uso de waitpid Uso de exit Control de la Sección Crítica, con Pipes......................................................................................16 Definición de Pipe Manejo de Pipe Problema de la Sección Crítica Solución a la Sección Crítica II. Compilación y Ejecución del programa (Makefile)................................................20 Explicación y Reglas del Makefile.............................................................................................. 20 Ejecución del programa............................................................................................................... 22 Paso de Parámetros III.Anexo – Código Fuente..........................................................................................28 alarma.c........................................................................................................................................28 Makefile....................................................................................................................................... 30 1
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Explicación del Programa desarrollado Programa Principal Variables Las variables definidas son de dos tipos fundamentales: globales y locales a la función main. La utilidad de las variables globales radica en el hecho de que serán usadas por funciones a las que se llama en el momento de producirse las señales, como la alarma, de modo que no pueden pasarse por parámetro. Las variables globales son las siguientes: 1. Tubería para el control de la concurrencia, mediante el uso de pipes (int tuberia[2]). 2. Tiempo de duración de la alarma (int tiempo_alarma = 4). Las variables locales son las siguientes: 1. Variables para controlar el movimiento de la bola: 1. Columna inicial, en la que empieza la bola (int columna = 0). 2. Columna máxima a la que puede llegar la bola (int max_columna = 40). 3. Paso, que indica el número de columnas que avanza la bola en cada iteración de muestreo de la misma (int paso = 1). 4. Velocidad de la bola, que se controla con el número de iteraciones de un bucle, de modo que mientras menor sea el valor de esta variable mayor será la velocidad y viceversa, aspecto que debe tenerse en cuenta al asignarle un valor (int velocidad = 30000000). 2. Variable para tomar el valor de las lecturas y escrituras de las tuberías (int valor). 3. Variable para la realización de iteraciones, y tareas auxiliares (int i). 4. Indicación de señales creadas (int senal_creada = 0;). 5. Ristra que representa la bola (char *bola = "o";). 6. Estructuras de las señales para la implementación con SIGACTION (struct sigaction senal_alarma, senal_int;). Se puede observar que la mayoría de las variables son inicializadas en el momento de su definición, de modo que así se tendrán valores por defecto para las mismas. 2
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Tarea que realiza El programa principal hará un uso intensivo de la salida por pantalla. Para ello se hace uso de la función printf. El uso de esta función obliga a incluir la librería . Una alternativa al uso de las funciones de la librería stdio es el uso de librerías especiales para el manejo de la impresión en el terminal. Destacan las librerías conio y ncurses. La librería ncurses es amplísima y de gran utilidad para el manejo del terminal de Linux y la impresión de cadenas de caracteres en el mismo. El problema de la librería ncurses, que se incluiría en nuestro programa como , es que maneja una pantalla propia, distinta a la que usa la función printf. Hasta aquí no hay problema, pero si se usa la función printf a la vez que se emplean función de la librería ncurses, como move y addch, una pantalla moverá a la otra y el resultado será desastroso; además, da un aspcto de concurrencia incontrolada, del todo indeseable para cualquier programa serio. Finalmente, la salida del programa para imprimir por pantalla se hará con la función printf. El programa desarrollado imprimirá en pantalla un bola que se moverá rebotando de derecha a izquierda, en una línea de la pantalla; sin tener en cuenta el efecto que tendrá el muestreo de la fecha, hasta que se introduzca ésta con la alarma. En realidad, se hará una emulación de la bola con un carácter. No obstante, el programa permitirá que dicha emulación de la bola pueda ser incluso una ristra de cualquier dimensión (en principio mayor que la unidad, es decir, que al menos sea un carácter). El programa principal main realizará una serie de tareas principales o pasos, que se indican y comentan a continuación: Paso 1: Parámetros de entrada El programa principal, una vez compilado y construido, en el momento de ejecutarse permitirá la rececpción de parámetros desde el terminal, para que la configuración del misma sea mayor y más flexible. De este modo la función main tiene la forma: int main(int argn, char *argv[]) { Estos parámetros permiten el control sobre aspectos como el carácter o ristra a usar para emular la bola, el número de columnas por el que se moverá ésta, el tipo de implementación para las 3
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo señales, el tiempo de duración de la alarma para imprimir la fecha y la velocidad de la bola (medida en iteraciones de un bucle). Todos los parámetros se explican en detalle en el apartado Parámetros. Paso 2: Inicializaciones Una vez se han tomado todos los parámetros, si es que se han introducido éstos, se procederá a la inicialización de determinadas variables y señales. En el caso de no indicarse ciertos parámetros, se usarán valores por defecto. En el caso de las señales, se usará la implementación con la función signal, por defecto, de modo que debe hacerse explícitamente se no se indica nada. // · Si no se han creado las señales, se usa SIGNAL por defecto if (!senal_creada) { signal(SIGALRM, muestra_fecha); signal(SIGINT, termina); } Puede verse como se usa la variable senal_creada, que se inicializa a 0: int senal_creada = 0; // Indica si se han creado las señales Si se indica algo por parámetros se pondrá a 1, de modo que no se tendrá que hacer uso de la inicialización por defecto; este paso, en el código, se encuentra antes de la inicialización, porque en ocasiones es necesario y en otras no, en función de los parámetros de entrada. }else if (strcmp(argv[p],"-s") == 0) { // · Tipo de implementación de señales senal_creada = 1; En las inicializaciones propiamente dichas, se hacen dos tareas: crear e inicializar la tubería o pipe, para el control de la concurrencia (sección crítica) y activar la alarma. // · Creación e inicialización del pipe pipe(tuberia); write(tuberia[1], &valor, sizeof(int)); fflush(NULL); // · Alarma alarm(tiempo_alarma); Paso 3: Bucle de muestreo de la bola Se trata de un bulce infinito en el que se estará controlando el muestreo de la bola y que en cada iteración del bucle la bola se moverá una posición, por lo general una columna. while(1) {/*...*/} 4
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Paso 3.1: Pausa entre movimientos de la bola Para que el movimiento de la bola sea más realista, tiene un control de velocidad con un bucle con un número de iteraciones controlado por la variable velocidad. for(i=0; i
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo pasa la opción --help, que es la ayuda, que indica como usar el programa, en cuanto al modo en que se le deben pasar los parámetros: if ((argn == 2) && (strcmp(argv[1],"--help") == 0)) { printf("Modo de empleo: ./alarma [OPCIÓN]\n"); printf("\t-b bola; Ristra que representa la bola\n"); printf("\t-c columna_máxima; Columna máxima a la que llega la bola\n"); printf("\t-s SIGNAL|SIGACTION; Implementación de señales (SIGNAL por defecto)\n"); printf("\t-t segundos; Tiempo entre muestreo de la fecha\n"); printf("\t-v iteraciones; Velocidad de la bola\n"); exit(0); } Las opciones serán, por tanto, las siguientes: -b bola --> Ristra que representa la bola; si tiene espacios o caracteres especiales debe ponerse entre comillas dobles. Se contendrá en la variable char *bola = "o";, que por defecto valdrá "o". -c columna_máxima --> Columna máxima a la que llega la bola, que será de tipo entero. Se contendrá en la variable int max_columna = 40;, que por defecto valdrá 40. -s SIGNAL|SIGACTION --> Indica el tipo de implementación de las señales, que puede ser SIGNAL o SIGACTION. Según su valor se hará uso de una u otra implementación, y por defecto SIGNAL. -t segundos --> Tiempo entre muestreo de la fecha, medido en segundos, que es la duración de la alarma. Se contendrá en la variable global int tiempo_alarma = 4;, que por defecto tiene el valor 4. -v iteraciones --> Indica la velocidad de la bola, pero indicando el número de iteraciones, de modo que su valor es inversamente proporcional a la velocidad de la bola. Se contendrá en la variable int velocidad = 30000000;, que por defecto vale 30000000. Si el número de parámetros es erróneo y no se trata de la opción --help, se tendrá un error. if ((argn%2) == 0) { printf("alarma: Forma de uso incorrecta\n"); printf("Pruebe: ./alarma --help\n"); exit(0); } Para tomar los parámetros, se hará uso de un bucle, en función del número de parámetros, iterando la mitad de ellos, pues uno dice el tipo y otro el valor. 6
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo int p; for(p=1; p
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo 1. Dibujar espacios blancos antes de la posición de la bola. for(i=0; i
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo columna anterior a la columna máxima. La ejecución del programa principal tal cual (sin uso de señales), proporcionaría la siguiente salida: [enrique@adsl p2]$ ./alarma o Aunque no puede verse, la bola simulado con la o se movería de un lado a otro rebotando en función de los márgenes definidos. Señales En Linux, los programas responderán a señales o llamadas. Éstas pueden declararse para ser atendidas por la función que deseemos o bien se atenderán de la forma por defecto. En general, cuando no se declara una señal, ésta se tratará por el Sistema Operativo y lo que hará es cerrar el programa. Por ello deben declararse las señales que sean susceptibles de ser recibidas por nuestro programa. Por otro lado, la señal KILL es la única que no podremos definir, ya que de lo contrario podríamos controlarla con una función que no obligara a que nuestro programa fuese “matado”. Para el control de señales existen dos técnicas diferentes: 1. Declaración con la función signal. Para cada señal que deseemos atender, bastará crear una función que lo haga e indicarlo de la siguiente forma: signal(SEÑAL, FUNCIÓN); Por ejemplo, para la señal de alarma (que tiene por valor el número 14, contenido en la macro SIGALRM), que controlaremos con la función muestra_fecha, haremos uso de la siguiente instrucción: signal(SIGALRM, muestra_fecha); En el caso de la alarma también será necesario llamar a la función alarm, posteriormente, de 9
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo la forma: alarm(tiempo_alarma); Sin embargo, para la mayoría de señales basta con hacer uso de la función signal, nada más. Como consideración adicional hay que indicar que es recomendable usar las macros de los nombres de las señales en lugar de los números de las mismas, pues pudieran variar sus valores numéricos de unas versiones a otras del kernel de Linux. 2. Estructuras sigaction. En este caso debemos declarar una estructura de tipo sigaction para cada señal que deseemos definir como tratar o atender. struct sigaction senal_alarma, senal_int; Esta forma es más compleja y tediosa que la declaración con la función signal, pero permite mucho más control. A continuación podemos ver los principales campos de esta estructura y las funciones de apoyo de las mismas, comentando la funcionalidad de cada una de ellas. Consultando el man de sigaction podemos ver que la estructura sigaction se define así: struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); } El campo más importante es el primero, sa_handler, en el que se indica la función que manejará la señal. A continuación se comentan los campos más importantes, incluyendo éste. 1. sa_handler: especifica la acción que se va a asociar con signum y puede ser SIG_DFL para la acción predeterminada, SIG_IGN para no tener en cuenta la señal, o un puntero a una función manejadora para la señal. 2. sa_flags: especifica un conjunto de opciones que modifican el comportamiento del proceso de manejo de señal. Se forma por la aplicación del operador de bits OR a cero o más de las constantes que admite. Por defecto actuará de forma normal, aunque si se desea puede asignársele 0 10
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo o bien hacer uso de la función sigemptyset que se comenta más adelante. 3. sa_mask: da una máscara de señales que deberían bloquearse durante la ejecución del manejador de señal. Además, la señal que lance el manejador será bloqueada, a menos que se activen las opciones SA_NODEFER o SA_NOMASK. Por defecto actuará de forma normal, aunque si se desea puede asignársele 0 o bien hacer uso de la función sigemptyset que se comenta más adelante. La función sigemptyset tiene la forma int sigemptyset(sigset_t *conjunto);. Con ella se inicia el conjunto de señales dado por conjunto al conjunto vacío, con todas las señales fuera del conjunto. Por lo general puede asginarse 0 y se obtiene un resultado similar o bien no hacer nada. Finalmente, y lo más importante para que la atención a la señal tenga efecto, debe hacerse uso de la función sigaction, que tiene la siguiente forma: int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); La llamada al sistema sigaction se emplea para cambiar la acción tomada por un proceso cuando recibe una determinada señal. El parámetro signum especifica la señal y puede ser cualquiera válida salvo SIGKILL o SIGSTOP. Si act no es nulo, la nueva acción para la señal signum se instala como act. Si oldact no es nulo, la acción anterior se guarda en oldact. Por ejemplo, en el caso de la señal de alarma, con sigaction haremos uso de las siguientes sentencias de inicialización o configuración de la estructura: senal_alarma.sa_handler = muestra_fecha; // Acción a Realizar sigaction(SIGALRM, &senal_alarma, NULL); Alarma (en Tiempo Real) En Linux puede conseguirse tiempo real, del orden de segundos, con la señal o llamada ALARM. Para ello, usaremos cualquiera de las dos técnicas antes mencionadas para la declaración de la señal y luego declararemos el tiempo de la alarma con: alarm(tiempo_alarma); La variable tiempo_alarma contiene los segundos de duración de la alarma, de modo que el kernel nos enviará dicha señal a nuestro programa pasados exactamente los segundos indicados en 11
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo dicha variable (en tiempo real). Hay que tener en cuenta que esto sólo indica que envíe dicha señal una vez, por lo que una vez se reciba la señal y se haga la tarea desea, habrá que volver a indicar al kernel que nos vuelva a enviar otra señal, volviendo a poner en el código lo mismo: alarm(tiempo_alarma); Esta segunda llamada se hará dentro del proceso de gestión de la alarma, que visto al estilo del signal se indicó de la siguiente forma: signal(SIGALRM, muestra_fecha); Con sigaction se haría de la siguiente forma: struct sigaction senal_alarma; senal_alarma.sa_handler = muestra_fecha; // Acción a Realizar sigaction(SIGALRM, &senal_alarma, NULL); De este modo, la función muestra_fecha es la que vuelve a pedir la alarma, una vez se ha realizado la tarea deseada en dicha función. Interrupción Adicionalmente a la alarma, como el programa desarrollado es un bucle infinito que hace un uso intensivo de la pantalla, para poder finalizarlo hay que enviarle la señal SIGINT, que no es más que escribir Ctrl+C desde el terminal en que se está ejecutando el programa. Esto provoca la finalización brusca del programa, de modo que hemos optado por controlar dicha señal con la función termina, para que finaliza mostrando un mensaje de finalización: void termina() { printf("El programa ha finalizado correctamente.\n"); exit(0); } Con signal declaramos la atención a la señal SIGINT de la siguiente forma: signal(SIGINT, termina); Mientras que con sigaction lo haremos de la siguiente forma: struct sigaction senal_int; senal_int.sa_handler = termina; // Acción a Realizar sigaction(SIGINT, &senal_int, NULL); 12
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Lanzamiento de Procesos Uso de fork El programa que maneja la alarma tiene la tarea de crear un proceso que muestra la fecha, para lo cual hará uso del programa date, existente en el sistema en la ruta /bin/date. Esto se hará con una de las funciones de tipo exec, pero previamente se requiere que se cree un proceso hijo, que ejecute la función muestra_fecha, que es la que se llama cuando se produce la alarma, pues al lanzar el exec, el proceso desaparece y es sustituido por el proceso que ejecuta el date. De este modo, usaremos la llamada al sistema fork, para que se cuando se esté ejecutando la función muestra_fecha por el proceso principal o padre, se cree una copia idéntica de este proceso, pero que será el proceso hijo. El proceso hijo lanzará el exec para que se ejecute el date y muestre la fecha, mientras que el padre seguirá haciendo uso del recurso pantalla, terminando la ejecución de la función muestra_fecha. El código, resumido, que permite esto es el siguiente: int pid; switch (pid = fork()) { case -1: // fork() falla printf("No se pudo crear otro proceso\n"); exit(1); case 0: // Hilo hijo /* ... */ default: // Hilo padre /* ... */ } En el switch se lanza el fork, de modo que a partir de ese momento existen dos procesos, el padre y el hijo, que son idénticos, pero con una ligera diferencia: el valor de la variable pid vale 0 para el hijo y para el padre toma un valor mayor que 0, que es el identificador del proceso (pid) del hijo. Con esta distinción, el hijo ejecutará el código del case 0:, y el padre el del default:. En el caso de que la función fork falle, es decir, no pueda duplicar el proceso, devolverá -1 en la variable pid, y se mostrará un mensaje de error y se abortará la función, según se ve en el case -1:. 13
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Lo que hace el proceso hijo y el padre, puede verse en la explicación de las llamadas al sistema exec y waitpid, que se muestran a continuación. Uso de exec La función exec permite lanzar un programa, pasándole argumentos si los tubiere. Esta función provoca la desaparición del programa que ejecutaba el proceso que lanza la función exec y todo su espacio de trabajo es ocupado por el del programa llamado por la función exec, es decir, es como si el programa que llama a exec muriese, por lo que debe hacer por el proceso hijo creado con el fork, según se vió en el apartado Uso de fork. El código que ejecutará el proceso hijo (el que está dentro del case 0:) y que hace uso de la llamada al sistema execl (que es una de las muchas variantes del exec) para lanzar el programa date, para que muestre la fecha, es (en este caso no se muestra el control de la sección crítica con pipes, por claridad): execl("/bin/date","fecha",NULL); Los parámetros de la función execl, de forma genérica son: execl(PROGRAMA, NOMBRE_PROGRAMA, ARG1, ARG2, ..., ARGn, NULL); Esto quiere decir que primero se indica el programa a ejecutar, que en nuestro caso es el date, y pasamos la ruta completa /bin/date. Luego se pone el nombre del programa, que en principio no tiene gran utilidad y luego la lista de parámetros, que deben ser de tipo char * (es decir, ristras), terminando con un NULL; esto hace que los argumentos formen un vector de ristras, que sería de tipo char *argv[], que es el típico parámetro de entrada en las funciones main, para tomar todos los argumentos, y que está precidido por el parámetro int argn, que indica el número de argumentos, incluyendo el nombre del programa. Al ejecutarse el execl muere o desaparece el proceso hijo, por lo que ni siquiera es necesario un break; al final del case 0:; lo mismo ocurre si pusiéramos un exit, pues nunca se ejecutaría, salvo que el execl fallara. El espacio de trabajo pasa a ser ocupado por el programa date. Cuando este programa finaliza su tarea, consistente en el muestreo de la fecha, por pantalla, se encargará de 14
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo indicar la finalización suya y, por tanto, la del proceso hijo que lanzó el execl, quedando indicado el estado de finalización del mismo, útil para la llamada al sistema waitpid, que se ve a continuación. Uso de waitpid El proceso padre se encargará de ver que el proceso hijo finaliza correctamente y en tal caso liberará su espacio de trabajo, sólo cuando el hijo haya terminado su tarea. La llamada al sistema waitpid es la que permite esto. El código que ejecutará el proceso padre, dentro del default:, será (obviando el control de la sección crítica con pipes): waitpid(pid,&estado,WNOHANG); alarm(tiempo_alarma); Se habrá declarado la variable estado, para recoger el estado del proceso hijo, pero no será necesario testear su valor, pues supondremos que es correcto, si bien es necesario pasarla por referencia como segundo argumento de la función waitpid. El primer parámetro es el identificador del proceso hijo (pid) por el que debe esperarse; también valdría poner -1 para esperar por cualquier proceso hijo, pero como sólo hay uno, no tiene sentido. El último parámetro es indicador del comportamiento del waitpid. Como por defecto, waitpid retendría el proceso padre en ese punto, lo que supondría un control intrínseco de la sección crítica, hacemos que el waitpid no sea bloqueante, con la opción WNOHANG. De esta forma, el proceso padre sigue su ejecución, y cuando el hijo termine, surgirá el efecto del waitpid, lo cual no significa que el contador de programa del proceso padre vuelva poner justo en el waitpid, sino que simplemente se liberará el espacio de trabajo del proceso hijo cuando éste haya terminado. Por otro lado, el proceso padre deberá volver a activar la alarma, para que el kernel vuelva a avisarle. alarm(tiempo_alarma); Uso de exit La llamada al sistema exit es la que permite que un proceso termine, y como parámetro permite que se indica un número entero indicativo del estado de terminación del proceso, para que el padre pueda testear dicho estado de finalización y actuar en consecuencia. Se hace uso del exit, en el case -1:, cuando el fork falla, mostrando un mensaje de erro y 15
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo finalizando el proceso con: exit(1); También en la función termina, que se llama por la señal SIGINT, se hace la finalizaicón con el exit: exit(0); En el primer caso se devuelve 1, indicando error, mientras que en el segundo caso se devuelve 0, que por convenio suele ser el estado correcto de finalización de un proceso. Control de la Sección Crítica, con Pipes Definición de Pipe Un pipe crea un par de descriptores de ficheros, que apuntan a un nodo-í de una tubería, y los pone en el vector de dos elementos apuntado por descf. Así, descf[0] es para lectura, descf[1] es para escritura. Esto se consigue facilmente declarando un vector de elementos y usando la función pipe: int tuberia[2]; pipe(tuberia); Una versión más compleja son las FIFOS, o pipes con nombre, que permiten el uso de las mismas entre programas que no estén relacionados, es decir, que no sean hijos del padre en que se declarán y crean los pipes. Pero como en nuestro caso todos los procesos están relacionados, pues uno es el padre y el otro es el hijo, no habrá problema con el uso de los pipes. El primer paso, para que la tubería funcione bien, es inicializarla introduciendo algo en la misma, para que así el primer proceso que quiera entrar en la sección crítica pueda hacerlo, por lo que prácticamente siempre se hará seguidamente a la creación del pipe, lo siguiente: write(tuberia[1], &valor, sizeof(int)); fflush(NULL); Esta simplemente provoca la escritura en el pipe. Es necesario el uso de la función fflush para que las escrituras o lecturas del pipe se hagan efectivas. Con NULL se indica que se escriba en todo los ficheros abiertos, vaciando los bufferes de lectura/escritura. Si no se usa el fflush, podrían tener problemas. 16
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Aunque esto ya es parte del manejo de los pipes, a continuación se comenta claramente como deben manejar, enfocando dicho manejo al control de la sección crítica como problema genérico de programación. Manejo de Pipe Los pipes crean un descritor de fichero de lectura y otro de escritura, de modo que podremos hacer uso de las funciones read y write para leer y escribir en el pipe, respectivamente. Los descriptores será el primer parámetro de estas dos funciones. En nuestro caso tenemos los siguientes descriptores: 1. Descriptor de lectura --> tuberia[0] 2. Descriptor de escritura --> tuberia[1] Para leer del pipe usaremos: read(tuberia[0], &valor, sizeof(int)); fflush(NULL); Para escribir en el pipe usaremos: write(tuberia[1], &valor, sizeof(int)); fflush(NULL); El funcionamiento del pipe es muy simple: si no hay nada escrito en el pipe, al leer se bloqueará el programa hasta que se escriba algo. Por otro lado, si se lee algo del pipe, éste se vaciará. No obstante, para que esto funcione así, es necesario que se escriba y lee en igual cantidad, por lo que se escribe y lee simplemente el tamaño de un entero (sizeof(int)), lo cual se indica en el tercer parámetro. Por otro lado, aunque no se use para nada el valor escrito o devuelto, éste debe indicarse, que en nuestro caso se hace con la variable valor, pasado por referencia como segundo parámetro. Adicionalmente, simpre se hace fflush(NULL); por seguridad. Si leemos del pipe al entrar en la sección crítica, el pipe se vaciará, de modo que si otro proceso va acceder a la sección crítica, cuando lea del pipe quedará bloqueado y no podrá entrar en la sección crítica. Cuando un proceso salga de la sección crítica, escribirá en el pipe, de modo que los procesos bloqueados leyendo del pipe, podrán entrar en la sección crítica. El proceso que entrará 17
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo será el primero que lea del pipe, por lo que pudiera existir inanición si hay muchos procesos esparando por entrar en la sección crítica, pero si solo hay un proceso esperando no existe inanición, como ocurre en nuestro caso. El esqueleto de código sería, por tanto, el siguiente: read(tuberia[0], &valor, sizeof(int)); fflush(NULL); /* SECCIÓN CRÍTICA */ write(tuberia[1], &valor, sizeof(int)); fflush(NULL); Este esqueleto sería válido y necesario para todos los procesos que quiesieran entrar en la sección crítica. Problema de la Sección Crítica En nuesto programa, el problema de la sección crítica es debido al uso de la pantalla por ambos procesos. De este modo, lo que se debe hacer es controlar que no escriban los dos procesos a la vez en la pantalla, pues en tal caso lo que escribe uno y otro se entrelazaría. Para evitar este problema se hace uso de pipes para el control de la sección crítica, que es el acceso o uso del recurso pantalla. La solución básica es el uso del esqueleto genérico visto anteriormente, pero en el siguiente apartado se comenta claramente como es la solución en nuestro programa, pues difiere ligeramente. Solución a la Sección Crítica En el programa principal o padre, que hace un uso intensivo de la pantalla, se rodea el uso de la pantalla, que es la sección crítica, por la lectura y escritura en el pipe, teniendo el siguiente código, en el que se muestra sombreado de gris la sección crítica (el uso de la pantalla): read(tuberia[0], &valor, sizeof(int)); fflush(NULL); printf("\r"); for(i=0; i
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo acceso a la sección crítica, antes de lanzar el programa date, pero luego, como muere, no podrá liberar la sección crítica. La solución se consigue haciendo que le proceso padre se encargue de liberar la sección crítica. Así, el proceso padre, justo después del waitpid, liberará la sección crítica, ya que el proceso hijo no puede. Aquí puede verse como el waitpid debe no ser bloqueante, es decir, tener la opción WNOHANG, pues si no, no sería necesario el control de salida de la sección crítica, si bien habría que hacerlo a la entrada, como ahora, para que no imprima la fecha mientras el proceso padre está imprimiendo en pantalla, y luego a la salida, después del waitpid, como ahora, para que el proceso principal no se bloquee al entrar en la sección crítica. Si no se usa WNOHANG, basta poner 0 en su lugar. El código, por tanto, del proceso padre e hijo, que es compartido por ambos, si bien uno ejecuta una parte y el otro otra parte, es e de la función muestra_fecha, y es el siguiente (para el control de la sección crítica, mostrada sombreada de gris): case 0: // Hilo hijo read(tuberia[0], &valor, sizeof(int)); fflush(NULL); execl("/bin/date","fecha",NULL); default: // Hilo padre waitpid(pid,&estado,WNOHANG); write(tuberia[1], &valor, sizeof(int)); fflush(NULL); Hecho todo esto, el programa producirá la siguiente salida, haciendo uso de los parámetros por defecto, una vez compilado y construido como se indica en el apartado Compilación y Ejecución del programa (Makefile). [enrique@adsl p2]$ ./alarma o mié mar 23 15:37:05 WET 2005 o mié mar 23 15:37:09 WET 2005 o mié mar 23 15:37:13 WET 2005 o El programa ha finalizado correctamente. 19
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Compilación y Ejecución del programa (Makefile) El programa desarrollado tiene su código fuente en el fichero alarma.c y se acompaña de un fichero Makefile para se compilación, construcción y ejecución más automática; además de limpiar ficheros creados en tales procesos. A continuación se comenta el fichero Makefile, que define una serie de reglas que podrán lanzarse desde el terminal con el comando make REGLA. Igualmente se comenta como ejecutar el programa desarrollado, así como el paso de parámetros al mismo. Explicación y Reglas del Makefile El fichero Makefile define las siguientes reglas: 1. Compilar Tiene la forma: compilar: limpiar $(COMPILADOR) $(FUENTES) -c $(OPCIONES) Lo primero que hace es llamar a la regla limpiar y luego compila el programa con el COMPILADOR que se indica, para todos los ficheros FUENTES que se indique y además, con las OPCIONES deseadas. Existe una regla alias de ésta que es compile. Su resultado es el siguiente: [enrique@adsl p2]$ make compilar rm -f alarma.o alarma gcc alarma.c -c -Wall -g [enrique@adsl p2]$ ls alarma.c alarma.o Makefile 2. Construir Tiene la forma: construir: compilar $(COMPILADOR) $(NOMBRE).o -o $(NOMBRE) 20
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Lo primero que hace es llamar a la regla compilar y luego construye el ejecutable, con el COMPILADOR indicado y con el NOMBRE proporcionado. Su resultado es el siguiente: [enrique@adsl p2]$ make construir rm -f alarma.o alarma gcc alarma.c -c -Wall -g gcc alarma.o -o alarma [enrique@adsl p2]$ ls alarma alarma.c alarma.o Makefile Memoria 3. Ejecutar Tiene la forma: ejecutar: construir ./$(NOMBRE) $(ARGUMENTOS) Lo primero que hace es llamar a la regla construir y luego ejecuta al programa NOMBRE con los ARGUMENTOS indicados. Por norma general querremos ejecutar el programa, de modo que podemos usar directamente el comando make run, que producirá el siguiente resultado: [enrique@adsl p2]$ make run rm -f alarma.o alarma gcc alarma.c -c -Wall -g gcc alarma.o -o alarma ./alarma -b "o" -c 40 -s SIGNAL -t 4 -v 30000000 o mié mar 23 15:41:07 WET 2005 o mié mar 23 15:41:11 WET 2005 o El programa ha finalizado correctamente. Puede verse como limpia, compila, construye y ejectua el programa alarma pasando una serie de argumentos por defecto. 4. Limpiar Tiene la forma: limpiar: $(RM) $(OBJETOS) $(EJECUTABLES) 21
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo El resultado de limpiar puede verse a continuación: [enrique@adsl p2]$ ls alarma alarma.c alarma.o Makefile [enrique@adsl p2]$ make clean rm -f alarma.o alarma [enrique@adsl p2]$ ls alarma.c Makefile Simplemente borra con el comando RM los ficheros OBJETOS y EJECUTABLES, que habrán creado las otras reglas. Las variables creadas son las siguientes: COMPILADOR = gcc OPCIONES = -Wall -g NOMBRE = alarma FUENTES = $(NOMBRE).c OBJETOS = $(NOMBRE).o EJECUTABLES = $(NOMBRE) TIPO_BOLA = "o" COLUMNA_MAX = 40 TIPO_SIGNAL = SIGNAL TIEMPO_ALARMA = 4 VELOCIDAD = 30000000 ARGUMENTOS = -b $(TIPO_BOLA) -c $(COLUMNA_MAX) -s $(TIPO_SIGNAL)\ -t $(TIEMPO_ALARMA) -v $(VELOCIDAD) RM = rm -f La variable ARGUMENTOS se compone de los argumentos que puede recibir el programa, que se explican en el siguiente apartado. Ejecución del programa Si simplemente ejecutamos el programa con ./alarma se hará uso de los valores por defectos, que hay dentro del código; en el caso del Makefile siempre puede alterarse modificando el propio fichero Makefile. Para tener mayor versatilidad, podemos hacer uso del paso de parámetros, que se explican a continuación, pues de lo contrario, por defecto se tomarán los siguientes valores: Bola = “o” Columna máxima = 40 22
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Tipo de señal = SIGNAL Tiempo de alarma = 4 Velocidad = 30000000 Paso de Parámetros Existen 5 parámetros distintos, a parte de la ayuda con --help. Dichos parámetros tiene una abreviatura para indicarlos y poner seguidamente el valor, de modo que no importa el orden de los mismos. Son los siguientes, si bien ya se comentaron en el apartado Parámetros. -b --> Ristra que emula la bola. -c --> Columna máxima. -s --> Tipo de implementación de las señales (con SIGNAL o con SIGACTION). -t --> Tiempo de la alarma. -v --> Velocidad de la bola; medida de forma inversa, como iteraciones de un bucle. A continuación se muestran algunos ejemplos de ejecución: 1. Modificación de la bola (-b). En el primer caso simplemente emulamos la bola con *, poniendo -b “*”. [enrique@adsl p2]$ ./alarma -b "*" * mié mar 23 15:42:21 WET 2005 * mié mar 23 15:42:25 WET 2005 * El programa ha finalizado correctamente. Hay que indicar que el sentido de la variable max_columna (que controla la columna máxima) es el número de columnas que recorre el primer caracter de la ristra que emula la bola, pues así, se mueve lo deseado, si bien el último caracter de la ristra llegara más allá de la columna indicada por max_columna, lo cual se observará viendo el siguiente ejemplo, donde la columna de escritura de la fecha está más a la derecha, en concreto 3 columnas más allá, pues se emula la bola con “hola”, que tiene 3 caracteres de más que la unidad. [enrique@adsl p2]$ ./alarma -b hola hola mié mar 23 15:42:41 WET 2005 hola mié mar 23 15:42:45 WET 2005 23
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo hola mié mar 23 15:42:49 WET 2005 hola mié mar 23 15:42:53 WET 2005 hola El programa ha finalizado correctamente. Finalmente, recordamos que no son necesarias las comillas, pero para ciertos caracteres son necesarias, y para ristras con espacios en blanco también, como se ve a continuación: [enrique@adsl p2]$ ./alarma -b "Esta es la bola: ()" Esta es la bola: () mié mar 23 15:44:18 WET 2005 Esta es la bola: () mié mar 23 15:44:22 WET 2005 Esta es la bola: () El programa ha finalizado correctamente. Los caracteres que requieren las comillas son aquellos con un significado especial en el terminal, ya que se sustituirán por otras cadenas. Por ejemplo: * --> indica cualquier fichero/carpeta, luego toma el valor del primer fichero/carpeta del directorio por orden alfabético. ~ --> toma el valor del directorio de trabajo del usuario. Etcétera. 2. Modificación de la columna máxima (-c). Se reduce o aumenta el número de columnas por los que se mueve la bola, como se ve en los siguientes ejemplos, en los que se reduce la columna máxima a 5 y 1, respectivamente; hay que recordar que la primera columna del terminal es la 0 y el valor de la columna máxima debe ser mayor que 0, pues de lo contrario fallará la simulación de la bola rebotando. [enrique@adsl p2]$ ./alarma -c 5 o mié mar 23 15:46:31 WET 2005 o mié mar 23 15:46:35 WET 2005 o mié mar 23 15:46:39 WET 2005 o El programa ha finalizado correctamente. [enrique@adsl p2]$ ./alarma -c 1 o mié mar 23 15:46:49 WET 2005 o El programa ha finalizado correctamente. 3. Modificación del tipo se señal (-s). A continuación se muestra la ejecución del programa usando señales implementadas con 24
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo SIGNAL y con SIGACTION, respectivamente. Y se observa que el comportamiento no precesente diferencias aparentes. [enrique@adsl p2]$ ./alarma -s SIGNAL o mié mar 23 15:48:01 WET 2005 o mié mar 23 15:48:05 WET 2005 o El programa ha finalizado correctamente. [enrique@adsl p2]$ ./alarma -s SIGACTION o mié mar 23 15:48:13 WET 2005 o mié mar 23 15:48:17 WET 2005 o El programa ha finalizado correctamente. 4. Modificación del tiempo de la alarma (-t). Si alteramos la velocidad de la alarma, la frecuencia con la que se muestra la fecha varía. Así, en los siguientes ejemplos puede verse claramente (se ha puesto 1, 4 y 7 segundos respectivamente): [enrique@adsl p2]$ ./alarma -t 1 o mié mar 23 15:49:09 WET 2005 o mié mar 23 15:49:10 WET 2005 o mié mar 23 15:49:11 WET 2005 o mié mar 23 15:49:12 WET 2005 o mié mar 23 15:49:13 WET 2005 o El programa ha finalizado correctamente. [enrique@adsl p2]$ ./alarma -t 4 o mié mar 23 15:49:23 WET 2005 o mié mar 23 15:49:27 WET 2005 o El programa ha finalizado correctamente. [enrique@adsl p2]$ ./alarma -t 7 o mié mar 23 15:49:39 WET 2005 o mié mar 23 15:49:46 WET 2005 o El programa ha finalizado correctamente. 5. Modificación de la velocidad de la bola (-v). La velocidad de la bola no puede apreciarse en las siguientes capturas, pero si pueden comentarse algunas conclusiones, aunque éstas también dependerán de la máquina en que se ejecute el programa. 25
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo [enrique@adsl p2]$ ./alarma -v 0 o mié mar 23 15:50:18 WET 2005 o El programa ha finalizado correctamente. [enrique@adsl p2]$ ./alarma -v 1000000 o mié mar 23 15:50:39 WET 2005 o El programa ha finalizado correctamente. [enrique@adsl p2]$ ./alarma -v 3000000 o mié mar 23 15:50:55 WET 2005 o El programa ha finalizado correctamente. [enrique@adsl p2]$ ./alarma -v 30000000 o mié mar 23 15:51:01 WET 2005 o El programa ha finalizado correctamente. Con 0 se tiene la velocidad más rápida y con 30000000 la más lenta, en la cual se aprecian saltos. Con 3000000 se ve un movimiento fluido y rápido pero más o menos perceptible. Algunos ejemplos más, de ejecución, haciendo uso de combinaciones de parámetros, se muestran a continuación: 1. Ejemplo 1 Se pone un tiempo de alarma de 1 segundo (-t 1), una velocidad de la bola rápida pero perceptible (-v 3000000) y se emula la bola con el carácter ¬. [enrique@adsl p2]$ ./alarma -t 1 -v 3000000 -b ¬ ¬ mié mar 23 15:53:56 WET 2005 ¬ mié mar 23 15:53:57 WET 2005 ¬ mié mar 23 15:53:58 WET 2005 ¬ mié mar 23 15:53:59 WET 2005 ¬ mié mar 23 15:54:00 WET 2005 ¬ mié mar 23 15:54:01 WET 2005 ¬ mié mar 23 15:54:02 WET 2005 ¬ El programa ha finalizado correctamente. 2. Ejemplo 2 Se pone un tiempo de alarma de 1 segundo (-t 1), una velocidad de la bola rápida pero perceptible (-v 3000000), se emula la bola con la ristra “DSO” y se limita la columna máxima a la 3, de modo que el primer carácter que emula la bola (D) sólo podrá llegar a la columna 3 como 26
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo máxima (podrá estar en las columnas 0, 1, 2 y 3); y el último carácter (O) podrá llegar a la columna 5, pues la ristra tiene 2 caracteres más que la unidad (3 + 2 = 5). [enrique@adsl p2]$ ./alarma -t 1 -v 3000000 -b "DSO" -c 3 DSO mié mar 23 15:56:11 WET 2005 DSO mié mar 23 15:56:12 WET 2005 DSO mié mar 23 15:56:13 WET 2005 DSO mié mar 23 15:56:14 WET 2005 DSO mié mar 23 15:56:16 WET 2005 DSO mié mar 23 15:56:16 WET 2005 DSO mié mar 23 15:56:17 WET 2005 DSOEl programa ha finalizado correctamente. 27
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo Anexo – Código Fuente alarma.c #include #include #include #include #include #include #include int tuberia[2], tiempo_alarma = 4; void muestra_fecha() { int pid, estado, valor; switch (pid = fork()) { case -1: // fork() falla printf("No se pudo crear otro proceso\n"); exit(1); case 0: // Hilo hijo // · Imprime fecha con el programa date (controlando sección crítica con pipe) read(tuberia[0], &valor, sizeof(int)); fflush(NULL); execl("/bin/date","fecha",NULL); default: // Hilo padre // Permite liberar el hilo hijo del árbol de procesos waitpid(pid,&estado,WNOHANG); write(tuberia[1], &valor, sizeof(int)); fflush(NULL); alarm(tiempo_alarma); } } void termina() { printf("El programa ha finalizado correctamente.\n"); exit(0); } int main(int argn, char *argv[]) { int columna = 0, max_columna = 40, paso = 1, velocidad = 30000000, valor, i; int senal_creada = 0; // Indica si se han creado las señales char *bola = "o"; struct sigaction senal_alarma, senal_int; // PASO 1: Parámetros de entrada // · Ayuda (--help) if ((argn == 2) && (strcmp(argv[1],"--help") == 0)) { printf("Modo de empleo: ./alarma [OPCIÓN]\n"); printf("\t-b bola; Ristra que representa la bola\n"); printf("\t-c columna_máxima; Columna máxima a la que llega la bola\n"); printf("\t-s SIGNAL|SIGACTION; Implementación de señales (SIGNAL por defecto)\n"); printf("\t-t segundos; Tiempo entre muestreo de la fecha\n"); 28
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo printf("\t-v iteraciones; Velocidad de la bola\n"); exit(0); } // · Uso Incorrecto if ((argn%2) == 0) { printf("alarma: Forma de uso incorrecta\n"); printf("Pruebe: ./alarma --help\n"); exit(0); } // · Toma de Parámetros int p; for(p=1; p
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo // PASO 3: Bucle de muestreo de la bola while(1) { // PASO 3.1: Pausa entre movimientos de la bola // (sleep(1), solo funciona para segundos enteros) for(i=0; i
Práctica 2: Realización de una Alarma Temporizada David J. Horat Flotats Diseño de Sistemas Operativos – U.L.P.G.C. Enrique Fernández Perdomo ejecutar: construir ./$(NOMBRE) $(ARGUMENTOS) limpiar: $(RM) $(OBJETOS) $(EJECUTABLES) #################### # Reglas en Inglés # #################### compile: compilar build: construir run: ejecutar clean: limpiar 31
También puede leer