PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

CUARTA SESIÓN


  1. Identificación de procesos.

    Identificación de procesos: todos los procesos tienen un número identificativo único (PID = Process Identification).

    Además, existe otro tipo de información asociada al proceso:



    Para conocer la información acerca de los procesos que están ejecutándose en el sistema, podemos usar la orden ps. Por ejemplo, para ver qué procesos estoy corriendo en estos momentos, puedo usar:
    <T>/usuarios/gyermo$ ps -fu gyermo
         UID   PID  PPID  C    STIME TTY       TIME COMMAND
      gyermo  7998  7984  7 12:19:33 pts/td    0:00 ps -fu gyermo
      gyermo  7984  7983  1 12:19:25 pts/td    0:00 -ksh
    El identificador de proceso aparece en la columna PID. Este número caracteriza al proceso de forma unívoca. Podemos ver que estoy ejecutando dos procesos, el correspondiente al propio ps y el asociado a la shell (el programa que admite órdenes del teclado y las ejecuta).

    Ya veremos en una próxima sesión qué son las señales. De momento, hay que saber que cada proceso en ejecución consume recursos de la máquina sobre la que está. Es por ello muy importante que no tengáis procesos ejecutándose de balde. Para matar (acabar con) un proceso, podéis usar la orden: kill n, donde n es el PID del proceso que queréis matar. Si el proceso no se muere así, podéis intentar matarlo con kill -9 n, pero sólo después de haberlo intentado de la forma anterior.

  2. Creación de nuevos procesos.

    Creación de procesos en UNIX: los procesos nuevos se crean por "clonación" de los procesos antiguos:

    Aunque son dos procesos iguales, cada uno continúa su ejecución de modo independiente. Esto es debido a que la función fork() devuelve un valor distinto a cada proceso:

    fork() devuelve:

      • cero, al proceso hijo.
      • el PID del nuevo hijo, al proceso padre.



    Introducid el código anterior y observad cómo se imprimen adecuadamente los mensajes. Añadid, a continuación código a los printfes para que cada proceso imprima su identificador de usuario. Por ejemplo:
    713: soy el padre.
    1022: soy el hijo.
    Añadid, al final, del programa la orden sleep(60);, incluyendo el fichero de cabecera que sea necesario. Ejecutad el programa en segundo plano para que podáis seguir tecleando aunque el programa se ejecute y dad la orden ps apropiada para ver en ejecución tanto al padre como al hijo.

    Para poner un proceso en segundo plano, cuando lo arranquéis añadid al final un &. Esto hace que retoméis el control inmediatamente y no tengáis que esperar a que el programa arrancado acabe. Probad la diferencia entre:
    sleep 20    
    y
    sleep 20 & 


  3. Ejecución de un programa nuevo por parte de un proceso.

    Un proceso puede cambiar el programa que está ejecutando mediante la llamada al sistema execve():

    #include <unistd.h>

    int execve (const char *nomfich, const char *argv[],

    const char *envp[]);

    El procedimiento habitual para crear un nuevo proceso es llamar primero a fork y, el nuevo hijo, que llame a execve.

    No sólo disponéis de execve para que un proceso deje definitivamente lo que está haciendo y pase a ejecutar otro programa. También existen otras funciones de la misma familia que permiten especificar los parámetros que se le pasan al nuevo programa de diferentes maneras o que realizan una búsqueda del programa por el PATH del usuario. Estas funciones las podéis encontrar en el resumen.

    Para probar el uso de la familia de funciones exec, haced un programa que ejecute un ls -l mediante una llamada al sistema de esa familia.

  4. Espera por la muerte de los procesos hijos.

    Cuando arrancamos un programa desde la shell, hemos visto que el programa puede llevar argumentos que le llegarán mediante los parámetros de su función main y devolver valores al Sistema Operativo mediante el valor devuelto por main.

    Cuando es un proceso el que carga un nuevo programa mediante una llamada al sistema de la familia exec, los argumentos que recibirá la función main del nuevo programa son los especificados en los parámetros de la llamada exec.

    Cuando un proceso tiene un hijo mediante una llamada al sistema fork, el valor que devuelve la función main ya no es devuelto al Sistema Operativo, sino que es devuelto a su proceso padre. Para que el proceso padre pueda recibir ese código de retorno del hijo, ha de efectuar una llamada al sistema wait, cuyo prototipo es:
    pid_t wait(int *stat_loc); 
    Esta llamada al sistema nos devuelve un tipo de datos específico, que será un entero, donde se especificará el error, si se produce, o el PID del proceso hijo a que corresponde la información suministrada por la llamada.

    Los datos del proceso de que se trate, nos lo devuelve codificados en un entero. Fijaos en que el parámetro de la función es un parámetro de retorno y hay que seguir la indicaciones dadas en la segunda sesión.

    Una vez dispongamos del valor de la información del proceso devuelto en una variable entera, podemos descodificarlo con ayuda de unas macros definidas en el fichero de cabecera <sys/types.h>. Por ejemplo, para saber el código de retorno de un hijo, podríamos hacer:
    #include <sys/types.h>
    [...]
    int valor_devuelto;
    pid_t pid;
    [...]
    pid=wait(&valor_devuelto);
    [... comprobación de errores estándar ...]
    if (WIFEXITED(valor_devuelto))
        printf("El valor devuelto por el proceso de PID %d es %d.\n",pid,WEXITSTATUS(valor_devuelto));          


  5. Llamadas al sistema bloqueantes y no bloqueantes.

    wait tiene una llamada prima hermana denominada waitpid. Su prototipo es:
    pid_t waitpid(pid_t pid, int *stat_loc, int options); 
    Como podéis ver en el resumen, los dos parámetros que se añaden a la función son el primero, que permite especificar el PID en concreto sobre el que deseamos conocer su información, y el último, que es un campo de bits que permite especificar opciones. De todas las opciones, sólo nos interesará WNOHANG.

    Las dos llamadas, wait y waitpid, son el primer ejemplo de llamadas al sistema que disponen de una versión bloqueante y no bloqueante. Recordemos el diagrama de estados de UNIX:

    Diagrama de estados en UNIX:

    Si un proceso solicita información mediante wait y esta información no está disponible, el proceso abandona el estado de listo y pasa al estado de dormido (bloqueado). Este estado se caracteriza porque el proceso no consume CPU, es decir, no sustrae tiempo de cálculo a otros procesos. Cuando la información está disponible, el proceso se desbloquea y continúa su ejecución. A efectos del programador, el programa se para en la llamada al sistema hasta que la información esté disponible.

    La otra cara de la moneda la constituyen las llamadas al sistema no bloqueantes. Son aquellas llamadas en las que, si lo que se solicita no está disponible, el proceso no se queda bloqueado, sino que devuelven un valor especial indicando la condición de información no disponible. waitpid tiene una versión no bloqueante y otra, bloqueante. Todo depende de si especificamos WNOHANG (no bloqueante) o no lo especificamos (bloqueante) en su tercer parámetro.

    Normalmente se usan las llamadas no bloqueantes en lo que se denomina espera ocupada. En esta espera, se sondea (to poll) continuamente mediante la llamada no bloqueante para ver si la información está disponible. Esta técnica es nefasta en sistemas multiprogramados pues consume innecesariamente tiempo de CPU que podrían aprovechar otros procesos y será penalizada fuertemente al que la use. Veamos un ejemplo de ambas técnicas:

    1. Bloqueante:
      waitpid(-1,&valor_devuelto,0);
                          
    2. No bloqueante:
      while (waitpid(-1,&valor_devuelto,WNOHANG)==0)
          /* espera ocupada */;
    Para comprobar la diferencia entre ambos enfoques, hágase un programa que tenga un hijo que duerma durante treinta segundos. Para lograr que duerma, usad la función de biblioteca sleep. El proceso padre esperará a que el hijo concluya mediante waitpid. Haced una versión bloqueante y otra no bloqueante y ved la diferencia ejecutando el programa en segundo plano y monitorizando su comportamiento mediante la orden ps.

    wait(&a) es equivalente a waitpid(-1,&a,0). waitpid es una llamada que incluye, como caso particular, a wait.

    Si la espera ocupada es tan mala, ¿no puede el proceso hacer otra cosa mientras espera a que un hijo acabe? No mediante espera ocupada. La solución a esto lo veremos en próximas sesiones.

  6. Procesos zombies.

    Se dice que un proceso está en el estado de zombie en UNIX cuando, habiendo concluido su ejecución, está a la espera de que su padre efectúe un wait para recoger su código de retorno (valor devuelto al Sistema Operativo).

    Para ver un proceso zombie, haced un programa que tenga un hijo que acabe inmediatamente. El padre dejadlo durmiendo durante 30 segundos y que luego acabe. Ejecutad el programa en segundo plano y monitorizad los procesos con la orden de la shell ps.

    Cuando muere el padre, sin haber tomado el código de retorno del hijo, el hijo es automáticamente heredado por el proceso init, que se encarga de "exorcizarlo".

  7. Práctica.

    La práctica que hay que realizar y que no hay que entregar consiste en generar una serie de procesos cuyas relaciones familiares sigan el esquema siguiente:



    Como veis, un padre tiene dos hijos, uno de ellos tiene otros dos hijos, uno de los cuales tiene un hijo. Cuando nace un proceso, crea los hijos que le corresponda crear, duerme cinco segundos para descansar del esfuerzo procreador, espera por la muerte y va sumando los códigos de retorno de sus hijos. Al resultado de la suma le suma, a su vez, la última cifra de su PID. El proceso imprimirá un mensaje del estilo al siguiente:
    Soy un proceso nieto1 (PID=23840). Mi suma es 0.
    Soy el proceso padre  (PID=23803). Mi suma es 21. 
    Al acabar, el proceso devuelve un código de retorno igual a la suma previamente calculada. De este modo, el padre de todos los procesos conocerá la suma de las últimas cifras del PID de todos sus descendientes, incluído él mismo.

    Nota: no creo que suponga ningún esfuerzo hallar la última cifra de un número, pero, por si acaso, podéis obtenerlas calculando el resto de dividir el número por 10.

    Soluciones y comentarios.

  8. Órdenes de la shell relacionadas.

    ps
    información de los procesos del sistema
    &
    permite ejecutar procesos en segundo plano. Ver ksh
    jobs
    para ver los procesos que tenemos en segundo plano
    exec
    sustituye la shell actual por el programa que se especifique como argumento


  9. Funciones de biblioteca relacionadas.

    system
    permite ejecutar una orden de la shell en nuestro programa, ver el resultado obtenido y continuar la ejecución
    sleep
    bloquea al proceso durante un tiempo predeterminado


  10. LPEs.


© 2000 Guillermo González Talaván.