Administración de procesos

Como les va, estimados programadores. Aqui Darth Buanzo va a intentar explicarles como administrar procesos que se originan desde un proceso actual. En el suplemento GNU/Linux de la USERS 139 les explique el manejo de señales. Espero que hayan practicado un poco, ya que ahora vamos a empezar a hacer uso de esas funciones. Entremos en tema, nomas y, como diria Hetitor Arena "Manos a la Obra!".

Procesos procesados

Cuando el nucleo Linux inicia llega un momento en que se debe ejecutar el primer programa, el cual se encarga de leer los scripts de inicio y dejar el sistema listo para el login. Este primer proceso es el INIT y recibe el identificador de proceso (PID) numero 1. De este proceso se desprenden varios procesos, algunos que se ejecutan una sola vez (scripts de runlevel), otros que se ejecutan repetitivamente (getty/mgetty, el programa que nos solicita login en cada terminal que definimos en /etc/inittab), etc, etc.

Pero en resumen, mas alla de cuando, como y cuanto se ejecutan, cada vez se define un nuevo PROCESO. La imagen de un proceso es el programa en cuestion. Ahora, se necesita una forma de saber cuando un proceso muere. Excelente, se hace con seniales, pero, a quien se le avisa? Al proceso que genero a dicho proceso "hijo". Las funciones que ahora vamos a ver nos permiten: crear procesos, reemplazar imagenes de procesos y administrar la muerte de procesos que nosotros (o, mejor dicho, nuestro programa) han creado. En resumen, vamos a ver...

Cuestiones de familia

Tenemos una funcion que se llama fork(), y cuyo prototipo es el siguiente:

pid_t fork(void);

Requiere la inclusion de los archivos y . Como ven, esta funcion es del tipo 'pid_t'. Recuerdan getpid()? Tambien nos daba un pid_t. Es solamente un numero representativo de este proceso. No toma parametros. Lo unico que importa de esta funcion es lo que devuelve. Igual, si no les explico para que sirve, como que mucho no importa no? OK: Fork() duplica el proceso actual, generando un proceso hijo que continua su ejecucion desde la instruccion inmediata al fork().

Esto quiere decir que se genera un proceso llamado "hijo" ("Child Process") que depende de su "padre" ("Parent Process"). Entonces tenemos dos procesos, es como haber ejecutado dos veces el mismo programa, excepto por el hecho de que el hijo empieza desde la instruccion que le sigue al fork(), en vez de empezar por el main().

Buenisimo. Pero en que proceso estamos cuando hacemos fork(), si justo en ese momento, digamos, el "flujo de ejecucion" se bifurca y convive? Justamente, todo depende de lo que fork() devuelva. Si estamos en el padre, nos va a devolver el PID (Process ID) del hijo, si estamos en el hijo nos va a devolver CERO (0), y si hay un error, nos va a devolver -1 y, por ende, nos vamos a encontrar en el padre, ya que el hijo no fue creado. Mejor entendamos esto con un cachito de codigo totalmente inutil:

#include 
#include 
#include 

int
main ()
{
  if (fork () == 0)
    {
      puts ("Estamos en el hijo");
    }
  else
    {
      puts ("Estamos en el padre");
    }
}

Claro. Este es un codigo totalmente inutil. Si lo ejecutan veran las frases "Estamos en el hijo" y "Estamos en el padre" una debajo de la otra. Para que nos sirve generar procesos-hijo? Bueno, imaginen lo siguiente: Tienen un servidor de chat. Cada vez que se conecta un usuario, el servidor de chat genera un proceso nuevo para que maneje todas las peticiones de ESE usuario, y el proceso PADRE, el principal, solo se encarga de lo antes descripto: derivar las conecciones. A esto se lo llama "forking server" ("servidor que hace fork()" (ver serie de articulos "A programar enchufes!").

Ahora, la cuestion es que cuando un usuario cierra su comunicacion, el proceso hijo probablemente finalice, luego de algun cierto proceso. Cuando finaliza, o sea, cuando muere, se envia una senial SIGCHLD al proceso PADRE. Atencion: Esta dualidad de PADRE-HIJO se mantiene siempre. No existen "nietos", sino que si un proceso hijo crea un proceso, sera PADRE de ese proceso.

Ahora, agreguemos un administrador de seniales para la senial SIGCHLD. Van a ver que voy a crear una funcion que se ejecutara cuando se reciba una senial SIGCHLD. Esta funcion tendra una llamada a la funcion wait() y la funcion pause(), las cuales explicare luego de mostrarles este ejemplo:

#include 
#include 
#include 
#include 

short muriohijo = 0;

void
hijitos (int senial)
{
  signal (senial, SIG_IGN);
  wait (NULL);
  signal (senial, hijitos);
  muriohijo = 1;
}

int
main ()
{
  signal (SIGCHLD, hijitos);
  if (fork () == 0)
    {
      puts ("\033[1;33mSoy un hijo.");
      puts ("Dentro de 10 segundos");
      puts ("me suicidare, y mi padre");
      puts ("se va a enterar.\033[0;37m");
      sleep (10);
      exit (1);
    }
  else
    {
      puts ("\033[1;32mSoy el padre.");
      puts ("Como solo tengo que esperar");
      puts ("que muera mi hijo... esperare\033[0;37m");
      for (;;)
	{
	  pause ();
	  if (muriohijo)
	    break;
	}

      puts ("Ya murio mi hijo, me voy.");
      exit (0);
    }
}

Cuando compilen y ejecuten este programita van a ver que pasados 10 segundos finaliza, por lo que les recomiendo que aprovechen ese tiempo para hacer 'ps ax' en otra consola, y asi ver los dos procesos, el padre y el hijo, ejecutandose. Veran que sus PIDs van a ser consecutivos. Por ejemplo, 1049 para el padre, y 1050 para el hijo. Si hubiera un hijo mas, que no es este el caso, porque el fork() se utiliza una sola vez, serian 1050 y 1051. - El sleep (10) se utiliza para 'simular' un poco de trabajo del hijo por diez segundos. Durante ese tiempo, y en este ejemplo, no siempre sera asi, el padre se queda esperando la senial SIGCHLD.

Para esto utilizamos la funcion pause(), que, justamente, detiene la ejecucion del proceso hasta que llegue una senial. Fijense el uso de la variable muriohijo. En este caso quiza es redundante o "sin sentido" (por no decir otra cosa de tendencia gaseosa), pero si utilizan un administrador de seniales que maneja mas de una senial, entonces puede llegar a ser util. Otra funcion utilizada es wait(). La funcion wait() espera que un hijo muera y luego libera los recursos que utilizara. En este caso, la ejecuto para que libere los recursos, y no para esperar que muera el hijo, ya que es algo que ya realice mediante la combinacion pause-signal-bandera. Les recomiendo revisar la seccion 2 de las paginas del manual, funciones wait() y pause().

Como me estoy quedando corto de tiempo, les voy a contar la introduccion a algo que se llama IPC: Inter-Process Communication ("Comunicacion entre Procesos"). Piensen en el programa de chat: el "servidor" (digamos, el proceso PADRE, aunque esto depende del disenio) y los hijos tienen que compartir informacion. Mensajes que los usuarios se envian, transferencias de archivos, estados, comandos, etc. Esta claro que necesitamos una forma de compartir datos y enviar mensajes entre procesos. Para este tipo de cosas es que IPC viene como anillo al dedo. Es un conjunto de funciones que nos permiten:

  • Definir memoria compartida entre varios procesos
  • Obtener metodos para enviar mensajes entre procesos
  • Utilizar senializacion del estilo "semaforos"

Lo de los semaforos tiene que ver con poner un "aviso" de si algo se puede hacer o no, por ejemplo, escribir a un archivo al que todos los procesos acceden. En el proximo numero empezaremos a utilizar estas funciones. Escribannos a linux@tectimes.com - Un abrazo a todos!