Esta es una vista previa del archivo. Inicie sesión para ver el archivo original
practica3-gii-ssoo-2018(1).pdf UNIVERSIDAD DE ALCALÁ Departamento de Automática Grado en Ingeniería Informática Práctica 3: Herramientas de desarrollo y Servicios POSIX para la gestión de procesos Sistemas Operativos Índice 1. Competencias asociadas a la práctica 4 2. Introducción 4 3. Ciclo de creación de un programa 5 3.1. Edición del archivo fuente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 3.2. Compilación y enlazado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7 3.3. Depuración . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 4. Automatización del proceso de desarrollo de aplicaciones: make 8 5. Modelo de los procesos en UNIX 8 6. Llamadas al sistema y servicios POSIX 9 7. Servicios POSIX para la gestión de procesos 10 7.1. Servicio POSIX fork() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 7.2. Servicio POSIX exec() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 7.3. Servicio POSIX exit() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 7.4. Servicios POSIX wait() y waitpid() . . . . . . . . . . . . . . . . . . . . . . . . 11 8. Servicios POSIX para comunicación entre procesos 12 8.1. Servicios POSIX de señales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 8.1.1. Servicio POSIX sigaction() . . . . . . . . . . . . . . . . . . . . . . . . . 13 8.1.2. Servicio POSIX kill() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 8.2. Servicios POSIX de pipes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 8.2.1. Servicio POSIX pipe() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 8.2.2. Servicio POSIX close() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 8.2.3. Servicio POSIX read() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 8.2.4. Servicio POSIX write() . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18 8.2.5. Servicios POSIX para redirecciones: dup() y dup2() . . . . . . . . . . . . 19 9. Intérprete de órdenes 22 9.1. Ciclo de ejecución del intérprete de órdenes . . . . . . . . . . . . . . . . . . . . . 22 10.Ejercicio 22 10.1.FASE 1: Ciclo de ejecución de órdenes . . . . . . . . . . . . . . . . . . . . . . . 24 10.2.FASE 2: Ejecución de órdenes externas simples en primer plano . . . . . . . . . 26 10.3.FASE 3: Ejecución de órdenes externas simples en segundo plano . . . . . . . . 29 10.4.FASE 4: Realización de Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 10.5.FASE 5: Ejecución de secuencia de órdenes . . . . . . . . . . . . . . . . . . . . 30 10.6.FASE 6: Tratamiento de redirecciones . . . . . . . . . . . . . . . . . . . . . . . . 31 10.7.FASE 7: Implementación de tuberías o pipes . . . . . . . . . . . . . . . . . . . . 32 A. Diagrama de la aplicación minishell 35 3 1. Competencias asociadas a la práctica 1. Distinguir las funcionalidades implementadas en una shell completa de Unix y las funcio- nalidades implementadas en el intérprete de órdenes de la práctica y lo que esto implica respecto a los resultados obtenidos al ejecutar diversas órdenes en ambos casos. 2. Identificar los servicios POSIX de gestión de procesos necesarios para la ejecución de órdenes concretas, internas y externas (en primer y segundo plano). 3. Programar el ciclo de ejecución del intérprete de órdenes. 4. Programar la ejecución de órdenes internas del intérprete de órdenes. 5. Programar la ejecución en primer plano de órdenes externas del intérprete de órdenes. 6. Programar la ejecución en segundo plano de órdenes externas del intérprete de órdenes. 7. Programar el uso de redirecciones de entrada y salida estándar (>, <) en el intérprete de órdenes. 8. Programar el uso de las tuberías sin nombre en el intérprete de órdenes. 9. Programar la secuencia de órdenes (órdenes separadas por ‘;’) en el intérprete de órde- nes. 10. Aplicar las herramientas de desarrollo clásicas en Unix: gcc, make, gdb y uso de bibliote- cas estáticas. 11. Utilizar un estilo de programación (documentación incluida) correcto y uniforme en la programación del intérprete de órdenes. 12. Desarrollar un proyecto en equipo. 13. Valorar de manera justificada y ética la participación de los compañeros al equipo en el proyecto desarrollado (actitud: perseverancia, responsabilidad, asistencia, capacidad de colaboración, capacidad de organización, de liderazgo y de resolución de conflictos). 2. Introducción En prácticas anteriores se ha introducido al alumno en el uso de un intérprete de órdenes, bash, como interfaz de usuario en Linux. Esta práctica tiene como objetivo principal introducir al alumno en la interfaz de aplicacio- nes, comenzando con el uso, a alto nivel, de las llamadas al sistema (en concreto, servicios POSIX) para gestionar procesos y su comunicación, y en menor medida, llamadas al siste- ma para gestión de archivos, como preámbulo a su estudio exhaustivo en la asignatura de Sistemas Operativos Avanzados, de segundo curso de los grados de Ingeniería Informática e Ingeniería de Computadores. Para lograr el objetivo previo, el alumno aplicará los conceptos vistos en las clases teóricas y en el laboratorio mediante la implementación parcial de un intérprete de órdenes en Unix (al que denominaremos minishell). Otro objetivo principal de esta práctica es que el alumno sea capaz de usar las herramien- tas de desarrollo clásicas utilizadas en entornos Unix, tal y como se han descrito también en las clases teóricas. Actualmente existen entornos de desarrollo (o IDEs) muy avanzados, tales como Eclipse, Netbeans, etc., que abstraen al desarrollador de casi todos los detalles relacio- nados con la compilación del proyecto, de manera que su productividad aumenta al permitir 4 que se concentre en la creación de código. Esta perspectiva, a pesar de ser la empleada co- múnmente en entornos profesionales, presenta dos problemas: por una parte esa abstracción impide conocer qué sucede en el proceso de compilación a bajo nivel y, por otra parte, ese desconocimiento reduce la capacidad de resolver problemas que pudieran surgir. Hablar de programación bajo Unix es hablar de C. C se creó con el único objetivo de recodificar Unix (que en sus orígenes estaba programado en ensamblador) en un lenguaje de alto nivel, decisión que, por cierto, fue revolucionaria en su época. Además, todo el desarrollo de Arpanet y su sucesora, Internet, se basó en la utilización de máquinas Unix. Por lo tanto, C ha tenido un impacto decisivo en el desarrollo de la informática, hasta el punto de que la práctica totalidad de los lenguajes más extendidos en la actualidad (Java, C++, C# o PHP, entre otros) han tomado elementos de C, o directamente son una evolución del mismo. Así pues, no es de sorprender que Unix y C tengan una relación más que estrecha, y por este motivo no se pueda abordar el estudio de Unix sin utilizar C. Adicionalmente, un subobjetivo importante dentro de la práctica es acostumbrar al alumno a trabajar en equipo, una cualidad fundamental de cara al mundo laboral. Este hecho impli- ca realizar un diseño adecuado de las aplicaciones, su modularización, así como una buena metodología de desarrollo y comunicación dentro del equipo1. 3. Ciclo de creación de un programa Como en cualquier otro entorno, crear un programa bajo UNIX requiere una serie de pasos, que deben ser ya de sobra conocidos por el alumno: Creación y edición del programa o código fuente sobre un archivo de texto, empleando para ello una herramienta denominada editor. En caso de programar en lenguaje C, el convenio es que dicho archivo tenga extensión “.c”. Compilación del código fuente mediante otra herramienta denominada compilador, ge- nerándose un archivo objeto, que en UNIX suele tener extensión “.o”. A veces, en lugar de generarse el archivo objeto directamente se genera un archivo intermedio en ensamblador (en UNIX típicamente con extensión “.s”) que, a continuación, es necesa- rio ensamblar con un ensamblador para obtener el archivo objeto. Además, en C existe una etapa previa a la compilación en la que el archivo fuente pasa por otra herramienta denominada preprocesador. Enlazado del archivo objeto con otros archivos objeto necesarios, así como con las biblio- tecas que sean necesarias para así obtener el archivo de programa ejecutable. Esta labor es llevada a cabo por un programa denominado enlazador. Finalmente, ejecutar el programa tal cual o, en caso de ser necesario, depurar el progra- ma con una herramienta denominada depurador o debugger. Si se detectan problemas en la ejecución, será necesario editar el programa fuente y corregir los fallos, reinicián- dose el ciclo de desarrollo hasta obtener un programa sin errores. El proceso anterior queda ilustrado con un ejemplo en la Figura 1. En un sistema Linux, como el empleado en el laboratorio, las herramientas clásicas encargadas de cada etapa son las siguientes: Editor: existe gran variedad de ellos, siendo los más populares emacs y vi. En el labora- torio se recomienda el uso del editor vi, o, si se prefiere, un editor en modo gráfico. 1En equipos de desarrollo profesionales, se suele utilizar la ayuda de sistemas de control de versiones como CVS o Subversion. Es un software imprescindible que permite que varias personas trabajen simultáneamente sobre un mismo código, manteniendo un registro de cambios. 5 Figura 1: Ejemplo de generación de un archivo ejecutable en lenguaje C. Preprocesador, compilador, ensamblador, y enlazador: estas herramientas pueden encontrarse de forma individual, pero en un sistema Linux típicamente encontramos la herramienta cc (realmente la herramienta que invoca la compilación en C y C++ de GNU, gcc), que se encarga de llamar al preprocesador, compilador, ensamblador, y enlazador, según se necesiten. En cualquier caso, y aunque por lo general no las utilizaremos de forma individual, las herramientas son: • Preprocesador: cpp. • Compilador: comp. • Ensamblador: as. • Enlazador: ld. Depurador: el depurador por excelencia en el entorno Linux (y en UNIX en general) es gdb. Vamos a analizar cada fase con más detalle. 3.1. Edición del archivo fuente La edición del código fuente se puede realizar con cualquier programa capaz de editar archivos en texto plano. Por su amplia difusión, gran versatilidad y velocidad de edición se utiliza mucho el editor vi, o versiones gráficas como gvim. El editor vi es mucho más potente y rápido que los editores típicos de MS-DOS o Windows, como el Bloc de Notas o el edit, y otros editores de entorno de ventanas, como gedit2. La forma de editar el programa será: user@host:$ vi programa.c Para salir del editor hay que pulsar escape y posteriormente teclear “:q!” si no se desea guardar los cambios, o bien “:wq” si se quieren guardar los cambios. 2El uso de vi al principio puede ser un poco frustrante por lo que, si el alumno no conoce el manejo de esta herramienta, es recomendable seguir alguno de los tutoriales existentes como, por ejemplo, el proporcionado en la página web de la asignatura. 6 3.2. Compilación y enlazado El programa gcc es el encargado de compilar, enlazar y generar el archivo ejecutable a partir del archivo fuente. La forma más sencilla de invocarlo es: user@host:$ gcc programa.c Esta orden preprocesa, compila, ensambla y enlaza, generando el archivo de salida eje- cutable a.out. Típicamente no se desea que esto sea así, por lo que se emplea la opción -o para establecer el nombre del archivo generado: user@host:$ cc programa.c -o programa Para ejecutar el programa es necesario invocarlo de la forma siguiente: user@host:$ ./programa Es necesario el empleo del ./ antes del nombre de programa para indicarle al intérprete de órdenes que el programa reside en el directorio actual de trabajo (.). En caso de no espe- cificarlo, sólo se buscaría el programa en aquellos directorios del sistema especificados en la variable de entorno PATH. user@host:$ echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games El programa gcc es muy flexible y soporta una gran cantidad de opciones que le permiten, por ejemplo, detenerse tras un determinado paso del proceso (por ejemplo, de la compilación) o aceptar varios archivos de entrada incluso de diferentes tipos (fuente, objeto o en ensam- blador), siendo capaz de procesar cada uno de ellos de la manera adecuada para generar el archivo de salida que se le pide. Por ejemplo, la siguiente orden compila el archivo programa.c y el objeto resultante lo enlaza con el objeto funciones.o, dando como resultado el programa de nombre programa: user@host:$ gcc programa.c funciones.o -o programa La siguiente orden compila los archivos fuente indicados, generando los archivos objeto de cada uno de ellos pero no continúa con el enlazado y generación del ejecutable final: user@host:$ gcc -c programa.c funciones.c A continuación se muestra un resumen de las opciones más frecuentes con las que se suele invocar gcc. Opción Descripción -E Parar tras el preprocesado. -S Parar tras el compilado (no ensamblar). -c Parar antes de enlazar. -o nombre Especificar el nombre del archivo de salida. -g Incluir información de depuración. -Wall Mostrar todos los avisos de compilación. -pedantic Comprobar que el programa es C estándar. -On Grado de optimización de código, donde n es un entero desde n = 0 (ninguna) a n = 3 (máxima). -D macro[=valor] Definir una macro (#define) y su valor (1 si se omite). -I directorio Indica un directorio en donde buscar archivos de cabecera (.h). -L directorio Indica un directorio en donde buscar bibliotecas compartidas. -lbiblioteca Utilizar la biblioteca compartida lbiblioteca.a. -static Enlazar bibliotecas estáticamente. 7 3.3. Depuración El depurador estándar de UNIX es gdb. Se trata de un depurador muy potente y extendido en la industria, pero sólo admite una interfaz de línea de órdenes que, a priori, puede resultar engorrosa y difícil de aprender. Existen front-ends gráficos para poder trabajar con él de un modo más cómodo, como por ejemplo la aplicación ddd. El uso del depurador es necesario para la rápida detección de errores en los programas, y por lo tanto debe ser una prioridad para el alumno. El ahorro de tiempo que conlleva su aprendizaje supera con mucho el tiempo perdido en perseguir errores difíciles de localizar de forma visual o mediante el uso de otras “técnicas” de depuración como sembrar los programas de llamadas a printf(). Como material de apoyo de la práctica se ha incluido un breve tutorial sobre el uso de gdb en la página web de la asignatura. La mejor depuración es la que no es necesario hacer, para ello se puede recurrir a mu- chos “trucos” que se aprenden con la experiencia, como desarrollar de manera incremental, añadiendo poco a poco funciones a nuestro programa, verificar la corrección de cada funcio- nalidad nueva, abordar un único problema a la vez, etc. Las actividades propuestas en esta práctica están pensadas en esta línea; se sugiere observar cómo se aborda la construcción de los programas. 4. Automatización del proceso de desarrollo de aplicaciones: make El desarrollo de un programa es una actividad cíclica en la que puede ser necesario re- compilar muchas veces múltiples archivos fuente dependientes unos de otros, y es posible que necesitemos usar múltiples opciones en línea de llamada a cc. Esto, aparte de la incomo- didad, puede ser causa de un desarrollo lento y sensible a errores. Para evitarlo en lo posible, en los entornos UNIX suele ser habitual el uso de la herramienta make para automatizar las reconstrucciones del código y minimizar el tiempo de compilación. Para ello, sólo es necesario crear un archivo llamado Makefile, que especifica la forma en que deben construirse los objetos, ejecutables o bibliotecas, y las dependencias entre ellos. Ade- más, make también es capaz de hacer otras cosas como ejecutar incondicionalmente conjuntos de órdenes. El programador novato, por lo general, es reacio a la utilización de esta herramienta, bien porque considera innecesario el uso de make, bien por pereza a la hora de crear el Makefile, bien por no dominar su uso. El empleo de make es requisito para todas las prácticas, y el alumno podrá comprobar de forma palpable la enorme ganancia de tiempo que conlleva su uso a cambio de unos minutos en la creación de un Makefile y el siempre necesario proceso de aprendizaje. Puede encontrar numerosa documentación en Internet acerca del uso del make3. 5. Modelo de los procesos en UNIX En Unix todo proceso es creado por el núcleo del SO, previa petición de otro proceso, estableciéndose una relación jerárquica entre el proceso que realiza la petición de creación, conocido como proceso padre, y el nuevo proceso, denominado proceso hijo. Un proceso padre puede tener varios hijos y todo hijo tiene únicamente un padre, tal y como se puede apreciar en la Figura 2. 3En la página web de la asignatura de Sistemas Operativos, en la sección Material complementario de la práctica 3, también se han incluido algunos enlaces de interés para el estudio de la herramienta make. 8 Figura 2: Jerarquía de procesos en UNIX. Hay una cuestión importante que cabe mencionar: si todo proceso necesita tener un pro- ceso padre, ¿cuál es el primer proceso del que se originan todos los procesos en UNIX? La respuesta se encuentra en el proceso init, que es un proceso lanzado por el SO durante el arranque del sistema y es el encargado de iniciar los procesos necesarios para poder operar. De hecho, todos los procesos que hay en el sistema descienden de una manera u otra del proceso init. La jerarquía de procesos existente en una máquina puede consultarse por medio de la orden pstree, si bien no está presente por defecto en todas las instalaciones de Linux. Por lo general, en un SO no todos los procesos tienen la misma prioridad. En el caso de Unix, a cada proceso se le puede asociar una prioridad distinta. La prioridad es un mecanismo que permite al SO dar un trato de privilegio a ciertos procesos a la hora de repartir la utilización de la CPU. Por ejemplo, resultaría poco eficiente asignar la misma prioridad a un proceso que se ejecuta en background que a un proceso interactivo, que tiene a un usuario pendiente de obtener su respuesta. Normalmente, es el propio SO quien se encarga de asignar prioridades. Sin embargo, en Unix es posible que un usuario modifique dichas prioridades por medio de la orden nice. Internamente, un SO necesita guardar información de todos los elementos del sistema, y para poder localizar dicha información necesita manejar una identificación de dichos elemen- tos, como sucede con el DNI para las personas. La forma más cómoda en el computador es trabajar con números, y de hecho, estos identificadores son tan importantes que se les ha da- do un nombre propio. A los identificadores de los archivos se les denomina número de nodo índice (nodo-i), mientras que los identificadores de los procesos se llaman PID (Process IDen- tificator ). Todo proceso en el sistema tiene un único PID asignado, y es necesario para poder identificar a dicho proceso en algunas órdenes. Asimismo, para que el SO pueda localizar con facilidad al proceso padre, todo proceso tiene asignado un segundo número conocido como PPID (o Parent Process IDentificator ). Se puede conocer el PID y PPID de los procesos que se están ejecutando en el sistema por medio de dos órdenes muy útiles para obtener información sobre procesos: ps y top. 6. Llamadas al sistema y servicios POSIX Las llamadas al sistema son el mecanismo proporcionado por el SO utilizado para que los desarrolladores puedan, en última instancia, acceder a los servicios ofrecidos por el SO. Estrictamente hablando, las llamadas al sistema se definen en ensamblador, y por lo tanto no son portables entre distintas arquitecturas. Esta situación hace que la programación sea dificultosa y no portable. Por este motivo, las llamadas al sistema se cubren con un envoltorio (o rutinas wrapper en Linux) en forma de función en lenguaje de alto nivel, típicamente C. De 9 este modo, el programador va a percibir la llamada al sistema como una mera llamada a una función y el código que desarrolle será portable a otras plataformas que soporten estas rutinas wrapper. El estándar POSIX precisamente define la firma (interfaz) de las funciones que compo- nen dicho envoltorio en sistemas UNIX. La presente práctica tiene como objeto introducir al alumno en la programación de llamadas al sistema por medio de la interfaz POSIX. Para ello, se utilizarán servicios POSIX relacionados con la gestión de procesos. 7. Servicios POSIX para la gestión de procesos Entre los aspectos más destacados de la gestión de procesos en UNIX/Linux se encuentra la forma en que éstos se crean y cómo se ejecutan nuevos programas. En esta sección se describen los principales servicios proporcionados por POSIX para el manejo de procesos. 7.1. Servicio POSIX fork() El servicio POSIX fork() permite crear un proceso. El sistema operativo trata este servicio llevando a cabo una clonación del proceso que lo invoca, conocido como proceso padre del nuevo proceso creado, denominado proceso hijo. Todos los procesos se crean a partir de un único proceso padre lanzado en el arranque del sistema, el proceso init, cuyo PID es 1 y que, por lo tanto, está situado en lo más alto en la jerarquía de procesos de UNIX, como ya se ha mencionado en la sección 5. El servicio POSIX fork() duplica el contexto del proceso padre y se le asigna este nuevo contexto al proceso hijo. Por lo tanto, se hace una copia del contexto de usuario del proceso padre -de su código, datos y pila-, y de su contexto de núcleo -que incluye, la entrada del bloque de control de procesos correspondiente al proceso padre-. Ambos procesos se diferenciarán esencialmente en el PID asociado a cada uno de ellos. Si la función se ejecuta correctamente, retorna al proceso padre el identificador (PID) del proceso hijo recién creado, y al proceso hijo el valor 0. Si, por el contrario, la función falla, retorna -1 al padre y no se crea ningún proceso hijo. Su sintaxis es la siguiente: pid_t fork(); 7.2. Servicio POSIX exec() El servicio POSIX exec() permite cambiar el programa que se está ejecutando, reempla- zando el código y datos del proceso que invoca esta función por otro código y otros datos procedentes de un archivo ejecutable. Si la función se ejecuta correctamente, el contenido del contexto de usuario del proceso que invoca a exec() deja de ser accesible y este contexto es reemplazado por el del nuevo programa. En estas condiciones, el programa antiguo es susti- tuido por el nuevo, y nunca se retornará a él para proseguir su ejecución. Si la función falla, devuelve -1 y no se modifica la imagen del proceso. La declaración de la familia de funciones exec es la siguiente: int execl (const char *camino, const char *arg0, ...); int execlp (const char *archivo, const char *arg0, ...); int execle (const char *camino, const char *arg0, ... , char *envp[]); int execv (const char *camino, char *const argv[]); int execvp (const char *archivo, char *const argv[]); int execve (const char *archivo, const char *argv[], char *envp[]); 10 Parámetros camino Ruta completa del nuevo programa a ejecutar. archivo Se utiliza la variable de entorno PATH para localizar el programa a ejecutar. No es necesario especificar la ruta absoluta del programa si éste se encuentra en alguno de los directorios especificados en PATH. argi Argumento i pasado al programa para su ejecución. argv[] Array de punteros a cadenas de caracteres que representan los argumentos pasados al programa para su ejecución. El último puntero debe ser NULL. envp[] Array de punteros a cadenas de caracteres que representan el entorno de ejecución del nuevo programa. 7.3. Servicio POSIX exit() El servicio POSIX exit() termina la ejecución del proceso que lo invoca. Como resultado, se cierran todos los descriptores de archivos abiertos por el proceso. Recuerde que, al abrir un archivo con el servicio POSIX open(), si la operación es válida, el sistema operativo devuelve un descriptor de archivo; un número entero correspondiente al índice de la entrada más baja libre de la tabla de descriptores de archivos (TDA) asociada al proceso. Estos descriptores identifican los archivos con los que puede trabajar el proceso. Por defecto, los tres primeros descriptores de archivo (0, 1 y 2) están asignados a la entrada estándar (por defecto teclado), salida estándar (por defecto, pantalla) y salida estándar de errores (por defecto, pantalla) del proceso, respectivamente. Además, no olvide que los dispositivos son tratados como archivos en Linux y la pantalla está asociada a dos archivos distintos: salida estándar y salida estándar de errores4. La sintaxis de exit() es la siguiente: void exit(int status); Parámetros status Almacena un valor que indica cómo ha finalizado el proceso: 0 si el proceso terminó correctamente, y distinto de 0 en caso de finalización anormal. La información de status, parámetro de exit(), podrá ser recuperada por el proceso padre a través del servicio POSIX wait(). 7.4. Servicios POSIX wait() y waitpid() wait() y waitpid() son dos servicios POSIX que esperan la finalización de un proceso hijo y permiten obtener información sobre su estado de terminación. Un ejemplo de uso de estos servicios es cuando un usuario escribe una orden en el in- térprete de órdenes de UNIX. El intérprete crea un proceso (shell hijo) que ejecuta la orden (el programa) correspondiente. Si la orden se ejecuta en primer plano (foreground), el padre esperará a que finalice la ejecución del shell hijo. Si no, el padre no esperará y podrá ejecutar otros programas. Un proceso puede terminar y su proceso padre no estar esperando por su finalización. En esta situación especial, el proceso hijo se dice que está en estado zombie; ha devuelto 4En la práctica 2 (“Material de apoyo") se explicaron las tablas involucradas en el acceso a los archivos en Linux. 11 todos sus recursos excepto su correspondiente entrada en la tabla de procesos. En este es- cenario, si el proceso padre invoca a wait(), se eliminará la entrada de la tabla de procesos correspondiente al proceso hijo muerto. La sintaxis del servicio wait() es la siguiente: pid_t wait(int *status); Parámetros status Si no es NULL, almacena el código del estado de terminación de un proceso hijo: 0 si el proceso hijo finalizó normalmente, y distinto de 0 en caso contrario. Si wait() se ejecuta correctamente, además retorna el PID (identificador) del proceso hijo cuya ejecución ha finalizado así como el código del estado de terminación del proceso hijo en el parámetro del servicio (si éste no es NULL). Por el contrario, el servicio devuelve -1 si el proceso no tiene hijos o éstos ya han terminado. waitpid() es un servicio más potente y flexible de espera por los procesos hijos ya que permite esperar por un proceso hijo particular. La sintaxis del servicio waitpid() es la siguien- te: pid_t waitpid(pid_t pid, int*status, int options) Este servicio tiene el mismo funcionamiento que el servicio wait() si el argumento pid es -1 y el argumento status es cero. Parámetros pid Si es -1, espera la finalización de cualquier proceso (como wait()). Si es >0, espera la finalización del proceso hijo con identificador pid. Si es 0, espera la finalización de cualquier proceso hijo cuyo identificador de grupo del proceso es igual que el del proceso que realiza la llamada (proceso padre). Si es <-1, espera la finalización de cualquier proceso hijo cuyo identificador de grupo del proceso sea igual al valor absoluto del valor de pid. status Igual que el servicio wait(). options Se construye mediante el OR binario (’|’) de cero o más valores definidos en el ar- chivo de cabecera sys/wait.h. Es de especial interés el valor de options definido con WNOHANG. Este valor especifica que la función waitpid() no suspenderá (no bloqueará) al proceso que realiza este servicio si el estado del proceso hijo especificado por pid no se encuentra disponible. Por lo tanto, esta opción permite que la llamada waitpid se comporte como un servicio no bloqueante. Si no se especifica esta opción, waitpid se comporta como un servicio bloqueante. 8. Servicios POSIX para comunicación entre procesos Los procesos no son entes aislados sino que, frecuentemente, es necesario que sean capaces de cooperar con otros procesos para lograr un objetivo común. Para facilitar esta tarea de colaboración, el sistema operativo ofrece mecanismos que permiten la transmisión de datos entre procesos (mecanismos de comunicación) y mecanismos que permiten a un proceso esperar o continuar su ejecución (despertarse) de acuerdo con ciertos eventos (mecanismos 12 de sincronización)5. Algunos de estos mecanismos pueden ser, a la vez, de comunicación y sincronización. El sistema operativo Unix proporciona diversos mecanismos de comunicación y sincroni- zación. Algunos de ellos son sólo de sincronización tales como señales, semáforos o mutex. Otros como tuberías, mensajes, memoria compartida, sockets, etc., son mecanismos de comu- nicación y sincronización. A continuación, se describen los servicios POSIX que implementan las operaciones básicas de dos de los mecanismos de comunicación y sincronización de Unix más utilizados; señales y tuberías. 8.1. Servicios POSIX de señales Las señales son un mecanismo de comunicación asíncrono gestionado por el sistema ope- rativo y muy utilizado para la notificación por software de eventos y situaciones especiales a los procesos. Este mecanismo tiene gran utilidad de cara a afrontar situaciones en las cuales se producen eventos en instantes de tiempo sin determinar, de manera que se interrumpe el flujo secuencial dentro de nuestra aplicación para activar la tarea asociada a la señal. El funcionamiento de las señales es muy similar al de las interrupciones pero la notificación del evento, a diferencia de las interrupciones, realizada por hardware (se activa una determina- da entrada de la CPU), es un mecanismo generado por el propio sistema operativo en función del evento asociado. Una vez que el sistema operativo (motivado por cuestiones internas o por otro proceso) genera una señal, un proceso la recibe y se ejecuta una rutina de tratamiento de esa señal (manejador de señal). Cuando un proceso que está en ejecución recibe una señal, detiene su ejecución en la instrucción máquina actual y, si existe una rutina de tratamiento de la señal, se ejecuta. Si la rutina de tratamiento no termina el proceso, retorna al punto en que se recibió la señal. Las señales se identifican mediante un número entero. Para facilitar su uso, todas las seña- les tienen un nombre simbólico que comienza por el prefijo SIG6. Ejemplos: un proceso padre recibe la señal SIGCHLD cuando termina un proceso hijo, SIGKILL (un proceso mata a otro pro- ceso), SIGALARM (señal enviada por el sistema operativo a un proceso para indicar que vence un temporizador), etc. 8.1.1. Servicio POSIX sigaction() El manejo de señales se realiza por medio del servicio POSIX sigaction(). El prototipo de este servicio es el siguiente: int sigaction (int sig, const struct sigaction *act, struct sigaction *oldact); sigaction() permite configurar la señal, es decir, especificar un manejador para la señal act. El manejador es la función que se ejecutará cuando se reciba la señal (si no es ignorada). Este servicio POSIX permite tres opciones cuando llega una señal a un proceso: 1. Ignorar la señal: No se ejecuta ningún manejador cuando se entrega la señal al proceso de destino pero éste puede realizar alguna acción. 2. Llamar a la rutina de tratamiento de la señal por defecto. 3. Llamar a una rutina de tratamiento de la señal propia. 5En realidad la sincronización puede verse como un tipo de comunicación de información entre procesos. 6Los nombres simbólicos de las señales están definidos en el archivo <sys/signal.h>. Si nuestro programa maneja señales, basta con incluir el archivo <signal.h>, que incluye al anterior. 13 La estructura sigaction para señales estándar7 es la siguiente: struct sigaction void(*sa_handler)(); sigset_t sa_mask; int sa_flags; donde: sa_handler Es un puntero a la función manejadora de la señal. Esta función tiene un úni- co parámetro entero, el número de la señal. Existen dos valores especiales para este campo: SIG_DFL Asigna un manejador por defecto. SIG_IGN Ignora la señal (no se ejecuta ningún manejador). sa_mask Especifica la máscara con las señales adicionales que deben ser bloqueadas (pen- dientes de ser recibidas) durante la ejecución del manejador. Normalmente, se asigna una máscara vacía. sa_flags Especifica opciones especiales. Sugerencia: se recomienda al alumno consultar el man para sigaction(). Parámetros sig Es el identificador (número entero o nombre simbólico) de la señal que queremos capturar. Se puede consultar un listado completo de señales en la sección 7 de la página man de sigaction. act Puntero a la estructura donde se debe especificar la configuración deseada de la señal de tipo sigaction, descrito previamente. oldact Puntero a una estructura de tipo sigaction donde se devuelve la configuración previa a la ejecución de la función sigaction(). Generalmente, este parámetro se pone a NULL para indicar que no devuelva dicha configuración. El servicio POSIX signal() devuelve 0 en caso de éxito o -1 si hubo algún error. Ejemplo: Imprimir un determinado mensaje cada 5 segundos e ignorar SIGINT (la señal SIGINT se genera con la combinación de teclas <CTRL> <C>.). #include <signal.h> #include <stdio.h> #include <string.h> void tratar_alarma(int sennal) { printf("Se␣activa␣la␣alarma\n\n"); } int main() { struct sigaction act; /* establecer manejador para SIGALRM */ act.sa_handler = tratar_alarma; /* función manejadora */ 7La estructura sigaction incluye información adicional para señales de tiempo real, pero está fuera del ámbito de esta práctica y de la asignatura. 14 act.sa_flags = 0; /* ninguna acción concreta */ sigaction(SIGALRM , &act , NULL); /* ignorar SIGINT */ act.sa_handler = SIG_IGN; /* no hay función manejadora. Se ignora SIGINT */ sigaction(SIGINT , &act , NULL); for (;;) { alarm (5); pause (); /* servicio para detener el proceso hasta que reciba una señal */ } return 0; } Compruebe el resultado de este programa. 8.1.2. Servicio POSIX kill() Un proceso puede enviar señales a otros procesos o incluso grupos utilizando el servicio POSIX kill(). int kill(pid_t pid, int sig) Parámetros pid Identifica al proceso al que se envía la señal (su PID). sig Número de la señal que se envía. Sus posibles valores son iguales que en el servicio POSIX signal(). Como es habitual en el resto de servicios POSIX, un valor de retorno 0 indica que kill() se ejecuta correctamente, mientras que un -1 indica que ocurrió un error durante su ejecución. 8.2. Servicios POSIX de pipes Las tuberías (pipes) son el mecanismo de comunicación y sincronización entre procesos más antiguo de UNIX. Constituye una solución elegante para que el shell, tras crear varios procesos, permita que los datos de salida producidos por uno de estos procesos (por ejemplo, de la ejecución de una orden) se utilicen como entrada de datos de otro proceso creado. El nombre de este mecanismo proviene de que, conceptualmente, se puede ver como una tubería o conducto real con dos extremos. Por uno de ellos, se escriben o se insertan datos y, por el otro, se leen o se extraen datos. El flujo de datos es unidireccional y con funcionamiento First-In-First-Out (FIFO), es decir, los datos se leen en el mismo orden en el que se escriben en la tubería, tal y como puede verse en la Figura 3. Figura 3: Flujo de datos en una tubería. El alumno ya se familiarizó en la práctica 2 con tuberías a nivel de intérprete de órdenes. Un ejemplo sencillo es el siguiente: ls | wc -l 15 Para ejecutar la orden anterior, el shell crea dos procesos (con el servicio POSIX fork()). Cada uno de ellos ejecuta, mediante un servicio POSIX de la familia exec(), las órdenes ls y wc -l, respectivamente (los servicios POSIX fork() y exec() ya se han descrito en la sección 7). Las tuberías o pipes, propiamente dichos, son un mecanismo de comunicación sin nombre. Por esta razón, sólo pueden usarse por el proceso que lo cree y sus procesos hijos, que heredan el pipe8. Desde el punto de vista de la implementación, una tubería es un byte stream, es decir, se pueden leer bloques de datos independiente del tamaño de los bloques escritos por el otro extremo de la tubería y el flujo es secuencial (los bytes se leen de la tubería en el mismo orden en que fueron escritos). Las tuberías normalmente se implementan como un buffer (general- mente circular) cuyo tamaño depende del sistema operativo (tIpicamente, de 4 KB). Se dice que es un pseudoarchivo mantenido en memoria por el sistema operativo. La lectura y escritura de y en la tubería, respectivamente, se realiza con los mismos servi- cios POSIX que para leer/escribir de/en archivos. Estas APIs se verán más adelante en esta sección. 8.2.1. Servicio POSIX pipe() Un pipe se identifica mediante dos descriptores de archivo en UNIX; uno para leer de la tubería y otro para escribir en ella. Cada proceso debe cerrar los descriptores que no utilice. El servicio POSIX que permite crear una tubería es pipe() cuya sintaxis es la siguiente: int pipe(int filedes[2]); Parámetros filedes[ ] Si pipe() se realiza correctamente, la llamada devuelve en este array los descripto- res de dos archivos abiertos que se utilizan como identificadores. filedes[0] se utiliza para leer de la tubería y filedes[1] para escribir en ella (véase Figura 4). Figura 4: Creación de una tubería con pipe(). El servicio POSIX pipe() devuelve 0 en caso de éxito y -1 si hubo algún error. 8.2.2. Servicio POSIX close() El servicio POSIX close() permite cerrar el descriptor de archivo asociado a una tubería. Cerrar los descriptores de archivo de una tubería no utilizados por los procesos que mane- jen el pipe es fundamental para su correcto uso, principalmente por las siguientes razones: 8Existe otra variación de tuberías, las tuberías FIFO o tuberías con nombre, utilizadas para comunicar y sincro- nizar varios procesos independientes. Este tipo de tuberías no será objeto de estudio en la asignatura de Sistemas Operativos. 16 El conjunto de descriptores de archivo abiertos es limitado. Si un descriptor no se usa, debe cerrarse para poder ser utilizado, si fuera necesario, por otro proceso. Si un proceso lee de la tubería, debe cerrar el descriptor de archivo para la escritura de tal modo que, cuando el otro proceso completa la escritura y cierra el descriptor asocia- do, el proceso lector detectará fin de archivo, tras haber leído los datos restantes en la tubería. En caso contrario, el proceso lector, al no cerrar el descriptor de escritura, no detectaría fin de archivo después de leer todos los datos de la tubería y una lectura pos- terior bloquearía la espera de datos dado que para el núcleo hay aún un descriptor de archivo de escritura abierto para la tubería. Si un proceso intenta escribir en una tubería para la que no hay ningún descriptor de archivo de lectura abierto, el núcleo del sistema operativo envía una señal SIGPIPE al proceso de escritura. Por defecto, esta señal mata al proceso que la recibe pero éste puede también establecer la captura o ignorar esta señal, en cuyo caso la escritura en la tubería falla con el error EPIPE (tubería rota). Recibir esa señal u obtener ese error es una indicación útil sobre el estado de la tubería por lo que se deben cerrar los descriptores de lectura no utilizados para la tubería. Además, si el proceso de escritura no cierra el extremo de lectura de la tubería, entonces, incluso después de que el otro proceso cierre el extremo de lectura de la tubería, el proceso de escritura seguirá siendo capaz de escribir en la tubería. Eventualmente, el proceso de escritura llenaría la tubería y un intento adicional de escritura bloquearía indefinidamente al proceso. Sólo después de que todos los descriptores de archivo en todos los procesos que utilicen la tubería se cierren, la tubería se destruye y sus recursos se liberan para su posible reutilización por otros procesos. En este punto, todos los datos no leídos de la tubería se pierden. int close(int fd); Parámetros fd Indica el descriptor de archivo que se pretende cerrar. El servicio POSIX close() devuelve 0 en caso de éxito y -1 si hubo algún error. En la Figura 5 se muestra el uso de una tubería creada con pipe() para comunicar dos procesos. El proceso hijo, creado mediante el servicio POSIX fork(), hereda una copia de los descriptores de archivo de su proceso padre; entre ellos, los descriptores de archivo de la tubería creada por él (ver Figura 5(a)). Como puede observarse en la Figura 5, inmediatamente después de usar fork(), el pro- ceso padre cierra su descriptor de lectura de la tubería y el hijo cierra su descriptor de escritura en la tubería. En este caso, la comunicación consiste en que el padre envía datos al hijo (ver Figura 5(b)). 8.2.3. Servicio POSIX read() El servicio POSIX read() se utiliza para leer datos de una tubería en el orden en el que fueron introducidos9. int read(int fd, char *buffer, int n); 9El servicio POSIX read() se utiliza también en UNIX para leer datos de un archivo. 17 (a) Después de usar fork() (b) Después de cerrar los descripto- res no usados Figura 5: Creación de un pipe para transferir datos de un proceso padre a un proceso hijo. Parámetros fd Descriptor de archivo utilizado para leer datos de la tubería. buffer Buffer de memoria donde se pretende almacenar los datos leídos del pipe (la operación actualiza el puntero de lectura en la tubería). n Número de bytes que se desean leer de la tubería. El servicio POSIX read() devuelve el número de bytes leídos en caso de éxito y -1 si hubo algún error. El proceso que lee de una tubería puede leer bloques de cualquier tamaño, independiente- mente del tamaño de los bloques escritos por el proceso de escritura en la tubería. A continuación, brevemente, se detalla la semántica de la operación de lectura de una tubería: Si el tamaño de la tubería es T y se desea leer de ella un número de bytes mayor, la operación devolverá T bytes. En caso contrario, la operación devuelve n bytes. En ambos escenarios, se eliminan los datos leídos de la tubería. Si se cierra el extremo final de escritura de la tubería y no hay procesos escritores, la operación de lectura de la tubería devolverá 0 bytes (final de archivo) una vez que haya leído los datos restantes de la tubería, pero no bloquea al proceso lector. La operación se lleva a cabo de forma atómica, es decir, si varios procesos intentan leer simultáneamente en una tubería, sólo uno de ellos podrá hacerlo y el resto de procesos se bloqueará hasta que finalice esa lectura. La atomicidad de esta operación se garantiza cuando el número de datos que se intentan leer es menor que el tamaño de la tubería. Si la tubería está vacía, la invocación a read() bloqueará al proceso que realiza la lectura hasta que otro proceso escriba datos en la tubería10. 8.2.4. Servicio POSIX write() El servicio POSIX write() se utiliza para escribir datos de forma ordenada en una tube- ría11. 10Se puede evitar el bloqueo usando modo de lectura no bloqueante con el servicio POSIX fcntl(). 11El servicio POSIX write() también se utiliza en UNIX para escribir datos en un archivo. 18 int write(int fd, char *buffer, int n); Parámetros fd Descriptor de archivo que se utiliza para escribir datos en la tubería. buffer Buffer de memoria donde se localizan los datos que se van a escribir en la tubería (la operación actualiza el puntero de escritura de la tubería). n Número de bytes a escribir en la tubería. El servicio POSIX write() devuelve el número de bytes escritos en caso de éxito y -1 si hubo algún error. A continuación, brevemente, se detalla la semántica de la operación de escritura en una tubería: Si no hay ningún proceso con la tubería abierta para lectura, el sistema operativo envía la señal SIGPIPE al proceso que pretende escribir. El proceso recibe la señal SIGPIPE y la operación de escritura (write()) devolverá un error. La operación se lleva a cabo de forma atómica, es decir, si varios procesos intentan escribir simultáneamente en una tubería, sólo uno de ellos podrá hacerlo y el resto de procesos se bloqueará hasta que finalice esa escritura. La atomicidad en la escritura se garantiza, al igual que para la lectura, cuando el número de datos que se intentan escribir es menor que el tamaño de la tubería. Si la tubería está llena o se llena durante la escritura actual, esta operación bloqueará al proceso escritor hasta que se pueda completar la operación. 8.2.5. Servicios POSIX para redirecciones: dup() y dup2() Las redirecciones ya se vieron en la práctica 2 a nivel del intérprete de órdenes y qué operaciones y tablas están involucradas en espacio de usuario y espacio de núcleo. Del mismo modo, este concepto es clave en el caso de uso de órdenes comunicadas con tuberías como el ejemplo ls | wc -l. En este caso, se trata de dos redirecciones; en primer lugar, la salida estándar de la orden ls debe ser redirigida a la entrada de la tubería (extremo de escritura) y la entrada de la orden wc -l debe ser redirigida a la salida de la tubería (extremo de lectura). Para realizar estas dos redirecciones deben usarse los servicios POSIX dup() o dup2(). Ambos, se utilizan para duplicar un descriptor de archivo. Los prototipos de estas dos funciones son los siguientes: int dup(int fd); int dup2(int fd, int new_fd); Parámetros fd Descriptor de archivo cuya entrada en la tabla de descriptores de archivo (TDA) va a ser duplicada en otra (en concreto, en la entrada más baja libre). new_fd Descriptor de archivo cerrado inicialmente por dup2() para, posteriormente, copiar en su entrada en la TDA la del primer parámetro, fd. 19 Los servicios POSIX dup y dup2 devuelven el nuevo descriptor de archivo en caso de éxito y -1 si hubo algún error. El siguiente ejemplo muestra cómo puede usarse dup() o dup2() para redigir la salida es- tándar de un proceso. /* uso de dup() */ int fd[2]; pipe(fd); /* código adicional */ close(STDOUT_FILENO); dup(fd[1]) Sin embargo, piense qué puede suceder si, con el fragmento de código anterior, utilizando dup() en vez de dup2(), la entrada estándar (STDIN_FILENO), con descriptor de archivo 0, ha sido cerrada antes de invocar a dup(). Para evitar errores, es conveniente usar dup2() como se muestra a continuación: /* uso de dup2() */ int fd[2]; pipe(fd); /* código adicional */ dup2(fd[1], STDOUT_FILENO) A continuación, como ejemplo de manejo de los servicios POSIX de pipes, se muestra el código que implementa la ejecución de la tubería ls | wc12. Además, en la Figura 6 se muestran las principales operaciones realizadas en la implementación del uso de la tubería por las dos órdenes implicadas en este ejemplo. #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/wait.h> int main (int argc , char *argv []) { int pipefd [2]; pipe(pipefd [2]); /* Creacion de la tuberia por el padre */ switch(fork ()) { case -1: perror("Error␣al␣crear␣el␣primer␣hijo"); exit (1); case 0: /* Primer hijo ejecuta ls y escribe en tuberia */ close(pipefd [0]); /* Descriptor de lectura no usado */ /* Duplicar entrada de descriptor de escritura de tuberia en la */ /* de stdout y cierre de descriptor */ if (pipefd [1] != STDOUT_FILENO) { dup2(pipefd [1], STDOUT_FILENO ); 12En él, por brevedad, se han obviado las comprobaciones de error de la mayoría de servicios POSIX utilizados (pipe(), close(), wait(), etc.). 20 close(pipefd [1]); } exclp("ls", "ls", (char *) NULL); /* Escribe en tuberia */ perror("El␣primer␣hijo␣fallo␣en␣exec"); exit (1); default: /* El padre pasa a crear otro hijo */ break; } switch(fork ()) { case -1: perror("Error␣al␣crear␣el␣segundo␣hijo"); case 0: /* Segundo hijo ejecuta el filtro wc leyendo de la tuberia */ close(pipefd [1]); /* Descriptor de escritura no usado */ /* Duplicar entrada de descriptor de lectura de tuberia en la */ /* de stdin y cierre de descriptor */ if (pipefd [0] != STDIN_FILENO) { dup2(pipefd [0], STDIN_FILENO ); close(pipefd [0]); } exclp("wc", "wc", (char *) NULL); /* Lee de tuberia */ perror("El␣segundo␣hijo␣fallo␣en␣exec"); exit (1); default: break; } /* El padre cierra los descriptores no usados de la tuberia */ close(pipefd [0]); close(pipefd [1]); /* Y espera a los procesos hijos */ for (i=0; i < 2; i++) wait(NULL); exit(EXIT_SUCCESS ); } Figura 6: Esquema sobre la implementación de la orden ls | wc -l. Compruebe el resultado de este programa. 21 9. Intérprete de órdenes El intérprete de órdenes es la puerta de entrada tradicional a UNIX. Comprender su fun- cionamiento interno ayuda a comprender muchos de los conceptos básicos de interacción con el sistema operativo, diferenciar bien los espacios de usuario y de sistema, así como algunos mecanismos de comunicación entre procesos. 9.1. Ciclo de ejecución del intérprete de órdenes Conviene conocer la secuencia de acciones que realiza el intérprete de órdenes, que con- siste básicamente en los siguientes pasos: 1. Imprimir del prompt. El intérprete se encuentra a la espera de que el usuario introduzca una orden. 2. Procesar la orden (parser). Se realizan un conjunto de transformaciones sobre la or- den del usuario. Por ejemplo, si el usuario ha introducido cat practica1.c, el intérprete transforma la cadena anterior en una estructura que pueda ser manejada más fácilmente en su ejecución (como ya veremos, consiste, esencialmente, en la transformación de la cadena en un array de vectores a las cadenas “cat” y “practica1.c”). 3. Interpretar la orden. Antes de ejecutar la orden, se realizan un conjunto de funcionali- dades como sustituir variables del shell por sus valores, generar nombres de archivos a partir de metacaracteres que aparezcan en la orden, o manejar las redirecciones y tuberías. Por ejemplo, si el usuario ha introducido la orden ls *.pdf, y en el directo- rio de trabajo hay dos archivos, examen.pdf y solucion.pdf, el intérprete transforma ls *.pdf en ls examen.pdf solucion.pdf. Este es un ejemplo de generación de nombres de archivo a partir del metacarácter ’*’ que realiza el shell. 4. Ejecutar la orden. En función de cuál sea el tipo de orden introducido, el proceso de ejecución de la orden es bien distinto. El shell verifica si es una orden interna. Si lo es, ejecuta su código, perteneciente (interno) al propio código del intérprete de órdenes. En caso contrario, se trata de un programa ejecutable de UNIX (independiente del shell); si es una orden externa, el intérprete busca su imagen binaria (ejecutable) para solicitar su ejecución al Sistema Operativo13. 10. Ejercicio Como aplicación de los servicios POSIX de gestión de procesos descritos, el alumno debe completar el desarrollo, en lenguaje C y sobre sistema operativo Linux, de una aplicación a la que denominaremos minishell. Esta aplicación se comportará como una versión reducida de un intérprete de órdenes de UNIX. Como cualquier proceso en UNIX, minishell utilizará por defecto la entrada estándar - teclado- (cuyo descriptor de archivo es 0) para leer las líneas de órdenes a interpretar y ejecutarlas, y la salida estándar -pantalla- (cuyo descriptor de archivo es 1) para presentar el resultado de las órdenes ejecutadas. Y, para notificar los errores que se puedan producir, usará por defecto la salida estándar de errores -pantalla- (cuyo descriptor de archivo es 2). Las tareas a realizar en el desarrollo de la minishell son las siguientes: 1. Implementar las funcionalidades siguientes en minishell : 13La petición de ejecución se realiza mediante el uso conjunto de los servicios POSIX fork() y exec(), respec- tivamente. 22 Figura 7: Ejecución de una orden externa dentro de un intérprete de órdenes. Ejecución de órdenes simples externas en primer plano (por ejemplo, ls -l, who, etc.). Para ejecutar una orden externa en primer plano debe crearse un proceso minishell hijo (veáse información con: man 2 fork) que ejecute mediante el servi- cio exec() (véase información con: man 2 execvp) el archivo binario asociado a la orden. Mientras tanto, el proceso padre (minishell padre) debe esperar a la finaliza- ción del proceso hijo (véase información con: man 2 wait). Al final de la orden no existe el símbolo ’&’. Ejecución de la orden interna exit. Cuando la aplicación minishell reciba esta or- den, finalizará su ejecución. Ejecución de órdenes en background (usando el carácter ’&’ al final de las órde- nes). Cuando la minishell detecte este símbolo al final de una orden no debe esperar a la terminación del proceso hijo creado para ejecutarla sino que, inmediatamente, imprimirá el prompt para aceptar una nueva orden. Ejecución de secuencias de órdenes separadas por el símbolo ’;’. Ejecución de órdenes con redirecciones de entrada (<) y salida (>). Ejecución de órdenes externas compuestas (separadas mediante tuberías). 2. Crear un archivo Makefile que permita la compilación automatizada de la aplicación. 3. Realizar las pruebas adecuadas de minishell. Para el desarrollo de este ejercicio utilice los archivos fuente y archivos de cabecera que se proporcionan como material de apoyo con la práctica. Realice con ellos las fases que a 23 continuación se detallan, de forma incremental y modular, siguiendo los pasos que se indican en cada una de ellas y probando poco a poco las funcionalidades que se vayan codificando. En el Anexo A se proporciona un diagrama de la estructura detallada de la aplicación minishell. El esquema muestra los diferentes archivos, la relación existente entre ellos y las funciones que incluyen distinguiendo si están ya implementadas o no. Se recomienda al alumno tenerlo a mano en todo momento para tener una visión global de toda la aplicación y poder ubicar cada función en el conjunto. Diagrama de la estructura de minishell A continuación, realice las fases que se van describiendo en las siguientes secciones para el desarrollo de minishell. 10.1. FASE 1: Ciclo de ejecución de órdenes Edite el archivo fuente denominado minishell.c. Complete este código definiendo la fun- ción principal (main()) encargada de realizar el ciclo de ejecución de órdenes del minishell (véase sección 9.1). Para ello, siga el diagrama de la Figura 8. Código 1: minishell.c incompleto 1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <sys/wait.h> 4 #include <fcntl.h> 5 #include <signal.h> 6 #include <unistd.h> 7 #include <string.h> 8 #include <sys/types.h> 9 #include <sys/stat.h> 10 #include <errno.h> 11 12 #include "internas.h" 13 #include "entrada_minishell.h" 14 #include "ejecutar.h" 15 16 17 int main (int argc , char *argv []) 18 { 19 20 char buf[BUFSIZ ]; 21 22 23 while (1) 24 { 25 26 27 28 29 30 31 32 33 34 35 36 37 38 } 39 40 return 0; 41 } 24 Figura 8: Diagrama de la función main() de minishell. Posponga el tratamiento de una orden externa (invocación a ejecutar_linea_ordenes() de la Figura 8) hasta la realización de la FASE 2 de la práctica (se trata de ir resolviendo y probando poco a poco las tareas que se solicitan). Como puede observar en la Figura 8, para implementar la función main() se utilizan sendas funciones: imprimir_prompt(), leer_linea_ordenes(), es_ord_interna() y ejecutar_ord_in- terna(). El código de estas funciones se proporciona como material de apoyo. Sólo debe in- vocarlas adecuadamente teniendo en cuenta en qué archivos se encuentran tanto la definición como el prototipo de cada una de ellas. A continuación, se proporciona también una descrip- ción de estas funciones14: void imprimir_prompt(). Imprime el prompt de la minishell. void leer_linea_ordenes(char *buf). Lee la línea de órdenes de la entrada estándar y la almacena como una cadena de caracteres en buf. int es_ord_interna(const char *buf). Devuelve si la orden que se pasa como argu- mento es interna (valor 1) o externa (valor 0). Las únicas órdenes internas implementa- das son: cd, pwd, declare y umask. ejecutar_ord_interna(const char *buf). Si la orden que se pasa como argumento es interna, la función ejecuta la orden. Las dos primeras funciones se encuentran en el archivo entrada_minishell.c propor- cionado como material de apoyo. Las dos últimas, relacionadas con órdenes internas, se proporcionan en el archivo en código objeto internas.o. 14Estas funciones se han codificado de forma sencilla para que el alumno las pueda interpretar fácilmente y vaya familiarizándose con el lenguaje C. Puede modificar y simplificar las funciones relacionadas con entrada/salida de C. La correcta adaptación de su codificación será valorada en la nota final de la práctica. 25 Consulte el diagrama de la aplicación minishell (Apéndice A) para comprobar los archivos que se proporcionan como material de apoyo. De momento, se recomienda no crear la biblio- teca estática libshell.a; ya lo hará más tarde. Para probar esta fase sólo precisa usar los archivos fuente y de cabecera requeridos así como el archivo internas.o en la compilación. Se recomienda probar de forma incremental las diferentes funcionalidades programadas en todas las fases. Antes de entregar la práctica, realice la validación con las pruebas adecuadas. Desarrollo incremental y pruebas 10.2. FASE 2: Ejecución de órdenes externas simples en primer plano Una vez realizadas las pruebas de la ejecución de órdenes internas, realice en esta fase la implementación de órdenes externas simples y en primer plano como las que a continuación se muestran en sendos ejemplos: minishell> cut -f5 -d: /etc/passwd minishell> sleep 30 & Sin embargo, una línea de órdenes como la siguiente es una orden compuesta, es decir, for- mada por varias órdenes comunicadas mediante tuberías, y ejecutada en background. De la implementación de órdenes compuestas no debe preocuparse ahora: minishell> cut -f5 -d: /etc/passwd | sort > usuarios.txt & Para realizar la tarea de esta fase, edite el archivo ejecutar.c proporcionado como ma- terial de apoyo. Este archivo contiene, incompletas, las funciones relacionadas directamente con la ejecución de una línea de órdenes externa. Finalice la implementación del código de ejecutar.c que se muestra a continuación. Código 2: ejecutar.c incompleto #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> #include "parser.h" #include "ejecutar.h" #include "libmemoria.h" pid_t ejecutar_orden (const char *orden , int *pbackgr) { char **args; pid_t pid; int indice_ent = -1, indice_sal = -1; /* por defecto , no hay < ni > */ if ((args = parser_orden(orden , &indice_ent , &indice_sal , pbackgr )) == NULL) { return (-1); } /* si la linea de ordenes posee tuberias o redirecciones , podra incluir */ /* aqui , en otras fases de la practica , el codigo para su tratamiento. */ 26 } void ejecutar_linea_ordenes(const char *orden) { pid_t pid; int backgr; /* Si la orden es compuesta , podra incluir aqui , en otra fase de la */ /* practica , el codigo para su tratamiento */ pid = ejecutar_orden(orden , &backgr ); } void ejecutar_linea_ordenes (const char *orden). Esta función se encarga de eje- cutar una orden externa introducida por el usuario. Como ya se ha indicado, sólo debe implementar en esta fase la ejecución de órdenes externas simples. Para ello, la fun- ción invoca a ejecutar_orden(), función cuya descripción se proporciona más adelante. Tras la llamada a ejecutar_orden(), ejecutar_linea_ordenes() debe comprobar la existencia o no del símbolo de background al final de la orden, información que es de- vuelta por ejecutar_orden(). En función de este chequeo, ejecutar_linea_ordenes() proporcionará el siguiente tratamiento: • Si no existe el símbolo ’&’. Se trata de una ejecución en primer plano (foreground). El proceso padre (minishell padre) debe esperar (invocando al servicio POSIX ade- cuado) a que el proceso hijo (minishell hija) finalice su ejecución. Cuando esto ocu- rra, el proceso minishell padre podrá realizar otra iteración del ciclo de la función main(). Por lo tanto, imprimirá el prompt y volverá a leer y ejecutar otra posible orden introducida por el usuario. • Si la orden termina con ’&’. Se trata de una ejecución en segundo plano (back- ground). El proceso minishell padre simplemente no debe esperar la finalización del proceso minishell hijo y podrá volver a ejecutar inmediatamente otra iteración del bucle de la función main(). Esto significa que el proceso minishell padre y el proceso minishell hijo podrán ejecutar concurrentemente órdenes. void ejecutar_orden (const char *orden, int *background). Función que crea un proceso minishell hijo (invocando al servicio POSIX adecuado) para ejecutar la orden ex- terna pasada como parámetro (orden). La función devuelve en background el valor 1 si hay un símbolo de background (’&’) al final de la orden, o 0 en caso contrario. Como puede observar en el código proporcionado previamente, la función ejecutar_orden() primero invoca a la función parser_orden() cuya descripción y prototipo se detallan más adelante. Esta última función es responsable de convertir la cadena orden, que almacena la orden externa introducida por el usuario, en una estructura que facilite su ejecución con el servicio POSIX correspondiente. La función parser_orden() ya está implementada y se incluye en el archivo objeto parser.o. Antes de finalizar la definición de la función ejecutar_orden(), deberá invocar a free_ar- gumentos() ya que la función parser_orden() crea un array dinámico que debe ser libe- rado para evitar lagunas de memoria. La función free_argumentos(), cuyo prototipo se muestra a continuación, ya está implementada en el archivo libmemoria.c. 27 void free_argumentos (char **argumentos). Esta función libera la estructura argumentos de tipo array dinámico. La existencia de la función ejecutar_linea_ordenes(), añadida a la de ejecutar_orden(), pre- tende ofrecer una descomposición más flexible de la minishell y la posibilidad de ampliar en ella la funcionalidad solicitada en la Fase 7, es decir, el tratamiento de órdenes compuestas. Puede ob- viar su uso y realizar sus tareas, indicadas previamente, en la propia función ejecutar_orden() u otra. Uso de la función ejecutar_linea_ordenes() parser_orden(): Descripción Función que convierte la cadena que representa la orden introducida al ejecutar minishell, en una nueva estructura: un array de punteros a cadenas. La finalidad es dejar preparada la orden para ser ejecutada adecuadamente con el correspondiente servicio POSIX. Más en de- talle, parser_orden analiza la cadena orden y genera la nueva estructura que consta de: a) Una cadena por cada uno de los argumentos de orden, eliminando los posibles blancos entre ellos (considerando también el nombre de la orden como argumento) y, b) si encuentra redirec- ciones, añade dos cadenas: una con el carácter de la redirección y otra, con la ruta del archivo asociado a ella. Asimismo, si en orden hay un símbolo ’&’ (background), la función lo elimina (no lo incluye en la estructura creada) pero devuelve un indicador de su existencia. Si no se elimina el símbolo ’&’ en la estructura devuelta por la función parser_orden() tendrá problemas para que el proceso minishell hijo ejecute correctamente la orden correspondiente ¿Por qué? ¿Por qué es necesario eliminar el ’&’? La función también devuelve, en sendos parámetros, la posición en la nueva estructura de los caracteres de redirección que puedan existir, o -1 en caso de que no existan. Prototipo char ** parser_orden(const char *orden, int *indentrada, int *indsalida, int *background) Parámetros orden Contiene la orden introducida por el usuario al ejecutar minishell. Esta orden va a ser analizada y convertida por la función en un array dinámico de punteros a cadenas. indentrada Puntero a un entero que representa el índice en el array devuelto por la función donde se localiza un posible símbolo de redirección de entrada en la orden. Si no existe este tipo de redirección, devuelve un puntero a -1. indsalida Puntero a un entero que representa el índice en el array devuelto por la función donde se localiza un posible símbolo de redirección de salida en la orden. Si no existe este tipo de redirección, devuelve un puntero a -1. 28 background Puntero a un entero que representa la existencia (1), o no (0), del símbolo de background (’&’). Valores retornados Array dinámico de punteros a cadenas descrito anteriormente. Además, la función devuel- ve en background la existencia (1) o no (0) del símbolo background eliminándolo del array devuelto, así como el índice en el array donde se encuentra una redirección de entrada, en indentrada, o una redirección de salida, en indsalida. En ambos casos, si no existe redirec- ción, devuelve -1 en el parámetro correspondiente. Ejemplo: Si la orden, de tipo cadena constante, introducida por el usuario, y argumento de la función parser_orden(), es la siguiente: orden = “ls -l > archivo &” (véase Figura 9(a)), los valores finales de cada parámetro son los que a continuación se detallan: indsalida = 2 indentrada = -1 background = 1 Además, la función devuelve un array (de tipo puntero a puntero a carácter) que se repre- senta en la Figura 9(b). (a) Ejemplo de argumento de parser_orden() (b) Estructura devuelta por parser_orden() Figura 9: Estructuras de datos utilizadas por parser_orden(). En esta fase no se tratan redirecciones pero la interfaz así definida de parser_orden() le permitirá más adelante incrementar fácilmente la funcionalidad de su minishell programando, tras la invocación a parser_orden(), y de acuerdo con los valores devueltos a través de los últimos parámetros mencionados (indentrada e indsalida), el tratamiento de las redirecciones (Fase 6, sección 10.6). Versatilidad de la función parser_orden() 10.3. FASE 3: Ejecución de órdenes externas simples en segundo plano Compruebe qué sucede cuando se introduce una orden en background con el código reali- zado hasta el momento. Para ello, consulte el estado de los procesos del sistema con la orden apropiada. Observe que el tratamiento del background hasta esta fase puede dejar al proceso minis- hell hijo en estado zombie que, como se ha visto en en el aula, es un estado de los procesos 29 en Linux no deseado. Razone por qué sucede esto y solucione este problema utilizando un manejador de la señal adecuada. Piense qué señal se podría utilizar para que, una vez que el proceso minishell padre la capture, el manejador se encargue de evitar que el proceso minis- hell hijo quede en estado zombie y dónde se debería instalar el manejador. Finalmente, una vez que tenga clara la solución al problema planteado, defina el manejador de la señal adecuadamente, de forma similar al ejemplo de manejo de la señal SIGINT (véase sección 8.1.1). Utilice para ello, ESTRICTAMENTE, la función sigaction(), no signal(). Asi- mismo, se recomienda usar la orden man para consultar el servicio POSIX sigaction(), tanto su descripción como la semántica asociada a sus parámetros. Pruebe a realizar una secuencia de órdenes en background, tal y como se muestra a con- tinuación: minishell> sleep 12 & minishell> sleep 10 & El resultado de ejecutar las órdenes previas debe ser el correcto. Una vez introducida cual- quier orden en background, el prompt automáticamente debe volver a salir en pantalla para introducir otra orden y, además, no debe haber procesos zombies en el sistema (no deje de probar esto último; piense con qué orden puede realizarlo). 10.4. FASE 4: Realización de Makefile Realice el Makefile que permita construir una nueva versión de la aplicación. Cree la biblioteca libshell.a, tal y como aparece en el diagrama de la aplicación minishell (Anexo A). Para ello, defina una regla específica en el Makefile que genere esta biblioteca está- tica con los archivos internas.o y parser.o, respectivamente, utilizando la herramienta ar15. libshell.a debe ser utilizada como parámetro en la invocación a gcc para que los archivos objeto junto con la biblioteca se enlacen en la última fase de compilación y se genere el archivo ejecutable minishell. Se puntuará con 0 puntos todo archivo Makefile que no incluya: a) definición adecuada y explícita de todas las dependencias entre los distintos archivos que componen la aplicación para generar el ejecutable minishell, b) regla de creación de la biblioteca estática y/o, c) regla ficticia clean. 10.5. FASE 5: Ejecución de secuencia de órdenes En esta fase se plantea poder ejecutar órdenes separadas por el carácter ’;’. Cuando la aplicación minishell detecte este símbolo, debe ejecutar de izquierda a derecha cada una de las órdenes. Resuelva este apartado declarando y definiendo su propia función e invocándo- la donde considere adecuado para que realice correctamente su funcionalidad. Piense que se trata de invocar a la función ejecutar_linea_ordenes() tantas veces como órdenes haya separadas por el carácter ’;’. Se recomienda utilizar para esta fase funciones de manejo de cadenas de caracteres de C que pueden ser muy útiles (strsep(), strtok(), etc.). 15Consulte qué parámetros debe pasarle a la orden ar para crear la librería con el nombre de la biblioteca y de los archivos objetos. Sugerencia: use man, busque en Internet o pregunte al profesor(a). 30 10.6. FASE 6: Tratamiento de redirecciones En esta fase debe resolver la posibilidad de ejecutar en la aplicación minishell órdenes ex- ternas con redirecciones de entrada o de salida. A continuación, se presenta un ejemplo con cada tipo de redirección y, finalmente, uno con ambos tipos en la misma orden: minishell> cut -f 5 -d : /etc/passwd > usuarios.tex minishell> wc -l < /etc/passwd minishell> sort < /etc/passwd > ordenado_passwd & Para resolver esta parte, edite el archivo redirecciones.c proporcionado como material de apoyo y que se muestra a continuación. Complete las funciones relacionadas directamente con el tratamiento de redirecciones. Código 3: redirecciones.c incompleto 1 #include <stdio.h> 2 #include <errno.h> 3 #include <sys/types.h> 4 #include <sys/stat.h> 5 #include <stdlib.h> 6 #include <fcntl.h> 7 8 /* funcion que abre el archivo situado en la posicion indice_entrada +1 */ 9 /* de la orden args y elimina de ella la redireccion completa */ 10 11 void redirec_entrada(char **args , int indice_entrada , int *entrada) 12 { 13 14 15 16 17 18 19 20 } 21 22 /* funcion que abre el archivo situado en la posicion indice_salida +1 */ 23 /* de la orden args y elimina de ella la redireccion completa */ 24 25 void redirec_salida(char **args , int indice_salida , int *salida) 26 { 27 28 29 30 31 32 33 } Observe que la función parser_orden() que se le ha proporcionado, descrita en la Fase 2 (sec- ción 10.2), devuelve en el parámetro indice_entrada la posición en la estructura args donde existe un ’<’ (redirección de entrada) o -1 en caso contrario. Del mismo modo, en el parámetro indice_salida para el símbolo ’>’ (redirección de salida) (véase Figura 9(b)). Piense qué de- be hacer el código del proceso minishell padre y del proceso minishell hijo, respectivamente, y retome la codificación de la función ejecutar_orden() para incluir el tratamiento de ambos tipos de redirecciones. Para ello, utilice también las funciones previamente implementadas en redirecciones.c. void redirec_entrada(char **args, int indice_entrada, int *entrada). Esta fun- ción abre convenientemente el archivo que acompaña a la redirección de entrada, locali- zado en args[indice_entrada+1] y devuelve en el parámetro entrada su descriptor del archivo. 31 void redirec_salida(char **args, int indice_salida, int *salida). Esta función devuelve en el parámetro salida el descriptor del archivo que acompaña a la redirección de salida, situado en args[indice_salida+1]. Al realizar esta fase, recuerde que, quizás, deba modificar el archivo Makefile para que, al invocar a make, la compilación de minishell se lleve a cabo correctamente teniendo en cuenta la nueva funcionalidad implementada. Modificaciones en el Makefile 10.7. FASE 7: Implementación de tuberías o pipes Una vez realizada la FASE 6, y si ha seguido las pautas indicadas en esta memoria, tiene resuelta parte de la codificación de esta última parte donde se deben tratar órdenes com- puestas. El uso de tuberías en realidad supone redireccionar, en este caso, no a un archivo cualquiera, como ocurre en el caso de uso de las redirecciones de entrada o salida, sino al extremo de escritura o lectura correspondiente de la tubería (utilizando el descriptor de archivo correspondiente) para cada orden que compone la orden compuesta introducida por el usua- rio, si fuera el caso (véase Figura 6). Por lo tanto, con la FASE 6 implementada, no le queda más que resolver la creación de las posibles tuberías y la preparación de las redirecciones para cada orden separada por tuberías. Esta tarea consiste, grosso modo, en detectar cada aparición del símbolo ’|’ que representa una tubería e invocar adecuadamente a la función ejecutar_orden() con cada una de las órdenes separada por tubería(s) y con el descriptor de archivo “que va a hacer las veces” de entrada o salida estándar para cada una de ellas. Comience por analizar los diferentes casos de órdenes separadas por tubería(s). Observa- rá que la invocación a ejecutar_orden() debe ser distinta, en este sentido, si se trata de una orden situada entre dos tuberías o acompañada sólo por una tubería (órdenes de los extre- mos). Y, adicionalmente, en este último caso, verá que es necesario discernir si es una orden con una tubería a la izquierda o a la derecha, respectivamente. En definitiva, una orden gené- rica con tuberías tendrá este formato: minishell> orden_1 | . . . | orden_i | . . . | orden_n Donde orden_1, orden_i y orden_n deben tratarse de forma diferente antes de invocar a ejecutar_orden(), en la que se debe indicar el descriptor de archivo que va a hacer el papel de entrada estándar y/o de salida estándar según el lugar que ocupe la orden en la línea de órdenes compuesta, tal y como se ha explicado anteriormente. La tarea de redireccionar co- rrectamente en la función ejecutar_orden(), antes de la propia ejecución de la orden, ya la debe tener resuelta en la FASE 6. Como apoyo en la implementación, se le propone completar el código que tenga hasta el momento de ejecutar.c incluyendo las líneas que se muestran a continuación. 32 Código 4: ejecutar.c incompleto 1 #include "minishell.h" 2 #include "parser.h" 3 #include "ejecutar.h" 4 #include "libmemoria.h" 5 6 int ** crear_pipes(int nordenes) 7 { 8 int **pipes = NULL; 9 int i; 10 11 pipes = (int **) malloc(sizeof(int *) * (nordenes - 1)); 12 for(i = 0; i < (nordenes - 1); i++) 13 { 14 pipes[i] = (int *) malloc(sizeof(int) * 2); 15 /* inserte linea de codigo para crear tuberia pipes[i] */ 16 } 17 return(pipes); 18 } 19 20 pid_t ejecutar_orden (const char *orden , int entrada , int salida , int *pbackgr) 21 { 22 char **args; 23 pid_t pid; 24 int indice_entrada = -1, indice_salida = -1; /* por defecto no hay < y > */ 25 26 if ((args=parser_orden(orden ,& indice_entrada ,& indice_salida ,pbackgr ))== NULL) 27 { 28 29 /* introducir codigo , para cerrar , si es necesario , entrada y salida */ 30 31 return (-1); 32 } 33 34 35 36 37 /* codigo ya insertado para FASE 2 y FASE 6 */ 38 39 40 41 42 43 44 45 46 47 48 49 50 } 51 52 void ejecutar_linea_ordenes(const char *orden) 53 { 54 char ** ordenes; 55 int nordenes; 56 pid_t *pids = NULL; 57 int **pipes; 58 int backgr; 59 60 ordenes = parser_pipes(orden , &nordenes ); 61 62 pipes = crear_pipes(nordenes ); 63 64 65 /* para cada orden , invocar adecuadamente a ejecutar_orden */ 66 67 68 69 70 71 72 73 74 75 76 77 33 78 79 80 81 82 83 /* tratamiento de background para ordenes simples y compuestas */ 84 85 /* liberar estructuras de datos dinamicas utilizadas */ 86 free_ordenes_pipes(ordenes , pipes , nordenes ); 87 free(pids); 88 } Como puede observar, se ha modificado sólo la interfaz de ejecutar_orden() para tratar ya ór- denes simples y compuestas. Simplemente, y sin variar el resto de parámetros y su semántica, ni el código ya implementado de la propia función para FASE 2 y FASE 6, se han introducido dos nuevos argumentos: entrada y salida, respectivamente, de tal forma que la invocación a ejecutar_orden() desde la función ejecutar_linea_ordenes() deberá incluir el valor para estos dos nuevos parámetros formales. Estos dos parámetros servirán para indicar los des- criptores de archivo a los que deben ser redireccionadas la entrada y/o salida estándar de las órdenes que componen una orden compuesta en el momento de invocar a ejecutar_orden(). Y, en el caso más sencillo, si la orden es simple, debe invocar a la función ejecutar_orden() con valor 0 para el parámetro entrada (descriptor de entrada estándar) y con valor 1 para salida (descriptor de salida estándar), respectivamente. Además, en este caso, sin necesidad de realizar redireccionamiento. Como apoyo para completar el código de ejecutar.c para la FASE 7, se proporcionan las siguientes funciones: int ** crear_pipes(int ordenes). Crea tantas tuberías como número de órdenes pa- sado como parámetro menos uno (si hay n órdenes, hay n-1 tuberías intermedias) y las añade a una estructura dinámica de punteros a arrays de dos enteros. Estos arrays co- rresponden a los dos descriptores de archivo, de entrada y salida, asociados a cada pipe creado. char ** parser_pipes(const char *orden, int *total). Convierte la cadena asocia- da a una orden (compuesta o simple), pasada a través del parámetro orden, en una estructura dinámica de cadenas independientes correspondientes a cada una de las ór- denes simples de que consta. Devuelve esta estructura y en el parámetro total el nú- mero de órdenes simples de orden. Esta función ya está implementada y se incluye en el archivo fuente parser.o. void free_ordenes_pipes(char **ordenes, int **pipes, int nordenes). Esta fun- ción libera las estructuras ordenes y pipes de tipo array dinámico para las nordenes existentes. Esta función está ya implementada en el archivo fuente libme- moria.c. Se deja intencionadamente al alumno que interprete el código proporcionado para las funciones crear_pipes() (en la que debe añadir una línea de código) y free_ordenes_pipes(). Interpretar el código de funciones 34 A. Diagrama de la aplicación minishell Finalmente, en la Figura 10 se muestran los diferentes archivos de la aplicación minishell y la relación entre ellos. Se representan con rectángulos de color naranja los archivos fuente con las funciones que debe codificar. Intencionadamente se han omitido las funciones corres- pondientes al desarrollo de las fases 3 y 5. El alumno debe decidir, en este caso, cuál es el archivo fuente donde considera más razonable ubicar ambas funciones así como sus prototi- pos o declaraciones. Asimismo, se muestran en el diagrama los archivos de cabecera y fuente ya codificados. En este último caso, se incluyen también las funciones definidas. Figura 10: Esquema general de la aplicación minishell. 35 extraordinario_planificacion_junio_2012.pdf Sistemas Operativos Junio 2012 Ejercicio de Sistemas Operativos Parte Práctica Problema sobre planificación de procesos Sea un sistema monoprocesador en el que se ejecutan tres tareas con las siguientes secuencias de ejecución: Tarea CPU E/S CPU T1 10 5 6 T2 3 5 2 T3 12 1 20 El dispositivo de E/S atiende las peticiones de una en una, en orden FIFO. El planificador a corto plazo es de tipo SJF. La predicción de la duración de la siguiente ráfaga se hace de acuerdo a la siguiente expresión: δi+1 = αn+ (1− α)δi δi+1 Es la predicción de la duración de la próxima ráfaga de CPU. δi Es la última predicción hecha sobre la duración de la ráfaga de CPU. n Es la duración real de la última ráfaga de CPU. α Es un factor de ponderación que para este ejercicio es fijo y vale 0, 5. En el instante T = 0 se sabe que las predicciones de duración de ráfaga δi para T1, T2 y T3 son 4, 5 y 10 respectivamente. 1. Complete la plantilla adjunta con los datos sobre el estado de las tareas. Debe rellenarse una fila de la plantilla cada vez que se ejecute el planificador, indicando en qué instante de tiempo se produjo la activación del mismo, el resultado del cálculo de predicción de la duración de la siguiente ráfaga y el estado en el que se encuentran cada una de las tareas. Deben consignarse los valores que quedaŕıan al terminar la ejecución del planificador, es decir, justo antes de ceder la CPU al proceso que deba ejecutarse en ese momento. Considere que cualquier otra cosa que no sea CPU(n) se ejecuta en tiempo cero, incluido el propio planificador. (5 puntos) 2. Rellene de nuevo la plantilla utilizando como planificador SJF con requisa. ¿Qué parámetro de planificación mejora esta variante? (5 puntos) 6 Apartado 1 Apellidos Nombre Instante de activación Duración de la última ráfaga de CPU Última predicción sobre la duración de la ráfaga Previsión futura sobre la duración de la ráfaga Estado del proceso. Puede ser LISTO BLOQUEADO EJECUCION T1 4 Ejecución T2 5 Listo T3 10 Listo T1 10 4 7 Bloqueado E/S T2 5 Ejecución T3 10 Listo T1 7 Bloqueado E/S T2 3 5 4 Bloqueado E/S T3 10 Ejecución T1 7 Listo T2 4 Ejecución T3 12 10 11 Bloqueado E/S T1 7 Ejecución T2 2 4 3 FIN T3 11 Listo T1 6 7 6,5 FIN T2 T3 11 Listo T1 T2 20 11 15,5 FIN T3 T1 T2 T3 T1 T2 T3 T1 T2 T3 T1 T2 T3 T1 T2 T3 T1 T2 T3 Plantilla de respuestas para el ejercicio de planificación SJF T=0 T=10 T=13 T=25 T=27 T=33 T=53 Apartado 2 Apellidos Nombre Instante de activación Duración de la última ráfaga de CPU Última predicción sobre la duración de la ráfaga Previsión futura sobre la duración de la ráfaga Estado del proceso. Puede ser LISTO BLOQUEADO EJECUCION T1 4 Ejecución T2 5 Listo T3 10 Listo T1 10 4 7 Bloqueado E/S T2 5 Ejecución T3 10 Listo T1 7 Bloqueado E/S T2 3 5 4 Bloqueado E/S T3 10 Ejecución T1 7 Ejecución T2 4 Bloqueado E/S T3 10 (-2) Listo T1 7 (-5) Ejecución T2 4 Listo T3 10 (-2) Listo T1 6 7 6,5 FIN T2 4 Listo T3 10 (-2) Listo T1 T2 2 4 3 FIN T3 10 (-2) Listo T1 T2 T3 12 10 11 Bloqueado E/S T1 T2 T3 12 10 11 Bloqueado E/S T1 T2 T3 20 11 15,5 FIN T1 T2 T3 T1 T2 T3 T1 T2 T3 Plantilla de respuestas para el ejercicio de planificación SJF T=33 T=34 T=54 T=13 T=15 T=20 T=21 T=23 T=0 T=10 planificador-mayo-2012.pdf Ejercicio de Sistemas Operativos Parte Práctica Problema sobre planificación de procesos Sea el sistema operativo WiNix diseñado con un planificador a corto plazo sin requisa y basado en prioridades dinámicas. Las prioridades están comprendidas entre 0 y 15 para hilos de usuario (prioridades dinámicas) y entre 16 y 30 para hilos del sistema (prioridades estáticas). El mecanismo de incremento y decremento de prioridades está basado en el del planificador de Windows. A continuación se describen algunas de las caracteŕısticas más relevantes del planificador de uso del procesador: Los hilos poseen inicialmente una prioridad base heredada de las tareas a las que pertene- cen. Los hilos pertenecientes a una misma prioridad se planifican con un algoritmo round robin con quantum de 2 u.t. Si un hilo agota su quantum se reduce su prioridad en 1 si y solo si la prioridad del hilo es mayor que la prioridad base. La prioridad de un hilo de usuario siempre estará comprendida entre su prioridad base y 15. A un hilo que despierta después un evento de E/S se le aumenta su prioridad en función del tipo de evento. Algunos de los incrementos aplicados se muestran en la siguiente tabla: Recurso Incremento de prioridad Vı́deo prioridad base+2 Teclado/Ratón prioridad base+6 Existe un hilo del sistema denominado Fair Scheduling Solver (HFSS). Este hilo se ejecuta con prioridad 16 y permanece en estado de Bloqueado hasta que un temporizador lo des- bloquea cada 11 u.t. HFSS requiere 1 u.t. para su ejecución (11 u.t bloqueado y 1 u.t en ejecución). Durante este tiempo se encarga de buscar hilos de usuario (hasta un máximo de 10 hilos) que lleven más de 9 u.t. sin ejecutarse y de incrementarles su prioridad en 52. Tras su ejecución, HFSS vuelve a bloquearse en espera de que el temporizador vuelva a despertarle. En este sistema se crean los hilos H0, perteneciente a la tarea T0, y H1 y H2 pertenecientes a la tarea T1. En la tabla siguiente se muestra el tiempo de uso de los recursos del sistema por cada hilo aśı como su prioridad base (Pribase). Hilo tllegada Pribase CPU E/S CPU E/S CPU H0 0 5 4 2 (Vı́deo) 1 - - H1 0 6 3 2 (Teclado) 3 3 (Vı́deo) 1 H2 1 6 3 3 (Teclado) 2 - - Sabiendo además que los dispositivos de E/S atienden peticiones siguiendo un orden FIFO, responda a las siguientes cuestiones: Sistemas Operativos Mayo 2012 P la nt ill a d e re sp u es ta p ar a el p ro b le m a 1 E je rc ic io 1 d e S is te m as O p er at iv os . G ra d o en In ge n ie ŕı a In fo rm át ic a / In ge n ie ŕı a en C om p u ta d or es . M ay o 20 12 R es p u es ta ap ar ta d o 1: t i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 E st ad o H 0 L E E L E E B B B E P ri or id ad H 0 5 5 7 E st ad o H 1 E E L L E B B L E E L L L E B B B E - - P ri or id ad H 1 6 12 11 8 - - E st ad o H 2 L E E L E B B B B E E - - - - - - - - P ri or id ad H 2 6 12 E st ad o H F S S B L E B P ri or id ad H F S S 16 V ı́d eo H 1 H 1 H 1 H 0 H 0 H 0 T ec la d o H 1 H 1 H 2 H 2 H 2 H 2 5 Sistemas Operativos Mayo 2012 1. Complete la plantilla adjunta indicando el estado de los hilos (de acuerdo a la leyenda anexa) y su prioridad, aśı como los hilos que esperan por cada recurso de E/S (v́ıdeo y teclado) en cada unidad de tiempo. (6 puntos) RESPUESTA: 2. Calcule el tiempo medio de estancia de los hilos de usuario en este sistema. (1 punto) RESPUESTA: Testancia = 20+18+11 3 ⇡ 16,3 3. ¿Cuál es la unidad de tiempo en la que se desencadena por primera vez los siguientes escenarios? (1 punto) Decay. Cesión voluntaria de la CPU. Requisa de la CPU tras una E/S. RESPUESTA: Decay. 10. Cesión voluntaria de la CPU. 5. Requisa de la CPU tras una E/S. Con el algoritmo descrito, sin requisa, no puede darse este escenario. 4. Suponiendo que al algoritmo de planificación descrito se le añade la siguiente caracteŕıstica: Un hilo que es expulsado de la CPU por la llegada de otro hilo de mayor prioridad se coloca al principio de la cola asociada a su prioridad actual y su quantum se reiniciará cuando el hilo reanude posteriormente su ejecución. ¿Este nuevo escenario de activación del planificador a corto plazo implicaŕıa algún cambio en los resultados obtenidos en la plantilla adjunta (apartado 1)? En caso afirmativo, indique los cambios sólo en la unidad de tiempo en la que por primera vez se produzcan tales variaciones. Razone la respuesta. (2 puntos) (10 puntos) RESPUESTA: Las primeras modificaciones respecto a lo obtenido en el apartado 1 se producen en la u.t. 7 y serán las siguientes: Estado de H1: Ejecución -E. Estado de H0: Listo -L. Justificación: Al hilo H0 que estaba ejecutándose con prioridad 5 hasta la u.t. 7 se le requisa la CPU porque el hilo H1 en ese instante despierta después de una operación de teclado de 2 u.t. y se le aumenta su prioridad hasta 12. Como la prioridad de H1 (12) es mayor que la prioridad de H0 en la u.t. 7, el hilo H0 pasa a estado de Listo y el hilo H1 a Ejecución. 2 Considere que la prioridad de los hilos se actualiza en la siguiente unidad de tiempo. 6 examen_ordinario_2016-17.pdf Examen de Sistemas Operativos Mayo 2017 Examen de Sistemas Operativos Evaluación del tema 4 Test 1. En el escenario de desbloqueo de un proceso tras una espera, ¿cuál de los siguientes tipos de planificadores se activaŕıa? (a) Cualquier algoritmo de planificación basado en prioridades. (b) SJF (c) SJF con requisa (d) Todas las respuestas anteriores son correctas. 2. En un sistema planificado mediante prioridades con requisa, ¿cuál de los siguientes escenarios puede implicar requisa? (a) El proceso que se está ejecutando realiza una transferencia a disco. (b) El proceso que está utilizando la CPU finaliza su ejecución. (c) Un proceso despierta como resultado del fin de una operación de E/S. (d) Todos los escenarios anteriores. 3. Indique cuál de las siguientes respuestas es correcta respecto a la prioridad de los procesos de usuario en un sistema UNIX 4.4 BSD: (a) Depende del número de trabajos que hay en el sistema. (b) Depende del tiempo de uso del procesador de los procesos de usuario pero no del tiempo de espera por E/S. (c) Depende del tiempo de espera por E/S pero no de su tiempo de uso del procesador. (d) No puede ser modificada por ningún usuario. 4. Respecto al cambio de contexto, ¿cuál de las siguientes afirmaciones es fal- sa? (a) Implica la salvaguarda del estado del proceso que se está ejecutan- do en ese momento y la restauración del estado del proceso que a continuación va a ejecutarse. (b) Implica el uso de la pila de los procesos implicados en el cambio. (c) Es implementado por el repartidor o dispatcher sin soporte hardware. (d) Supone cambio de estado del proceso que se está ejecutando cuando se produce el cambio de contexto. 2 Examen de Sistemas Operativos Mayo 2017 5. En un sistema se ejecutan los trabajos T0, T1 y T2, con instantes de llegada t=0, t=1 y t=2 y ráfagas de uso del procesador de 3 u.t., 3 u.t. y 6 u.t., respectivamente. Suponiendo que se emplea un algoritmo de planificación First In First Out (FIFO), indique la respuesta correcta: (a) El tiempo medio de estancia es 6 u.t. (b) El tiempo medio de espera es 3 u.t. (c) El grado de uso del procesador es del 99,9%. (d) El tiempo medio de retorno es de 7 u.t. 6. Considerando el diagrama básico de estados de un proceso en una máquina multiprocesador, se puede afirmar que ... (a) Puede haber varios procesos en estado de espera. (b) Puede haber varios procesos en estado de listo. (c) Puede haber varios procesos en estado de ejecución. (d) Todas las afirmaciones anteriores son correctas. 7. Indique cuál de las siguientes afirmaciones respecto a sistemas operativos en tiempo compartido es correcta. (a) Con procesos muy intensivos en CPU, se comportan igual que un sistema multiprogramado. (b) Con procesos muy poco intensivos en CPU, se comportan igual que un sistema multiprogramado. (c) Son el paso previo que permitió el diseño de los sistemas en tiempo real. (d) No tienen utilidad en sistemas multiprocesador. 8. El planificador a corto plazo: (a) Controla el grado de multiprogramación, es decir, el número de pro- cesos cargados en memoria. (b) Selecciona de entre los trabajos cargados en memoria, y que están preparados para ejecutarse, cuál hará uso del procesador. (c) Carga y descarga trabajos desde el disco a la memoria y de la memoria al disco. (d) Todas las respuestas son correctas. 9. En la planificación del procesador: (a) El algoritmo Round-Robin permite que un proceso se apropie de forma indefinida del procesador. (b) El algoritmo SJF (Shortest Job First) es el más sencillo de implemen- tar. (c) El algoritmo FIFO es el más complejo de implementar. (d) La asignación de prioridades puede generar inanición en los procesos de menor prioridad. 3 Examen de Sistemas Operativos Mayo 2017 10. Indique cuál de las siguientes afirmaciones es correcta: (a) El dispatcher decide cuándo hay que desalojar al proceso que está en CPU. (b) El scheduler define mecanismos mientras que el dispatcher establece poĺıticas. (c) Siempre que hay procesos en estado Listo y se ejecuta el scheduler hay una asignación de CPU a uno de dichos procesos. (d) Siempre que hay procesos en estado Listo y se ejecuta el dispatcher hay una asignación de CPU a uno de dichos procesos. 4 sistemas operativos 25 de mayo de 2017 prueba final Ordinaria dpto. de automática curso 2016/17 Apellidos: Nombre: IMPORTANTE ⌧ Duración del examen: 2 horas. i No olvide anotar el nombre y los apellidos en todas las hojas de exa- men, incluido el enunciado de examen. i No se permite ningún tipo de documentación. i Las respuestas correspondientes a cada problema deben entregarse ex- clusivamente en las plantillas adjuntas. i Se entregarán todas las hojas de respuestas, incluido el enunciado de examen, dobladas por la mitad. grado en ingenieŕıa informática sistemas operativos 25 de mayo de 2017 prueba final Ordinaria dpto. de automática curso 2016/17 Problema de procesos en Unix 1. (10 puntos) Responda a las siguientes cuestiones relacionadas con procesos en sistemas UNIX en la plantilla adjunta. (a) (4 puntos) Teniendo en cuenta que un usuario ejecuta las órdenes mostradas a con- tinuación, indique en las columnas correspondientes de la plantilla cuándo se pro- ducen llamadas al sistema relacionadas con procesos (fork, exec, wait/waitpid, kill o exit) aśı como cuándo se recibe una señal SIGCHLD o SIGINT. Considere que la orden pwd es interna y que su código sólo realiza una llamada a getcwd(). Utilice las diferentes filas para mostrar el orden de ejecución de los diferentes even- tos, es decir, que si el evento B ocurre después de haber terminado el evento A, una fila debeŕıa rellenarse con el evento A y la siguiente con el evento B. Utilice cada co- lumna para diferenciar los procesos que pueden estar en ejecución simultáneamente. Tenga en cuenta que no todas las filas tienen por qué estar rellenas. 1 gcc -Wall examen.c -o examen 2 sleep 123 & 3 sleep 10 4 Pulsar Ctrl+C antes de terminar la orden anterior 5 ps 6 pwd Solución: Plantilla adjunta (b) (2 puntos) Dibuje un árbol con la jerarqúıa de procesos resultante de ejecutar el programa examen cuyo código se muestra a continuación. Considere que todas las llamadas al sistema se ejecutan correctamente. Identifique los procesos en el árbol con los d́ıgitos que mostrarán en pantalla después de su creación y en el orden correcto, es decir, que si un proceso va a a mostrar 4, 2, 1 y 3, su nombre en la jerarqúıa será ”4213”(si dos procesos muestran los mismos números, se denominarán igual). No represente el proceso de la shell en la jerarqúıa. Contenido del archivo examen.c: 1 #include <unistd.h> 2 #include <sys/wait.h> 3 4 #define STDOUT 1 5 6 int main() { 7 int pid; 8 9 write (STDOUT , "1", 1); 10 pid = fork (); 11 write (STDOUT , "2", 1); 12 wait(NULL); 13 write (STDOUT , "3", 1); 14 if (pid == 0) 15 execlp ("echo", "echo", NULL); 16 write (STDOUT , "4", 1); 17 18 return 0; 19 } –2– sistemas operativos 25 de mayo de 2017 prueba final Ordinaria dpto. de automática curso 2016/17 Solución: Plantilla adjunta (c) (4 puntos) Teniendo en cuenta que la llamada al sistema write (STDOUT, "X", 1) del código muestra el carácter X por pantalla, indique todas las posibles secuencias de letras que podŕıa mostrar la ejecución del programa examen por pantalla (escriba sólo las letras, sin saltos de ĺınea). Solución: 122334 123234 –3– sistemas operativos 25 de mayo de 2017 prueba final Ordinaria dpto. de automática curso 2016/17 Problema de planificación y sincronización 2. (10 puntos) En el sistema operativo MagicOS se va a utilizar un nuevo algoritmo de pla- nificación de hilos expulsivo (con requisa) y basado en prioridades, cuyas caracteŕısticas se describen a continuación: Existen 21 colas de hilos, una por cada prioridad. Valores más altos de prioridad su- ponen prioridad mayor; por ejemplo, un hilo con prioridad 11 tiene mayor prioridad que un hilo con prioridad 6. El algoritmo de planificación utilizado en cada cola es SJF con requisa . El planificador siempre elige como siguiente hilo a ejecutarse aquél de la cola de mayor prioridad cuya siguiente ráfaga predicha de CPU sea más corta. La predicción se realiza a partir de la siguiente fórmula: ⌧n+1 = ↵tn + (1� ↵)⌧n donde: • tn: Longitud de la n-ésima ráfaga de CPU. • ⌧n: Longitud predicha para la n-ésima ráfaga de CPU. • ↵: Parámetro de ajuste. ↵ = 1/2. Las prioridades de 10 a 20 se reservan para hilos de tiempo real y son estáticas. Las prioridades desde 0 a 9 se reservan para los hilos ordinarios. A estos hilos se les asigna en su creación una prioridad base heredada de la tarea a la que pertenecen. Durante la existencia de estos hilos, su prioridad está comprendida entre la prioridad base y 9. Esta prioridad se ajusta dinámicamente según los siguientes escenarios de planificación: • El hilo que se está ejecutando en la CPU acumula cinco unidades de tiempo de ejecución (no necesariamente consecutivas): El planificador dismi- nuye en uno la prioridad actual del hilo pasando éste a la cola correspondiente a dicha prioridad. • Un hilo transita del estado de Bloqueado a Listo: Su prioridad aumenta en función del tipo de evento por el que espera: El incremento es de dos sobre la prioridad actual del hilo si la espera ha sido por un semáforo o por la finalización de un temporizador y de uno por el resto de eventos. • Requisa por la llegada al sistema de un hilo de mayor prioridad: El hilo que se está ejecutando es expulsado de la CPU y pasa a la cola correspondiente a su prioridad. El nuevo hilo, de mayor prioridad, pasa a ejecutarse. A continuación, se muestran las funciones ejecutadas por los hilos hijos definidas a nivel de diseño: –4– sistemas operativos 25 de mayo de 2017 prueba final Ordinaria dpto. de automática curso 2016/17 Entrada Rapida() P(acceso_prioritario); entradas_rapidas = entradas_rapidas + 1; if (entradas_rapidas == 1) P(acceso_atraccion); V(acceso_prioritario); sleep(5); /* tiempo en atraccion */ P(acceso_prioritario); entradas_rapidas = entradas_rapidas - 1; if (entradas_rapidas == 0) V(acceso_atraccion); V(acceso_prioritario); Entrada Normal() P(acceso_atraccion); sleep(5); /* tiempo en atraccion */ V(acceso_atraccion); Considere para este problema los siguientes supuestos: Los semáforos son binarios, compartidos por todos los hilos, y gestionan una cola FIFO de hilos bloqueados por ellos. La función sleep(t) provoca que el hilo que la ejecuta se quede inmediatamente bloqueado hasta que transcurran las t u.t. del temporizador asociado. Las operaciones aritméticas, de comparación, de manejo de semáforos (indepen- dientemente de su comportamiento en cada escenario concreto), consumen 1 u.t. de CPU. Por lo tanto, una sentencia if como la que aparece en las funciones ejecuta- das por los hilos supone 1 u.t. cuando no se cumple su condición y 2 u.t. cuando se cumple su condición (en este último caso, por la operación de comparación de la condición y la operación sobre el semáforo correspondiente). El estado inicial del sistema desde t=0 hasta t=4, inclusive, es el mostrado en la plantilla del problema. Como puede observar, en t=5 se han creado todos los hilos: H11, H12 y H2 con prioridad base 5. Los hilos H11 y H12 tienen asociada en su creación la función Entrada Rapida() y el hilo H2 la función Entrada Normal(), respectivamente. La predicción de duración de la siguiente ráfaga en t = 4 para los hilos H11, H12 y H2 es de 4, 5 y 2, respectivamente. Responda a las siguientes preguntas en la hoja de respuestas adjunta: (a) (6.5 puntos) Complete el diagrama de la hoja adjunta con la siguiente información: el estado en el que se encuentra cada hilo en cada instante (Ejecución, Listo o Bloqueado), el valor que toman los dos semáforos implicados en este escenario, el valor la variable compartida entradas rapidas y el estado de las colas asociadas a los semáforos desde t=5. Solución: En hoja de respuestas adjunta. –5– sistemas operativos 25 de mayo de 2017 prueba final Ordinaria dpto. de automática curso 2016/17 (b) (2.5 puntos) Obtenga la predicción de duración de siguiente ráfaga de CPU en los instantes de tiempo en los que se activa el planificador SJF con requisa. Para ello, complete la tabla que se proporciona en la hoja de respuestas del problema. Observe que en cada fila sólo debe registrar, para cada activación del planificador, los datos correspondientes al hilo que haya ejecutado la última ráfaga de CPU antes de dicha activación. Solución: u. t. activación del planificador Hilo con última ráfaga de CPU tn ⌧n ⌧n + 1 t = 5 H2 1 2 1,5 t =9 H11 4 4 4 t =10 H12 1 5 3 t =11 H2 1 1,5 1,25 t =12 H11 1 4 2,5 t =15 H12 3 3 3 t = 21 H12 1 3 2 t = 24 H12 4 2 3 t = 28 H11 4 2,5 3,25 (c) (1 punto) Indique claramente en la u.t. t = 21 cómo decide el planificador SJF con requisa cuál es el siguiente hilo a ejecutar. Solución: En t = 21 sólo existen dos hilos en el sistema y ambos en la misma cola: H11, que está en la cola de prioridad 8 desde la u.t. t = 20, y el hilo H12 que, tras haber sido decrementada su prioridad, también pasa a esa misma cola. Por lo tanto, en este escenario, el planificador SJF con requisa selecciona el hilo cuya ráfaga siguiente predicha sea más corta. Para H11, la predicción de duración de siguiente ráfaga es de 2,5 y para H12 es de 2 < 2,5. Por lo tanto, el algoritmo de planificación elige H12 como siguiente hilo a ejecutarse en t = 21 y H11 se mantiene en estado Listo en la cola de prioridad 8. –6– Plantilla de respuestas para las cuestiones de Procesos Sistemas Operativos - Grado en Ingeniería Informática 25 de mayo de 2017 Escuela Politécnica Superior. Universidad de Alcalá Apellidos SOLUCIÓN Nombre DNI Cuestión 1. Terminal Proceso shell Proceso Hijo 1 Proceso Hijo 2 $ gcc -Wall examen.c -o examen fork() waitpid() exec(“gcc”) exit() SIGCHLD $ sleep 123 & [1] 15400 fork() exec(“sleep”) $ sleep 10 (no se espera su finalización antes de la siguiente acción) fork() waitpid() exec(“sleep”) El usuario pulsa Ctrl+C SIGINT SIGCHLD waitpid() $ ps PID TTY TIME CMD 15269 pts/1 00:00:00 bash 15400 pts/1 00:00:00 sleep 15470 pts/1 00:00:00 ps fork() waitpid() exec(“ps”) exit() SIGCHLD $ pwd /home/usuario/directorio getcwd() Nota: en la columna del terminal, los textos en negrita indican las órdenes introducidas por el usuario, y los textos en cursiva, la respuesta mostrada por la shell. Cuestión 2. Cuestión 3. Posible salida 1 122334 Posible salida 2 123234 Posible salida 3 Posible salida 4 Posible salida 5 Posible salida 6 Posible salida 7 Posible salida 8 Posible salida 9 Posible salida 10 H oj a de re sp ue st as de l pr ob le m a 2 t i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 H 1 1 L E E E E B B E L L L B B B B B L L L L E E E E - P r. 5 7 6 8 C ol a H 1 2 L E B B E E E B B B B B E E E E - P r. 5 7 9 8 C ol a H 2 L E B B B B B E - P r. 5 7 C ol a a c c e s o a t r a c c i o n 1 0 1 a c c e s o p r i o r i t a r i o 1 0 1 0 1 0 e n t r a d a s r a p i d a s 0 1 2 1 0 C o l a H 1 1 - a c c e s o a t r a c c i o n C o l a H 1 2 - a c c e s o p r i o r i t a r i o L e y e n d a : E E j e c u c i ó n . L L i s t o . B B l o q u e a d o . planificacionMayo2015.pdf sistemas operativos 21 de mayo de 2015 prueba final Ordinaria dpto. de automática curso 2014/15 Problema de planificación de procesos y sincronización 2. (10 puntos) Se desea realizar un conjunto de pruebas en el sistema operativo GeekS- quared utilizando un nuevo algoritmo de planificación de hilos expulsivo (con requisa) y basado en prioridades, cuyas caracteŕısticas se describen a continuación: Maneja 32 colas de hilos, una por cada prioridad. Si el valor de prioridad de un hilo es mayor que el de otro hilo, esto implica que el primero tiene mayor prioridad. Por ejemplo, un hilo con prioridad 20 tiene más prioridad que uno con prioridad 18. El algoritmo de planificación utilizado en cada cola es round-robin con un quantum de 5 u.t. El planificador siempre elegirá como siguiente hilo a ejecutarse el situado en la cabeza de la cola de mayor prioridad. Las prioridades de 16 a 31 se reservan para hilos de tiempo real y son estáticas. Las prioridades desde 0 a 15 se reservan para los hilos ordinarios. Estos hilos heredan en su creación la prioridad de la tarea a la que pertenecen (prioridad base). Durante la existencia de estos hilos, su prioridad está comprendida entre la prioridad base y 15 y se ajusta dinámicamente según los siguientes escenarios de planificación: • El hilo agota su quantum : Sólo si el valor de la prioridad del hilo es mayor que la prioridad base, el planificador disminuye en uno la prioridad del hilo. En cualquier caso, a continuación, el planificador coloca al hilo al final de la cola correspondiente a su prioridad. • Un hilo transita del estado de Bloqueado a Listo: Aumenta su prioridad en función del tipo de evento por el que espera. Por ejemplo, el boosting es de 1 u. t. sobre la prioridad actual cuando la espera es por un semáforo. • Requisa por la llegada al sistema de un hilo de mayor prioridad: El hilo que se está ejecutando es expulsado de la CPU y pasa a la cabeza de la cola correspondiente a su prioridad reiniciándose a 0 el valor de su quantum . El nuevo hilo, de mayor prioridad, pasa a ejecutarse. En este sistema existe en t=5 únicamente un hilo principal, Hp, con prioridad 5, y tres hilos creados por él en este instante: H10, H00 y H01. Los hilos H00 y H01 tienen asociada en su creación la función Alumno() y el hilo H10 la función Profesor(), respectivamente. A continuación, se muestran las funciones ejecutadas por los hilos hijos definidas a nivel de diseño: –6– sistemas operativos 21 de mayo de 2015 prueba final Ordinaria dpto. de automática curso 2014/15 Alumno() P(exmut); if (esperando < MAX) { esperando = esperando + 1; V(alumnos); V(exmut); P(profe); Recibir_Tutoria(); /* 4 u.t. */ } else { V(exmut); } Profesor() while(TRUE) { P(alumnos); P(exmut); esperando = esperando - 1; V(profe); V(exmut); Realizar_Tutoria(); /* 4 u. t. */ } Considere para este problema los siguientes supuestos: MAX y TRUE son constantes; MAX definida con valor 10 y TRUE con valor 1. Los semáforos son binarios, compartidos por todos los hilos, y gestionan una cola FIFO de hilos bloqueados por ellos. Realizar Tutoria() y Recibir Tutoria() son invocaciones a dos funciones. Am- bas suponen 4 u. t. de CPU. Por simplicidad, operaciones como las siguientes: operaciones aritméticas, de com- paración, de manejo de semáforos -independientemente de su comportamiento en cada escenario concreto-, consumen 1 u.t. de CPU. Por lo tanto, observe que en la ejecución de una sentencia if-else, como la que aparece en las función Alumno(), debe considerar 1 u.t. por la evaluación de su condición, además de las u.t. asocia- das a la ejecución de cada sentencia del bloque if/else, según el resultado de la evaluación de la condición. De forma similar se supondrá para la sentencia while1. A continuación, responda a las siguientes preguntas: (a) (7 puntos) Complete la plantilla adjunta partiendo del estado inicial del sistema que aparece en la unidad de tiempo t=5. Tenga en cuenta además que el orden que ocupan los hilos hijos en la cola de prioridad 5 en t=5 coincide con su orden de creación: H10, H00 y H01, y que en t=6, el hilo principal Hp queda en espera inmediatamente por la finalización de sus hilos hijos, en el mismo orden en que éstos se crearon. Rellene en el diagrama de la hoja adjunta: el estado en el que se encuentra cada hilo en cada instante (Ejecución, Listo o Bloqueado), el valor que toman los tres semáforos implicados en este escenario, el valor la variable compartida esperando y el estado de las colas asociadas a los semáforos, desde la u.t. t=6 hasta t= 32. 1 1 u. t. por la evaluación de su condición y 1 u. t. por cada operación ejecutada en el bloque del while, excepto Realizar Tutoria(), que supone 4 u.t. de CPU. –7– sistemas operativos 21 de mayo de 2015 prueba final Ordinaria dpto. de automática curso 2014/15 Solución: Se adjunta la plantilla al final de la solución. (b) (3 puntos) Indique el instante de tiempo en el que se producen por primera vez los siguientes eventos: Desalojo o requisa. Cesión voluntaria de la CPU. Finalización de quantum. Solución: Expulsión o desalojo. En t=12, por la llegada del hilo H10, de mayor prio- ridad, tras su bloqueo en la cola asociada al semáforo alumnos. Cesión voluntaria de la CPU. En t=6 el hilo Hp, al quedarse bloqueado en espera de sus tres hilos hijos. Finalización de quantum. El primer quantum que finaliza se produce en t=19 para el hilo H10; este hilo es el de mayor prioridad en esa unidad de tiempo por lo que se reinicia su quantum a 0 y sigue ejecutándose. –8– t i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 H 0 0 L E E E E L E L E E E E E P r. 5 C ol a H 0 1 L E E E E L P r. 5 C ol a H 1 0 L E E B E B E E E E E E E E E B E P r. 5 6 7 6 7 C ol a H p E E E B P r. 5 C ol a e x m u t 1 0 1 0 a l u m n o s 0 p r o f e 0 1 0 e s p e r a n d o 0 1 0 1 C o l a H 1 0 – e x m u t C o l a H 1 0 – H 1 0 – a l u m n o s C o l a p r o f e L ey en d a : E E j e c u c i ó n . L L i s t o . B B l o q u e a d o . planif-sincroniz-junio-2014.pdf Examen de Sistemas Operativos Junio 2014 Examen de Sistemas Operativos Parte Práctica Planificación de procesos y sincronización Un determinado sistema operativo monoprocesador tiene que ejecutar un pro- grama de cálculo que se compone de tres hilos de ejecución, tal y como aparece a continuación: // Variables globales a todos los hilos bool finh2=false; bool finh3=false; int resultadoA; int resultadoB; int resultadoC; void hilo3() { resultadoA=calcula (); finh3=true; } void hilo2() { resultadoB=calcula (); finh2=true; } void hilo1() { while (! finh2 && !finh3) {}; resultadoC=resultadoA+resultadoB; } Para responder a las siguientes cuestiones, considere que los tres hilos son los únicos hilos en estado ready del sistema y que en dicha cola hilo1 es el prime- ro, luego hilo2 y por último hilo3. Además, considere que todas las instruc- ciones tardan 1 u.t en ejecutarse excepto la que contiene la llamada a la función calcula(), que tarda 4 u.t. Por instrucción entenderemos cada ĺınea del programa, teniendo en cuenta que si la instrucción es un bucle, cada iteración contará como una instrucción. 1. El responsable del proyecto se pregunta si seŕıa posible utilizar planificación FIFO para los hilos, o por lo contrario habŕıa que recurrir a un sistema más caro con planificación round-robin con un q=2. Complete el cronograma 1 para ambos esquemas de planificación, FIFO y round-robin y a la vista de los resultados haga una propuesta justificada (máximo 5 ĺıneas). 3 puntos. RESPUESTA: No se puede usar FIFO, ya que el bucle de espera activa monopolizaŕıa la CPU y no dejaŕıa que otro proceso utilizara la CPU. El cronograma resultante seŕıa el siguiente: +� +� +� +� +� +� +� +� +� +� +� +� +� +� +� Seŕıa necesario utilizar RR para que esta solución funcionara. +� +� +� +� +� +� +� +� +� +� +� +� +� +� +� 2. Uno de los ingenieros propone la siguiente mejora basada en el uso de semáfo- ros. 8 Examen de Sistemas Operativos Junio 2014 1 2 // Variables globales a todos los hilos 3 semaphore S2 <- 0; 4 semaphore S3 <- 0; 5 int resultadoA; 6 int resultadoB; 7 int resultadoC; 8 9 void hilo3 () { 10 resultadoA=calcula (); 11 V(S3); 12 } 13 14 void hilo2 () { 15 resultadoB=calcula (); 16 V(S2); 17 } 18 19 void hilo1 () { 20 P(S3); 21 P(S2); 22 resultadoC=resultadoA+resultadoB; 23 } Las instrucciones de las ĺıneas 3 y 4 indican la declaración de dos variables de tipo semáforo inicializadas ambas a 0. Las operaciones P y V, como cualquier otra operación, tardan 1 u.t, independientemente de que provoquen bloqueo o no. Hay quien dice que esta solución no es válida porque lleva a bloqueo, otros dicen que no mejora a la anterior porque obliga a utilizar planificación round- robin que es más cara. Rellene el cronograma 2 para ambos esquemas de planificación, FIFO y round-robin y a la vista de los resultados formule su propia opinión justificada. 7 puntos. (10 puntos) RESPUESTA: La solución por semáforos no provoca abrazo mortal ni obliga a utilizar round- robin. Simplemente lleva a perfiles de ejecución diferentes. +� +� +� +� +� +� +� +� +� +� +� +� +� +� +� Con RR seŕıa aśı: +� +� +� +� +� +� +� +� +� +� +� +� +� +� +� 9 sol-planificador-Jun-2011.pdf Sistemas Operativos Junio 2011 Examen de Sistemas Operativos Parte Práctica Problema 1 Un sistema operativo posee un algoritmo de planificación de hilos expulsivo y basado en prioridades con las siguientes caracteŕısticas: El algoritmo de planificación es válido para sistemas con varios procesadores. Las prioridades de 1 a 15 se reservan para hilos ordinarios. Estos hilos heredan en su creación la prioridad de la tarea a la que pertenecen (prioridad base). Durante la existencia del hilo su prioridad es dinámica, pero siempre comprendida entre la prioridad base y 15. Las prioridades de 16 a 31 se reservan para hilos con restricciones de tiempo real y son prioridades estáticas. Para cada nivel de prioridad existe una cola asociada. El algoritmo de planificación utili- zado en cada cola de hilos en estado de Listo es round-robin con un quantum de 3 u.t. El planificador siempre elegirá como siguiente hilo a ejecutarse el situado en la cabeza de la cola de mayor prioridad. Si un hilo agota su quantum y la prioridad es mayor que la prioridad base, el planificador disminuye en uno la prioridad del hilo y lo coloca al final de la cola asociada a su prioridad. Si a un hilo se le requisa la CPU como consecuencia de la llegada de un hilo de mayor prioridad, el hilo que se estaba ejecutando pasa a la cabeza de la cola correspondiente a su prioridad restaurándose su quantum, es decir, cuando se le vuelva a asignar la CPU al hilo, se iniciará su quantum a 0. Si un hilo transita del estado de Bloqueado a Listo tras una espera por un evento (operación de E/S, semáforo, espera por un mensaje, etc.), se incrementa la prioridad del hilo en función del evento por el que esperaba. Por ejemplo, si el bloqueo es por una operación de disco se incrementa la prioridad del hilo en: prioridad hilo = prioridad base + 2. Si el bloqueo es por una operación de v́ıdeo, se incrementa la prioridad en: prioridad hilo = prioridad base + 4. Si varios procesos esperan por el mismo recurso, son atendidos en orden FIFO. El sistema operativo está instalado en una arquitectura con dos procesadores (CPU1 y CPU2, respectivamente). Suponiendo que en un instante determinado se lanzan 4 hilos: H01 asociado a la tarea T0, H11 y H12 asociados a la tarea T1, y H21 asociado a la tarea T2, y que las ráfagas asociadas a la ejecución de los cuatros hilos son las que se muestra a continuación. 12 Sistemas Operativos Junio 2011 /** * ráfagas de Hilo H01 */ rafaga_CPU (6); rafaga_Disco (2); rafaga_CPU (2); /** * ráfagas de Hilo H11 e Hilo H12 */ rafaga_CPU (4); rafaga_Disco (2); rafaga_CPU (5); rafaga_Video (1); rafaga_CPU (2); /** * ráfagas de Hilo H21 */ rafaga_CPU (4); rafaga_Disco (6); rafaga_CPU (1); Donde rafaga CPU(n) especifica una ráfaga de CPU de n unidades de tiempo, y rafaga Ei(n) especifica una ráfaga de E/S de n unidades de tiempo asociada al recurso o evento Ei. Por ejem- plo, una ráfaga de espera por una lectura de disco de 3 u.t. se expresa como rafaga Disco(3)2. Asimismo, suponga que el instante de creación de los cuatro hilos y su prioridad base son los que se muestran en la siguiente tabla. Hilo tllegada Pbase H01 0 18 H11 1 5 H12 2 5 H21 4 13 Se pide: 1. Represente en la plantilla adjunta el estado de los hilos, su prioridad, y su posible espera por un recurso (disco o v́ıdeo) en cada unidad de tiempo. Utilice para ello estrictamente la leyenda anexa. Suponga en la resolución de este apartado que, cuando el planificador selecciona un hilo para ejecutarse a continuación, y las dos CPUs del sistema están libres, se le asigna CPU1. (1.25 puntos) 2Como puede observarse, los hilos H11 y H12, pertenecientes a la misma tarea, ejecutan el mismo código y, por lo tanto, tienen idénticas ráfagas. 13 Sistemas Operativos Junio 2011 RESPUESTA: t i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 H 01 X 1 X 1 X 1 X 1 X 1 X 1 B B X 2 X 2 FI N 18 18 H 11 X 2 X 2 X 2 L L L L L X 1 B B B B B B X 1 X 1 X 1 X 1 X 1 B X 2 X 2 5 7 6 9 H 12 L L L L X 1 X 1 X 1 L X 1 B B B B B B B X 2 X 2 X 2 X 1 X 1 B 5 7 6 H 21 X 2 X 2 X 2 X 2 B B B B B B X 1 FI N 13 15 D is co H 01 H 01 H 21 H 21 H 11 H 21 H 12 H 11 H 21 H 12 H 11 H 21 H 12 H 11 H 21 H 12 H 11 H 12 H 11 H 12 H 12 Ví de o H 11 H 12 14 Sistemas Operativos Junio 2011 2. Indique el porcentaje de uso de los dos procesadores existentes en este sistema (CPU1 y CPU2). (0.25 puntos) RESPUESTA: UsoCPU1 = 19/24 � 0, 79 ⇒� 79% UsoCPU2 = 14/24 � 0, 58 ⇒� 58% 3. Se desea dar prioridad a las tareas en primer plano sobre el resto. ¿Cómo mejoraŕıa el algoritmo para lograr este objetivo? Razone la respuesta. (0.5 puntos) (2 puntos) RESPUESTA: Una manera sencilla de mejorar el algoritmo planteado, de tal modo que logre beneficiar a ciertas tareas como, por ejemplo, las tareas en primer plano, consistiŕıa en, tras la espera por un evento, aplicarles a los hilos asociados a este tipo de tareas un incremento de prioridad (boosting) pero a su prioridad actual, en vez de a la prioridad base tal y como se describe en el enunciado. 15 AC_enero2012.pdf AC 2012 t = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 P1 5/5 3/3 1/1 Priorid 15 14 13 17 16 17 P2 1/1 7/7 1/1 Priorid 15 17 16 15 14 16 P3 3/6 6/6 5/5 1/1 Priorid 15 14 13 15 14 13 15 P4 1/1 3/3 3/3 Priorid 15 16 15 16 15 CPU P2 P4 P3 P2 P1 P3 P1 P4 P2 P1 P3 Uso R1 Uso R2 Uso R3 Uso R4 Leyenda: Ejecución Listo Bloqueado ¿Cuá́l es el primer instante de tiempo en que se desencadena por primera vez los siguientes escenarios? • Finalizació́n del quantum t= 2 • Requisa después de una E/S t= 9 • Boosting t= 9 • Decremento de prioridad t= 2 • Espera después de una E/S t= 27 2/3 P2 5/6 P4 R1 R1 6/7 R2 R4 P2 P3 R2 R2 2/3 P4 P3 P1 P2 P1 P3 P4 P3 2/3 R1 2/5 4/5 P1P1 P3 P1 P4 2/6 2/5 4/5 R2 2/7 4/7 ejPlaniWindows.pdf Ejercicio de la Parte IV Se pretende diseñar el planificador de procesador para el nuevo sistema operativo VenanciOS. Para ello se ha pensado en un algoritmo expulsivo con prioridades dinámicas comprendidas entre 0 y 31, en las que los números bajos indican prioridad baja y lo altos prioridad alta. Los procesos parten siempre de una prioridad fija de valor 15. Si se agota su quantum, su prioridad disminuye en una unidad (como mı́nimo hasta valor 0) y si el proceso no lo agota y es expulsado, se mantiene con la misma prioridad. Si el proceso se duerme a la espera de un recurso, al despertarse incrementa su prioridad (boosting), dependiendo del tipo de recurso, acorde con la tabla siguiente: Recurso Incremento de prioridad R1 1 R2 2 R3 3 R4 4 Caracteŕısticas de la planificación: El incremento de prioridad se aplica a la prioridad con la que el proceso se durmió. Si dos o más procesos se bloquean en el mismo recurso, éstos serán atendidos en orden FIFO. El sistema utiliza un valor de quantum de 2 unidades de tiempo. Un proceso que pasa al estado de listo después de una espera, se encola al final de la cola correspondiente a su prioridad. Un proceso que agota su quantum, se encola al final de la cola correspondiente a su prio- ridad. Un proceso que es requisado por otro de mayor prioridad es encolado a la cabeza de la cola correspondiente a su prioridad. Cuando un proceso requisado se reanuda, se le vuelve a asignar un quantum completo. La tabla siguiente muestra las ráfagas de CPU de los distintos trabajos aśı como el tiempo de acceso a cada dispositivo: Proceso CPU E/S CPU E/S CPU P1 5 4R4 3 5R1 1 P2 1 6R2 7 14R2 1 P3 6 7R2 5 10R2 1 P4 1 12R1 3 6R1 3 Se debe suponer que todos los trabajos llegan en el instante de tiempo 0, en el orden P1, P2, P3 y P4. Se pide: Dibuje en la gráfica adjunta qué trabajo se ejecuta en cada momento, aśı como su prioridad. Muestre en la misma gráfica por qué recurso espera cada proceso en cada instante. ¿Cuál es el primer instante de tiempo en que se desencadena por primera vez los siguientes escenarios? • Finalización del quantum. Examen de Arquitectura de Computadores Enero 2012 • Requisa después de una E/S. • Boosting. • Decremento de prioridad. • Espera después de una E/S. 2 examen_extraordinario.pdf Examen de Sistemas Operativos Grados de Ingenieŕıa en Informática e Ingenieŕıa de Computadores 16 de mayo de 2013 Normas No se puede utilizar ningún tipo de documentación durante todo el examen. Este ejercicio consta de dos partes independientes: • Test de evaluación continua de la unidad 4: ◦ 10 preguntas tipo test (+1/respuesta acertada). ◦ Esta parte supondrá un 7,5 % de la calificación de la asignatura. ◦ Cada pregunta no contestada no puntúa. ◦ Se dispondrá de 20 minutos para esta parte. ◦ Se contestará a esta parte en la plantilla de respuestas incluida en este cuadernillo. Sólo se entregará dicha plantilla de respuestas, que debe ser por lo tanto arrancada del cuadernillo. No olvidar consignar en la plantilla el nombre, apellidos y DNI del alumno. • Ejercicio práctico de la evaluación continua: ◦ Esta parte supondrá un 40 % de la calificación de la asignatura. ◦ Cada uno de los dos ejercicios tiene el mismo valor. ◦ Se dispondrá de 2 horas para realizar este examen. ◦ Se contestará a cada problema por separado con el nombre, apellidos y DNI en todas las hojas, que estarán numeradas. No está permitido escribir en color rojo. Se valorará/penalizará la claridad y limpieza de la solución entregada. La publicación de las calificaciones se realizará a través de la Intranet de la UAH (http://atc1.aut.uah.es) el d́ıa x de mayo, y en los tablones del Departamento durante los dos d́ıas siguientes. La revisión tendrá lugar el d́ıa X de junio a las YY:ZZ en el laboratorio Este L5. Examen de Sistemas Operativos Junio 2013 2 P la nt ill a d e re sp u es ta p ar a el p ro b le m a 2 E je rc ic io d e S is te m as O p er at iv os . G ra d o en In ge n ie ŕı a In fo rm át ic a / In ge n ie ŕı a en C om p u ta d or es A p el li d os , N om b re : .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. .D N I: .. .. .. .. .. .. .. .. .. .. .. .. .. .. .. t i 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 P ro c. A P ro c. B P ro c. C P ro c. D P ro c. E C O L A d e L is to s C P U E /S L e y e n d a : E E je cu ci ón . L L is to . B B lo q u ea d o. Examen de Sistemas Operativos Junio 2013 Examen de Sistemas Operativos Parte Práctica Problema 1 Dada la siguiente tabla donde se indica el tiempo de llegada y duración de ráfagas de CPU y E/S (entrada/salida) de los procesos existentes en el sistema (A, B, C, D y E). Proceso tllegada CPU E/S CPU A 0 4 2 1 B 2 5 - - C 4 4 3 3 D 6 2 4 2 E 8 2 1 2 Sabiendo además que la E/S se realiza sobre el mismo dispositivo y que se atienden estas peticiones siguiendo un orden FIFO. 1. Realice los siguientes tipos de planificación de uso de CPU utilizando la plantilla adjunta. a) FIFO b) SJF c) SJF con requisa d) Round-Robin con quantum = 3 u.t. 1 Examen de Sistemas Operativos Junio 2013 Answer Key for Exam A 1 Examen de Sistemas Operativos Parte Práctica Problema 1 Dada la siguiente tabla donde se indica el tiempo de llegada y duración de ráfagas de CPU y E/S (entrada/salida) de los procesos existentes en el sistema (A, B, C, D y E). Proceso tllegada CPU E/S CPU A 0 4 2 1 B 2 5 - - C 4 4 3 3 D 6 2 4 2 E 8 2 1 2 Sabiendo además que la E/S se realiza sobre el mismo dispositivo y que se atienden estas peticiones siguiendo un orden FIFO. 1. Realice los siguientes tipos de planificación de uso de CPU utilizando la plantilla adjunta. a) FIFO b) SJF c) SJF con requisa d) Round-Robin con quantum = 3 u.t. RESPUESTA: Examen de Sistemas Operativos Junio 2013 S J F co n re qu is a t i 0 1 2 3 4 5 6 7 8 9 10 11 12 1 3 14 15 16 17 1 8 1 9 20 21 22 23 2 4 2 5 26 27 28 29 3 0 P ro c. E E E E B B E A P ro c. L L L L L L L L L L L L L L L L L L E E E E E B P ro c. E E L E E B B B L E E E C P ro c. L L L E E B B B B B E E D P ro c. L L L E E B B B B L E E E C O L A B B B B B B B B B B B B B B B B B B d e C D D E E C E L is to s D E C P U A A A A C C A C C D D E E C C C D D E E B B B B B E /S A A C C C D D D D E D E E E L e y e n d a : E E je cu ci ón . L L is to . B B lo q u ea d o. 3 R o u n d -R o bi n q= 3 u . t. t i 0 1 2 3 4 5 6 7 8 9 10 11 12 1 3 14 15 16 17 1 8 1 9 20 21 22 23 2 4 2 5 26 27 28 29 3 0 P ro c. E E E L L L E B B L L L L L L L E A P ro c. L E E E L L L L L L E E B P ro c. L L L E E E L L L L L L L E B B B L E E E C P ro c. L L L L E E B B B B L L E E D P ro c. L L L L L L E E B L L L E E E C O L A B A A A C D D D B B E E A A C D E E C d e C C D B B B E E A A C C D E L is to s B E E A A C C A C C C P U A A A B B B A C C C D D B B E E A C D D E E C C C E /S A A D D D D E C C C L e y e n d a : E E je cu ci ón . L L is to . B B lo q u ea d o. EjerciciosPlanificacion.pdf Ejercicio 1 El algoritmo de planificación de un sistema operativo es SJF. Considérese la siguiente tabla en la que se representan cinco trabajos diferentes así como sus duraciones respectivas: Trabajo T0 T1 T2 T3 T4 t0 0 2 5 16 18 CPU 12 6 9 5 2 A partir de esta tabla, representar el estado de cada trabajo (listo, ejecución) en cada instante de tiempo, calculando los tiempos medios de estancia de los trabajos. Considere el caso a) algoritmo SJF sin requisa y el caso b) algoritmo SJF sin requisa. El tiempo invertido en la planificación y el cambio de contexto es despreciable. T0 T1 T2 T3 T4 CPU Ejercicio 2 Considérese la siguiente tabla en la que se representan 3 procesos planificados según FIFO (FCFS) : Trabajo P0 P1 P2 t0 0 2 5 ráfaga CPU 7 1 1 Operación E/S 4 2 2 Cada trabajo requiere 3 ráfagas de CPU y 2 de E/S para su finalización. Todas las operaciones de E/S se realizan sobre el mismo dispositivo. Las solicitudes de entradas salida también se planifican según el algoritmo FIFO. a) Representar el estado de cada trabajo (Listo, Ejecución y Bloqueado) en cada instante de tiempo, calculando los tiempos medios de estancia de los trabajos y el porcentaje de utilización de la CPU y del dispositivo de E/S. Considérese que el tiempo invertido en la planificación y el cambio de contexto es despreciable. P0 P1 P2 CPU E/S b) Realizar el mismo estudio pero considerando que cada proceso trabaja sobre un dispositivo de E/S distinto. P0 P1 P2 CPU c) Realizar de nuevo el mismo estudio pero considerando que el computador cuenta con 2 CPUs y que cada proceso trabaja sobre un dispositivo de E/S distinto. P0 P1 P2 CPU0 CPU1 E/S0 E/S1 E/S2 E/S0 E/S1 E/S2 Solución ejercicio 1 (sin requisa) t = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 T0 T1 T2 T3 T4 Uso CPU Leyenda: Ejecución Listo t estacia medio = (12 + 16 + 29 + 9 + 2) / 5 = 13,6 u.t. % uso CPU = 34 / 34 = 100% Solución ejercicio 1 (con requisa) t = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 T0 2/12 T1 T2 T3 1/5 T4 Uso CPU T3 Leyenda: Ejecución Listo t estacia medio = (34 + 6 + 12 + 8 + 2) / 5 = 12,4 u.t. % uso CPU = 34 / 34 = 100% T0 T1 T4 T3 T2 T0T4T0 T1 T2 T3 Solución ejercicio 2.a) t = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 P0 P1 (1) (1) (1) (1) (1) (1) P2 (1) (1) (1) (1) (1) (1) (1) (1) Uso CPU P1 P2 P1 P2 P0 P1 Uso E/S Leyenda: Ejecución Listo Bloqueado (1) El proceso permanece bloqueado hasta que el dispositivo de E/S está disponible. t estacia medio = (29 + (30 - 2) + (31 - 5)) / 3 = 27,67 u.t. % uso CPU = 27 / 31 = 87,1% % uso E/S = 16 / 31 = 51,61% Solución ejercicio 2.b) t = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 P0 P1 P2 Uso CPU P1 P2 P1 P2 P1 P2 Uso E/S0 Uso E/S1 Uso E/S2 Leyenda: Ejecución Listo Bloqueado t estacia medio = (29 + (20 - 2) + (22 - 5)) / 3 = 21,33 u.t. % uso CPU = 27 / 29 = 93,1% % uso E/S0 = 8 / 29 = 27,59% % uso E/S1 = 4 / 29 = 13,79% % uso E/S2 = 4 / 29 = 13,79% P2 P0 P0 P0 P0 P0 P0 P0 P1 P2 P0 P1 P0 P0 P2 P1 P2 P1 Solución ejercicio 2.c) t = 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 P0 P1 P2 Uso CPU0 Uso CPU1 P1 P2 P1 P2 P1 P2 Uso E/S0 Uso E/S1 Uso E/S2 Leyenda: Ejecución Listo Bloqueado t estacia medio = (29 + (10 - 2) + (12 - 5)) / 3 = 14,67 u.t. % uso CPU0 = 21 / 29 = 72,41% % uso CPU1 = 6 / 29 = 20,69% % uso E/S0 = 8 / 29 = 27,59% % uso E/S1 = 4 / 29 = 13,79% % uso E/S2 = 4 / 29 = 13,79% P0 P0 P0 P0 P0 P2 P1 P2 P1 EjerciciosPlanificacion SolEjerciciosPlanificacion1 SolEjerciciosPlanificacion2 archivos_apoyo_practica3(1).zip archivos_c_h_apoyo/.DS_Store __MACOSX/archivos_c_h_apoyo/._.DS_Store archivos_c_h_apoyo/ejecutar.c #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> #include "parser.h" #include "ejecutar.h" #include "libmemoria.h" pid_t ejecutar_orden(const char *orden, int *pbackgr) { char **args; pid_t pid; int indice_ent = -1, indice_sal = -1; /* por defecto, no hay < ni > */ if ((args = parser_orden(orden, &indice_ent, &indice_sal, pbackgr)) == NULL) { return(-1); } /* Si la linea de ordenes posee tuberias o redirecciones, podra incluir */ /* aqui, en otras fases de la practica, el codigo para su tratamiento. */ } void ejecutar_linea_ordenes(const char *orden) { pid_t pid; int backgr; /* Si la orden es compuesta, podra incluir aqui, en otra fase de la */ /* practica, el codigo para su tratamiento */ pid = ejecutar_orden(orden, &backgr); } __MACOSX/archivos_c_h_apoyo/._ejecutar.c archivos_c_h_apoyo/entrada_minishell.c #include <stdio.h> #include <string.h> #include <stdlib.h> #include "entrada_minishell.h" void imprimir_prompt() { printf("minishell> "); /* mostrar en pantalla la cadena que servirá de prompt: entrada de órdenes en la consola */ fflush(stdout); /* vacía el buffer intermedio de salida y se envía el texto a la pantalla */ } void eliminar_salto_linea(char *cad) { int i, longitud; longitud = strlen(cad); /* cad es una cadena de caracteres con la orden leída.*/ for(i = longitud-1; i >= 0; i--) /* se busca el carácter de final de línea introducido por fgets y se sustituye por '\0' para indicar el final de orden */ if (cad[i] == '\n') { cad[i] = 0; break; } } void leer_linea_ordenes(char *buf) { memset(buf, 0, sizeof(BUFSIZ)); if (fgets(buf, BUFSIZ-1, stdin) == NULL) /* fgets almacena la orden leída introduciendo también el carácter de fin de línea */ { buf[0] = '\0'; return; } eliminar_salto_linea(buf); } __MACOSX/archivos_c_h_apoyo/._entrada_minishell.c archivos_c_h_apoyo/entrada_minishell.h #ifndef ENTRADA_MINISHELL_H #define ENTRADA_MINISHELL_H void leer_linea_ordenes(char *cad); void imprimir_prompt(); #endif __MACOSX/archivos_c_h_apoyo/._entrada_minishell.h archivos_c_h_apoyo/internas.h #ifndef INTERNAS_H #define INTERNAS_H struct tipo_interna { char *nombre; void (*func)(const char *); }; int es_ord_interna(const char *); void ejecutar_ord_interna(const char *); #endif __MACOSX/archivos_c_h_apoyo/._internas.h archivos_c_h_apoyo/libmemoria.c #include <stdlib.h> #include "libmemoria.h" void free_argumentos(char **args) { int i = 0; while(args[i]) free(args[i++]); free(args); } void free_ordenes_pipes(char **ordenes, int **fds, int nordenes) { int i = 0; for (i = 0; i < nordenes; i++) { free(ordenes[i]); if (i < (nordenes - 1)) free(fds[i]); } free(ordenes); free(fds); } __MACOSX/archivos_c_h_apoyo/._libmemoria.c archivos_c_h_apoyo/libmemoria.h #ifndef LIBMEMORIA_H #define LIBMEMORIA_H void free_argumentos(char **args); void free_ordenes_pipes(char **ordenes, int **fds, int nordenes); #endif __MACOSX/archivos_c_h_apoyo/._libmemoria.h archivos_c_h_apoyo/minishell.c #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <fcntl.h> #include <signal.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include "internas.h" #include "entrada_minishell.h" #include "ejecutar.h" int main(int argc, char *argv[]) { char buf[BUFSIZ]; while (1) { } return 0; } __MACOSX/archivos_c_h_apoyo/._minishell.c archivos_c_h_apoyo/parser.h #ifndef PARSER_H #define PARSER_H enum TEstado { INICIO_ARG, ARG }; char ** parser_pipes (const char *orden, int *numordenes); char ** parser_orden (const char *orden, int *indentrada, int *indsalida, int *background); #endif __MACOSX/archivos_c_h_apoyo/._parser.h archivos_c_h_apoyo/redirecciones.c #include <stdio.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <fcntl.h> /* funcion que abre el archivo situado en la posicion indice_entrada+1 */ /* de la orden args y elimina de ella la redireccion completa */ void redirec_entrada(char **args, int indice_entrada, int *entrada) { } /* funcion que abre el archivo situado en la posicion indice_salida+1 */ /* de la orden args y elimina de ella la redireccion completa */ void redirec_salida(char **args, int indice_salida, int *salida) { } __MACOSX/archivos_c_h_apoyo/._redirecciones.c __MACOSX/._archivos_c_h_apoyo archivos_objeto/.DS_Store __MACOSX/archivos_objeto/._.DS_Store archivos_objeto/archivos_objeto_Linux_64bits/.DS_Store __MACOSX/archivos_objeto/archivos_objeto_Linux_64bits/._.DS_Store archivos_objeto/archivos_objeto_Linux_64bits/internas.o __MACOSX/archivos_objeto/archivos_objeto_Linux_64bits/._internas.o archivos_objeto/archivos_objeto_Linux_64bits/parser.o __MACOSX/archivos_objeto/archivos_objeto_Linux_64bits/._parser.o __MACOSX/archivos_objeto/._archivos_objeto_Linux_64bits archivos_objeto/archivos_objeto_OSX_64bits/.DS_Store __MACOSX/archivos_objeto/archivos_objeto_OSX_64bits/._.DS_Store archivos_objeto/archivos_objeto_OSX_64bits/internas.o __MACOSX/archivos_objeto/archivos_objeto_OSX_64bits/._internas.o archivos_objeto/archivos_objeto_OSX_64bits/parser.o __MACOSX/archivos_objeto/archivos_objeto_OSX_64bits/._parser.o __MACOSX/archivos_objeto/._archivos_objeto_OSX_64bits __MACOSX/._archivos_objeto