Llamadas al sistema para gestión de procesos |
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.
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:
- PID
- archivos: El hijo obtiene una copia de la tabla de descriptores de archivo del proceso padre, con lo que comparten el puntero del archivo. Si uno de los procesos cambia ese puntero (realiza una operación de E/S) la siguiente operación de E/S realizada por el otro proceso se hará a partir de la posición indicada por el puntero modificado por el primer proceso. Sin embargo, al existir dos tablas de descriptores de archivos si un proceso cierra su descriptor, el otro no se ve afectado.
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 */Apis.ual.es > cc ej_fork.c -o ej.fork
printf ("Fin del proceso cuya variable pidHijo vale %i\n", pidHijo);
}
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>
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. |
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>El resultado de la llamada al sistema exec sólo esta disponible si la llamada fracasa (-1).
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[])
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 | sí |
execvp | array | automático | sí |
El nuevo programa puede acceder a los argumentos a través de
argc
y argv de su función main.
donde:signal (sig, func)
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 => Número entero que representa una señal definida en /usr/include/signal.hfunc => Especifica la dirección de un manejador de señal, dada por el usuario en el proceso.
SIG_DFL => Manejador por defectoSIG_IGN => Manejador que ignora la señal recibida.
donde:ret = kill (pid, sig)
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.pid => Identificador del proceso al cual va dirigida la señal.sig => Señal enviada.
ret => 0 (Éxito) 1 (Error)