Servicios IPC universales
Las primeras distribuciones de UNIX únicamente disponían de tres mecanismos que podían ser utilizados para la comunicación entre procesos: las señales, las tuberías y el seguimiento de procesos.
Señales
Las señales se utilizan principalmente para notificar a un proceso eventos asíncronos. Originariamente fueron concebidas para el tratamiento de errores, aunque también pueden ser utilizadas como mecanismo IPC. Las versiones modernas de UNIX reconocen más de 31 señales diferentes. La mayoría tienen un significado predefinido, pero existen dos, SIGUSR1 y SIGUSR2, que pueden ser utilizadas por los usuarios según sus necesidades. Un proceso puede enviar una señal a un proceso o a un grupo de procesos usando por ejemplo la llamada al sistema kill. Además el núcleo genera señales internamente en respuesta de distintos eventos.
Como mecanismo IPC, las señales poseen varias limitaciones
- Las señales resultan costosas en relación a las tareas que suponen para el sistema. El proceso que envía la señal debe realizar una llamada al sistema; el núcleo debe interrumpir al proceso receptor y manipular la pila de usuario de dicho proceso, para invocar al manipulador de la señal y posteriormente poder retomar la ejecución del proceso interrumpido.
- Tienen un ancho de banda limitado, ya que solamente existen 31 tipos de señales distintas (en SVR4 y BSD4.3).
- Una señal puede transportar una cantidad limitada de información.
En conclusión, las señales son útiles para la notificación de eventos, pero resultan poco útiles como mecanismo IPC.
Tuberías
En su implementación tradicional, una tubería es un mecanismo de comunicación unidireccional, que permite la transmisión de un flujo de datos no estructurados de tamaño fijo. Unos procesos (emisores) pueden escribir datos en un extremo de la tubería y otros procesos (receptores) pueden leer estos datos en el otro extremo (ver Figura 7.1). Si bien debe quedar claro que en un cierto instante de tiempo solamente un proceso estará usando la tubería, bien para escribir o bien para leer. Una vez que los datos son leídos por un proceso, estos son borrados de la tubería y en consecuencia ya no pueden ser leídos por otros procesos.
![]() |
|---|
Figura 7.1: Datos fluyendo a través de una tubería
Las tuberías proporcionan un mecanismo de control del flujo de datos bastante simple. Un proceso intentando leer de una tubería vacía se bloqueará hasta que se escriban datos en la tubería. Asimismo, un proceso intentando escribir en una tubería llena se bloqueará hasta que otro proceso lea (y entonces se borren) los datos de la tubería.
Existen dos tipos de tuberías, las tuberías sin nombre (llamadas simplemente tuberías) y las tuberías con nombre o ficheros FIFO 2.
Tuberías sin nombre
Las tuberías sin nombre se crean invocando a la llamada al sistema pipe y solamente pueden ser utilizadas por el proceso que hace la llamada y sus descendientes. La sintaxis de esta llamada es:
resultado=pipe(tubería);
donde tubería es un array entero de dos elementos, mientras que resultado es una variable entera. Si la llamada al sistema se ejecuta con éxito en resultado se almacenará el valor 0 y en tubería se habrán almacenado dos descriptores de ficheros. Para leer de la tubería hay que usar el descriptor almacenado en tubería[0], mientras que para escribir en la tubería hay que usar el descriptor almacenado en tubería[1]. En caso de error durante la ejecución de pipe en resultado se almacenará el valor -1.
Como se describió en la sección 5.2 cuando un proceso invoca a la llamada al sistema fork para crear un proceso hijo éste hereda todos los descriptores de ficheros de su progenitor. Esta es la razón por la que un proceso hijo puede también acceder a una tubería creada por su progenitor. Este mismo razonamiento se aplica para los descendientes de éste proceso hijo. De esta forma, en cada tubería pueden escribir y leer varios procesos relacionados genealógicamente. Cada uno de estos procesos puede escribir o/y leer en la tubería.
Normalmente, no obstante, una tubería suele ser compartida entre dos procesos, cada uno poseyendo un extremo. Aplicaciones típicas, como los intérpretes de comandos, manipulan de forma automática los descriptores para que en una tubería solamente pueda escribir un proceso y solamente pueda leer otro proceso (relacionado genealógicamente con el primero), usándola así para transmitir un flujo de datos en una sola dirección.
Como mecanismo IPC, las tuberías proporcionan una forma eficiente de transferir datos de un proceso a otro. Sin embargo poseen algunas limitaciones importantes:
- Una tubería no puede ser utilizada para transmitir datos a múltiples procesos receptores de forma simultánea, ya que al leer los datos de la tubería estos son borrados.
- Si existen varios procesos que desean leer en un extremo de la tubería, un proceso que escriba en el otro extremo no puede dirigir los datos a un proceso en concreto. Asimismo, si existen varios procesos que desean escribir en la tubería, no existe forma de determinar cual de ellos envía los datos.
- Si un proceso envía varios mensajes de diferente longitud en una sola operación de escritura en la tubería, el proceso que lee el otro extremo de la tubería no puede determinar cuantos mensajes han sido enviados, o donde termina un mensaje y donde empieza el otro, ya que los datos en la tubería son tratados como un flujo de bytes no estructurados de tamaño fijo.
Existen varias formas de implementar las tuberías. La aproximación tradicional (en SVR2, por ejemplo) es utilizar los mecanismos del sistema de ficheros y asociarla un nodo-i y una entrada en la tabla de ficheros.
La mayoría de las distribuciones basadas en BSD utilizan conectores (sockets) para implementar una tubería. Los conectores son un tipo de fichero que se utiliza como canal de comunicación entre procesos. Aunque un conector es tratado sintácticamente como un fichero; semánticamente no lo es. Esto significa que no tiene los problemas de velocidad inherentes al acceso a disco. Los conectores se utilizan sobre todo para la implementación de comunicaciones en red.
SVR4 proporciona tuberías bidireccionales basadas en streams. Un stream es una ruta de transferencia de datos entre un driver en el espacio del núcleo y un proceso en el espacio de usuario. Un stream posiblita una comunicación full-duplex, es decir, permite que un proceso pueda actuar como emisor o receptor en cualquier instante.
- Ejemplo 7.1:
El siguiente programa en C ilustra el envío de mensajes entre un proceso emisor y otro receptor a través de una tubería sin nombre.
#include <stdio.h>
#define MAX 256
main()
{
int tuberia[2];
int pid;
char mensaje[MAX];
[1] if (pipe(tuberia)==-1)
{
[2] perror("pipe");
[3] exit(-1);
}
[4] if ((pid=fork())==-1)
{
perror("fork");
exit(-1);
}
else if (pid==0)
{
[5] while (read(tuberia[0], mensaje, MAX)>0 && strcmp(mensaje,"FIN")!=0)
[6] printf("\nProceso receptor. Mensaje: %s\n", mensaje);
close(tuberia[0]);
close(tuberia[1]);
exit(0);
}
else
{
[7] while(printf("Proceso emisor. Mensaje: ")!=0...
&& gets(mensaje)!=NULL...
&& write(tuberia[1], mensaje, strlen(mensaje)+1)>0...
&& strcmp(mensaje,"FIN")!=0);
close(tuberia[0]);
close(tuberia[1]);
exit(0);
}
}
Programa 7.1
En primer lugar [1] se invoca a la llamada al sistema pipe para crear una tubería sin nombre, y se comprueba si se ha ejecutado con éxito. Si es así en tuberia[0] se habrá almacenado un descriptor de fichero para poder leer en la tubería, mientras que en tubería[1] se habrá almacenado un descriptor de fichero para poder escribir en la tubería, además la llamada devuelve un 0. Si se produce un error durante la ejecución de pipe la llamada devuelve un -1, se imprime en pantalla [2] pipe seguido de ":" y del mensaje asociado al identificador de error contenido en la variable errno. Acto seguido se invoca [3] a la llamada exit para finalizar el programa.
A continuación, se invoca a la llamada [4] al sistema fork para crear un proceso hijo y se comprueba que se ha ejecutado con éxito.
El proceso hijo (receptor) se va a encargar [5] de leer un mensaje de la tubería y [6] presentarlo en pantalla. El ciclo de lectura y presentación termina al leer el mensaje "FIN".
Por otra parte, el proceso padre (emisor) se va a encargar [7] de leer un mensaje de la entrada estándar y, acto seguido, escribirlo en la tubería para que lo reciba el proceso hijo. El ciclo de lectura de la entrada estándar y escritura en la tubería terminará cuando se introduzca el mensaje "FIN".
En dicho caso, tanto el proceso padre como el hijo procederán a cerrar los descriptores de ficheros asociados a la tubería mediante el uso de la llamada al sistema close y ha finalizar su ejecución mediante la invocación de exit.
Tuberías con nombre o ficheros FIFO
Las tuberías con nombre o ficheros FIFO se crean invocando a la llamada al sistema mknod y pueden ser utilizadas por cualquier proceso siempre que disponga de los permisos adecuados. La sintaxis de esta llamada es:
resultado= mknod(ruta, modo, 0);
El parámetro de entrada ruta permite especificar el nombre del fichero FIFO. Mientras que modo permite especificar el tipo de fichero (S_IFIFO) y los usuales permisos de acceso. Si la llamada al sistema se ejecuta con éxito en resultado se almacenará el valor 0, en caso contrario se almacenará el valor -1.
- Ejemplo 7.2:
La línea de código C:
mknod("fifo1",S_IFIFO|0666,0)
invoca a la llamada al sistema mknod para crear en el directorio actual un fichero FIFO de nombre fifo1 con permisos de lectura y escritura para todos los usuarios.
También existe un comando mknod que puede usarse desde la línea de ordenes del terminal.
Los ficheros FIFO poseen las siguientes ventajas sobre las tuberías sin nombre:
- Tienen un nombre en el sistema de archivo.
- Pueden ser accedidos por procesos sin ninguna relación familiar.
- Son persistentes, es decir, continúan existiendo hasta que un proceso los desenlaza explícitamente. Por tanto son útiles para mantener datos que deban sobrevivir a los usuarios activos
Asimismo, los ficheros FIFO poseen las siguientes desventajas con respecto a las tuberías sin nombre:
- Deben ser borrados de forma explícita cuando no son usados.
- Son menos seguros que las tuberías, puesto que cualquier proceso con los privilegios adecuados puede acceder a ellos.
- Son difíciles de configurar y consumen más recursos.
Lectura y escritura en las tuberías (sin nombre y ficheros FIFO)
La E/S en una tubería es como la E/S en un fichero y de hecho también se realiza usando las llamadas al sistema read y write sobre los descriptores de la tubería. Un proceso es incapaz de darse cuenta de que el fichero que esta leyendo es en realidad una tubería.
Los procesos emisores añaden datos al final, mientras que los procesos receptores leen datos desde la cabecera. Una vez que un dato ha sido leído, es eliminado de la tubería y no está disponible para otros procesos receptores. El núcleo define un parámetro denominado PIPE_BUF (5120 bytes por defecto), que limita la cantidad de datos que una tubería puede mantener. Si un proceso emisor produjese que una tubería rebosara, éste proceso se bloquearía hasta que se habilitase espacio en la tubería mediante las operaciones de lecturas oportunas. Si un proceso intenta escribir más de PIPE_BUF bytes en una sola llamada, el núcleo no puede garantizar la atomicidad de la escritura.
El tratamiento de la operación de lectura es ligeramente diferente. Si el tamaño requerido es mayor que la cantidad de datos existentes actualmente en la tubería, el núcleo lee los datos que están disponibles y retorna el número de bytes leídos al proceso solicitante. Si no existe disponible ningún dato, el proceso receptor se bloqueará hasta que otro proceso escriba en la tubería. La especificación de la opción O_NDELAY en el campo modo de mknod pone a la tubería en modo no bloqueante, es decir, las lecturas y escrituras se completarán sin bloquear, transfiriendo tantos datos como sea posible.
Las tuberías mantiene un contador de los procesos receptores y de los procesos emisores activos. Cuando el último proceso emisor activo cierra la tubería, el núcleo despierta a todos los procesos receptores, para que puedan leer, si lo desean, los datos que quedan en la tubería. Una vez que la tubería está vacía, los procesos receptores obtendrán un valor de retorno de 0 desde la siguiente llamada a read y lo interpretarán como el final del fichero. Si el último proceso receptor cierra la tubería, el núcleo envía una señal SIGPIPE a los procesos emisores bloqueados. Las siguientes operaciones de escritura devolverán un error EPIPE.
Seguimiento de procesos
La llamada al sistema ptrace suministra un conjunto de servicios para el seguimiento de procesos. Principalmente es utilizada por programas depuradores. Utilizando ptrace, un proceso puede controlar la ejecución de un proceso hijo. Su sintaxis es:
ptrace(cmd,id,addr,data);
donde id es el pid del proceso hijo, addr se refiere a una posición en el espacio de direcciones del hijo, y la interpretación del argumento data depende de cmd. El argumento cmd permite al padre realizar las siguientes operaciones:
- Lectura o escritura de una palabra en el espacio de direcciones, en el área U o en los registros de propósito general asociados al proceso hijo.
- Interceptar determinadas señales. Cuando una señal interceptada es generada para el hijo, el núcleo suspenderá al hijo y notificará al padre el evento.
- Configura puntos de chequeo en el espacio de direcciones del hijo.
- Reanudar la ejecución de un hijo suspendido o parado.
- Reanudar la ejecución del hijo pero solo durante una instrucción, ejecutada la cual volverá a suspenderse el hijo
- Terminar al proceso hijo.
Típicamente un proceso padre crea un hijo, y éste invoca a la llamada ptrace para permitir al padre controlarle. El padre entonces utiliza la llamada al sistema wait para esperar por un evento que cambie el estado del proceso hijo. Cuando el evento ocurre, el núcleo despierta al padre. El valor de retorno de wait indica que el hijo se ha parado y suministra información sobre el evento que ha causa esta parada. El padre entonces controla al hijo mediante las operaciones que se hayan especificado en ptrace.
Aunque ptrace ha permitido el desarrollo de muchos depuradores, tiene varios inconvenientes y limitaciones:
- Un proceso solo puede controlar la ejecución de su hijo. Si éste hijo crea otro proceso, el depurador no puede controlar la ejecución de este nuevo proceso o sus descendientes.
- ptrace es extremadamente ineficiente, requiere de varios cambios de contexto para transferir una sola palabra desde el hijo al padre. Estos cambios de contexto son necesarios porque el depurador no tiene acceso directo al espacio de direcciones del hijo.
- Un depurador no puede seguir a un proceso que ya se está ejecutando, puesto que el hijo primero necesita llamar a ptrace para informar al núcleo de que desea ser seguido.
Durante mucho tiempo, ptrace era la única herramienta para depurar programas. Los sistemas UNIX modernos tales como SVR4 o Solaris suministran servicios de depuración más eficientes.

