Programacion C Avanzada – T2 – Forks, pipes y como comunicar programa padre con hijo

[notice]Desde que este post fue creado han pasado 10 años así que puede que hayan cambiado cosas.[/notice]

Aquí os pongo un ejemplo de como trabajar con Forks, pipes y como realizar la comunicación entre un proceso padre y uno de sus hijos.

¿Que es un fork?

Se encarga de duplicar un proceso que estamos ejecutando. La copia es exacta por lo que las variables declaradas antes del fork existen en el padre y en el hijo y tienen el mismo valor justo después de crearse el proceso duplicado salvo por la variable que guardará la salida del fork. Una vez realizado el fork prosigue la ejecución desde la linea donde se ha quedado (justo la siguiente del fork) en padre y en hijo.

Guardamos la salida del fork en una variable integer «x» (en el código que veis mas abajo se llama pidHijo). Fork devolverá un integer con 3 posibles valores.

  • -1: Imposible crear el fork
  • 0: Se ha creado el fork, pero estamos en el proceso hijo.
  • Mayor a 0: Se ha creado el fork, estamos en el proceso padre y nos ha devuelto el PID del hijo.

Como al crear el fork se ha creado el proceso hijo y hemos igualado la variable pidHijo a lo que devuelve el fork, en el padre se ha asignado uno de los dos primeros valores ( error o  el pid del hijo ), pero en el hijo se ha asignado el valor 0. Ambos programas, siguen la ejecución por debajo del fork, por lo tanto padre e hijo entraran en un switch, haciendo que el padre realice una función y el hijo realice otra (al entrar en diferentes case).

¿Que es una pipe?

Una pipe viene a ser una tubería de comunicación entre el padre y el proceso hijo. Se crean antes de realizar el fork porque así, tanto padre como hijo tienen definidas las mismas pipes (si las creáramos después, serian diferentes y no funcionarían). Para ello definimos un array de integer de dos posiciones «int pipeA[2]». Usaremos la posición «0» a modo de lectura y la posición «1» a modo de escritura. Las pipes «solo» tienen un sentido, si queremos hablar del padre al hijo, no podremos usar esa misma pipe para hablar del hijo al padre, para ello necesitamos crear otra pipe.

 

/* **********************************************
*
* @Archivo : S4.c
* @Finalidad : Practica de la sesion 4
* @Autor : Guille Rodriguez Gonzalez, Itziar Sanchez
* @Fecha : 10/10/2014
*
*********************************************** */

// Includes propios
#include 
#include 
#include 
#include 
#include 
#include 
#include 


// Globales
int nAlarmas = 0;
int timeAlarma = 2;


// Cabeceras funciones
void raiseAlarma(void);

int main(void) {

	// Variables comunes padre e hijo
	int pidPadre=getpid();	// Obtenemos el Pid del Padre
	int pidHijo=0;			// Variable para guardar la salida del fork
	int bucle=1;			// Padre e hijo usaran un bucle, por lo tanto definimos el valor a "true"
	int nbytes=0;			// Se usara para saber cuantos bytes se han escrito/leido de la pipe.

	int pipePaH[2];			// Pipe de comunicacion Padre -> Hijo
	int pipeHaP[2];			// Pipe de comunicacion Hijo -> Padre
	char strGeneroso[256];	// String que usaremos para mostrar mensajes y almacenar lo que escribiremos/leeremos por pipe.
	
	// Variables padre
	char opcion[2];			// Usaremos para guardar la opcion del menu seleccionada.
	
	// Creamos las pipes, si fallan (-1) mostramos error. Y enviamos una señal SIGKILL (kill -9 pid) para finalizar el programa.
	if ( -1 == pipe(pipePaH) ) {
			
			bzero(strGeneroso,256);
			sprintf(strGeneroso,"Error al crear la pipe Padre a Hijo\n");
			write(1,strGeneroso,strlen(strGeneroso));
		raise(SIGKILL);
	}
	if ( -1 == pipe(pipeHaP) ) {
			bzero(strGeneroso,256);
			sprintf(strGeneroso,"Error al crear la pipe Hijo a Padre\n");
			write(1,strGeneroso,strlen(strGeneroso));
		raise(SIGKILL);
	}
	
	
	// Creamos el proceso duplicado (hijo) y guardamos el retorno en la variable.
	pidHijo=fork();
	switch (pidHijo) {
	
		case -1:
			// Imposible crear el fork -> Matamos el programa
			bzero(strGeneroso,256);
			sprintf(strGeneroso,"Imposible crear un fork! \n");
			write(1,strGeneroso,strlen(strGeneroso));
			raise(SIGKILL);
		break;
		case 0:
			// Fork ha ido ok
			// SOY EL PROCESO HIJO
			
			// Redefinimos la RSI de SIGALRM y creamos una interrupcion para dentro de X segundos.
			signal(SIGALRM, (void*)raiseAlarma);
			alarm(timeAlarma);
			
			
			// Cerramos el canal de escritura de Padre -> Hijo (recuerda que estamos en el hijo!)
			close(pipePaH[1]);
			
			// Cerramos el canal de lectura de Hijo -> Padre (recuerda que estamos en el hijo!)
			close(pipeHaP[0]);
			

			/*
				Cuando creamos una pipe:
					0 - Es por donde se le lee
					1 - Es por donde se escribe.
			
				Estamos definiendo la direccion de comunicacion de la pipe. En el canal P->H
				el padre hablara y el hijo escuchara, por lo que es tonteria en el hijo tener
				abierto el canal de escritura de una pipe donde solo puede leer. close(pipePaH[1]);

				Ademas cerramos el canal de lectura de la pipe H a P, puesto que en ese canal el 
				hijo solo va a escribir, no va a leer (a tiene la otra pipe para ello).
				
				Lo mismo pasara en el padre (cerrando el canal que toque).
			*/
			
			
			while (bucle) {
			
				/*
				 Bucle infinito que pondra al hijo en modo escucha y esperara a que se reciba algo por la pipe de PaH
				 en el canal de lectura ( 0 ) y lo guardara en el buffer (strGeneroso), leyendo hasta un max de 256 bytes.
				 Eso no significa que recibamos 256bytes, recibiremos lo que se envie, si se envian 3, pues recibiremos 3.
				 
				 La funcion read, devuelve (int) cuantos bytes se han leido, asi que lo guardamos en un integer para luego
				 cuando debamos mostrarla, saber cuantos bytes debemos mostrar.
				*/
				bzero(strGeneroso,256);
				nbytes = read(pipePaH[0],strGeneroso,256);
				
				
				/*
				Dependiendo lo que hayamos recibido por la pipe, realizamos una accion o otra.
				En este caso el padre envia 3 mensajes.
				
					PA -> Peticion de Alarma: 	El hijo contara cuantas alarmas ha tenido y se las enviara al padre.
					S9 -> Peticion de final:	El hijo saldra del bucle y finalizara su ejecucion.
					CX -> Cambio de tiempo alarmas: El hijo cambiara el tiempo entre alarmas. "X" sera un numero de 1 a 9 (segundos)
													que se guardara en la variable GLOBAL del hijo, llamada TIMEALARMA.
				
				*/
				if ( strncmp("PA", strGeneroso, 2) == 0) {
					bzero(strGeneroso,256);
					sprintf(strGeneroso,"\nEl numero de alarmas es: %d \n\n",nAlarmas);
					write(pipeHaP[1],strGeneroso,strlen(strGeneroso));
				} else if ( 0 == strncmp("S9",strGeneroso,2) ) {
					bucle=0;
				} else if ( 0 == strncmp("C",strGeneroso, 1) ) {
					// Cambiamos el tiempo entre alarmas.
					timeAlarma = atoi(&strGeneroso[1]);	// Atoi: Funcion encargada de pasar de Ascii to Integer.
				}
				
			}
		break;
		default:
			// Fork ha ido ok.
			// SOY EL PROCESO PADRE
			
			// Cerramos los canales de las pipes que el padre no vaya a usar.
			close(pipeHaP[1]);
			close(pipePaH[0]);
			
			// Mostramos un mensaje con el PID del padre y el PID del hijo (por hobby).
			bzero(strGeneroso,256);
			sprintf(strGeneroso,"[START] Padre, con el pid %d, mi hijo con el pid %d \n",pidPadre,pidHijo);
			write(1,strGeneroso,strlen(strGeneroso));
			
			while (bucle) {
				
				// Mostramos el menu de opciones del padre.
				bzero(strGeneroso,256);
				sprintf(strGeneroso,"\n\n\t *** MENU ***\n\n1. Consultar numero de alarmas. \n2. Cambiar el tiempo de alarma.\n3. Salir del programa\n\n\tOpcion: ");
				write(1,strGeneroso,strlen(strGeneroso));
				
				// Leemos la opcion por pantalla
				read(0,opcion,2);
				
				// Quitamos el \n que se ha leido al hacer enter, de ahi que sea un char de [2], para evitar ese \n haga estragos...
				opcion[1]='\0';
				
				switch ( atoi(&opcion[0]) ) {		// Pasamos el caracter numerico leido a integer.
					case 1:
						// Enviamos la señal PA al hijo
						write(pipePaH[1],"PA",2);
						
						// Limpiamos el buffer
						bzero(strGeneroso,256);
						
						// Esperamos respuesta del hijo
						nbytes = read(pipeHaP[0],strGeneroso,256);
						
						// Escribimos por pantalla lo que nos ha comunicado el hijo.
						write(1,strGeneroso,nbytes);
					break;
					case 2:
						// Preguntamos el nuevo tiempo de alarma.
						bzero(strGeneroso,256);
						sprintf(strGeneroso,"\nNuevo tiempo de alarma [1  9]: \n ");
						write(1,strGeneroso,strlen(strGeneroso));
						
						
						// Leemos el nuevo valor de alarma
						nbytes = read(0,opcion,2);
						opcion[1]='\0';
						
						// Comprobamos si el valor esta entre 1 y 9 (ambos incluidos)
						if ( 1 <= atoi(&opcion[0]) && 9 >= atoi(&opcion[0])) {
							bzero(strGeneroso,256);
							// Preparamos el mensaje para el hijo C + el numro que hemos leido.
							sprintf(strGeneroso,"C%c",opcion[0]);
							// Escribimos en la pipe el mensaje.
							write(pipePaH[1],strGeneroso,2);
						} else {
							// Mostramos error y volvemos al menu.
							bzero(strGeneroso,256);
							sprintf(strGeneroso,"\nEl nuevo tiempo de alarma es incorrecto. Volviendo a mostrar menú. \n ");
							write(1,strGeneroso,strlen(strGeneroso));
						}
					break;
					case 3:
						// Enviamos por la pipe el mensaje de finalizacion al hijo-
						write(pipePaH[1],"S9",2);
						// Esperamos a que el hijo muera (al morir envia un señal de finalizacion que capturamos con wait).
						wait(&pidHijo);
						// Salimos del bucle
						bucle=0;
						
					break;
					default:
						// Mostramos mensaje de error
							bzero(strGeneroso,256);
							sprintf(strGeneroso,"\nHas elegido una cpcion no valida. \n ");
							write(1,strGeneroso,strlen(strGeneroso));
					break;
				}
			}
		break;	
	}
	return 0;
}


void raiseAlarma(void) {
	// La signal SIGALRM ha sido redefinida en el hijo para que ejecute esta funcion.
	// Cada vez que se reciba una, sumaremos 1 al numero de interrupciones alarma recibidas.
	nAlarmas++;		
	// Programamos la siguiente alarma para dentro de X segundos.
	alarm(timeAlarma);
}

 

Si os preguntáis porque no uso printf o scanf para mostrar mensajes por pantalla, es porque los tenemos prohibidos (facilitan y malgastan memoria). Tenemos que usar las funciones read, write, open, close y los filedescriptors que necesitemos. El file descriptor 0 es el teclado y el filedescriptor 1 es la pantalla. Por ello en los write veis que viene seguido de un 1 y en los read seguido de un 0 (salvo en el caso de que se escriba en una pipe).

 

 

3 comentarios

  1. bueno, no se si tu me podrías aclarar esta duda con el pipe, que pasaría si en vez de pasarle una cadena(al final de cuenta un puntero) , le paso un doble puntero int y le especifico el tamaño de este con sizeof, seria algo a si:

    int **datito;

    /*mucho codigo despues*/
    //entro en el hijo:

    //este caso es para el pipe que tiene comunicación hijo -> padre
    write(pipeHaP[1],datito,sizeof(datito));

  2. Muchas gracias Guillermo, estaba trancado porque creaba el pipe despues del fork() y hacia el write. Si bien no daba error, el programa terminaba y no sabia porque. Saludos desde Montevideo Uruguay.

    1. Me alegro que te haya servido! Yo no suelo programar en C casi nada, algo de microcontroller pero poco mas. Suelo hacer uso de Java y Php en el trabajo

Deja una respuesta

Tu dirección de correo electrónico no será publicada.

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.