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

En la pasada entrega estuvimos tratando múltiples procesos. En esta ocasión veremos la forma más rudimentaria de comunicación entre procesos: las señales.

 

Comunicación entre procesos

En un sistema multiprogramado, con un montón de procesos funcionando al mismo tiempo, es necesario establecer mecanismos de comunicación entre los procesos para que puedan colaborar entre ellos. Existen varios enfoques a la hora de implementar esta comunicación.

Podemos considerar a las señales como la forma más primitiva de comunicación entre procesos. El sistema utiliza señales para informar a un determinado proceso sobre alguna condición, realizar esperas entre procesos, etc. Sin embargo la señal en sí no es portadora de datos, a fin de cuentas es una “seña” que se hacen de un proceso a otro, que permite a un proceso enterarse de una determinada condición, pero sin poder transmitirse cantidades grandes de información entre ambos procesos. Un gesto con la mano puede servirte para detenerte mientras vas andando por un pasillo, pero difícilmente te transmitirá toda la información contenida en “El Quijote” (al menos con las técnicas que yo conozco). Por lo tanto, además de las señales, es preciso disponer de mecanismos que permitan intercambiar datos entre los procesos.

El enfoque más obvio de todos es utilizar ficheros del sistema para poder escribir y leer de ellos, pero esto es lento, poco eficiente e inseguro, aunque muy sencillo de hacer. El siguiente paso podría ser utilizar una tubería o un FIFO para intercomunicar los procesos a través de él. El rendimiento es superior respecto al enfoque anterior, pero sólo se utilizan en casos sencillos. Imaginemos lo costoso que sería implementar un mecanismo de semáforos de esta manera.

Como evolución de todo lo anterior llegó el sistema IPC (Inter Process Communication) de System V, con sus tres tipos de comunicación diferentes: semáforos, colas de mensajes y segmentos de memoria compartida. Actualmente el estándar de IPC System V ha sido reemplazado por otro estándar, el IPC POSIX. Ambos implementan características avanzadas de los sistemas de comunicación entre procesos de manera bastante eficiente, por lo que convendría pensar en su empleo a la hora de realizar una aplicación multiproceso bien diseñada.

Señales

Cuando implementamos un programa, línea a línea vamos definiendo el curso de ejecución del mismo, con condicionales, bucles, etc. Sin embargo hay ocasiones en las que nos interesaría contemplar sucesos asíncronos, es decir, que pueden suceder en cualquier momento, no cuando nosotros los comprobemos. La manera más sencilla de contemplar esto es mediante el uso de señales. La pérdida de la conexión con el terminal, una interrupción de teclado o una condición de error como la de un proceso intentando acceder a una dirección inexistente de memoria podrían desencadenar que un proceso recibiese una señal. Una vez recibida, es tarea del proceso atrapar o capturarla y tratarla. Si una señal no se captura, el proceso muere.

En función del sistema en el que nos encontremos, bien el núcleo del Sistema Operativo, bien los procesos normales pueden elegir entre un conjunto de señales predefinidas, siempre que tengan los privilegios necesarios. Es decir, no todos los procesos se pueden comunicar con procesos privilegiados mediante señales. Esto provocaría que un usuario sin privilegios en el sistema sería capaz de matar un proceso importante mandando una señal SIGKILL, por ejemplo. Para mostrar las señales que nos proporciona nuestro núcleo y su identificativo numérico asociado, usaremos el siguiente comando:

txipi@neon:~$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
 5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE
 9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2
13) SIGPIPE     14) SIGALRM     15) SIGTERM     17) SIGCHLD
18) SIGCONT     19) SIGSTOP     20) SIGTSTP     21) SIGTTIN
22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO
30) SIGPWR      31) SIGSYS      32) SIGRTMIN    33) SIGRTMIN+1
34) SIGRTMIN+2  35) SIGRTMIN+3  36) SIGRTMIN+4  37) SIGRTMIN+5
38) SIGRTMIN+6  39) SIGRTMIN+7  40) SIGRTMIN+8  41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14
50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10
54) SIGRTMAX-9  55) SIGRTMAX-8  56) SIGRTMAX-7  57) SIGRTMAX-6
58) SIGRTMAX-5  59) SIGRTMAX-4  60) SIGRTMAX-3  61) SIGRTMAX-2
62) SIGRTMAX-1  63) SIGRTMAX

La mayoría de los identificativos numéricos son los mismos en diferentes arquitecturas y sistemas UNIX, pero pueden cambiar, por lo que conviene utilizar el nombre de la señal siempre que sea posible. Linux implementa las señales usando información almacenada en la task_struct del proceso. El número de señales soportadas está limitado normalmente al tamaño de palabra del procesador. Anteriormente, sólo los procesadores con un tamaño de palabra de 64 bits podían manejar hasta 64 señales, pero en la versión actual del kernel (2.4.19) disponemos de 64 señales incluso en arquitecturas de 32bits.

Todas las señales pueden ser ignoradas o bloqueadas, a excepción de SIGSTOP y SIGKILL, que son imposibles de ignorar. En función del tratamiento que especifiquemos para cada señal realizaremos la tarea predeterminada, una propia definida por el programador, o la ignoraremos (siempre que sea posible). Es decir, nuestro proceso modifica el tratamiento por defecto de la señal realizando llamadas al sistema que alteran la sigaction de la señal apropiada. Pronto veremos cómo utilizar esas llamadas al sistema en C.

Una limitación importante de las señales es que no tienen prioridades relativas, es decir, si dos señales llegan al mismo tiempo a un proceso puede que sean tratadas en cualquier orden, no podemos asegurar la prioridad de una en concreto. Otra limitación es la imposibilidad de tratar múltiples señales iguales: si nos llegan 14 señales SIGCONT a la vez, por ejemplo, el proceso funcionará como si hubiera recibido sólo una.

Cuando queremos que un proceso espere a que le llegue una señal, usaremos la función pause(). Esta función provoca que el proceso (o thread) en cuestión “duerma” hasta que le llegue una señal. Para capturar esa señal, el proceso deberá haber establecido un tratamiento de la misma con la función signal(). Aquí tenemos los prototipos de ambas funciones:

int pause(void);

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

La función pause() no parece tener demasiada complicación: no recibe ningún parámetro y retorna –1 cuando la llamada a la función que captura la señal ha terminado. La función signal() tiene un poco más de miga: recibe dos parámetros, el número de señal que queremos capturar (los números en el sistema en concreto en el que nos encontremos los podemos obtener ejecutando “kill –l”, como ya hemos visto), y un puntero a una función que se encargará de tratar la señal especificada. Esto puede parecer confuso, así que aclaremos esto con un ejemplo:

#include <signal.h>
#include <unistd.h>

void trapper(int);

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

	for(i=1;i<=64;i++)
		signal(i, trapper);

	printf("Identificativo de proceso: %d\n", getpid() );
	pause();
	printf("Continuando...\n");

	return 0;
}

void trapper(int sig)
{
	signal(sig, trapper);
	printf("Recibida la señal: %d\n", sig);
}

La explicación de este pequeño programa es bastante simple. Inicialmente declaramos una función que va a recibir un entero como parámetro y se encargará de capturar una señal ( trapper() ). Seguidamente capturamos todas las señales de 1 a 64 haciendo 64 llamadas a signal(), pasando como primer parámetro el número de la señal (i) y como segundo parámetro la función que se hará cargo de dicha señal (trapper). Seguidamente el programa indica su PID llamando a getpid() y espera a que le llegue una señal con la función pause(). El programa esperará indefinidamente la llegada de esa señal, y cuando le enviemos una (por ejemplo, pulsando Control+C), la función encargada de gestionarla ( trapper() ) será invocada. Lo primero que hace trapper() es volver a enlazar la señal en cuestión a la función encargada de gestionarla, es decir, ella misma, y luego saca por la salida estándar la señal recibida. Al terminal la ejecución de trapper(), se vuelve al punto donde estábamos ( pause() ) y se continua:

txipi@neon:~$ gcc trapper.c -o trapper
txipi@neon:~$ ./trapper
Identificativo de proceso: 15702
Recibida la señal: 2
Continuando...
txipi@neon:~$

Como podemos observar, capturar una señal es bastante sencillo. Intentemos ahora ser nosotros los emisores de señales a otros procesos. Si queremos enviar una señal desde la línea de comandos, utilizaremos el comando “kill”. La función de C que hace la misma labor se llama, originalmente, kill(). Esta función puede enviar cualquier señal a cualquier proceso, siempre y cuando tengamos los permisos adecuados (las credenciales de cada proceso, explicadas anteriormente, entran ahora en juego (uid, euid, etc.) ). Su prototipo es el siguiente:

int kill(pid_t pid, int sig);

No tiene mucha complicación, recibe dos parámetros, el PID del proceso que recibirá la señal, y la señal. El tipo pid_t es un tipo heredado de UNIX, que en Linux en concreto corresponde con un entero. El siguiente código de ejemplo realiza la misma función que el comando “kill”:

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

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

	if(argc==3)
	{
		pid=(pid_t)atoi(argv[1]);
		sig=atoi(argv[2]);

		kill(pid, sig);
	} else {
		printf("%s: %s pid signal\n", argv[0], argv[0]);
		return -1;
	}

	return 0;
}

Para probarlo, he programado un pequeño shell script que capturará las señales SIGHUP, SIGINT, SIGQUIT, SIGFPE, SIGALARM y SIGTERM:

#!/bin/sh
echo "Capturando signals..."
trap "echo SIGHUP recibida" 1
trap "echo SIGINT recibida " 2
trap "echo SIGQUIT recibida " 3
trap "echo SIGFPE recibida " 8
trap "echo SIGALARM recibida " 14
trap "echo SIGTERM recibida " 15

while true
do
  :
done

Simplemente saca un mensaje por pantalla cuando reciba la señal en concreto y permanece en un bucle infinito sin hacer nada. Vamos a enviarle unas cuantas señales desde nuestro programa anterior:

txipi@neon:~$ gcc killer.c -o killer
txipi@neon:~$./trap.sh &
1 15736
txipi@neon:~$ Capturando signals...

txipi@neon:~$./killer
./killer: ./killer pid signal
txipi@neon:~$./killer 15736 8
txipi@neon:~$ SIGFPE recibida

txipi@neon:~$./killer 15736 15
txipi@neon:~$ SIGTERM recibida

txipi@neon:~$ ./killer 15736 9
txipi@neon:~$ pgrep trap.sh
1+  Killed                  ./trap.sh

Primeramente llamamos al shell script “trap.sh” para que se ejecute en segundo plano (mediante “&”). Antes de pasar a segundo plano, se nos informa que el proceso tiene el PID 15736. Al ejecutar el programa “killer” vemos que recibe dos parámetros: el PID y el número de señal. Probamos a mandar unas cuantas señales que tiene capturadas y se comporta como es esperado, mostrando la señal recibida por pantalla. Cuando le enviamos la señal 9 (SIGKILL, incapturable), el proceso de “trap.sh” muere.

cursoc04.gif

Procesos recibiendo señales, rutinas de captura y proceso de señales.

En la figura anterior observamos el comportamiento de diferentes procesos en función de las señales que reciben y sus rutinas de tratamiento de señales: el primero de ellos no está preparado para capturar la señal que le llega, por lo que terminará su ejecución al no saber cómo tratar la señal. El segundo tiene una rutina asociada que captura señales, trapper, por lo que es capaz de capturar la señal SIG_USR1 y gestionarla adecuadamente. El tercer proceso también dispone de la rutina capturadora de señales y de la función asociada, pero le llega una señal incapturable, SIG_KILL, por lo que no es capaz tampoco de gestionarla y termina su ejecución.

Existe una manera de utilizar kill() de forma masiva: si en lugar de un PID le pasamos como parámetro “pid” un cero, matará a todos los procesos que estén en el mismo grupo de procesos que el actual. Si por el contrario pasamos “-1” en el parámetro “pid”, intentará matar a todos los procesos menos al proceso “init” y a sí mismo. Por supuesto, esto debería usarse en casos muy señalados, nunca mejor dicho O:-).

Una utilización bastante potente de las señales es el uso de SIGALARM para crear temporizadores en nuestros programas. Con la función alarm() lo que conseguimos es que nuestro proceso se envíe a sí mismo una señal SIGALARM en el número de segundos que especifiquemos. El prototipo de alarm() es el siguiente:

unsigned int alarm(unsigned int seconds);

En su único parámetro indicamos el número de segundos que queremos esperar desde la llamada a alarm() para recibir la señal SIGALARM.

cursoc05.gif

La llamada a la función alarm() generará una señal SIG_ALARM hacia el mismo proceso que la invoca.

El valor devuelto es el número de segundos que quedaban en la anterior alarma antes de fijar esta nueva alarma. Esto es importante: sólo disponemos de un temporizador para usar con alarm(), por lo que si llamamos seguidamente otra vez a alarm(), la alarma inicial será sobrescrita por la nueva. Veamos un ejemplo de su utilización:

#include <signal.h>
#include <unistd.h>

void trapper(int);

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

	signal(14, trapper);

	printf("Identificativo de proceso: %d\n", getpid() );

	alarm(5);
	pause();
	alarm(3);
	pause();
	for(;;)
	{
		alarm(1);
		pause();
	}

	return 0;
}

void trapper(int sig)
{
	signal(sig, trapper);
	printf("RIIIIIIIIING!\n");
}

Este programa es bastante similar al que hemos diseñado antes para capturar señales, sólo que ahora en lugar de capturarlas todas, capturará únicamente la 14, SIGALARM. Cuando reciba una señal SIGALARM, sacará “RIIIIIIIIING” por pantalla. El cuerpo del programa indica que se fijará una alarma de 5 segundos y luego se esperará hasta recibir una señal, luego la alarma se fijará a los 3 segundos y se volverá a esperar, y finalmente se entrará en un bucle en el que se fije una alarma de 1 segundo todo el rato. El resultado es que se mostrará un mensaje “RIIIIIIIIING” a los 5 segundos, luego a los 3 segundos y después cada segundo:

txipi@neon:~$ gcc alarma.c -o alarma
txipi@neon:~$ ./alarma
Identificativo de proceso: 15801
RIIIIIIIIING!
RIIIIIIIIING!
RIIIIIIIIING!
RIIIIIIIIING!
RIIIIIIIIING!
RIIIIIIIIING!
RIIIIIIIIING!

Para terminar esta sección, veremos cómo es relativamente sencillo que procesos creados mediante fork() sean capaces de comunicarse utilizando señales. En este ejemplo, el hijo envía varias señales SIGUSR1 a su padre y al final termina por matarlo, enviándole la señal SIGKILL (hay muchos hijos que son así de agradecidos):

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

void trapper(int sig)
{
 	signal(sig, trapper);
	printf("SIGUSR1\n");
}

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

	padre = getpid();

	signal( SIGUSR1, trapper );

	if ( (hijo=fork()) == 0 )
	{ /* hijo */
		sleep(1);
		kill(padre, SIGUSR1);
		sleep(1);
		kill(padre, SIGUSR1);
		sleep(1);
		kill( padre, SIGUSR1);
		sleep(1);
		kill(padre, SIGKILL);
		exit(0);
	}
	else
	{ /* padre */
		for (;;);
	}

	return 0;
}

Con la función sleep(), el hijo espera un determinado número de segundos antes de continuar. El uso de esta función puede intervenir con el uso de alarm(), así que habrá que utilizarlas con cuidado. La salida de este parricidio es la siguiente:

txipi@neon:~$ gcc signalfork.c -o signalfork
txipi@neon:~$./signalfork
SIGUSR1
SIGUSR1
SIGUSR1
Killed

25 pensamientos en “Curso de programación en C para GNU/Linux (VI)

  1. Joan

    Hola,
    tengo una duda:
    estoy ejecutando el segundo ejemplo de este capitulo y cuando le hago la llamada al trap.sh para que se ejecute en segundo plano, me dice:

    [1] 5762
    bash: ./trap.sh: Ha sido denegado el permiso

    y no sigue.

    Puedes indicarme que estoy haciendo mal? porqué me sale?

    Saludos y gracias por tu ayuda.

    Joan

    Responder
  2. duende

    Hola a todos, estas clases son muy buenas y faciles de entender, yo estoy recibiendo clases de señales ahora mismo y se entiende perfectamente, unicamente tengo una duda que aun no he podido comprender, en las funciones que creas llamadas trapper, porque vuelve a haber un signal() llamando a la misma funcion?, no quedaria en una especie de bucle infinito? Muchas graciass, y adelante con estas clases 😉

    Responder
  3. txipi

    @duende: el hecho de usar signal no provoca que se envíe una señal (que se hace con kill) sino que se capture de nuevo esa señal (tampoco lanza la rutina de captura, solamente indica que queremos capturarla). Realmente no es necesaria esa llamada, pero antiguamente se ponía porque algunas implementaciones hacían que una vez capturada la señal, se dejaba de capturar.

    Si tienes cualquier otra duda, aquí me tienes 😉

    Responder
  4. duende

    txipi gracias por contestar, tienes muchisima razon en lo que me contestaste, estube preguntandole a mi profesor y realmente esa es la única razon por la cual se vuelve a hacer referencia a signal(). Muchas gracias por todo ;). A la pregunta de Sara realmente no se me ocurre nada :S xD, pero podrias probar con un contador y un while preguntando si esa posicion de la cadena es diferente al \0, es decir: while(cad[cont]!= ‘\0’) cont++; Espero ke t sirva de ayuda. De nuevo gracias a todos 😀

    Responder
  5. zuljin

    Antes de nada, pedazo de manual, está de coña!!Felicidades. Soy nuevo por aquí y tengo una duda, a ver si alguien puede ser tan amable de decirme que podría hacer; Ahora mismo estoy haciendo un juego server/client, y quiero que cuando seintroduzca en el server un ctrl+c me mate todos los procesos hijos y dado que uso memoria compartida en todo ellos también la cierra, que debería hacer? (La verdad que de señales no se mucho, llevo solo 1 semana mirando info y no me aclaro mucho)

    en principio en el proceso padre he puesto un signal(SIGINT,handlerALL) antes del tipico for(;;), al introducir un ctrl+c me lo detecta y salta a la función pero despues me da un error y sale del programa sin haber eliminado todos los procesos hijos, alguien me podría dar alguna idea de como hacer, sin tener que usar señales del tipo: struct sigaction nom_sa; (esta no me van en mi distribución :S)

    Ahora mismo no tengo el código a mano pero recuerdo que la función handlerAll erá algo parecido a esta:

    void handlerAll(){
    while(waitpid(-1,null,WNOHANG) > 0);
    }

    Gracias por la atención. Un saludo

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

  7. Jorge Luis

    Hola
    alguien tiene un programa que tenga proceso un proceso padre y un hijo y que se intercalen… osea que muestre “Soy el parde… soy el hijo… soy el padre…” y el control+c solo funcione en el padre

    gracias

    Responder
  8. Laru

    Literalmente este curso me ha salvado la vida, buenisimos los ejemplos, directo al grano y muy bien explicado (las coñas entre ejemplo y ejemplo ayudan a suavizar y a la vez a mantenerte atento XD) .

    Muchas gracias.

    Solo una pequeña pega, o bueno, lo unico que a mi me ha resultado un poco dificil de leer.

    Lineas como:

    if ( (hijo=fork()) == 0 )

    Que normalmente cuando se trabaja como principante o en cursos se suele evitar, y se prefiere una forma menos compacta pero mas clara

    hijo=fork();
    if (hijo==0)

    No es tan dificil de entender pero a alguna gente le puede resultar confuso.

    Minucias, por lo demas me parece perfecto, gracias otra vez

    Responder
  9. Longinos

    Es la segunda vez que caigo en este sitio y las dos veces han sido de enorme ayuda, por lo que como mínimo he de darte un millón de gracias.

    Has creado un sitio de referencia, enhorabuena.

    Saludos desde tierras del Quijote.

    Responder
  10. Pingback: Señales y Alarmas en C – Linux – Programación

Deja un comentario

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