Curso de programación en C para GNU/Linux (V)

En la pasada entrega estuvimos enredando con los directorios y con los modos de acceso a los ficheros. Vamos a tratar ahora la gestión de múltiples procesos y pronto entraremos en la comunicación entre ellos 😉

 

Creación y duplicación de procesos

Una situación muy habitual dentro de un programa es la de crear un nuevo proceso que se encargue de una tarea concreta, descargando al proceso principal de tareas secundarias que pueden realizarse asíncronamente o en paralelo. Linux ofrece varias funciones para realizar esto: system(), fork() y exec().

Con system() nuestro programa consigue detener su ejecución para llamar a un comando de la shell (“/bin/sh” típicamente) y retornar cuando éste haya acabado. Si la shell no está disponible, retorna el valor 127, o –1 si se produce un error de otro tipo. Si todo ha ido bien, system() devuelve el valor de retorno del comando ejecutado. Su prototipo es el siguiente:

int system(const char *string);

Donde “string” es la cadena que contiene el comando que queremos ejecutar, por ejemplo:

system(“clear”);

Esta llamada limpiaría de caracteres la terminal, llamando al comando “clear”. Este tipo de llamadas a system() son muy peligrosas, ya que si no indicamos el PATH completo (“/usr/bin/clear”), alguien que conozca nuestra llamada (bien porque analiza el comportamiento del programa, bien por usar el comando strings, bien porque es muy muy muy sagaz), podría modificar el PATH para que apunte a su comando clear y no al del sistema (imaginemos que el programa en cuestión tiene privilegios de root y ese clear se cambia por una copia de /bin/sh: el intruso conseguiría una shell de root).

La función system() bloquea el programa hasta que retorna, y además tiene problemas de seguridad implícitos, por lo que desaconsejo su uso más allá de programas simples y sin importancia.

La segunda manera de crear nuevos procesos es mediante fork(). Esta función crea un proceso nuevo o “proceso hijo” que es exactamente igual que el “proceso padre”. Si fork() se ejecuta con éxito devuelve:

  • Al padre: el PID del proceso hijo creado.
  • Al hijo: el valor 0.

Para entendernos, fork() clona los procesos (bueno, realmente es clone() quien clona los procesos, pero fork() hace algo bastante similar). Es como una máquina para replicar personas: en una de las dos cabinas de nuestra máquina entra una persona con una pizarra en la mano. Se activa la máquina y esa persona es clonada. En la cabina contigua hay una persona idéntica a la primera, con sus mismos recuerdos, misma edad, mismo aspecto, etc. pero al salir de la máquina, las dos copias miran sus pizarras y en la de la persona original está el número de copia de la persona copiada y en la de la “persona copia” hay un cero:

cursoc03.gif

Duplicación de procesos mediante fork().

En la anterior figura vemos como nuestro incauto voluntario entra en la máquina replicadora con la pizarra en blanco. Cuando la activamos, tras una descarga de neutrinos capaz de provocarle anginas a Radiactivoman, obtenemos una copia exacta en la otra cabina, sólo que en cada una de las pizarras la máquina ha impreso valores diferentes: “123”, es decir, el identificativo de la copia, en la pizarra del original, y un “0” en la pizarra de la copia. No hace falta decir que suele ser bastante traumático salir de una máquina como esta y comprobar que tu pizarra tiene un “0”, darte cuenta que no eres más que una vulgar copia en este mundo. Por suerte, los procesos no se deprimen y siguen funcionando correctamente.

Veamos el uso de fork() con un sencillo ejemplo:

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

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

	if ( (pid=fork()) == 0 )
	{ /* hijo */
		printf("Soy el hijo (%d, hijo de %d)\n", getpid(),
        getppid());
	}
	else
	{ /* padre */
		printf("Soy el padre (%d, hijo de %d)\n", getpid(),
        getppid());
	}

	return 0;
}

Guardamos en la variable “pid” el resultado de fork(). Si es 0, resulta que estamos en el proceso hijo, por lo que haremos lo que tenga que hacer el hijo. Si es distinto de cero, estamos dentro del proceso padre, por lo tanto todo el código que vaya en la parte “else” de esa condicional sólo se ejecutará en el proceso padre. La salida de la ejecución de este programa es la siguiente:

txipi@neon:~$ gcc fork.c –o fork
txipi@neon:~$ ./fork
Soy el padre (569, hijo de 314)
Soy el hijo (570, hijo de 569)
txipi@neon:~$ pgrep bash
314

La salida de las dos llamadas a printf(), la del padre y la del hijo, son asíncronas, es decir, podría haber salido primero la del hijo, ya que está corriendo en un proceso separado, que puede ejecutarse antes en un entorno multiprogramado. El hijo, 570, afirma ser hijo de 569, y su padre, 569, es a su vez hijo de la shell en la que nos encontramos, 314. Si quisiéramos que el padre esperara a alguno de sus hijos deberemos dotar de sincronismo a este programa, utilizando las siguientes funciones:

pid_t wait(int *status)
pid_t waitpid(pid_t pid, int *status, int options);

La primera de ellas espera a cualquiera de los hijos y devuelve en la variable entera “status” el estado de salida del hijo (si el hijo ha acabado su ejecución sin error, lo normal es que haya devuelto cero). La segunda función, waitpid(), espera a un hijo en concreto, el que especifiquemos en “pid”. Ese PID o identificativo de proceso lo obtendremos al hacer la llamada a fork() para ese hijo en concreto, por lo que conviene guardar el valor devuelto por fork(). En el siguiente ejemplo combinaremos la llamada a waitpid() con la creación de un árbol de procesos más complejo, con un padre y dos hijos:

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

int main(int argc, char *argv[])
{
	pid_t pid1, pid2;
	int status1, status2;

	if ( (pid1=fork()) == 0 )
	{ /* hijo */
		printf("Soy el primer hijo (%d, hijo de %d)\n",  getpid(), getppid());
	}
	else
 	{ /*  padre */
 		if ( (pid2=fork()) == 0 )
 		{ /* segundo hijo  */
 			printf("Soy el segundo hijo (%d, hijo de %d)\n",  getpid(), getppid());
		}
		else
		{ /* padre */
/* Esperamos al primer hijo */
			waitpid(pid1, &status1, 0);
/* Esperamos al segundo hijo */
			waitpid(pid2, &status2, 0);
			printf("Soy el padre (%d, hijo de %d)\n", getpid(), getppid());
 		}
	}

	return 0;
}

El resultado de la ejecución de este programa es este:

txipi@neon:~$ gcc doshijos.c –o doshijos
txipi@neon:~$ ./ doshijos
Soy el primer hijo (15503, hijo de 15502)
Soy el segundo hijo (15504, hijo de 15502)
Soy el padre (15502, hijo de 15471)
txipi@neon:~$ pgrep bash
15471

Con waitpid() aseguramos que el padre va a esperar a sus dos hijos antes de continuar, por lo que el mensaje de “Soy el padre…” siempre saldrá el último.

Se pueden crear árboles de procesos más complejos, veamos un ejemplo de un proceso hijo que tiene a su vez otro hijo, es decir, de un proceso abuelo, otro padre y otro hijo:

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

int main(int argc, char *argv[])
{
	pid_t pid1, pid2;
	int status1, status2;

	if ( (pid1=fork()) == 0 )
	{ /* hijo (1a generacion) = padre */
		if ( (pid2=fork()) == 0 )
		{ /* hijo (2a generacion)  = nieto */
			printf("Soy el nieto (%d, hijo de %d)\n",
getpid(), getppid());
		}
		else
		{ /* padre (2a generacion) = padre */
			wait(&status2);
			printf("Soy el padre (%d, hijo de %d)\n",
getpid(), getppid());
		}
	}
	else
	{ /* padre (1a generacion) = abuelo */
		wait(&status1);
		printf("Soy el abuelo (%d, hijo de %d)\n", getpid(),
getppid());
	}

	return 0;
}

Y el resultado de su ejecución sería:

txipi@neon:~$ gcc hijopadrenieto.c -o hijopadrenieto
txipi@neon:~$ ./hijopadrenieto
Soy el nieto (15565, hijo de 15564)
Soy el padre (15564, hijo de 15563)
Soy el abuelo (15563, hijo de 15471)
txipi@neon:~$ pgrep bash
15471

Tal y como hemos dispuesto las llamadas a wait(), paradójicamente el abuelo esperará a que se muera su hijo (es decir, el padre), para terminar, y el padre a que se muera su hijo (es decir, el nieto), por lo que la salida de este programa siempre tendrá el orden: nieto, padre, abuelo. Se pueden hacer árboles de procesos mucho más complejos, pero una vez visto cómo hacer múltiples hijos y cómo hacer múltiples generaciones, el resto es bastante trivial.

Otra manera de crear nuevos procesos, bueno, más bien de modificar los existentes, es mediante el uso de las funciones exec(). Con estas funciones lo que conseguimos es reemplazar la imagen del proceso actual por la de un comando o programa que invoquemos, de manera similar a como lo hacíamos al llamar a system(). En función de cómo queramos realizar esa llamada, elegiremos una de las siguientes funciones:

int execl( const char *path, const char *arg, ...);
int execlp( const char *file, const char *arg, ...);
int execle( const char * path, const  char  *arg  ,  ..., char * const envp[]);
int execv( const char * path, char *const argv[]);
int execvp( const char *file, char *const argv[]);
int  execve(const  char  *filename, char *const argv [], char *const envp[]);

El primer argumento es el fichero ejecutable que queremos llamar. Las funciones que contienen puntos suspensivos en su declaración indican que los parámetros del ejecutable se incluirán ahí, en argumentos separados. Las funciones terminadas en “e” ( execle() y execve() ) reciben un último argumento que es un puntero a las variables de entorno. Un ejemplo sencillo nos sacará de dudas:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char *argv[])
{
	char *args[] = { "/bin/ls", NULL };

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

	printf("Se ha producido un error al ejecutar execv.\n");

	return 0;
}

La función elegida, execv(), recibe dos argumentos, el path al fichero ejecutable (“/bin/ls”) y un array con los parámetros que queremos pasar. Este array tiene la misma estructura que argv, es decir, su primer elemento es el propio programa que queremos llamar, luego se va rellenando con los argumentos para el programa y por último se finaliza con un puntero nulo (NULL). El printf() final no debería salir nunca, ya que para ese entonces execv() se habrá encargado de reemplazar la imagen del proceso actual con la de la llamada a “/bin/ls”. La salida de este programa es la siguiente:

txipi@neon:~$ gcc execv.c -o execv
txipi@neon:~$./execv
doshijos    execv    fil2    files.c         hijopadrenieto.c
doshijos.c  execv.c  fil2.c  hijopadrenieto

36 pensamientos en “Curso de programación en C para GNU/Linux (V)

  1. Elias Morales Escalante

    la cracion de procesos que muestra esta pagina esta muy buena
    en la cual me srvio bastante ahora lo quiero saber es como compilarlo en linux el archivo creado en C

    Responder
  2. zixit

    Muy bien explicado una excelente aportacion Gracias por la dedicacion y el tiempo 🙂

    (muchas paginas solo te confunden mas, esta te explica paso a paso)

    Responder
  3. juan diego

    Soy un estudiante de la universidad de alicante. En un ejercicio de una asignatura debemos crear un arbol de procesos. Me ha sido muy util esta pagina para saber como crear hermanos, ya que como yo lo hacia me creaba tres hijos, un dos los cuales tenia dos padre, un padre era el hermano de los tres, y el otro padre era uno de los hijos. <un jaleo, asi que muchas gracias. No se muy bien como funciona el foro asi que agradeceria una respuesta personal a mi correo.

    Mi duda es la siguiene:

    Quiero poner un "for" para hacer un arbol segun un parametro que el usuario pasa por parametro al programa, pero no se donde poner el "for".
    mUCHAS GRACIAS Y UN SALUDO.

    Responder
  4. txipi

    Me alegro que os haya servido el tutorial 🙂

    @juan diego: ¿cómo es ese parámetro que te pasan? Quizá tengas que parsearlo primero con strtok() o similares.

    Responder
  5. Emilio

    Cuando haces un:

    pid=wait(&status);

    cómo puedes extraer el valor de salida, el status da un número raro cuando digo que salga con 1 me da un 256, no sé a qué se debe o si tengo que hacer un & con algún valor.

    Responder
  6. Chori

    muchas gracias por esta guia, esta muy clara.

    @Carlos: yo no soy ningún experto ni mucho menos en este tema, pero por lo que aprendí en mi clase de Sistemas Operativos podes usar algún Pipe (tubería). para no confundirte te diría que lo mejor es que busques en Internet como funcionan.

    Saludos

    Responder
  7. Pingback: Curso de programación en C para GNU/Linux (VI) « txipi:blog

  8. manuzafra

    hola, muy buen tutorial, no tenia ni idea de hijos y padres y cosas de esas, y me lo has aclarado todo.

    Una cosa, necesito saber mas sobre la ultima parte, sobre el uso de funciones exec()
    tengo que realizar una practica que haga basicamente lo que hace la funcion system() pero sin usar system() claro esta.

    Te puedo preguntar, me pudes informar, sabes donde hay info especifica.
    Ya le he dado mil vueltas al google.
    MUchas gracias. ¿Puedes responderme a mi email?. Gracias

    Responder
  9. txipi

    @manuzafra: es bastante sencillo el uso de exec() y similares. Solamente tienes que preparar un array tal y como lo necesita la función y listo. De todas maneras, para llamar a comandos lo mejor y más cómodo es popen().

    Responder
  10. antoñito

    Podrias decir como supeditar la creacion de hijos a un numero pedido por teclado.
    Es decir, el programa te pide un numero por teclado, mas o menos bajo, <10 por ejemplo y el programa debe crear tantos hijos (o padres o nietos, el caso es que sean distintos procesos) como el numero pedido.

    Responder
  11. Edwin

    gracias por los items del tutorial, me han ayudado mucho para comprender la relacion entre procesos. Ahora estoy construyendo un pequeño shell como proyecto, quisiera un poco mas de informacion sobre el EXEC() y señales de enmascaramiento (SIGPROCMASK) ¿donde podria encontrarla please?.

    Responder
  12. mc_disck

    Ke onda, está chido el tutorial pero tengo una duda con respecto a la familia de funciones exec, quisiera saber si se puede llamar a cualquier programa ejecutable es decir que si puedo llamar a un programa que yo haya hecho??

    Responder
  13. Clima

    Hola, con tres procesos A, B, C en que A es Abuelo, B, Padre y C nieto, tenemos Pipes entre A==B==C

    Seria posible crear una Pipe entre A y C directamente para que C escribiera algun mensaje a A? Como se debería hacer??

    Gracias, J.

    Responder
  14. Oriol

    Buenas.
    Exelente blog bajo mi humilde punto de vista. Me estoy iniciando en el mundo de los procesos, y la verdad es que me ha sido de gran ayuda. Eso si, estoy haciendo una practica y no consigo sacarla, haber si me podeis hechar un pequeño cable.
    Tengo que sacar 100 hijos, y los dos primeros tienen que tener un nieto, pero este nieto saldrá antes que sus padres, y por ultimo, despues de los 100 hijos, aparecerá el padre. Gracias por la ayuda de antemano 😉

    Oriol

    Responder
  15. txipi

    @rgx112: probablemente muchos sean muy parecidos porque en su día leí ese libro y recuerdo que probé bastantes de las cosas que contaba.

    Lo más seguro es que si hay diferencias, sean para peor, porque este curso ha sido poco más que la «formalización» de unos apuntes que preparé hace tiempo y lejos queda de un libro en condiciones como pueda ser el de Kurt Wall. Así que ante la duda, os recomiendo su libro porque seguro que cuenta más cosas y con más nivel de detalle 😉

    Responder
  16. civac

    Muchas gracias por tu esfuerzo y dedicación. No todo el mundo se molesta en ayudar a los demás y este post sin duda me ha servido de mucha ayuda. Muy buena la explicación. Y sobretodo gracias por no pedir que te dejen comentarios! jajaja no soporto la gente que hace eso. De nuevo muchas gracias por el post! 😀

    Responder
  17. omar

    excelente explicacion, muchas gracias por poner esto ayuda mucho a la resolucion de problemas y felicidades al creador de esta pagina

    Responder
  18. Arturo

    Hola que tal! lo primero de todo, decir que me ha parecido muy buena la explicación de los fork y los exec, me ha gustado porque has sido muy gráfico. Pero he estado compilando los ejemplos y con el primer ejemplo del fork hay un error, que no se ve a simple vista, y es que el padre no hace wait para esperar al proceso hijo… por lo cuál, si el padre se ejecuta primero (que al haber concurrencia es perfectamente posible) puede morir antes que el hijo y el proceso hijo quedaría en estado «zombie». El problema reside en que si el hijo zombie saca getppid() de su padre, al estar muerto, devuelve un pid = 1, que es un proceso especial que rescata procesos zombie.

    solución posible:

    pid_t pid;
    int status;

    if ( (pid=fork()) == 0 )
    { /* hijo */
    printf(«Soy el hijo (%d, hijo de %d)\n», getpid(),
    getppid());
    exit(1);
    }
    else
    { /* padre */
    wait(&status); //espera a que el hijo termine <==== SOLUCIÓN
    printf("Soy el padre (%d, hijo de %d)\n", getpid(),
    getppid());
    }

    Un saludo!

    Responder
  19. Rafael Sobrevilla

    Muchas gracias me has ayudo mucho, muy buena explicación y entindes muy bien que hace el sistema en cada uno de los casos, de que país eres ? Te gustaría trabajar en desarrollo de sistemas embebidos linux en la empresa que trabajo?

    Rafael

    Responder
  20. Pingback: Actividad Control de procesos del Sistema Operativo « Cursos Rubisoll

  21. Pingback: Creación y duplicación de procesos en C – Linux – Programación

Responder a Oriol Cancelar respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *