Está en la página 1de 20

Escuela Politécnica Superior de Elche

Ingeniería de Telecomunicación

Llamadas al sistema Procesos


Curso: 4º IT Docencia: C Aula Practicas

Documento llasis 2007(procesos)

Mecanismos para ejecución de Procesos en


LINUX
1. Objetivo
Con la lectura de este documento y la realización de sus ejemplos, el alumno se familiarizará
con las llamadas al sistema relacionadas con la creación y destrucción de procesos.

2. Introducción
El manejo de concurrencia exige a su vez la necesidad de mecanismos que hagan posible la
solución de los problemas que surgen de la propia definición de concurrencia, problemas tales
como el acceso a una región crítica (exclusión mutua), el interbloqueo, la sincronización entre
procesos, etc. El S.O. UNIX/Linux al ser un Sistema Operativo Multiusuario y Multitarea, nos
permite la posibilidad de realizar procesos concurrentes, además, posee un conjunto de
recursos para comunicación y sincronización entre procesos, que pueden utilizarse mediante
llamadas a funciones del sistema formando parte de un programa en lenguaje C. Este conjunto
de recursos (semáforos, paso de mensajes, etc.) se encuentran en la librería IPC (InterProcess
Communication).

Un proceso es un programa que está siendo ejecutado por un procesador. Un programa queda
completamente definido por el código que resulta de la compilación y del enlazado del código
fuente del programa. Este código contiene una serie de instrucciones que debe ejecutar un
procesador para realizar una determinada tarea y es una estructura pasiva. Sin embargo el
proceso es una estructura activa que incluye el código del programa y sus datos, también el
estado de aquellos recursos que necesita el programa para ejecutarse.

Un programa será un archivo que reside en el disco de sistema y que se crea con otros
programas, como por ejemplo el compilador de C o el editor vi, mientras que un proceso es una
copia del programa en la memoria que está haciendo algo. Un proceso comprende todo el
entorno de ejecución del programa, es decir, desde las variables, los ficheros abiertos, el
directorio en el que reside, información acerca de usuario que ejecuta el proceso y el terminal
donde lo ejecuta así como el código del programa.

Entre los aspectos más destacados de la gestión de procesos en Unix/Linux se encuentra el


modo en que estos se crean. El modo de creación de nuevos procesos consite en realizar una
llamada al sistema desde un proceso que crea una copia de dicho proceso. Cuando un proceso
termina, realiza una llamada al sistema indicando cual es su valor de retorno. Toda la
información del proceso finalizado se mantiene cargada en memoria mientras su proceso padre
no solicite al Sistema Operativo el valor de retorno del hijo.

Algunas de las funciones relacionadas con el manejo de procesos son las siguientes:

1. fork crea un nuevo proceso.

1
2. wait espera a que el proceso hijo pare o termine.

3. exit termina el proceso en curso.

4. _exit termina el proceso en curso sin limpieza de I/O.

5. execl ejecuta un programa con una lista de argumentos.

6. execv ejecuta un programa con un vector de argumentos.

7. execle ejecuta un programa con una lista de argumentos y un vector de entorno.

8. execlp ejecuta un programa con una lista de argumentos y un PATH de búsqueda.

9. execvp ejecuta un programa con un vector de argumentos y un PATH de búsqueda.

10. getuid obtiene el user-ID.

11. getpid obtiene el process-ID.

12. getppid obtiene el parent-process-ID.

13. getpgr obtiene el process-group-ID.

14. getgid obtiene el group-ID.

Estas rutinas se usan para crear y manipular procesos y programas.

3. Revisión de llamadas al sistema para


procesos
A continuación se hace una revisión de las principales llamadas al sistema para la gestión de
procesos. Pero antes comentar brevemente el estándar que vamos a seguir para la
especificación de las sintaxis y semántica de las llamadas, el estándar POSIX.

POSIX
Las diferencias a nivel de sistema entre los diferentes sistemas Unix de diferentes vendedores,
provocó la aparición de diferentes estándares en la especificación de la interfaz de los sistemas
con los programas de aplicación. Nosotros vamos a seguir los siguientes estándares: ANSI C,
POSIX y Spec 1170.

La IEEE desarrolló una serie de estándares denominados POSIX ( Portable Operating System
Interface) que especifican la interfaz entre el sistema operativo y el usuario de modo que los
programas sean transportables a través de diferentes plataformas.

De entre los diferentes miembros del grupo de estándares nosotros nos centraremos en el
POSIX.1 que especifica la API (Lenguaje C).

Credenciales de usuario
Cada usuario del sistema se identifica por un número único denominado ID de usuario o UID, y
pertenece a un grupo de usuarios, que posee su ID de grupo o GID. Estos identificadores
afectan a la propiedad de los archivos, a los permisos de acceso, y a la capacidad de enviar
señales a otros procesos. Estos atributos se denominan globalmente credenciales.

El sistema reconoce a un usuario privilegiado denominado superusuario (normalmente este


usuario accede al sistema como root). El superusuario tiene UID = 0 y GID = 1. El superusuario
tiene muchos privilegios como el acceso a archivos de otros usuarios, a pesar de la protección
de estos, y puede ejecutar un cierto número de llamadas al sistema privilegiadas, como por

2
ejemplo, mknod para crear un nodo del sistema de archivos. Muchos sistemas UNIX actuales
tales como SVR4.1/ES soportan mecanismos de seguridad incrementada . Estos sistemas
sustituyen la abstracción de superusuario privilegiado único con privilegios para operaciones
diferentes.

Cada proceso tiene dos pares de IDs -real y efectivo. Cuando un usuario entra al sistema, el
pograma de entrada (login) establece ambos pares a los UID y GID especificados en la base
de datos de password (el archivo /etc/passwd, o algún mecanismo distribuido como el
Network Information Service (NIS) de Sun Microsystems). Cuando un proceso invoca a fork,
el hijo hereda las credenciales de su padre.

El UID efectivo y el GID efectivo afectan a la creación y acceso de archivos.

Durante la creación de un archivo, el kernel establece los atributos de propiedad del archivo a
los UID y GID efectivos del proceso creador. Durante el acceso al archivo, el kernel utiliza los
UID y GID efectivos del proceso para determinar si puede acceder al archivo. Los UID y GID
reales identifican al propietario real del proceso y afectan a los permisos para enviar señales.
Un proceso sin privilegios de superusuario puede enviar señales a otro proceso sólo si el UID
real o efectivo del emisor iguala al UID real del receptor.

Existen tres llamadas al sistema que pueden cambiar las credenciales. Si un proceso llama a
exec para ejecutar un programa instalado en modo suid, el kernel cambia el UID efectivo del
proceso al del propietario del archivo. De la misma forma, si el programa esta instalado en
modo sgid, el kernel cambia el GID efectivo del proceso llamador.

UNIX suministra este mecanismo para otorgar privilegios especiales a usuarios para realizar
tareas concretas. El ejemplo clásico es el programa passwd, que permite a un usuario cambiar
su propio password. Este programa debe escribir en la base de datos de password, en la cual
no esta permitida la escritura directa por parte de los usuarios (para evitar la modificación de
password de otros usuarios). Así, el programa passwd el propiedad del superusuario y tiene
activo el bit SUID. Esto permite al usuario obtener los privilegios de superusuario mientras
ejecuta el programa passwd.

Un usuario puede cambiar sus credenciales llamando a setuid o setgid. El superusuario


puede invocar estas llamadas al sistema para cambiar tanto los UID y GID efectivos como los
reales. Los usuarios ordinarios pueden utilizarlas para cambiar sus UID y GID efectivos y
devolverlos a los reales.

Existen algunas diferencias en el tratamiento de la credenciales entre System V y BSD. SVR3


también mantiene los UID salvado y GID salvado, que son los valores del UID y GID efectivos
antes de llamar a exec. Las llamadas setuid y setgid también restauran los IDs efectivos a
los valores salvados. Mientras que 4.3BSD no soporta esta característica, este permite a un
usuario pertenecer a un conjunto de grupos suplementarios (utilizando la llamada setgroups).
Mientras que los archivos creados por un usuario pertenecen a su grupo primario , el usuario
puede acceder a archivos que pertenecen a su grupo principal o a un grupo suplementario
(pensando que el archivo permite el acceso a los miembros del grupo). SVR4 incorpora todas
las características mencionadas antes.

Soporta grupos suplementarios, y mantiene los UID y GID salvados a través de exec. A
continuación se recogen las funciones con las cuales podemos consultar los identificadores de
proceso, del padre del proceso, el identificador de usuario real y efectivo, y el identificador de
grupo real y efectivo, del proceso llamador, respectivamente.

# include <sys/types.h>

# include <unistd.h>

pid_t getpid(void) Retorna: ID del proceso que la invoca

3
pid_t getppid(void) Retorna: ID del padre

pid_t getuid(void) Retorna: ID real de usuario

pid_t geteuid(void) Retorna: ID efectivo de usuario

pid_t getgid(void) Retorna: ID real del grupo

pid_t getegid(void) Retorna: ID efectivo del grupo

Observad que ninguna de estas funciones devuelve código de error.

fork
El único modo de que el núcleo de unix cree un nuevo proceso es mediante la ejecución de la
llamada fork() realizada por un proceso existente. El nuevo proceso que se crea se llama
“proceso hijo ”. El proceso al que llama fork lo llamaremos padre y al nuevo proceso (la
copia) lo llamará hijo. Esta función devuelve un valor numérico que será distinto para el padre y
para el hijo. El núcleo devuelve un valor cero para el proceso hijo, mientras que al proceso
padre le devuelve el identificador del proceso hijo. El motivo de esto es que un proceso padre
puede tener varios hijos mientras que los hijos tienen un solo padre (existe una llamada al
sistema que puede permitir al hijo obtener el identificador de su proceso padre: getppid()).

Ambos procesos siguen su ejecución en la instrucción siguiente a la llamada fork(). El


proceso hijo es una copia exacta del proceso padre, de hecho el proceso hijo tiene una copia
del espacio de datos, heap (memoria dinámica) y de la pil a. Sin embargo, el espacio de
direcciones de datos del proceso padre será distinto del espacio de direcciones del proceso
hijo, lo cual implica que ambos procesos no comparten los datos, sólo se hace un copia para el
hijo. Habitualmente el padre y el hijo compartirán el segmento de código ya que este es de sólo
lectura.

Normalmente, el segmento de código es compartido si es de sólo lectura. Algunas


implementaciones no realizan una copia completa del padre dado que normalmente la
operación fork es seguida de exec. En su lugar se emplea la técnica copia-sobre-escritura.

Una vez creado el proceso ambos siguen ejecutándose por la instrucción que sigue a fork. En
general, no conoceremos nunca si el hijo se ejecuta antes que el padre o la inversa. Esto
dependerá del algoritmo de planificación.

fork retorna diferentes valores para cada proceso. Para el padre fork retorna el processID, y
para el hijo fork retorna cero. Como ejemplo de uso véase el siguiente programa.
#include <stdio.h>
#include <unistd.h>

main () {

printf ("Ejemplo de fork.\n");


printf ("Inicio del proceso padre. PID=%d\n", getpid ());

if (fork() == 0) { /* Proceso hijo */


printf ("Inicio proceso hijo. PID=%d, PPID=%d\n",
getpid (), getppid ());
sleep (1);
}
else { /* Proceso padre */
printf ("Continuación del padre. PID=%d\n", getpid ());
sleep (1);
}
printf ("Fin del proceso %d\n", getpid ());
exit (0);
}

4
Cuando ejecutemos el programa lo que obtendremos será :

Ejemplo de fork.

Inicio del proceso padre. PID=1056

Inicio proceso hijo. PID=1057, PPID=1056

Continuación del padre. PID=1056

Fin del proceso 1057

Fin del proceso 1056

exit
Esta función pone fin a la ejecución de un proceso. Su sintaxis es:

#include <stdlib.h>

void exit (int estado);

#include <unistd.h>

void _exit(int estado)

Ambas funciones devuelven un valor del estado de finalización al proceso padre. El estado de
finalización es indefinido si:

1. la función exit se invoca sin valor de estado

2. main realiza un return sin valor de retorno

3. finaliza main sin un return explícito.

Esta función termina el proceso que la invoca, con un código de status igual al byte más a la
derecha del status. Cierra todos los descriptores del proceso.

Es conveniente devolver un valor en exit() ya que de lo contrario se devuelve basura en el


código del estado del proceso. Por convención se devuelve cero si no hay error y un código
distinto de cero si hay error.

5
En el siguiente programa el padre espera a que el hijo salga y luego imprime el valor de status
del hijo.

#include <stdio.h>

#include <stdlib.h>

main () {

unsigned int status;

if ( fork() == 0 ){ /* ==0 en el hijo */

scanf("%d",&status); /*pide un entero*/

exit (status);

else { /* !=0 en el padre */

wait (status);

printf ("hijo exit status = %d\n",status >> 8);

exit (0);

El hijo lee el exit status que va a ser introducido por el terminal y entonces devuelve este valor
al padre a través del exit. Entonces el padre imprime el exit status del hijo.

Respecto a la finalización de un proceso, debemos de estudiar dos casos:

1. ¿Qué ocurre cuando el padre finaliza antes de que el hijo lo haga? En este caso el
proceso init se hacer cargo del proceso, lo hereda. De esta forma garantizamos que
todos los proceso tienen un padre.

Cuando un proceso heredado por init finaliza, init esta diseñado para invocar a la
función wait para obtener el estado de finalización.

2. ¿Qué ocurre si el hijo finaliza antes que el padre, y el padre no puede leer el valor de
retorno del hijo? En la terminología UNIX se denomina zombi al proceso que a
terminado pero su padre no ha esperado su fin. De un proceso zombi solo existe la
estructura proc, para mantener la información de estado y recursos utilizados, los
demás recursos son liberados por el kernel. El truco para no tener que esperar por un
hijo y que este no se convierta en zombi, es llamar a fork dos veces (mecanismo que
es utilizado por todos los demonios).

6
wait y waitpid
Cuando un proceso hijo acaba se debe notificar a su padre el estado de terminación de éste.
De algún modo el proceso padre deberá estar esperando la respuesta del hijo. Esto se realiza
mediante la llamada al sistema wait(). El proceso que ejecuta la llamada al sistema wait()
quedará en espera hasta que se le notifique que cualquiera de sus hijos ya ha acabado y
seguirá ejecutando la línea de código siguiente a wait(). Esta llamada tiene un parámetro
donde recogerá el estado de terminación del primer hijo que haya acabado, además de que
devuelve el número de proceso del hijo que ha acabado. Si no nos interesa el estado de
terminación del proceso hijo se le puede pasar NULL. El estado que recoge la función wait()
se interpreta mediante una serie de macros ya definidas (ver hojas del manual de UNIX).

Existe una variante de esta llamada, es waitpid(). En este caso el proceso que ejecuta
waitpid() esperará a un hijo concreto.

Podemos controlar la ejecución del proceso hijo llamando a wait en el padre. wait fuerza al
padre a detener la ejecución hasta que el proceso hijo haya terminado. wait retorna el
procesáis del proceso hijo que termina y almacena su status en el entero al que apunta el
puntero que debe llevar como argumento ( statusp ) a menos que el argumento sea NULL en
cuyo caso no almacena el status. wait retorna -1 en caso de fallo.

El siguiente programa es exactamente igual al anterior sólo que garantiza que el proceso hijo
termina antes que el padre.

#include <stdio.h>

#include <stdlib.h>

main () {

if ( fork() == 0 )

printf ("Este es el hijo\n");

else {

wait (NULL);

printf ("Este es el padre\n");

exit (EXIT_SUCCESS);

La salida que obtendremos para el anterior programa será :

Este es el hijo

7
Este es el padre

La finalización de un proceso se notifica a su proceso padre a través de una señal - SIGCHLD.


Dado que es un evento asíncrono, el padre puede elegir entre ignorar la señal o puede
suministrar una función que se ejecute cuando se reciba la señal -un manejador de la señal.
Veremos esto con más detalle en el tema 5. De momento, debemos ser conscientes de que un
proceso que llama a wait o puede:

· bloquearse (si todos sus hijos se están ejecutando), o

· retornar inmediatamente con el estado de finalización de un hijo que ha finalizado), o

· retornar inmediatamente con código de error (si no tiene ningún hijo).

La sintaxis de las funciones es:

#include <sys/types.h>

#include <sys/wait.h>

pid_t wait(int *stacloc);

pid_t waitpid (pid_t pid, int *statloc, int opcion);

Retornan: PID si OK, 0 (ver comentarios), ó -1 si error.

La diferencia entre las dos funciones es:

· wait puede bloquear al llamador hasta que un hijo termine, mientras que waitpid tiene una
opción que evita el bloqueo.

· waitpid no espera la terminación del primer hijo, tiene una opción que permite controlar a
que proceso esperar. Para ambas funciones, el argumento statloc es un puntero a un
entero. Si este argumento no es un puntero nulo, el estado de terminación del hijo se almacena
en la posición apuntada por el argumento. Si no estamos interesados por el valor, podemos
pasar un puntero nulo. Tradicionalmente, el valor de estado devuelto por estas dos funciones
ha sido definido por la implementación, con determinados bits indicando el estado de
finalización. POSIX.1 especifica para la visualización del estado de finalización varias macros
mutuamente exclusivas definidas en <sys/wait.h>:

· WIFEXITED(status) - Devuelve cierto si el hijo ha terminado normalmente, en cuyo caso


puede ejecutarse WEXITSTATUS(status) para obtener los 8 bits de orden inferior del
argumento pasado por el hijo con exit o _exit.

· WIFSIGNALED(status) - Devuelve cierto si el hijo terminó anormalmente (recibió una


señal que no pudo manejar). Ejecutaremos WTERMSIG(status) para obtener el número de la
señal. Además, SVR4 y 4.3+BSD definen la macro WCOREDUMP(status) que devuelve cierto
si se generó un archivo 'core'.

· WIFSTOPPED(status) - Devuelve cierto si el valor de status fue devuelto por un hijo que
está actualmente detenido. Podemos ejecutar WSTOPSIG(status) para obtener el número
de la señal que paró al hijo.

Respecto a los valores que puede tomar opcion en waitpid:

· WNHANG - En este caso waitpid no será bloqueante si el hijo especificado por pid no esta
disponible inmediatamente. En este caso devuelve 0.

8
· WUNTRACED - Si la implementación soporta control de trabajos, se devuelve el estado del hijo
especificado por pid que ha sido detenido y cuyo estado no ha sido devuelto desde su
detención. La macro WIFSTOPPED(status) determina si el valor de retorno corresponde a
un hijo parado.

La interpretación del argumento pid para waitpid depende de su valor:

• pid == -1 espera por cualquier hijo (equivalente a wait).

• pid > 0 espera por el hijo cuyo PID es igual a pid.

• pid == 0 espera por cualquier hijo cuyo ID grupo es igual al del proceso llamador.

• pid < -1 espera por cualquier hijo cuyo ID grupo es igual al valor absoluto de pid.

Wait da error sólo si el proceso llamador no tiene hijos. waitpid dará error si el proceso o
grupo especificado no existe o no es hijo del llamador. Otro posible error de retorno es la
interrupción de la llamada por una señal. El siguiente programa muestra el uso de wait.

Un programa sencillo ilustrando el uso de wait.

#include <sys/types.h>

#include <sys/wait.h>

#include <unistd.h>

#include <stdio.h>

main () {

pid_t childpid;

int status;

if ((childpid = fork()) == -1) {

perror ("Fork ha fallado");

exit(1);

else if (childpid == 0)

fprintf(stderr, "Soy el hijo con pid = %ld\n",(long)getpid());

else if (wait(&status) != childpid)

fprintf (stderr, "Wait interrumpido por una señal\n");

else

fprintf (stderr, "Soy el padre con pid = %ld\n",(long)getpid());

9
exit(0);

exec
Uno de los usos de la orden fork() es la de crear otro proceso que ejecutará un programa
diferente al del padre mediante la orden exec(). Cuando un proceso hace una llamada
exec(), su código se reemplaza por el correspondiente al del nuevo programa que se quiere
ejecutar. Concretamente, exec() reemplaza el segmento de código, datos, heap y pila del
proceso que hace la llamada por el correspondiente al programa que se quiere ejecutar. El
identificador del proceso no cambia ya que no se ha creado ningún nuevo proceso (además
hay otras características del proceso invocante que se mantienen como: identificador real de
usuario y de grupo, el identificador de sesión, el identificador de grupo de procesos, directorio
actual de trabajo, directorio root, máscara de creación de ficheros, señales pendientes, ...

El identificador efectivo de usuario y grupo dependerá de si está activo o no el bit de usuario o


grupo para el fichero que se vaya a ejecutar). Cuando un proceso invoca a la función exec, el
programa que ejecuta es totalmente sustituido por uno nuevo (nuevos segmentos de texto,
datos, pila y heap), y el nuevo programa comienza ejecutando su función main. Existen seis
funciones exec que únicamente se diferencian en la forma de pasar los argumentos. Sus
sintaxis son:

#include <unistd.h>

int execl(const char *pathname, const char *arg0,

... /* (char *) 0 */ );

int execv(const char *pathname, char *const argv[] );

int execle(const char *pathname,const char *arg0,

.../*(char *) 0,char *const envp[]*/ );

int execve (const char *pathname, char *const argv[],

char *const envp[] );

int execlp (const char *filename, const char *arg0,

... /* (char *) 0 */ );

int execvp (const char *filename, char *const argv[] );

Retornan: no retornan si tienen éxito, -1 si error.

La primera diferencia entre estas funciones es que las cuatro primeras aceptan un pathname
de archivo como argumento y las dos últimas un nombre de archivo. Cuando especificamos un
nombre de archivo, si este contiene una barra se interpreta como un camino, en cualquier otro
caso se busca un ejecutable en los directorios especificados en la variable del entorno PATH. Si
el archivo especificado no es ejecutable, se asume que es un programa shell e intenta invocar
a /bin/sh como el nombre de archivo como argumento.

10
Otra diferencia afecta al paso de la lista de argumentos ( l de lista y v de vector). Las funciones
execl, execlp y execle, requieren que los argumentos del nuevo programa se especifiquen
por separados y que se marque el fin de los argumentos con un puntero nulo.

Para las otras funciones, construiremos una matriz de punteros a los argumentos, y se pasa
como argumento la dirección de la matriz.

La diferencia final esta en el paso de la lista de variables de entorno al nuevo programa. Las
funciones que acaban con e permiten pasar un puntero a una matriz de punteros a la cadena
de entorno. Las otras funciones usan la variable environ (variable global que contiene la
dirección de la matriz de punteros, extern char **environ ) en el proceso llamador para
copiar la lista de entorno. Las variables de entorno se definen de la forma "nombre=valor". La
Figura 1.2 ilustra un entorno que consta de cinco cadenas.

En la mayoría de las implementaciones sólo una de estas funciones (execve) es una llamada
al sistema. Las otras son funciones de biblioteca que la invocan.

El nuevo programa hereda del proceso invocador las siguientes propiedades: los
identificadores del proceso, de grupo y de sesión, el terminal de control, tiempo restante del
reloj de alarma, directorios actual y root, mascara de creación de archivos, bloqueo de
archivos, mascara de señales y señales pendientes, límites de recursos. El manejo de los
archivos abiertos depende del valor del indicador close-on-exec para cada descriptor (ver
función fcntl).

El siguiente programa muestra el uso de la función exec.

Programa ejemploexec.c que crea un proceso para ejecutar una orden pasada como
argumento.

#include <sys/types.h>

#include <sys/wait.h>

#include <unistd.h>

#include <stdio.h>

#include <errno.h>

main(int argc, char *argv[]) {

pid_t childpid;

int status;

11
if ((childpid = fork()) == -1) {

perror("Fork ha fallado");

exit(1);

} else if (childpid == 0) { /*codigo del hijo*/

if (execvp(argv[1], &argv[1]) < 0) {

perror("exec ha fallado");

exit(1);

} else /*codigo del padre*/

while (childpid != wait(&status))

if ((childpid == -1) && (errno != EINTR)) break;

exit(0);

Si ejecutamos $gcc ejemploexec.c luego podemos hacer $./a.out ls

Las rutinas exec son llamadas para ejecutar un programa. Esto lo hacen sustituyendo el
programa que se está ejecutando por el nuevo programa especificado en exec.

Las rutinas exec no retornan valor cuando terminan adecuadamente y -1 cuando lo hacen mal.

Hay seis rutinas que pueden ser llamadas:

execl() Toma el Path del programa ejecutable como primer argumento. El resto de los
argumentos son una lista de argumentos de la línea de comandos para el nuevo programa
(argv[]).

#include <unistd.h>

...

execl ("/bin/cat", "cat", "f1", "f2", NULL);

execl ("a.out", "a.out", NULL);

execle() Igual que execl , excepto que el final de la lista de argumentos esta seguido por un
puntero a una lista de caracteres terminada por un NULL que se pasa como el entorno del
nuevo programa .

#include <unistd.h>

...

12
static char *env[] = {"TERM=ansi","PATH=/bin:/usr/bin",NULL};

...

execle ("/bin/cat","cat","f1","f2",NULL,env);

execv() Toma el nombre del Path del programa ejecutable como su primer argumento. El
segundo argumento es un puntero a una lista de punteros a caracteres que se pasa como
argumentos en la linea de comandos para el nuevo programa.

#include <unistd.h>

...

static char *args[] = {"cat","f1","f2",NULL};

...

execv ("/bin/cat",args);

execve() Igual que execv excepto que el tercer argumento el un puntero a una lista de
caracteres que pasa el entorno para el nuevo programa.

#include <unistd.h>

...

static char *env[] = {"TERM=ansi","PATH=/bin:/usr/bin",NULL};

static char *args[] = {"cat","f1","f2",NULL};

...

execle ("/bin/cat",args,env);

execlp() Igual que execl con la diferencia de que el nombre del programa no tiene que ser el
nombre del Path y puede ser un programa shell en lugar de un módulo ejecutable.

#include <unistd.h>

...

execlp ("ls", "ls", "-1", "/usr", NULL);

execvp() Igual que execv, solo que el nombre del programa no tiene que ser el nombre de
Path, y puede ser un programa Shell en lugar de un módulo ejecutable.

#include <unistd.h>

...

static char *args[] = { "cat", "f1", "f2", NULL };

13
...

execv ("cat", args);

14
4. Ejecución de un ejemplo:
El presente ejemplo incluye la realización de varios programas en C que ilustran el
funcionamiento de las librerías de funciones para comunicar procesos.

Estos programas se pueden realizar utilizando el editor vi, joe o nano. Recordar la llamada al
compilador c de gnu (gcc nombre_programa.c –o nombre_ejecutable)

Este programa sirve para mostrar el funcionamiento de la llamada al sistema fork(). Esta es
la única función en UNIX que nos permite crear nuevos procesos.

PADRE . C
Con el siguiente programa no sólo creamos un nuevo proceso sino que vamos a ver como
ejecutarlo. Aunque fork() es una herramienta muy poderosa, su acción suele combinarse en
la mayoría de las ocasiones con otra llamada al sistema: execl()

Si fork() era la única manera de crear procesos en UNIX, execl() es la única manera de
ejecutarlos.

La llamada execl() deber llevar como argumentos mínimos el PATH (camino absoluto o
relativo) del proceso hijo y el nombre del proceso hijo. A partir de ahí se le pueden dar todos los
argumentos que se deseen, siempre que la lista termine en un NULL. De esta forma, con esta
llamada se sustituye en la copia del proceso primitivo su segmento de datos y de instrucciones
con lo que los procesos dejan de ser iguales y se puede empezar a hablar de creación de un
verdadero proceso hijo.

El programa padre creará y ejecutará un proceso hijo (hijo1.c). Antes de compilar y ejecutar el
proceso padre, hay que compilar el código del hijo y al ejecutable llamarle hijo1, los dos en el
mismo directorio.

/*padre.c. Programa que crea un proceso hijo y ejecuta el proceso ejemplo con execl()*/

#include <stdio.h>

main() {

int pid; /*En pid almacenaremos el nº deidentificación de proceso devuelto por


fork()*/

pid=fork(); /*fork() crea un clon del proceso primitivo*/

switch(pid) {

case -1: /*fork devuelve -1 en caso de error*/

printf("\nNo se puede crear proceso hijo");

exit(0); /* salimos al S.O.*/

case 0: /*el valor 0 es asignado al hijo, mientras que el */

/* proceso padre recibe el número de identificación del hijo*/

printf("\n Ahora se ejecuta el hijo que lanzará el proceso ejemplo");

execl("ejemplo","ejemplo",NULL); /* ejecutamos el hijo */

15
exit(0);

default:

printf("\n número de identificación del PADRE %d\n",pid);

sleep(1); /*E1 programa espera un segundo y termina */

EJEMPLO.C
Es ejecutado desde el programa padre. Lo único que hace es obtener una salida por pantalla

/*Ejemplo.c. Programa que es ejecutado desde PADRE. para mostrar la cooperación entre fork()
execl()*/

#include <stdio.h>

main() {

printf ("\n\n ------Proceso que ejecuta el hijo-----\n\n");

16
5. Señales
Las señales son interrupciones software que nos permiten manejar eventos asíncronos. Se
puede definir una señal como un mensaje enviado desde un proceso a otro, siendo dicho
mensaje un número entero que identifica la señal enviada. En POSIX las señales tienen
nombres. Sus nombres empiezan siempre con las letras SIG. Por ejemplo:

SIGTERM: señal para terminar un proceso

SIGABRT: señal de abortar un proceso. Se genera con la función abort()

SIGALRM: señal de alarma que se genera cuando vence el tiempo que se ha


establecido mediante alarm()

SIGINT: señal de interrupción del proceso. Se genera desde el terminal por Ctrl+C

Se pueden generar señales de muchos modos:

• mediante una combinación de teclas. Por ejemplo Ctrl+C genera una señal ( SIGINT,
interrupción de teclado) que provoca la terminación del proceso que se está ejecutando
en primer plano, o por ejemplo Ctrl+Z (SIGTSTP) que provoca la parada de todos los
procesos en primer plano.

• por excepciones hardware: por ejemplo una división por 0, hacer referencia a una
dirección de memoria inválida. Normalmente estas condiciones las detecta el hardware
y lo notifica al núcleo. Entonces el núcleo genera la señal apropiada al proceso que se
estaba ejecutando cuando se produjo la excepción. Por ejemplo, un proceso que hace
referencia a una dirección de memoria inválida hará que el núcleo mande la señal
SIGSEGV a ese proceso.

• mediante kill(), esta función nos permite enviar una señal a otro proceso, siempre
que el que genere la señal sea el propietario del proceso o sea el superusuario.

• mediante el comando kill. Esta orden es en realidad una interfaz de la llamada al


sistema kill

• por condiciones software. Por ejemplo, se genera la señal SIGALRM cuando vence el
tiempo establecido mediante alarm() o SIGPIPE que se genera cuando un proceso
escribe en una tubería después de que el lector de la tubería haya acabado.

Cuando un proceso recibe una señal, este puede optar por tres alternativas posibles:

1.- que ignore la señal. Esto es así para muchas señales pero hay dos que no se
pueden ignorar nunca: SIGSTOP y SIGKILL.

2.- que capture la señal para que se ejecute un trozo de código definido por el usuario.
Por ejemplo un proceso ha creado ficheros temporales, al capturar la señal SIGTERM
se asocia una función que borre estos ficheros antes de terminar el proceso.

3.- permitir que se ejecute la acción por defecto asociada a la señal. Para muchas
señales la acción por defecto de las señales es la finalización del proceso.

Todas las señales tienen la misma prioridad. A diferencia de las interrupciones hardware, las
señales se procesan siguiendo la filosofía FIFO.

Si por la acción del envío de una señal a un proceso se activa el manejador correspondiente,
mientras este no termine su ejecución, ninguna otra señal podrá ser recibida por el proceso
receptor.
17
Asignación de un manejador de señal, signal()

La llamada al sistema signal() permite modificar el manejador asociado a una señal


determinada. Las asignaciones de manejadores persisten durante el ciclo de vida del proceso.
Su sintaxis es:

sighandler_t signal(int signum, sighandler_t handler)

donde

signum: entero que representa una de las señales definidas en el archivo de cabecera
/usr/include/signal.h

handler: especifica la dirección de una manejador de señal, dada por el usuario en el


proceso. Existen dos manejadores predefinidos en /usr/include/signal.h que junto al definido
por el usuario, proporcionan las tres posibilidades que tiene un proceso de recibir una señal:

SIG_DFL: manejador por defecto

SIG_IGN: manejador que ignora la señal recibida.

El valor que devuelve es la dirección del manejador asignado antes de la ejecución de la


llamada al sistema o SIG_ERR en caso de error.

El manejador asociado será una función que no devolverá ningún valor y recibirá un argumento
entero que contendrá el valor numérico de la señal recibida. Esta función se ejecutara cada vez
que el proceso reciba la señal asociada (siempre que la señal sea recibida después de haber
realizado la llamada a signal()). La sintaxis de la función es:

void handler(int signum);

No es posible asignar un manejador que no sea el manejador por defecto para las señales
SIGKILL, SIGSTOP y SIGCONT.

Otras llamadas al sistema relacionadas con señales.


Para cambiar la acción por defecto de una señal utilizaremos la función sigaction.

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

Esta función espera tres parámetros, el primero es la señal, el segundo define la nueva función
a la que se debe transferir el control al recibir la señal y el tercero devuelve el antiguo
manejador establecido. La estructura struct sigaction tiene dos miembros muy importantes:

• void (*)() sa_handler Puntero a la función que recibe la señal o bien, SIG_DFL para
volver al tratamiento normal o SIG_IGN para deshabilitar su tratamiento.

• sigset_t sa_mask Mascara con las señales adicionales a bloquear cuando se ejecuta la
función que trata la señal. Para inicializar un conjunto de señales se utiliza sigemptyset()
y para añadir una nueva señal a ese conjunto se utiliza sigaddset(). Se obtiene más
información en las páginas correspondientes del manual de Unix.

• Alarm() Establece una alarma. Cuando ha vencido el tiempo establecido se genera la


señal SIGALRM.

La llamada al sistema kill()

Permite enviar una señal a otro proceso. Su sintaxis es

18
int kill(pid_t pid, int sig)

donde:

pid: entero que identifica alproceso al cual va dirigida la señal (PID)

sig: entero que representa la señal enviada al proceso.

Valor devuelto: entero que indica si la llamada se ha ejecutado de forma correcta (0) o erronea
(-1)

6. Algunos comandos básicos sobre gestión de


procesos.
La orden ps
El sistema operativo nos permite ver el estado de los procesos en un determinado momento
mediante la orden ps. Por ejemplo este puede ser el listado que nos presenta la orden ps (sin
argumentos) en un determinado momento:

PID TTY STAT TIME COMMAND

1615 p2 S 0:00 -bash

2420 p2 S 0:00 man ps

2422 p2 S 0:00 sh -c /usr/bin/gunzip -c /usr/man/cat1/ps.1.gz | /usr/bin/l

2424 p2 S 0:00 /usr/bin/less -is

2427 p5 S 0:00 -bash

2649 p5 R 0:00 ps

donde:

PID es el identificador de proceso.

TTY terminal asociada a ese proceso.

STAT estado en el que están los procesos, S dormido, R ejecutándose, ...

TIME es el tiempo que lleva el proceso ejecutándose.

COMMAND es la orden correspondiente al proceso.

Mediante la orden man consultaremos las distintas opciones de la orden ps. Algunas
interesantes:

-l: listado largo

-u: nombre del usuario

-a: procesos de otros usuarios

19
-f: representa en una especie de árbol el parentesco entre los procesos

Existen otras opciones que se pueden mirar mediante el comando man.

La orden top
Nos permite ver de forma dinámica el estado del sistema en general y el estado de los
procesos, el tiempo de CPU consumido, la memoria consumida, la prioridad asignada a los
procesos, etc.

El comando kill
Envía la señal especificada a un determinado proceso desde la línea de comandos. Si no se
especifica la señal, por defecto se envía la señal SIGTERM (terminación de un proceso).

Para enviar una señal haríamos:


kill –SIGTERM <pid del proceso>

La opción –l nos permite obtener un listado de todas las señales.

20

También podría gustarte