[LinuxFocus-icon]
Hogar  |  Mapa  |  Indice  |  Busqueda

Noticias | Arca | Enlaces | Sobre LF
Este documento está disponible en los siguientes idiomas: English  Castellano  ChineseGB  Deutsch  Francais  Italiano  Nederlands  Russian  Turkce  Polish  

convert to palmConvert to GutenPalm
or to PalmDoc

[Leonardo]
por Leonardo Giordani
<leo.giordani(at)libero.it>

Sobre el autor:

Estudiante de la Facultad de Ingenierías Politécnicas en Telecomunicaciones de Milán, trabaja como administrador de red y le interesa la programación (mayormente en ensamblador y C/C++). Desde 1999 trabaja solamente casi con Linux/Unix.

Taducido al español por:
Carlos Mayo (homepage)

Contenidos:


 

Programación concurrente - Principios e introducción a procesos

[run in paralell]

Resumen:

Esta serie de artículos tiene el propósito de introducir al lector el concepto de multitarea y a su implementación en el sistema operativo Linux. Comenzando por unos conceptos teóricos de la base de la multitarea, acabaremos escribiendo una aplicación completa demostrando la comunicación entre procesos, mediante un protocolo de comunicaciones simple pero eficiente. Los pre-requisitos para entender el artículo son:

  • Mínimo conocimiento del shell
  • Conocimientos básicos del lenguaje C (sintaxis, bucles, librerías)
Todas las referencias a las páginas del manual están puestas entre paréntesis después del nombre del comando. Todas las funciones de glibc están documentadas en las páginas info gnu (info Libc, o escribe info:/libc/Top en konqueror).
_________________ _________________ _________________

 

Introducción

Uno de los momentos mas importantes de la historia de los sistemas operativos fue el concepto de la multiprogramación, una técnica para entrelazar la ejecución de varios programas y conseguir un uso constante de los recursos del sistema. Pensemos en una estación de trabajo, donde un usuario puede ejecutar al mismo tiempo un procesador de textos, un reproductor de sonido, una cola de impresión, un navegador web, y algunos mas. Es un concepto importante en los sistemas operativos modernos. Como descubriremos, esta pequeña lista es solamente una mínima parte de todo el conjunto de programas que se están ejecutando actualmente en nuestra máquina.  

El concepto de proceso

Para entrelazar programas es necesaria una notable complicación del sistema operativo; para evitar conflictos entre los programas que se están ejecutando, una opción inevitable es encapsular cada uno de ellos con toda la información necesaria para su ejecución.

Antes de explorar qué ocurre en nuestra máquina Linux, un poco de vocabulario técnico: dado un PROGRAMA en ejecución, en un momento determinado, el CODIGO es el conjunto de instrucciones que lo constituyen, el ESPACIO DE MEMORIA es la cantidad de memoria de la máquina ocupada por sus datos y el ESTADO DEL PROCESADOR es el valor de los parámetros del microprocesador, como los flags o el Contador de Programa (dirección de la siguiente instrucción a ejecutar).

Definimos el término PROGRAMA EN EJECUCION como un número de objetos constituidos de CODIGO, ESPACIO DE MEMORIA y ESTADO DEL PROCESADOR. Si en un momento dado del funcionamiento de la máquina, grabamos esta información y la reemplazamos por el mismo conjunto de información obtenidos de otro programa en ejecución, el flujo de este último continuará desde el punto en el que se paró: haciendo esto una y otra vez con el primer y segundo programa, conseguiremos el entrelazamiento descrito anteriormente. El término PROCESO (o TAREA) se usa para describir a dicho programa en ejecución.

Expliquemos qué ocurría en la estación de trabajo de la que hablamos en la introducción: en cada momento solamente una tarea está en ejecución (sólo hay un microprocesador y no puede hacer dos cosas a la vez), y la máquina ejecuta parte de su código; después de una cierta cantidad de tiempo llamada QUANTUM, el proceso en ejecución se suspende, su información se guarda y se reemplaza por algún otro proceso en espera, cuyo código será ejecutado por un quantum de tiempo, y así sucesivamente. Esto esto lo que llamamos multitarea.

Como indiqué anteriormente, la introducción de multitarea origina una serie de problemas, la mayoría de los cuales no son triviales, como el mantenimiento de la cola de procesos en espera (PLANIFICACION); sin embargo se debe tener en cuenta la arquitectura de cada sistema operativo: quizás sea el tema principal de otro artículo, introduciendo quizá algunas partes del código del núcleo de Linux.  

Procesos en Linux y Unix

Descubramos algo sobre los procesos que se ejecutan en nuestra máquina. El comando que nos da tal información es ps(1) que son las siglas de "estado de proceso". Abriendo un shell normal de texto y tecleando el comando ps obtendremos una salida como esta

  PID TTY          TIME CMD
 2241 ttyp4    00:00:00 bash
 2346 ttyp4    00:00:00 ps

Sé que esta lista no esta completa, pero concentrémosno en ésta por el momento: ps nos ha devuelto una lista de cada proceso en ejecución en el terminal actual. Reconocemos en la última columna el nombre por el cual el proceso se ha ejecutado (como "mozilla" para el Navegador Web Mozilla y "gcc" para el Compilador C GNU). Evidentemente, "ps" aparece en la lista porque se estaba ejecutando cuando la lista de procesos en ejecución se imprimió. El otro proceso listado es el Bourne Again Shell, el shell que se ejecuta en mi terminal.

Dejemos (por el momento) la información acerca TIME y TTY y veamos PID, el IDentificador de Proceso. El pid es un número positivo (no cero) único que se le asigna a cada proceso en ejecución; una vez que el proceso ha terminado el pid puede ser reutilizado, pero tenemos asegurado que durante la ejecución de un proceso su pid será el mismo. Todo esto implica que la salida de cada uno de vosotros del comando ps sea diferente a la del ejemplo de arriba. Para comprobar que esto diciendo la verdad, abramos otra shell sin cerrar la primera y escribe el comado ps: esta vez la salida da la misma lista de procesos pero con diferentes números pid, demostrado que son dos procesos diferentes incluso si el programa es el mismo.

Podemos obtener también una lista de todos los procesos en ejecución de nuestro Linux: la página del manual del comando ps dice que la opcion -e significa "seleccionar todos los procesos". Escribamos "ps -e" en una terminal y ps imprimirá una larga lista como la vista arriba. Para analizar mas cómodamente esta lista, podemos redirigir la salida de ps en el fichero ps.log:

ps -e > ps.log

Ahora podemos leer este fichero editándolo con nuestro editor preferido (o simplemente con el comando less); como se dijo al comienzo de este artículo el número de procesos en ejecución es mayor del que podemos esperar. Observamos que la lista no contiene solamente los procesos comenzados por nosotros (a través de la linea de comandos o nuestro entorno gráfico) sino también un conjunto de procesos, alguno de los cuales con nombres extraños: el número y la identidad de los procesos listados dependen de la configuración de vuestro sistema, pero hay algunas cosas comunes. Primeramente, no importa que tipo de configuración tienes en el sistema, el proceso cuyo pid es igual a 1 es siempre "init", el padre de todos los procesos; posee el pid número 1 porque es siempre el primer proceso ejecutado por el sistema operativo. Otra cosa que podemos ver es la presencia de muchos procesos, cuyos nombres terminan con una "d": se llaman también "demonios" y son algunos de los procesos mas importantes del sistema. Estudiaremos con mas detalles init y los demonios en próximos artículos.  

Multitarea en la libc

Ya conocemos el concepto de proceso y cuanto de importante es para nuestro sistema operativo: continuaremos y comenzaremos a escribir código multitarea; desde la trivial ejecución simultánea de procesos nos trasladaremos a un nuevo problema: la comunicación entre procesos concurrentes y su sincronización; descubriremos dos soluciones elegantes a este problema, mensajes y semáforos, pero este último se abordará con mas profundidad un próximo artículo sobre hilos. Después de los mensajes va siendo hora de empezar a escribir nuestra aplicación basada en todos estos conceptos.

La librería estándar de C (libc, implementada en Linux con la glibc) usa las facilidades de la multitarea de Unix System V; el Unix System V (desde ahora SysV) es una implementación comercial de Unix, es el fundador de una de las dos familias mas importantes de Unix, la otra es Unix BSD.

En la libc el tipo pid_t está definido como un entero capaz de contener un pid. Desde ahora lo usaremos para almacenar el valor de un pid, pero solo por claridad: usar un entero es lo mismo.

Descubramos que función nos dice cual es el pid del proceso que contiene nuestro programa

pid_t getpid (void)

(la cual está definida con pid_t en unistd.h y sus/types.h) y escriba un programa cuyo objetivo sea imprimir por la salida estándar su pid. Con cualquier editor escriba el siguiente código

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main()
{
  pid_t pid;

  pid = getpid();
  printf("The pid assigned to the process is %d\n", pid);

  return 0;
}
Guarde el programa como print_pid.c y compílalo
gcc -Wall -o print_pid print_pid.c
esto creará un ejecutable llamado print_pid. Te recuerdo que si el directorio actual no esta en el path es necesario ejecutar el programa como "./print_pid". Al ejecutar el programa no nos llevaremos grandes sorpresas: imprime un número positivo y, si se ejecuta mas veces, se puede ver que ese número se incrementa uno a uno; esto no es obligatorio, ya que se puede crear otro proceso entre una ejecución de print_pid y la siguiente. Intenta, por ejemplo, ejecutar ps entre dos ejecuciones de print_pd...

Ya es hora de aprender a crear procesos, pero debo añadir algunas palabras sobre que ocurre realmente durante esta acción. Cuando un programa (contenido en el proceso A) crea otro proceso (B) los dos son idénticos, tienen el mismo código, la memoria llena de los mismos datos (no la misma memoria) y el mismo estado del procesador. A partir de este punto los dos se ejecutarán de manera diferente, por ejemplo dependiendo de la entrada del usuario o algún dato aleatorio. El proceso A es el "proceso padre" mientras que el B es el "proceso hijo"; ahora es mas fácil de entender el concepto "padre de todos los procesos" dado a init. La función que crea un nuevo proceso es

pid_t fork(void)
y su nombre viene de la propiedad de bifurcar la ejecución de procesos. El número devuelto es un pid, pero merece una atención particular. Dijimos que el actual proceso se duplica en un padre y un hijo, que se ejecutarán entrelazándose con los otros procesos en ejecución, haciendo diferentes trabajos; pero inmediátamente después de la duplicación, ¿que proceso será ejecutado, el padre o el hijo? Bueno, la respuesta es simple: uno de los dos. La decisión de que proceso debe ser ejecutado la toma una parte del sistema operativo llamado planificador, y no presta atención si un proceso es padre o hijo, sino que sigue un algoritmo basado en otros parámetros.

De todas formas, es importante conocer que proceso está en ejecución, ya que el código es el mismo. Ambos procesos contendrán el código del padre y del hijo, pero cada uno de ellos debe ejecutar solo uno de los códigos. Para clarificar este concepto, veamos el siguiente algoritmo:

- FORK
- IF YOU ARE THE SON EXECUTE (...)
- IF YOU ARE THE FATHER EXECUTE (...)
que representa en un pequeño pseudocódigo el código de nuestro programa. Vamos a revelar el misterio: la función fork devuelve '0' al proceso hijo y el pid del hijo al padre. Así que es suficiente comprobar si el pid devuelto es cero y sabremos que proceso esta ejecutando el código. En el lenguaje C obtendremos
int main()
{
  pid_t pid;

  pid = fork();
  if (pid == 0)
  {
    CODE OF THE SON PROCESS
  }
  CODE OF THE FATHER PROCESS
}
Es hora de escribir el primer ejemplo real de código multitarea: puedes grabarlo en un fichero fork_demo.c y compilarlo como se hizo anteriormente. He puesto el número de las lineas sólo por claridad. El programa se bifurcará a si mismo y el padre y el hijo escribirán algo en pantalla; la salida final será el entrelazamiento de las dos salidas (si todo va bien).
(01) #include <unistd.h>
(02) #include <sys/types.h>
(03) #include <stdio.h>

(04) int main()
(05) {
(05)   pid_t pid;
(06)   int i;

(07)   pid = fork();

(08)   if (pid == 0){
(09)     for (i = 0; i  < 8; i++){
(10)       printf("-SON-\n");
(11)     }
(12)     return(0);
(13)   }

(14)   for (i = 0; i < 8; i++){
(15)     printf("+FATHER+\n");
(16)   }

(17)   return(0);
(18) }

Las lineas número (01)-(03) contienen los includes de las librerías necesarias (E/S estándar, multitarea).
El main (como siempre en GNU), devuelve un entero, que es normalmente cero si el programa llega al final sin errores o un código de error si va algo mal; supondremos por ahora que todo marcha sin errores (añadiremos control de errores cuando estén claros los conceptos básicos). Luego definimos un tipo de dato para contener un pid (05) y un entero para usarlo como contador del bucle (06). Estos dos tipos, como se dijo antes, son idénticos, pero están ahí para mayor claridad.
En la linea (07) llamamos a la función fork, la cual devolverá cero al programa ejecutado en el proceso hijo y el pid del proceso hijo al padre; la comprobación está en la linea (08). Ahora el código de las lineas (09)-(13) se ejecutarán en el proceso hijo, mientras que el resto (14)-(16) las ejecutará el padre.
Las dos partes simplemente escriben 8 veces en la salida estándar la palabra "-SON-" o "+FATHER+", dependiendo en que proceso se ejecuta, y luego termina devolviendo 0. Esto es verdaderamente importante, ya que sin este último "return" el proceso hijo, una vez que el bucle termine, seguirá ejecutando el código del padre (compruébalo, no dañará tu máquina, simplemente no hará lo que queremos). Por eso, un error será realmente difícil de encontrar, desde la ejecución de un programa multitarea (especialmente uno complejo) se da diferentes resultados en cada ejecución, haciendo la depuración basada en los resultados simplemente imposible.

Ejecutando el programa quizás quedes insatisfecho: no puedo asegurar de que el resultado será una mezcla real entre dos cadenas, y esto es debido a la velocidad de ejecución del pequeño bucle. Probablemente tu salida sea una sucesión de cadenas "+FATHER+" seguidas por unas de "-SON-" o viceversa. Sin embargo, intenta ejecutar mas de una vez el programa y el resultado podrá cambiar.

Insertando un retraso aleatorio antes de cada llamada a printf, obtendremos un mejor efecto visual de la multitarea: lo hacemos con las funciones sleep y rand.

sleep(rand()%4)
esto hace que el programa duerma durante un número aleatorio de segundos entre 0 y 3 (% devuelve el resto de una división entera). Ahora el código es
(09)  for (i = 0; i < 8; i++){
(->)    sleep (rand()%4);
(10)    printf("-FIGLIO-\n");
(11)  }
y lo mismo para el código del padre. Guárdalo como fork_demo2.c, compílalo y ejecútalo. Ahora es mas lento, pero notamos la diferencia en el orden de salida:
[leo@mobile ipc2]$ ./fork_demo2
-SON-
+FATHER+
+FATHER+
-SON-
-SON-
+FATHER+
+FATHER+
-SON-
-FIGLIO-
+FATHER+
+FATHER+
-SON-
-SON-
-SON-
+FATHER+
+FATHER+
[leo@mobile ipc2]$

Ahora miremos los problemas que tenemos que ahora hacer frente: podemos crear un cierto número de procesos hijo dado un proceso padre, de modo que ejecuten operaciones diferentes a las que ejecute el proceso padre en un entorno de procesamiento concurrente; a menudo, el padre necesita comunicarse con sus hijos o al menos sincronizarse con ellos, para ejecutar operaciones en el orden correcto. Un primer modo para obtener dicha sincronización entre procesos es la función wait

pid_t waitpid (pid_t PID, int *STATUS_PTR, int OPTIONS)
donde PID es el PID del proceso cuyo fin estamos esperando, STATUS_PTR un puntero a un entero el cual contendrá el estado del proceso hijo (NULL si no se necesita la información) y OPTIONS un conjunto de opciones que no debemos tener en cuenta por ahora. Este es un ejemplo de un programa en el cual el padre crea un proceso hijo y espera a que acabe
#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>

int main()
{
  pid_t pid;
  int i;

  pid = fork();

  if (pid == 0){
    for (i = 0; i < 14; i++){
      sleep (rand()%4);
      printf("-SON-\n");
    }
    return 0;
  }

  sleep (rand()%4);
  printf("+FATHER+ Waiting for son's termination...\n");
  waitpid (pid, NULL, 0);
  printf("+FATHER+ ...ended\n");

  return 0;
}
La función sleep en el código del padre se ha insertado para diferenciar las ejecuciones. Graba el código como fork_demo3.c, compílalo y ejecútalo. ¡Acabamos de escribir nuestra primera aplicación multitarea sincronizada!

En el próximo artículo aprenderemos más sobre sincronización y comunicación entre procesos; ahora escribe tus programas usando las funciones descritas y envíamelas y así podré usar alguna de ellas para mostrar soluciones buenas o errores. Envíame el fichero .c con el código comentado y un pequeño fichero de texto con la descripción del programa, tu nombre y tu dirección de e-mail. ¡Buen trabajo!  

Lecturas recomendadas

 

Formulario de "talkback" para este artículo

Cada artículo tiene su propia página de "talkback". A través de esa página puedes enviar un comentario o consultar los comentarios de otros lectores
 Ir a la página de "talkback" 

Contactar con el equipo de LinuFocus
© Leonardo Giordani, FDL
LinuxFocus.org
Información sobre la traducción:
it --> -- : Leonardo Giordani <leo.giordani(at)libero.it>
it --> en: Leonardo Giordani <leo.giordani(at)libero.it>
en --> es: Carlos Mayo (homepage)

2002-12-09, generated by lfparser version 2.34