Subsecciones

20. Control de procesos: <stdlib.h>, <unistd.h>

Un proceso es básicamente un único programa ejecutándose. Este podría ser un programa del ''sistema´´ (por ejemplo, login, csh, update, etc). un programa iniciado por el usuario (gimp o alguno hecho por el usuario).

Cuando UNIX ejecuta un proceso le asigna a cad proceso un número único -- process ID o pid.

El comando ps de UNIX lista todos los procesos que se están ejecutando en la máquina, listando también el pid.

La función de C int getpid() regresará el pid de un proceso que llame a esta función.

Un programa usualmente ejecuta un sólo proceso. Sin embargo despues se verá como se puede hacer que un programa corra como varios procesos separados comunicados.

20.1 Ejecutando comandos de UNIX desde C

Se pueden ejecutar comandos desde un programa de C como si se estuviera en la línea de comandos de UNIX usando la función system(). NOTA: se puede ahorrar bastante tiempo y confusión en vez de ejecutar otros programas, scripts, etc. para hacer las tareas.

int system(char *mandato) -- donde mandato puede ser el nombre de una utilería de UNIX, un shell ejecutable o un programa del usuario. La función regresa el status de salida del shell. La función tiene su prototipo en <stdlib.h>

Ejemplo: llamada del comando ls desde un programa

main()
{
        printf("Archivos en el directorio son:\n");
        system("ls -l");
}

La función system es una llamada que esta construida de otras 3 llamadas del sistema: execl(), wait() y fork() (las cuales tienen su prototipo en <unistd.h>).

20.2 execl()

La función execl tiene otras 5 funciones relacionadas -- ver las páginas de man.

La función execl realiza la ejecución (execute) y sale (leave), es decir que un proceso será ejecutado y entonces terminado por execl.

Esta definida como:

execl (const char *camino, const char *arg0, ..., char *argn, 0);

El último parámetro deberá ser siempre $0$. Este es un terminador Nulo. Como la lista de argumentos sea variable se debe indicar de alguna forma a C cuando se termine la lista. El terminador nulo hace lo anterior.

El apuntador camino indica el nombre de un archivo que contiene el comando que será ejecutado, arg0 apunta a una cadena que es el mismo nombre (o al menos su último componente).

arg1, ... , argn son apuntadores a los argumentos para el comando y el $0$ solamente indica el fin de la lista de argumentos variables.

Por lo tanto nuestro ejemplo queda de la siguiente forma:

#include <unistd.h>

main()
{
        printf("Los archivos en el directorio son:\n");
        execl("/bin/ls","ls","-l",0);
		  printf("¡¡¡ Esto no se ejecuta !!!\n");
}

20.3 fork()

La función int fork() cambia un proceso único en 2 procesos idénticos, conocidos como el padre (parent) y el hijo (child). En caso de éxito, fork() regresa $0$ al proceso hijo y regresa el identificador del proceso hijo al proceso padre. En caso de falla, fork() regresa $-1$ al proceso padre, pone un valor a errno, y no se crea un proceso hijo.

El proceso hijo tendrá su propio identificador único (PID).

El siguiente programa ilustra un ejemplo sencillo del uso de fork, donde dos copias son hechas y se ejecutan juntas (multitarea).

main() 
{ 
	int valor_regr=0;
                
	printf("Bifurcando el proceso\n");
	valor_regr=fork();
	printf("El id del proceso es %d y el valor regresado es %d\n",
		getpid(), valor_regr);
	execl("/bin/ls","ls","-l",0);
	printf("Esta linea no es impresa\n");
}

La salida podría ser como la siguiente:

Bifurcando el proceso
El id del proceso es 2662 y el valor regresado es 2663
El id del proceso es 2663 y el valor regresado es 0

Los procesos tienen un único identificador, el cual será diferente en cada ejecución.

Es imposible indicar con antelación cual proceso obtendrá el CPU. Cuando un proceso es duplicado en 2 procesos se puede detectar fácilmente (en cada proceso) si el proceso es el hijo o el padre ya que fork() regresa $0$ para el hijo. Se pueden atrapar cualquier error, ya que fork() regresa un $-1$, por ejemplo:

int pid;		/* identificador del proceso */

pid = fork();
if ( pid < 0 )
{
	printf("¡¡ No se pudo duplicar !!\n");
	exit(1);
}

if ( pid == 0 )
{
	/* Proceso hijo */
	......
}
else
{
	/* Proceso padre */
	......

}

20.4 wait()

La función int wait() (int *status) forzará a un proceso padre para que espere a un proceso hijo que se detenga o termine. La función regresa el PID del hijo o $-1$ en caso de errror. El estado de la salida del hijo es regresado en status.

20.5 exit()

La función void exit(int status) termina el proceso que llama a esta función y regresa en la salida el valor de status. Tanto UNIX y los programas bifurcados de C pueden leer el valor de status.

Por convención, un estado de $0$ significa terminación normal y cualquier otro indica un error o un evento no usual. Muchas llamadas de la biblioteca estándar tienen errores definidos en la cabecera de archivo sys/stat.h. Se puede fácilmente derivar su propia convención.

Un ejemplo completo de un programa de bifurcación se muestra a continuación:

/* 
	fork.c - ejemplo de un programa de bifurcación
	El programa pide el ingreso de comandos de UNIX que son dejados en una
	cadena. La cadena es entonces "analizada" encontrando blancos, etc
	Cada comando y sus correspondientes argumentos son puestos en 
	un arreglo de argumentos, execvp es llamada para ejecutar estos comandos
	en un proceso hijo  generado por fork()
*/

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

main()
{
	char buf[1024];
	char *args[64];

	for (;;) 
	{
		/*
		* Pide y lee un comando.
		*/
		printf("Comando: ");

		if (gets(buf) == NULL) 
		{
			printf("\n");
			exit(0);
		}

		/*
		* Dividir la cadena en argumentos.
		*/
		parse(buf, args);

		/*
		* Ejecutar el comando.
		*/
		ejecutar(args);
	}
}

/*
* parse--divide el comando que esta en buf
*        en argumentos. 
*/
parse(char *buf, char **args)
{
	while (*buf != (char) NULL) 
	{
		/*
		* Quitar blancos. Usar nulos, para que
		* el argumento previo sea terminado 
		* automaticamente.
		*/
		while ( (*buf == ' ') || (*buf == '\t') )
			*buf++ = (char) NULL;
	
		/*
		* Guardar los argumentos 
		*/
		*args++ = buf;
	
		/*
		* Brincar sobre los argumentos. 
		*/
		while ((*buf != (char) NULL) && (*buf != ' ') && (*buf != '\t'))
			buf++;
	}
	
	*args = (char) NULL;
}


/*
* ejecutar--genera un proceso hijo y ejecuta
*           el programa.
*/
ejecutar(char **args)
{
	int pid, status;

	/*
	* Obtener un proceso hijo.
	*/
	if ( (pid = fork()) < 0 ) 
	{
		perror("fork");
		exit(1);

		/* NOTA: perror() genera un mensaje de error breve en la 
		* salida de errores describiendo el ultimo error encontrado
		* durante una llamada al sistema o funcion de la biblioteca.
		*/
	}

	/*
	* El proceso hijo ejecuta el codigo dentro del if.
	*/
	if (pid == 0) 
	{
		execvp(*args, args);
		perror(*args);
		exit(1);

	/* NOTA: las versiones execv() y execvp() de execl() son utiles cuando
	   el numero de argumentos es desconocido previamente.
		Los argumentos para execv() y execvp() son el nombre del archivo que
		sera ejecutado y un vector de cadenas que contienen los argumentos.
		El ultimo argumento de cadema debera ser un apuntador a 0 (NULL)

		execlp() y execvp() son llamados con los mismos argumentos que 
		execl() y execv(), pero duplican las acciones del shell en
		la busqueda de un archivo ejecutable en un lista de directorios.
		La lista de directorios es obtenida del ambiente.
	*/

	}

	/*
	* El padre ejecuta el wait.
	*/
	while (wait(&status) != pid)
		/* vacio */ ;
}

20.6 Ejercicios

  1. Crear un programa en C en donde se use la función popen() para entubar la salida del comando rwho de UNIX en el comando more de UNIX.