Logo Passei Direto

UAH _Universidad de Alcalá de Henares_ _ Grado en Ingeniería Informática _ Sistemas Operativos _ SSOO_rar

User badge image
Diego Pereira

en

Herramientas de estudio

Material

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