.1 Procesos concurrentes: llamadas fork y wait

Para crear nuevos procesos, el UNIX dispone únicamente de una llamada al sistema, fork, sin ningún tipo de parámetros. Su prototipo es

int fork();

Al llamar a esta función se crea un nuevo proceso (proceso hijo), idéntico en código y datos al proceso que ha realizado la llamada (proceso padre). Los espacios de memoria del padre y el hijo son disjuntos, por lo que el proceso hijo es una copia idéntica del padre que a partir de ese momento sigue su vida separada, sin afectar a la memoria del padre; y viceversa.

Siendo más concretos, las variables del proceso padre son inicialmente las mismas que las del hijo. Pero si cualquiera de los dos procesos altera una variable, el cambio sólo repercute en su copia local. Padre e hijo no comparten memoria.

El punto del programa donde el proceso hijo comienza su ejecución es justo en el retorno de la función fork, al igual que ocurre con el padre.

Si el proceso hijo fuera un mero clon del padre, ambos ejecutarían las mismas instrucciones, lo que en la mayoría de los casos no tiene mucha utilidad. El UNIX permite distinguir si se es el proceso padre o el hijo por medio del valor de retorno de fork. Esta función devuelve un cero al proceso hijo, y el identificador de proceso (PID) del hijo al proceso padre. Como se garantiza que el PID siempre es no nulo, basta aplicar un if para determinar quién es el padre y quién el hijo para así ejecutar distinto código.

Con un pequeño ejemplo:

main()

{

int x=1;

if ( fork()==0 )

{

printf ("Soy el hijo, x=%d\n",x);

} else {

x=33;

printf ("Soy el padre, x=%d\n",x);

}

}

Este programa mostrará por la salida estándar las cadenas "Soy el hijo, x=1" y "Soy el padre, x=33", en un orden que dependerá del compilador y del planificador de procesos de la máquina donde se ejecute.

Como aplicación de fork a la ejecución de programas, véase este otro pequeño ejemplo, que además nos introducirá en nuevas herramientas:

1 if ( fork()==0 )

2 {

3 execlp ("ls","ls","-l","/usr/include",0);

4 printf ("Si ves esto, no se pudo ejecutar el \

4b programa\n");

5 exit(1);

6 }

7 else

8 {

9 int tonta;

10 wait(&tonta);

11 }

Este fragmento de código lanza a ejecución la orden ls -l /usr/include, y espera por su terminación.

En la línea 1 se verifica si se es padre o hijo. Generalmente es el hijo el que toma la iniciativa de lanzar un fichero a ejecución, y en las líneas 2 a la 6 se invoca a un programa con execlp. La línea 4 sólo se ejecutará si la llamada execlp no se ha podido cumplir. La llamada a exit garantiza que el hijo no se dedicará a hacer más tareas.

Las líneas de la 8 a la 11 forman el código que ejecutará el proceso padre, mientras el hijo anda a ejecutar sus cosas. Aparece una nueva llamada el sistema, la función wait. Esta función bloquea al proceso llamador hasta que alguno de sus hijos termina. Para nuestro ejemplo, dejará al padre bloqueado hasta que se ejecute el programa lanzado o se ejecute la línea 5, terminando en ambos casos el discurrir de su único hijo.

Es decir, la función wait es un mecanismo de sincronización entre un proceso padre y sus hijos.

La llamada wait recibe como parámetro un puntero a entero donde se deposita el valor devuelto por el proceso hijo al terminar; y retorna el PID del hijo. El PID del hijo es una información que puede ser útil cuando se han lanzado varios procesos hijos y se desea discriminar quién exactamente ha terminado. En nustro ejemplo no hace falta este valor, porque sólo hay un hijo por el que esperar.

Como ejemplo final, esta función en C es una implementación sencilla de la función system, haciendo uso de fork, wait y una función exec.

int system ( const char* str )

{

int ret;

if (!fork())

{

execlp ( "sh", "sh", "-c", str, 0 );

return -1;

}

wait (&ret);

return ret;

}

.2 Comunicación entre procesos

Los procesos en UNIX no comparten memoria, ni siquiera los padres con sus hijos. Por tanto, hay que establecer algún mecanismo en caso de que se quiera comunicar información entre procesos concurrentes. El sistema operativo UNIX define tres clases de herramientas de comunicación entre procesos (IPC): los semáforos, la memoria compartida y los mensajes.

En este apartado se describirán brevemente algunas llamadas al sistema disponibles para el uso de las IPCs dentro de la programación en C.

.1 Semáforos

¿Qué es un semáforo para el UNIX? Formalmente es muy similar a la definición clásica de Dijkstra, en el sentido de que es una variable entera con operaciones atómicas de inicialización, incremento y decremento con bloqueo.

El UNIX define tres operaciones fundamentales sobre semáforos:

semget Crea o toma el control de un semáforo

semctl Operaciones de lectura y escritura del estado del semáforo. Destrucción del semáforo

semop Operaciones de incremento o decremento con bloqueo

Como el lenguaje C no tiene un tipo "semáforo" predefinido, si queremos usar semáforos tenemos que crearlos mediante una llamada al sistema (semget). Esta llamada permite crear un conjunto de semáforos, en lugar de uno solo. Las operaciones se realizan atómicamente sobre todo el conjunto; esto evita interbloqueos y oscuras programaciones en muchos casos, pero para esta práctica es más bien un engorro.

Al crear un semáforo se nos devuelve un número identificador, que va a funcionar casi igual que los identificadores de fichero de las llamadas open, creat, etc. La función semget nos permite además "abrir" un semáforo que ya esté creado. Así, por ejemplo, si un proceso crea un semáforo, otros procesos pueden sincronizarse con aquél (con ciertas restricciones) disponiendo del semáforo con semget.

Para darle un valor inicial a un semáforo, se utiliza la función semctl.

El UNIX no ofrece las funciones clásicas P y V o equivalentes, sino que dispone de una función general llamada semop que permite realizar una gama de operaciones que incluyen las P y V.

semctl también se emplea para destruir un semáforo.

.2 Llamadas al sistema para semáforos

Esta es una descripción resumida de las tres llamadas al sistema para operar con semáforos (semget, semctl y semop). Para una información más completa y fidedigna, diríjanse al manual de llamadas al sistema (sección 2).

Para el correcto uso de todas estas funciones, han de incluir el fichero cabecera <sys/sem.h>

Las tres funciones devuelven -1 si algo ha ido mal y en tal caso la variable errno informa del tipo de error.

n Apertura o creación de un semáforo

Sintaxis:

int semget ( key_t key, int nsems, int semflg );

semget devuelve el identificador del semáforo correspondiente a la clave key. Puede ser un semáforo ya existente, o bien semget crea uno nuevo si se da alguno de estos casos:

a) key vale IPC_PRIVATE. Este valor especial obliga a semget a crear un nuevo y único identificador, nunca devuelto por ulteriores llamadas a semget hasta que sea liberado con semctl.

b) key no está asociada a ningún semáforo existente, y se cumple que (semflg & IPC_CREAT) es cierto.

A un semáforo puede accederse siempre que se tengan los permisos adecuados.

Si se crea un nuevo semáforo, el parámetro nsems indica cuántos semáforos contiene el conjunto creado; los 9 bits inferiores de semflg contienen los permisos estilo UNIX de acceso al semáforo (usuario, grupo, otros).

semflg es una máscara que puede contener IPC_CREAT, que ya hemos visto, o IPC_EXCL, que hace crear el semáforo, pero fracasando si ya existía.

Ejemplo:

int semid = semget ( IPC_PRIVATE, 1, IPC_CREAT | 0744 );

n Operaciones de control sobre semáforos

Sintaxis:

int semctl ( int semid, int semnum, int cmd... );

Esta es una función compleja (y de interfaz poco elegante) para realizar ciertas operaciones con semáforos. semid es un identificador de semáforo (devuelto previamente por semget) y semnum, el semáforo del conjunto sobre el que quieren trabajar. cmd es la operación aplicada; a continuación puede aparecer un parámetro opcional según la operación definida por cmd.

Las operaciones que les interesan a ustedes son

GETVAL semctl retorna el valor actual del semáforo

SETVAL se modifica el valor del semáforo (un cuarto parámetro entero da el nuevo valor)

IPC_RMID destruye el semáforo

Ejemplos:

valor = semctl (semid,semnum,GETVAL);

semctl (semid,semnum,SETVAL,nuevo_valor);

n Operaciones sobre semáforos

Sintaxis:

int semop ( int semid, struct sembuf* sops, unsigned nsops );

Esta función realiza atómicamente un conjunto de operaciones sobre semáforos, pudiendo bloquear al proceso llamador. semid es el identificador del semáforo y sops es un apuntador a un vector de operaciones. nsops indica el número de operaciones solicitadas.

La estructura sembuf tiene estos campos:

struct sembuf {

unsigned short sem_num; // número del semáforo dentro del conjunto

short sem_op; // clase de operación

// según sea >0, <0 o ==0

short sem_flg; // modificadores de operación

};

Cada elemento de sops es una operación sobre algún semáforo del conjunto de semid. El algoritmo simplificado de la operación realizada es éste (semval es el valor entero contenido en el semáforo donde se aplica la operación).

si semop<0

si semval >= |semop|

semval -= |semop|

si semval < |semop|

si (semflag & IPC_NOWAIT)!=0

la función semop() retorna

si no

bloquearse hasta que semval >= |semop|

semval -= |semop|

si semop>0

semval += semop

si semop==0

si semval = 0

SKIP

si semval != 0

si (semflag & IPC_NOWAIT)!=0

la función semop() retorna

si no

bloquearse hasta que semval == 0

Podemos afirmar sin mucho desacierto que si el campo semop de una operación es positivo, se incrementa el valor del semáforo. Asimismo, si semop es negativo, se decrementa el valor del semáforo si el resultado no es negativo; en caso contrario el proceso espera a que se dé esa circunstancia. Es decir, semop==1 produce una operación V y semop==-1, una operación P.

.3 Ejemplos de uso de semáforos en UNIX

Para ilustrar de forma concreta el empleo de semáforos bajo UNIX, les mostramos dos ejemplos de subrutinas en C que les pueden servir como modelos para elaborar sus rutinas de sincronización en las prácticas de la asignatura.

n Ejemplo 1: bloqueo y desbloqueo de un fichero

Este pequeño programa utiliza un semáforo para bloquear y desbloquear el uso de un fichero. Dos procesos concurrentes compiten por el uso exclusivo del fichero.

La llamada a fork en el programa principal lanza los dos procesos; como son idénticos no aparece la típica construcción if (fork()==0), pero adviertan que esto no es habitual.

#include <sys/types.h>

#include <sys/ipc.h> /* Constantes IPC_xxx */

#include <sys/sem.h> /* Funciones y tipos de semáforos */

#define SEMKEY 123456

#define PERMISOS 0644

struct sembuf op_bloquea [] =

{

0, 0, 0, /* Esperar que semval==0 */

0, 1, SEM_UNDO /* Incrementa en 1 el semáforo */

};

struct sembuf op_desbloquea [] =

{

0, - 1, (IPC_NOWAIT|SEM_UNDO) /* Decrementa semval */

};

int semid = - 1;

int bloquea ()

{

if (semid==- 1) /* Si no está abierto, créalo y ábrelo */

if ( (semid=semget(SEMKEY,1,IPC_CREAT|PERMISOS)) == - 1 )

return - 1;

return

semop(semid,op_bloquea,2); /* El proceso puede quedarse bloqueado */

}

int desbloquea ()

{

return semop(semid,op_desbloquea,1);

}

void usa_fichero()

{

for (;;) /* Hasta que apruebes Operativos */

{

bloquea();

... usa el fichero

desbloquea();

}

}

main() /* Programa principal */

{

fork();

usa_fichero(); /* Los dos procesos ejecutan esto */

}

n Ejemplo 2: Implementación de las funciones P y V en UNIX.

Estas funciones definen la operatoria de un semáforo clásico (inicialización, incremento y decremento con posible bloqueo del proceso llamador). Así definidas, o con pocas modificaciones, les pueden servir como la interfaz para uso de semáforos en la aplicación que escriban.

#include <sys/types.h> /* para key_t */

/* Crea un semáforo con un valor inicial, dada una clave */

/* Devuelve el identificador (válido o no) del semáforo */

int crea_sem ( key_t clave, int valor_inicial );

void sem_P ( int semid ); /* Realiza una operación P sobre un semáforo */

void sem_V ( int semid ); /* Realiza una operación V sobre un semáforo */

/***********************************/

/******** IMPLEMENTACIÓN *******/

/***********************************/

#include <sys/ipc.h>

#include <sys/sem.h>

#define PERMISOS 0644

int crea_sem ( key_t clave, int valor_inicial )

{

int semid = semget( /* Abre o crea un semáforo */

clave, /* con una cierta clave */

1, /* con un solo elemento */

IPC_CREAT|PERMISOS /* lo crea (IPC_CREAT) con unos PERMISOS */

);

if ( semid==- 1 ) return - 1;

/* Da el valor inicial al semáforo */

semctl ( semid, 0, SETVAL, valor_inicial );

return semid;

}

int abre_sem (key_t clave) /* Abrir un semáforo que otro proceso ya creó */

{

return semget(clave,1,0);

}

void sem_P ( int semid ) /* Operación P */

{

struct sembuf op_P [] =

{

0, - 1, 0 /* Decrementa semval o bloquea si cero */

};

semop ( semid, op_P, 1 );

}

void sem_V ( int semid ) /* Operación V */

{

struct sembuf op_V [] =

{

0, 1, 0 /* Incrementa en 1 el semáforo */

};

semop ( semid, op_V, 1 );

}

.4 Memoria compartida y mensajes

Otras modalidades de IPCs son la memoria compartida y los mensajes. Las utilidades de memoria compartida permiten crear segmentos de memoria a los que pueden acceder múltiples procesos, pudiendo definirse restricciones de acceso (sólo lectura). El UNIX permite también manejar colas de mensajes, definiendo operaciones de inserción y extracción de información en esas colas.

El tipo de llamadas al sistema para estos IPCs es análogo al de los semáforos: existen sendas funciones shmget y msgget para crear o enlazarse a un segmento de memoria compartida o a una cola de mensajes, respectivamente. Para alterar propiedades de estos IPCs, incluyendo su borrado, están las funciones shmctl y msgctl.

Para trabajar con un segmento de memoria compartida, es necesario crear un vínculo (attachment) entre la memoria local del proceso interesado y el segmento compartido. Esto se realiza con la función shmat. El proceso que vincula un segmento de memoria compartida cree estar trabajando con ella como si fuera cierta área de memoria local. Para deshacer el vínculo está la función shmdt.

Para enviar o recibir mensajes, se utilizan las funciones msgsnd y msgrcv.

Existe información en línea, mediante man, sobre todas estas funciones.