Diseño de sistemas operativos. Febrero de 2001. Ejercicio 1


Enunciado

Se plantean una serie de cuestiones sobre la implementación de procesos en UNIX.

a) ¿Qué valor inicial toma el contador de programa en un FORK, en un EXEC y en un pthread_create? ¿Y el puntero de pila? ¿Cuál es el contenido inicial de la nueva pila para estas tres llamadas? ¿Qué valores iniciales tomarán los registros generales?

Se distinguen las siguientes operaciones internas del sistema operativo:

Se pide especificar, basándose en las anteriores operaciones, cómo se llevan a cabo las siguientes llamadas:

b) FORK

c) EXEC

d) EXIT y WAIT (tenga en cuenta la sincronización asociada a la terminación de procesos en UNIX).

e) En Linux existe una llamada denominada CLONE que generaliza el FORK convencional, permitiendo especificar que el padre y el hijo compartan realmente parte de su información, en lugar de tratarse de una copia. Puesto que se trata de un servicio destinado principalmente al soporte de threads, analice qué tipo de información debería ser compartida entre ambos y cuál no. ¿Cómo debería rediseñarse el BCP para implementar esta funcionalidad extendida de Linux?

Solución

a) Se analiza, en primer lugar, qué valor toma el contador de programa en las llamadas especificadas: A continuación, se analiza qué valor tomará el puntero de pila y cuál será el contenido inicial de la nueva pila para cada una de las tres llamadas: Por último, se analiza qué ocurre con los valores almacenados en los registros de propósito general del procesador. b) Las operaciones básicas asociadas al FORK serían las siguientes: c) Las operaciones básicas asociadas al EXEC serían las siguientes: d) Las operaciones básicas asociadas al EXIT serían las siguientes: Las operaciones básicas asociadas al WAIT serían las siguientes: e) Puesto que se trata de un servicio cuyo principal uso es el soporte de threads, se debería compartir la información del proceso que normalmente comparten los threads: Es importante resaltar que, para conseguir una semántica adecuada para la implementación de threads, es necesario que realmente se comparta esta información, no siendo suficiente el uso de un duplicado de la misma. Así, si, por ejemplo, un proceso abre un fichero, un proceso hijo creado mediante CLONE deberá poder acceder al fichero abierto por el padre usando el mismo descriptor. Nótese que este comportamiento no se da con un proceso hijo creado con FORK. Por lo que se refiere el mapa de memoria, éste debería ser realmente compartido entre el padre y el hijo en el caso del CLONE (no se usaría, por tanto, copy-on-write), de manera que, si un proceso proyecta un fichero, éste estuviera también accesible al proceso hijo en el mapa de memoria compartido.

Por lo que se refiere a qué información no debe compartirse, sería aquélla que es específica de cada proceso, o sea, la que en un sistema con threads fuera propia de cada thread. Se podría resaltar la siguiente:

Por último, con respecto al BCP, sería necesario rediseñarlo de manera que sea válido tanto para procesos creados con FORK como mediante CLONE. Para ello, se puede hacer que los campos que corresponden con información a compartir (CLONE) o a duplicar (FORK) no almacenen el valor en sí, sino una referencia (puntero) al mismo. Así, si se trata de una llamada CLONE, sólo habrá que hacer que el puntero correspondiente del BCP referencie al mismo objeto que el del padre (o sea, copiar el puntero). Si se trata de un FORK, habrá que reservar una nueva zona de memoria para almacenar el campo, hacer un duplicado del campo correspondiente del padre y hacer que el puntero en el BCP del hijo apunte a la nueva zona reservada.

A continuación, usaremos como ejemplo la máscara de creación de ficheros (umask). Supongamos que en el BCP original está definida de la siguiente manera:

struct BCP {
	......
	int umask;
	......
}
En el BCP rediseñado, la definición de este campo usaría un puntero:
struct BCP_nuevo {
	......
	int *umask;
	......
}
En la llamada CLONE el tratamiento de este campo sería el siguiente:
clone() {
	......
	hijo->umask=padre->umask;
	......
}
Mientras que en la llamada FORK, se realizaría de la siguiente forma:
fork() {
	......
	hijo->umask=malloc(sizeof(int));
	*hijo->umask=*padre->umask;
	......
}