Además de poder comunicar ambos procesos con cualquiera de los mecanismos habituales (semáforos, colas de mensajes o memoria compartida), tenemos otra posibilidad gracias a la función pipe().
La función pipe() abre una "tubería" de comunicación y nos devuelve dos descriptores de fichero abiertos, uno por cada extremo de la tubería. Por el primero de ellos se puede leer (con read()) lo que se escriba por el segundo (con write()). La ventaja de este mecanismo, es que si creamos la tubería antes de crear el proceso hijo (antes de llamar a fork()), como el proceso hijo se hace copia de todo lo del padre, también copia la tubería. Es decir, tanto el padre como el hijo tienen los dos descriptores de la tubería abiertos y utilizables. La función pipe() devuelve -1 en caso de error. Nuestro código de ejemplo quedaría
int descriptorTuberia [2];
int idProceso;
...
/* Devuelve en descriptorTuberia[0] el descriptor de lectura y en descriptorTuberia[1]
el descriptor de escritura */
pipe (descriptorTuberia);
...
idProceso = fork(); /* se hace
fork() después de pipe() */
...
No es buena idea que ambos procesos intenten escribir en la misma tubería, salvo que los sincronicemos. Si lo hacen a la vez, los datos entrarán entremezclados en la tubería y sería imposible separarlos luego, en la lectura. Tampoco es buena idea leer los dos por el lado de lectura. Si el proceso padre escribe en la tubería y va a leer antes que el hijo, recogerá su propio mensaje. Si ambos intentan leer a la vez, recogerán cada uno trozos del mensaje. Por ello, es mejor utilizar la tubería para que un proceso única y exclusivamente envíe datos y sea el otro el único encargado de recibirlos. Si queremos una comunicación bidireccional, es mejor abrir dos tuberías (llamar dos veces a la función pipe()).
Para evitar equivocaciones, y sobre todo para no mantener recursos ocupados innecesariamente, el proceso padre debe cerrar el lado de la tubería que no vaya a utilizar. El hijo tiene su propia copia de ese lado, así que aunque el padre lo cierre, para él sigue abierto y utilizable. El hijo debe cerrar el otro extremo. En el ejemplo cerraremos el descriptor de lectura en el padre y el de escritura en el hijo.
Una vez hecho esto, el padre pude escribir por descriptorTuberia[1] y el hijo leer por descriptorEscritura[0], como si se tratara de un fichero normal. Obviamente ambos procesos deben estar de acuerdo en qué estructuras de datos se van a enviar. Nuestro código de ejemplo quedaría:
En el caso del hijo con el read(), esta función lo dejará bloqueado hasta que el padre envíe su dato. Es más, si le mandamos leer, por ejemplo, 10 caracteres, la función read() puede volver cuando sólo ha leido 4. Para leer de forma segura, hay que hacer algo parecido a lo que se hizo en el código de ejemplo de sockets: un bucle de lectura hasta que hayamos leido todo lo que queríamos leer.
En código del ejemplo lo tienes en pipefork.c que puedes compilar con un Makefile y el comando make pipefork. Puedes descargarlos y quitarles la extensión .txt. Al ejecutarlo el hijo escribirá en pantalla una cadena de caracteres que ha recibido del padre. Luego ambos procesos terminan.