Programación concurrente en Linux

# Los procesos UNIX

Decíamos que el concepto de fichero es una abstracción fundamental en el mundo UNIX/Linux, en torno a la cual gira toda la entrada-salida. Existe una segunda abstracción fundamental en UNIX/Linux, el proceso, en torno a la cual se describe todo lo relativo a la ejecución de los programas.

Hasta ahora hablábamos de la “ejecución de un programa”. Ahora sabemos que este término es demasiado simple para describir las posibilidades de la ejecución de programas en el entorno UNIX/Linux.

Un programa se identifica mediante el nombre del fichero ejecutable que contiene su código, representado de acuerdo a un formato determinado. Un programa es por lo tanto una entidad estática. Una característica del fichero ejecutable es que el sistema operativo lo puede ejecutar, lo que significa cargarlo en memoria y cederle el control, tal como hace el shell.

Cuando el programa se ejecuta hay que considerar más información que la que contiene el fichero ejecutable. Por ejemplo, hay que asignar direcciones de memoria a variables y código, una tabla de descriptores de ficheros con sus descriptores estándar, y una pila para la ejecución, además de un puntero al programa (o contador de programa). Finalmente, el programa podría requerir recursos adicionales durante su ejecución, por ejemplo cuando abre un fichero, o cuando requiere memoria dinámicamente mediante `malloc(3)`.

¿Qué pasa si ejecutamos el mismo programa dos veces concurrentemente? Por ejemplo:

    ./sim_pos & ./sim_pos

Esto deja bien claro que el nombre del programa no es válido como identificador del programa en ejecución. Por si fuera poco, la llamada `fork()` permite crear un flujo de ejecución nuevo en el programa, que es una copia del código y las variables del padre (esto incluye la tabla de descriptores de ficheros y más cosas). Es como si el programa se hubiese clonado a sí mismo. Definitivamente, necesitamos una forma de identificar y describir estas entidades de ejecución dinámicas, a diferencia de los ficheros ejecutables, estáticos.

El concepto de proceso es precisamente eso, un flujo de ejecución del programa, identificado por un identificador de proceso único en el sistema (de nuevo, no tendremos que preocuparnos de rebasar el número máximo de identificadores, ya que es muy elevado). Además de su identificador, hay mucha información que describe a un proceso UNIX/Linux:

> - El código que ejecuta el proceso.
> - Las variables globales del proceso.
> - La pila asignada al proceso, donde se almacenan las variables locales.
> - Los descriptores de ficheros.
> - El estado de las señales.
> - El usuario y grupo propietarios del proceso.
> - El identificador del proceso padre.
> - Información para contabilidad: tiempos, recursos consumidos, etc.

Toda esta información conforma el contexto privado del proceso. El proceso dispone de una copia propia de todo ello durante toda su vida, que se libera cuando el proceso acaba (ya sea ejecutando `exit()` o de formas más expeditivas).

A modo de ejemplo, el shell contiene un buen número de variables, las variables de entorno, que forman parte de su contexto privado. Esto lo podemos comprobar con el `bash` al ejecutar la siguiente secuencia de órdenes:

    pwd
    bash
    pwd
    cd un_subdirectorio
    exit
    pwd

Ahora podemos ser más precisos. Un proceso se crea cuando otro proceso ejecuta `fork()`. El proceso recién creado, proceso hijo, hereda el contexto del proceso padre al proporcionarle el sistema operativo una copia idéntica a la del padre. Con una sola diferencia: el sistema asigna un identificador de proceso nuevo al proceso hijo. Así, a partir del valor devuelto por `fork()` podemos hacer que el código sea ejecutado solo por el hijo, solo por el padre, o por ambos. Observa finalmente que un proceso puede conocer su nombre (llamada al sistema `getpid()`) y el nombre de su padre (llamada `getppid()`), así como los nombres de sus hijos a través del valor de retorno de `fork()`.

De igual forma que representamos todo el sistema de ficheros de Linux en un árbol, también podemos dibujar el árbol genealógico de los procesos de Linux en un momento dado. ¿Cuál es el proceso progenitor de todos los procesos, la raíz del árbol? Podrás identificarlo con `ps aux` como el proceso con identificador 1. Este proceso, tradicionalmente conocido como proceso init, es también el proceso encargado de adoptar como hijos a los procesos que se quedan huérfanos. Como ves, la historia de los procesos en Linux se presenta apasionante :-)