Llamadas al sistema para gestión de procesos

 

Contenidos

Objetivos

Con la realización de las prácticas de este módulo, el alumno se familiarizará con llamadas al sistema relacionadas con la creación y destrucción de procesos así como el lanzamiento de nuevos programas (fork, exec, wait, etc.), además deberá asentar los conocimientos adquiridos en anteriores prácticas.

Introducción

El sistema operativo UNIX dispone de un conjunto de llamadas al sistema que definen una poderosa interfaz para la programación de aplicaciones (API) que involucren multiples procesos; abriendo las puertas a la programación concurrente. Este interfaz suministra al desarrollador de software herramientas tanto para la creación, sincronización y comunicación de nuevos procesos, como la capacidad de ejecutar nuevos programas.

Entre los aspectos más destacados de la gestión de procesos en UNIX se encuentra la forma en que éstos se crean y cómo se ejecutan nuevos programas. Aunque se mostrarán las llamadas al sistema correspondientes más adelante en esta práctica, es conveniente presentar una visión inicial conjunta que permita entender mejor la forma en que estas llamadas se utilizan.

El kernel crea un nuevo proceso, proceso hijo, realizando una copia (clonación) del proceso que realiza la llamada al sistema fork (proceso padre). Así, salvo el PID y el PPID los dos procesos serán inicialmente idénticos. De esta forma los nuevos procesos obtienen una copia de los recursos del padre (heredan el entorno).

Sin embargo no se ejecuta ningún nuevo programa, para conseguir esto, uno de los procesos ha de realizar otra llamada al sistema, exec, para reinicializar (recubrir) sus segmentos de datos de usuario e instrucciones a partir de un programa en disco. En este caso no aparece ningún proceso nuevo.

Cuando un proceso termina (muere), el sistema operativo lo elimina recuperando sus recursos para que puedan ser usados por otros.

Creación de procesos. System call fork

#include <unistd.h>

int fork ()

fork crea un nuevo proceso; pero no lo inicia desde un nuevo programa. Los segmentos de datos de usuario y del sistema y el segmento de instrucciones del nuevo proceso (hijo) son copias casi exactas del proceso que realizó la llamada (padre). El valor de retorno de fork es:
 

Proceso hijo 0
Proceso padre PID del proceso hijo
fork fracasa y no puede crear un nuevo proceso. -1 

El proceso hijo hereda la mayoría de los atributos del proceso padre, ya que se copian de su segmento de datos del sistema. Sólo algunos atributos difieren entre ambos:

Apis.ual.es> cat ej_fork.c
#include <unistd.h>
#include <stdio.h>
main (arc, argv)
int argc;
char *argv[];
{
int pidHijo;
printf ("Ejemplo de fork. Este proceso va a crear otro proceso\n");
if (pidHijo=fork()) /* Código ejecutado por el padre */
printf ("Proceso PADRE: He creado un nuevo proceso cuyo PID es %i\n", pidHijo);
else /* Código ejecutado por el hijo */
printf ("Proceso HIJO: El contenido de mi variable PID es %i\n", pidHijo);
/* Esta línea es ejecutada por los dos procesos */
printf ("Fin del proceso cuya variable pidHijo vale %i\n", pidHijo);
}
Apis.ual.es > cc ej_fork.c -o ej.fork
Apis.ual.es > ej_fork
Ejemplo de fork. Este proceso va a crear otro proceso
Proceso HIJO: El contenido de mi variable PID es 0
Proceso PADRE: He creado un nuevo proceso cuyo PID es 23654
Fin del proceso cuya variable pidHijo vale 23654
Fin del proceso cuya variable pidHijo vale 0
Apis.ual.es>

Terminación de procesos. System call exit y wait

#include <unistd.h>

void exit (int status)

exit finaliza al proceso que la llamó, con un código de estado igual al byte menos significativo del parámetro entero status. Todos los descriptores de archivo abiertos son cerrados y sus buffers sincronizados. Si hay procesos hijo cuando el padre ejecuta un exit, el PPID de los hijos se cambia a 1 (proceso init). Es la única llamada al sistema que nunca retorna.

El valor del parámetro status se utiliza para comunicar al proceso padre la forma en que el proceso hijo termina. Por convenio, este valor suele ser 0 si el proceso termina correctamente y cualquier otro valor en caso de terminación anormal. El proceso padre puede obtener este valor a traves de la llamada al sistema wait.

#include <unistd.h>

int wait (int *statusp)

Si hay varios procesos hijos, wait espera hasta que uno de ellos termina. No es posible especificar por qué hijo se espera. wait retorna el PID del hijo que termina (o -1 si no se crearon hijos o si ya no hay hijos por los que esperar) y almacena el código del estado de finalización del proceso hijo (parámetro status en su llamada al sistema exit) en la dirección apuntada por el parámetro statusp.

Un proceso puede terminar en un momento en el que su padre no le esté esperando. Como el kernel debe asegurar que el padre pueda esperar por cada proceso, los procesos hijos por los que el padre no espera se convierten en procesos zombie (se descartan su segmentos pero siguen ocupando una entrada en la tabla de procesos del kernel). Cuando el padre realiza una llamada wait, el proceso hijo es eliminado de la tabla de procesos.

No es obligatorio que todo proceso padre espere a sus hijos.

Un proceso puede terminar por:
 

Causa de terminación Contenido de *statusp
Llamada al sistema exit byte más a la derecha = 0
byte de la izquierda contiene el valor del parámetro status de exit
Recibe una señal En los 7 bits más a la derecha se almacena el número de señal que termino con el proceso. Si el 8º bit más a la derecha está a 1 el proceso fue detenido por el kernel y se generó un volcado del proceso en un archivo core.
Caída del sistema
(p.e: pérdida de la alimentación del equipo.)
Todos los procesos desaparecen bruscamente. No hay nada que devolver.

Ejecución de nuevos programas. System call exec

La llamada al sistema exec permite remplazar los segmentos de instrucciones y de datos de usuario por otros nuevos a partir de un archivo ejecutable en disco, con lo que se consigue que un proceso deje de ejecutar instrucciones de un programa y comience a ejecutar instrucciones de un nuevo programa. exec no crea ningún proceso nuevo.

Como el proceso continua activo su segmento de datos del sistema apenas es perturbado, la mayoría de sus atributos permanecen inalterados. En particular, los descriptores de archivos abiertos permanecen abiertos después de un exec. Esto es importante puesto que algunas funciones de la librería C (como printf) utilizan buffers internos para aumentar el rendimiento de la E/S; si un proceso realiza un exec y no se han volcado (sincronizado) antes los buffers internos, los datos de estos buffers se perderán. Por ello es habitual cerrar los descriptores abiertos antes de realizar una llamada al sistema exec.

Hay 6 formas de realizar una llamada al sistema exec:

#include <unistd.h>
int execl (char *path, char *arg0, char *arg1, . . . ,char *argN, char *null)
int execle (char *path, char *arg0, . . . ,char *argN, char *null, char *envp[])
int execlp (char *file, char *arg0, char *arg1, . . . ,char *argN, char *null)
int execv (char *path, char *argv[])
int execve (char *path, char *argv[], char *envp[])
int execvp (char *file, char *argv[])
El resultado de la llamada al sistema exec sólo esta disponible si la llamada fracasa (-1).

Descripción de los argumentos:
 

Nombre del argumento Descripción
path, file nombre del nuevo programa a ejecutar con su trayectoria. Ejemplo: "/bin/cp"

Las versiones de exec que utilizan file en lugar de path utilizan la variable de entorno PATH para localizar el programa a ejecutar, por lo que en esos casos no es necesario especificar la trayectoria al programa si este se encuentra en alguno de los directorios especificados en PATH

arg0 primer argumento del programa. Por convención suele asignarse el nombre del programa sin la trayectoria. Ejemplo: "cp"
arg1 ... argN

null

Conjunto de parámetros que recibe el programa para su ejecución. Ejemplo:
toupper.c  seguridad.
El parámetro formal null debe ser remplazado por el parámetro real NULL.
argv Matriz de punteros a cadenas de caracteres. Estas cadenas de caracteres constituyen la lista de argumentos disponibles para el nuevo programa. El último de los punteros debe ser NULL. Por convención, este array debe contener al menos un elemento (nombre del programa).
envp Matriz de punteros a cadenas de caracteres. Estas cadenas de caracteres constituyen el entorno de ejecución del nuevo programa.

A continuación se presenta una tabla comparativa de las diferentes versiones de exec:
 

Versión Formato argumentos Paso del entorno ¿Utiliza PATH?
execl lista automático no
execv array automático no
execle lista manual no
execve array manual no
execlp lista automático
execvp array automático

El nuevo programa puede acceder a los argumentos a través de argc y argv de su función main.
 

Envío de señales entre procesos

Se define una señal, como un mensaje enviado a un proceso determinado. Este mensaje no es más que un número entero. Un proceso cuando recibe una señal puede optar por tres posibles alternativas para procesarla: Algunas de las señales más comunes, utilizadas en UNIX son: El resto de señales están definidas en el archivo de cabecera /usr/include/signal.h

Prioridades de las señales

Todas las señales tienen la misma prioridad. A diferencia de las interrupciones Hardware, las señales se procesan siguiendo la filosofía FIFO (First In First Out). Cuando una señal se envía a un proceso, se ejecuta el manejador correspondiente, y mientras este manejador no termine su ejecución, ninguna otra señal podrá ser recibida por el proceso anterior.

Asignación de un manejador de señal

Como se ha comentado anteriormente, puede ocurrir que un proceso no quiera ejecutar el manejador por defecto asociado a un tipo de señal. Para este caso, existe una función signal() en la biblioteca stándard ANSI C, la cual tiene la siguiente sintaxis, (para una información más detallada consultar el manual):
signal (sig, func)
donde:
sig => Número entero que representa una señal definida en /usr/include/signal.h

func => Especifica la dirección de un manejador de señal, dada por el usuario en el proceso.

Para las señales SIGKILL, SIGSTOP y SIGCONT no es posible asignar un manejador que no sea el de defecto. Existen dos manejadores predefinidos en /usr/include/signal.h:
SIG_DFL => Manejador por defecto

SIG_IGN => Manejador que ignora la señal recibida.

Envío de señales a otros procesos

Para poder enviar una señal a otro proceso, es necesario realizar la llamada al sistema kill(). Su formato o sintaxis es, (para una información más detallada consultar el manual):
ret = kill (pid, sig)
donde:
pid => Identificador del proceso al cual va dirigida la señal.

sig => Señal enviada.

ret => 0 (Éxito) 1 (Error)

La llamada al sistema pause(), provoca la suspensión de la ejecución del proceso, hasta que se recibe una señal. Siempre retorna -1.