Introducción a Procesos y Programación Concurrente, Study Guides, Projects, Research of Computer science

Este documento proporciona una introducción a los conceptos básicos de procesos en sistemas operativos, incluyendo la creación, gestión y sincronización de procesos. Se exploran las primitivas posix para la creación de procesos, la gestión de estados de procesos y la sincronización mediante mecanismos como los cerrojos y el paso de mensajes. Además, se aborda el problema del interbloqueo y se presenta el algoritmo del banquero para la detección y prevención de este problema.

Typology: Study Guides, Projects, Research

2022/2023

Uploaded on 09/17/2024

alberto-rugerio
alberto-rugerio 🇺🇸

1 document

1 / 42

Toggle sidebar

This page cannot be seen from the preview

Don't miss anything!

bg1
Capítulo 1
Conceptos Básicos
En este capítulo se plantean los aspectos básicos relativos al
concepto de proceso, estableciendo las principales diferen-
cias con respecto a una hebra o hilo y haciendo especial hin-
capié en su creación y gestión mediante primitivas POSIX. Así mismo,
también se establece un marco general para el estudio de los fun-
damentos de programación concurrente. Estos aspectos se discutirán
con más detalle en sucesivos temas.
Comunicando procesos
Los procesos necesitan algún
tipo de mecanismo explícito
tanto para compartir infor-
mación como para sincroni-
zarse. El hecho de que se
ejecuten en una misma má-
quina física no implica que
los recursos se compartan de
manera implícita sin proble-
mas.
La problemática que se pretende abordar mediante el estudio de
la programación concurrente está vinculada al concepto de sistema
operativo multiproceso, donde los procesos comparten todo tipo de
recursos, desde la CPU hasta una impresora. Este planteamiento me-
jora la eficiencia del sistema, pero plantea la cuestión de la sincro-
nización en el acceso a los recursos. Típicamente, esta problemática
no se resuelve a nivel de sistema operativo, siendo responsabilidad del
programador el garantizar un acceso consistente a los recursos.
Este planteamiento general se introduce mediante el problema clá-
sico del productor/consumidor, haciendo hincapié en la necesidad de
compartir un buffer, y da lugar al concepto de sección crítica. Entre
las soluciones planteadas, destaca el uso de los semáforos y el paso
de mensajes como mecanismos clásicos de sincronización.
1
pf3
pf4
pf5
pf8
pf9
pfa
pfd
pfe
pff
pf12
pf13
pf14
pf15
pf16
pf17
pf18
pf19
pf1a
pf1b
pf1c
pf1d
pf1e
pf1f
pf20
pf21
pf22
pf23
pf24
pf25
pf26
pf27
pf28
pf29
pf2a

Partial preview of the text

Download Introducción a Procesos y Programación Concurrente and more Study Guides, Projects, Research Computer science in PDF only on Docsity!

Capítulo 1

Conceptos Básicos

E

n este capítulo se plantean los aspectos básicos relativos al

concepto de proceso , estableciendo las principales diferen-

cias con respecto a una hebra o hilo y haciendo especial hin-

capié en su creación y gestión mediante primitivas POSIX. Así mismo,

también se establece un marco general para el estudio de los fun-

damentos de programación concurrente. Estos aspectos se discutirán

con más detalle en sucesivos temas.

Comunicando procesos

Los procesos necesitan algún tipo de mecanismo explícito tanto para compartir infor- mación como para sincroni- zarse. El hecho de que se ejecuten en una misma má- quina física no implica que los recursos se compartan de manera implícita sin proble- mas.

La problemática que se pretende abordar mediante el estudio de

la programación concurrente está vinculada al concepto de sistema

operativo multiproceso , donde los procesos comparten todo tipo de

recursos, desde la CPU hasta una impresora. Este planteamiento me-

jora la eficiencia del sistema, pero plantea la cuestión de la sincro-

nización en el acceso a los recursos. Típicamente, esta problemática

no se resuelve a nivel de sistema operativo, siendo responsabilidad del

programador el garantizar un acceso consistente a los recursos.

Este planteamiento general se introduce mediante el problema clá-

sico del productor/consumidor, haciendo hincapié en la necesidad de

compartir un buffer, y da lugar al concepto de sección crítica. Entre

las soluciones planteadas, destaca el uso de los semáforos y el paso

de mensajes como mecanismos clásicos de sincronización.

[ 2 ] CAPÍTULO 1. CONCEPTOS BÁSICOS

instr_

instr_

instr_

instr_i

instr_n

carga del ejecutable en memoria

programa proceso

datos

pila

memoria

texto

Figura 1.1: Esquema gráfico de un programa y un proceso.

1.1. El concepto de proceso

Informalmente, un proceso se puede definir como un programa en

ejecución. Además del propio código al que está vinculado, un proceso

incluye el valor de un contador de programa y el contenido de ciertos

registros del procesador. Generalmente, un proceso también incluye

la pila del proceso, utilizada para almacenar datos temporales, como

variables locales, direcciones de retorno y parámetros de funciones, y

una sección de datos con variables globales. Finalmente, un proceso

también puede tener una sección de memoria reservada de manera

dinámica. La figura 1.1 muestra la estructura general de un proceso.

1.1.1. Gestión básica de procesos

A medida que un proceso se ejecuta, éste va cambiando de un es-

tado a otro. Cada estado se define en base a la actividad desarrollada

por un proceso en un instante de tiempo determinado. Un proceso

puede estar en uno de los siguientes estados (ver figura 1.2):

Nuevo , donde el proceso está siendo creado.

En ejecución , donde el proceso está ejecutando operaciones o

instrucciones.

En espera , donde el proceso está a la espera de que se produzca

un determinado evento, típicamente la finalización de una opera-

ción de E/S.

Preparado , donde el proceso está a la espera de que le asignen

alguna unidad de procesamiento.

[ 4 ] CAPÍTULO 1. CONCEPTOS BÁSICOS

a fork() se genera una copia exacta que deriva en un nuevo proceso,

el proceso hijo, que recibe una copia del espacio de direcciones del

proceso padre. A partir de ese momento, ambos procesos continúan

su ejecución en la instrucción que está justo después de fork(). La

figura 1.3 muestra de manera gráfica las implicaciones derivadas de

la ejecución de fork() para la creación de un nuevo proceso.

fork() devuelve 0 al proceso hijo y el PID del hijo al padre.

Listado 1.2: La llamada fork() al sistema

1 #include <sys/types.h> 2 #include <unistd.h> 3 4 pid_t fork (void);

Independencia

Los procesos son indepen- dientes entre sí, por lo que no tienen mecanismos implíci- tos para compartir informa- ción y sincronizarse. Incluso con la llamada fork(), los pro- cesos padre e hijo son total- mente independientes.

Recuerde que fork() devuelve el valor 0 al hijo y el PID del hijo al

padre. Dicho valor permite distinguir entre el código del proceso pa-

dre y del hijo, con el objetivo de tener la posibilidad de asignar un

nuevo fragmento de código. En otro caso, la creación de dos procesos

totalmente idénticos no sería muy útil.

proceso_A

fork()

proceso_A

(cont.)

proceso_B

(hijo)

hereda_de

Figura 1.3: Creación de un proceso mediante fork().

1.1. El concepto de proceso [ 5 ]

El siguiente listado de código muestra un ejemplo muy básico de

utilización de fork() para la creación de un nuevo proceso. No olvide

que el proceso hijo recibe una copia exacta del espacio de direcciones

del proceso padre.

Listado 1.3: Uso básico de fork()

1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 6 int main (void) { 7 int (^) *valor = malloc(sizeof(int)); (^8) *valor = 0; 9 fork(); (^10) *valor = 13; 11 printf(" %ld: %d\n", (long)getpid(), (^) *valor); 12 13 free(valor); 14 return 0; 15 }

La salida de este programa sería la siguiente^1 :

¿Cómo distinguiría el código del proceso hijo y el del padre?

Después de la ejecución de fork(), existen dos procesos y cada uno

de ellos mantiene una copia de la variable valor. Antes de su ejecución,

solamente existía un proceso y una única copia de dicha variable. Note

que no es posible distinguir entre el proceso padre y el hijo, ya que no

se controló el valor devuelto por fork().

Típicamente será necesario crear un número arbitrario de procesos,

por lo que el uso de fork() estará ligado al de algún tipo de estructura

de control, como por ejemplo un bucle for. Por ejemplo, el siguiente lis-

tado de código genera una cadena de procesos, tal y como se refleja de

manera gráfica en la figura 1.4. Para ello, es necesario asegurarse de

que el proceso generado por una llamada a fork(), es decir, el proceso

hijo, sea el responsable de crear un nuevo proceso.

(^1) El valor de los ID de los procesos puede variar de una ejecución a otra.

1.1. El concepto de proceso [ 7 ]

proceso_A

fork()

proceso_A

(cont.)

wait()

proceso_B

(hijo)

execl()

exit()

Limpieza

tabla proc

SIGCHLD

zombie

Figura 1.5: Esquema gráfico de la combinación fork()+exec().

El uso de operaciones del tipo exec implica que el proceso padre

tenga que integrar algún tipo de mecanismo de espera para la correcta

finalización de los procesos que creó con anterioridad, además de lle-

var a cabo algún tipo de liberación de recursos. Esta problemática se

discutirá más adelante. Antes, se estudiará un ejemplo concreto y se

comentarán las principales diferencias existentes entre las operacio-

nes de la familia exec, las cuales se muestran en el siguiente listado

de código.

Listado 1.5: Familia de llamadas exec

1 #include <unistd.h> 2 3 int execl (const char (^) *path, const char (^) *arg, ...); 4 int execlp (const char (^) *file, const char (^) *arg, ...); 5 int execle (const char (^) *path, const char (^) *arg, ..., 6 char (^) *const envp[]); 7 8 int execv (const char (^) *path, char (^) *const argv[]); 9 int execvp (const char (^) *file, char (^) *const argv[]); 10 int execve (const char (^) *path, char (^) *const argv[], 11 char (^) *const envp[]);

La llamada al sistema execl() tiene los siguientes parámetros:

1. La ruta al archivo que contiene el código binario a ejecutar por

el proceso, es decir, el ejecutable asociado al nuevo segmento de

código.

[ 8 ] CAPÍTULO 1. CONCEPTOS BÁSICOS

2. Una serie de argumentos de línea de comandos , terminado por

un apuntador a NULL. El nombre del programa asociado al proce-

so que ejecuta execl suele ser el primer argumento de esta serie.

La llamada execlp tiene los mismos parámetros que execl, pero

hace uso de la variable de entorno PATH para buscar el ejecutable

del proceso. Por otra parte, execle es también similar a execl , pero

añade un parámetro adicional que representa el nuevo ambiente del

programa a ejecutar. Finalmente, las llamadas execv difieren en la

forma de pasar los argumentos de línea de comandos, ya que hacen

uso de una sintaxis basada en arrays.

El siguiente listado de código muestra la estructura de un progra-

ma encargado de la creación de una serie de procesos mediante la

combinación de fork() y execl() dentro de una estructura de bucle.

Como se puede apreciar, el programa recoge por línea de órdenes el

número de procesos que se crearán posteriormente^2. Note cómo se ha-

ce uso de una estructura condicional para diferenciar entre el código

asociado al proceso padre y al proceso hijo. Para ello, el valor devuelto

por fork(), almacenado en la variable childpid, actúa de discriminante.

padre

h 1 h 2 ... hn

Figura 1.6: Representación gráfica de la creación de va- rios procesos hijo a partir de uno padre.

En el case 0 de la sentencia switch (línea

12 ) se ejecuta el código

del hijo. Recuerde que fork() devuelve 0 al proceso hijo, por lo que en

ese caso habrá que asociar el código del proceso hijo. En este ejemplo,

dicho código reside en una nueva unidad ejecutable, la cual se en-

cuentra en el directorio exec y se denomina hijo. Esta información es

la que se usa cuando se llama a execl(). Note cómo este nuevo proceso

acepta por línea de órdenes el número de procesos creados (argv[1]),

recogido previamente por el proceso padre (línea

Compilación

La compilación de los pro- gramas asociados al proceso padre y al hijo, respectiva- mente, es independiente. En otras palabras, serán objeti- vos distintos que generarán ejecutables distintos.

Listado 1.6: Uso de fork+exec

1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 6 int main (int argc, char (^) *argv[]) { 7 pid_t childpid; 8 int n = atoi(argv[1]), i; 9 10 for (i = 1; i <= n; i++) 11 switch(childpid = fork()) { 12 case 0: // Código del hijo. 13 execl("./exec/hijo", "hijo", argv[1], NULL); 14 break; // Para evitar entrar en el for. 15 } 16 17 // Se obvia la espera a los procesos 18 // y la captura de eventos de teclado. 19 20 return 0; 21 }

(^2) Por simplificación no se lleva a cabo un control de errores

[ 10 ] CAPÍTULO 1. CONCEPTOS BÁSICOS

Listado 1.9: Uso de fork+exec+wait

1 #include <sys/types.h> 2 #include <unistd.h> 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <signal.h> 6 #include <wait.h> 7 8 #define NUM_HIJOS 5 9 10 void finalizarprocesos (); 11 void controlador (int senhal); 12 13 pid_t pids[NUM_HIJOS]; 14 15 int main (int argc, char (^) *argv[]) { 16 int i; 17 char (^) num_hijos_str; 18 num_hijos_str = (char)malloc(sizeof(int)); 19 sprintf(num_hijos_str, " %d", NUM_HIJOS); 20 21 // Manejo de Ctrol+C. 22 if (signal(SIGINT, controlador) == SIG_ERR) { 23 fprintf(stderr, "Abrupt termination.\n"); 24 exit(EXIT_FAILURE); 25 } 26 27 for (i = 0; i < NUM_HIJOS; i++) 28 switch(pids[i] = fork()) { 29 case 0: // Código del hijo. 30 execl("./exec/hijo", "hijo", num_hijos_str, NULL); 31 break; // Para evitar entrar en el for. 32 } 33 34 free(num_hijos_str); 35 36 // Espera terminación de hijos... 37 for (i = 0; i < NUM_HIJOS; i++) 38 waitpid(pids[i], 0, 0); 39 40 return EXIT_SUCCESS; 41 } 42 43 void finalizarprocesos () { 44 int i; 45 printf ("\n-------------- Finalizacion de procesos ------------- \n"); 46 for (i = 0; i < NUM_HIJOS; i++) 47 if (pids[i]) { 48 printf ("Finalizando proceso [ %d]... ", pids[i]); 49 kill(pids[i], SIGINT); printf("\n"); 50 } 51 } 52 53 void controlador (int senhal) { 54 printf("\nCtrl+c captured.\n"); printf("Terminating...\n\n"); 55 // Liberar recursos... 56 finalizarprocesos(); 57 exit(EXIT_SUCCESS); 58 }

1.1. El concepto de proceso [ 11 ]

El anterior listado muestra la inclusión del código necesario pa-

ra esperar de manera adecuada la finalización de los procesos hijo y

gestionar la terminación abrupta del proceso padre mediante la com-

binación de teclas Ctrl+C.

En primer lugar, la espera a la terminación de los hijos se con-

trola mediante las líneas

37-38 utilizando la primitiva waitpid. Esta

primitiva se comporta de manera análoga a wait, aunque bloquean-

do al proceso que la ejecuta para esperar a otro proceso con un pid

concreto. Note como previamente se ha utilizado un array auxiliar de-

nominado pids (línea

13 ) para almacenar el pid de todos y cada uno

de los procesos hijos creados mediante fork (línea

28 ). Así, sólo habrá

que esperarlos después de haberlos lanzado.

Por otra parte, note cómo se captura la señal SIGINT, es decir, la

terminación mediante Ctrl+C, mediante la función signal (línea

22 ), la

cual permite asociar la captura de una señal con una función de re-

trollamada. Ésta función definirá el código que se tendrá que ejecutar

cuando se capture dicha señal. Básicamente, en esta última función,

denominada controlador, se incluirá el código necesario para liberar

los recursos previamente reservados, como por ejemplo la memoria

dinámica, y para destruir los procesos hijo que no hayan finalizado.

Si signal() devuelve un código de error SIG_ERR, el programador es

responsable de controlar dicha situación excepcional.

Listado 1.10: Primitiva POSIX signal

1 #include <signal.h> 2 3 typedef void (*sighandler_t)(int); 4 5 sighandler_t signal (int signum, sighandler_t handler);

1.1.3. Procesos e hilos

Sincronización

Conseguir que dos cosas ocurran al mismo tiempo se denomina comúnmente sin- cronización. En Informática, este concepto se asocia a las relaciones existentes en- tre eventos, como la seriali- zación (A debe ocurrir antes que B) o la exclusión mutua (A y B no pueden ocurrir al mismo tiempo).

El modelo de proceso presentado hasta ahora se basa en un único

flujo o hebra de ejecución. Sin embargo, los sistemas operativos mo-

dernos se basan en el principio de la multiprogramación , es decir, en

la posibilidad de manejar distintas hebras o hilos de ejecución de ma-

nera simultánea con el objetivo de paralelizar el código e incrementar

el rendimiento de la aplicación.

Esta idea también se plasma a nivel de lenguaje de programación.

Algunos ejemplos representativos son los APIs de las bibliotecas de

hilos Pthread, Win32 o Java. Incluso existen bibliotecas de gestión

de hilos que se enmarcan en capas software situadas sobre la capa

del sistema operativo, con el objetivo de independizar el modelo de

programación del propio sistema operativo subyacente.

En este contexto, una hebra o hilo se define como la unidad básica

de utilización del procesador y está compuesto por los elementos:

Un ID de hebra , similar al ID de proceso.

1.1. El concepto de proceso [ 13 ]

Capacidad de respuesta , ya que el uso de múltiples hilos pro-

porciona un enfoque muy flexible. Así, es posible que un hilo se

encuentra atendiendo una petición de E/S mientras otro conti-

núa con la ejecución de otra funcionalidad distinta. Además, es

posible plantear un esquema basado en el paralelismo no blo-

queante en llamadas al sistema, es decir, un esquema basado en

el bloqueo de un hilo a nivel individual.

Compartición de recursos , posibilitando que varios hilos mane-

jen el mismo espacio de direcciones.

Eficacia , ya que tanto la creación, el cambio de contexto, la des-

trucción y la liberación de hilos es un orden de magnitud más

rápida que en el caso de los procesos pesados. Recuerde que las

operaciones más costosas implican el manejo de operaciones de

E/S. Por otra parte, el uso de este tipo de programación en ar-

quitecturas de varios procesadores (o núcleos) incrementa enor-

memente el rendimiento de la aplicación.

Caso de estudio. POSIX Pthreads

Pthreads es un estándar POSIX que define un interfaz para la crea-

ción y sincronización de hilos. Recuerde que se trata de una especifica-

ción y no de una implementación, siendo ésta última responsabilidad

del sistema operativo.

POSIX

Portable Operating System In- terface es una familia de es- tándares definidos por el co- mité IEEE con el objetivo de mantener la portabilidad en- tre distintos sistemas opera- tivos. La X de POSIX es una referencia a sistemas Unix.

El mánejo básico de hilos mediante Pthreads implica el uso de las

primitivas de creación y de espera que se muestran a continuación.

Como se puede apreciar, la llamada pthread_create() necesita una fun-

ción que defina el código de ejecución asociado al propio hilo (definido

en el tercer parámetro). Por otra parte, pthread_join() tiene un propó-

sito similar al ya estudiado en el caso de la llamada wait(), es decir, se

utiliza para que la hebra padre espera a que la hebra hijo finalice su

ejecución.

Listado 1.11: Primitivas POSIX Pthreads

1 #include <pthread.h> 2 3 int pthread_create (pthread_t (^) *thread, 4 const pthread_attr_t (^) *attr, 5 void (^) (start_routine) (void (^) *), 6 void (^) *arg); 7 8 int pthread_join (pthread_t thread, void (^) **retval);

El listado de código de la página siguiente muestra un ejemplo muy

sencillo de uso de Pthreads en el que se crea un hilo adicional que tie-

ne como objetivo realizar el sumatorio de todos los números inferiores

o iguales a uno pasado como parámetro. La función mi_hilo() (líneas

27-36 ) es la que realmente implementa la funcionalidad del hilo crea-

do mediante pthread_create() (línea

19 ). El resultado se almacena en

una variable definida globalmente. Para llevar a cabo la compilación y

[ 14 ] CAPÍTULO 1. CONCEPTOS BÁSICOS

ejecución de este ejemplo, es necesario enlazar con la biblioteca pth-

reads:

$ gcc -lpthread thread_simple.c -o thread_simple $ ./thread_simple

El resultado de la ejecución de este programa para un valor, dado

por línea de órdenes, de 7 será el siguiente:

$ Suma total: 28.

Listado 1.12: Ejemplo de uso de Pthreads

1 #include <pthread.h> 2 #include <stdio.h> 3 #include <stdlib.h> 4 5 int suma; 6 void (^) *mi_hilo (void (^) *valor); 7 8 int main (int argc, char (^) *argv[]) { 9 pthread_t tid; 10 pthread_attr_t attr; 11 12 if (argc != 2) { 13 fprintf(stderr, "Uso: ./pthread \n"); 14 return -1; 15 } 16 17 pthread_attr_init(&attr); // Att predeterminados. 18 // Crear el nuevo hilo. 19 pthread_create(&tid, &attr, mi_hilo, argv[1]); 20 pthread_join(tid, NULL); // Esperar finalización. 21 22 printf("Suma total: %d.\n", suma); 23 24 return 0; 25 } 26 27 void (^) *mi_hilo (void (^) *valor) { 28 int i, ls; 29 ls = atoi(valor); 30 i = 0, suma = 0; 31 32 while (i <= ls) 33 suma += (i++); 34 35 pthread_exit(0); 36 }

[ 16 ] CAPÍTULO 1. CONCEPTOS BÁSICOS

while (1) {

// Produce en nextP.

while (cont == N);

// No hacer nada.

buffer[in] = nextP;

in = (in + 1) % N;

cont++;

while (1) {

while (cont == 0);

// No hacer nada.

nextC = buffer[out];

out = (out + 1) % N;

cont--;

// Consume nextC.

r

11

= cont

r 1 = r 1 + 1

cont = r

1

r

22

= cont

r 2 = r 2 - 1

cont = r

2

cont++

cont--

p: r

1

= cont

p: r 1 = r 1 + 1

c: r

2

= cont

c: r 2 = r 2 - 1

p: cont = r 1

c: cont = r 2

Figura 1.8: Código para los procesos productor y consumidor.

1.2.2. La sección crítica

El segmento de código en el que un proceso puede modificar va-

riables compartidas con otros procesos se denomina sección crítica

(ver figura 1.9). Para evitar inconsistencias, una de las ideas que se

plantean es que cuando un proceso está ejecutando su sección crítica

ningún otro procesos puede ejecutar su sección crítica asociada.

El problema de la sección crítica consiste en diseñar algún tipo de

solución para garantizar que los procesos involucrados puedan operar

sin generar ningún tipo de inconsistencia. Una posible estructura para

abordar esta problemática se plantea en la figura 1.9, en la que el

código se divide en las siguientes secciones:

Sección de entrada , en la que se solicita el acceso a la sección

crítica.

Sección crítica , en la que se realiza la modificación efectiva de

los datos compartidos.

Sección de salida , en la que típicamente se hará explícita la sa-

lida de la sección crítica.

Sección restante , que comprende el resto del código fuente.

Normalmente, la sección de entrada servirá para manipular algún

tipo de mecanismo de sincronización que garantice el acceso exclusivo

a la sección crítica de un proceso. Mientras tanto, la sección de sa-

lida servirá para notificar, mediante el mecanismo de sincronización

1.2. Fundamentos de programación concurrente [ 17 ]

SECCIÓN_ENTRADA

SECCIÓN_SALIDA

SECCIÓN_CRÍTICA

SECCIÓN_RESTANTE

do {

} while (1);

Figura 1.9: Estructura general de un proceso.

correspondiente, la salida de la sección crítica. Este hecho, a su vez,

permitirá que otro proceso pueda acceder a su sección crítica.

Cualquier solución al problema de la sección crítica ha de satisfacer

los siguientes requisitos:

1. Exclusión mutua , de manera que si un proceso pi está en su

sección crítica, entonces ningún otro proceso puede ejecutar su

sección crítica.

2. Progreso , de manera que sólo los procesos que no estén en su

sección de salida, suponiendo que ningún proceso ejecutase su

sección crítica, podrán participar en la decisión de quién es el

siguiente en ejecutar su sección crítica. Además, la toma de esta

decisión ha de realizarse en un tiempo limitado.

3. Espera limitada , de manera que existe un límite en el número

de veces que se permite entrar a otros procesos a sus secciones

críticas antes de que otro proceso haya solicitado entrar en su

propia sección crítica (y antes de que le haya sido concedida).

Las soluciones propuestas deberían ser independientes del número

de procesos, del orden en el que se ejecutan las instrucciones máqui-

nas y de la velocidad relativa de ejecución de los procesos.

Una posible solución para N procesos sería la que se muestra en el

siguiente listado de código. Sin embargo, esta solución no sería válida

ya que no cumple el principio de exclusión mutua , debido a que un

proceso podría entrar en la sección crítica si se produce un cambio de

contexto justo después de la sentencia while (línea

1.2. Fundamentos de programación concurrente [ 19 ]

Listado 1.15: Solución 2 procesos con array de booleanos

1 do { 2 // SECCIÓN DE ENTRADA 3 flag[i] = true; 4 while (flag[j]); // No hacer nada. 5 // SECCIÓN CRÍTICA. 6 // ... 7 // SECCIÓN DE SALIDA. 8 flag[i] = false; 9 // SECCIÓN RESTANTE. 10 }while (1);

¿Qué ocurre si se invierte el orden de las operaciones de la SE?

Listado 1.16: Solución 2 procesos con array de booleanos

1 do { 2 // SECCIÓN DE ENTRADA 3 while (flag[j]); // No hacer nada. 4 flag[i] = true; 5 // SECCIÓN CRÍTICA. 6 // SECCIÓN DE SALIDA. 7 flag[i] = false; 8 // SECCIÓN RESTANTE. 9 }while (1);

Esta solución no cumpliría con el principio de exclusión mutua ,

ya que los dos procesos podrían ejecutar su sección crítica de manera

concurrente.

Solución de Peterson (2 procesos)

En este apartado se presenta una solución al problema de la sin-

cronización de dos procesos que se basa en el algoritmo de Peterson,

en honor a su inventor, y que fue planteado en 1981. La solución de

Peterson se aplica a dos procesos que van alternando la ejecución de

sus respectivas secciones críticas y restantes. Dicha solución es una

mezcla de las dos soluciones propuestas anteriormente.

Los dos procesos comparten tanto el array flag, que determina los

procesos que están listos para acceder a la sección crítica, como la

variable turn, que sirve para determinar el proceso que accederá a su

sección crítica. Esta solución es correcta para dos procesos, ya que

satisface las tres condiciones anteriormente mencionadas.

Para entrar en la sección crítica, el proceso pi asigna true a flag[i] y

luego asigna a turn el valor j, de manera que si el proceso pj desea en-

trar en su sección crítica, entonces puede hacerlo. Si los dos procesos

[ 20 ] CAPÍTULO 1. CONCEPTOS BÁSICOS

intentan acceder al mismo tiempo, a la variable turn se le asignarán

los valores i y j (o viceversa) en un espacio de tiempo muy corto, pe-

ro sólo prevalecerá una de las asignaciones (la otra se sobreescribirá).

Este valor determinará qué proceso accederá a la sección crítica.

Listado 1.17: Solución 2 procesos; algoritmo de Peterson

1 do { 2 // SECCIÓN DE ENTRADA 3 flag[i] = true; 4 turn = j; 5 while (flag[j] && turn == j); // No hacer nada. 6 // SECCIÓN CRÍTICA. 7 // ... 8 // SECCIÓN DE SALIDA. 9 flag[i] = false; 10 // SECCIÓN RESTANTE. 11 }while (1);

A continuación se demuestra que si p 1 está en su sección crítica,

entonces p 2 no está en la suya.

  1. p1 en SC (premisa)
  2. p1 en SC --> flag[1]=true y (flag[2]=false o turn!=2) (premisa)
  3. flag[1]=true y (flag[2]=false o turn!=2) (MP 1,2)
  4. flag[2]=false o turn!=2 (A o B) (EC 3)

Demostrando A

  1. flag[2]=false (premisa)
  2. flag[2]=false --> p2 en SC (premisa)
  3. p2 en SR (p2 no SC) (MP 5,6)

Demostrando B

  1. turn=!2 (premisa)
  2. flag[1]=true (EC 3)
  3. flag[1]=true y turn=1 (IC 8,9)
  4. (flag[1]=true y turn=1) --> p2 en SE (premisa)
  5. p2 en SE (p2 no SC) (MP 10,11)

Solución de Lamport (n procesos)

El algoritmo para n procesos desarrollado por Lamport es una solu-

ción general e independiente del número de procesos inicialmente con-

cebida para entornos distribuidos. Los procesos comparten dos arrays,

uno de booleanos denominado eleccion, con sus elementos inicializa-

dos a false, y otro de enteros denominado num, con sus elementos

inicializados a 0.

Este planteamiento se basa en que si pi está en la sección crítica y

pj intenta entrar, entonces pj ejecuta el segundo bucle while y detecta

que num[i] 6 = 0, garantizando así la exclusión mutua.