Sólo Programadores Banner 468x60

<//--tabla3-->

<//!--tabla2-->
  <//!--tabla1-->
 
Este mes en Sólo Programadores
Contenido del CD-ROM
Índice temático
Suscripción
 

<//!--fin de tabla1-->

   

<//!--fin tabla2-->


Linux

Linux posee un eficaz sistema de planificación de procesos basado en tres colas, dos de ellas para procesos de tiempo real y la tercera para otros tipos de procesos. En este artículo se verá de una forma práctica cómo se puede trabajar con las prioridades, pero antes se explicarán unos conceptos básicos.

Generalmente se suele definir un proceso como un programa que se está ejecutando, aunque de forma más correcta se puede considerar que un proceso está formado por el programa que se ejecuta, sus datos (en memoria dinámica o estática), su pila, el valor de los registros generales de la CPU y toda la información de control relativa al proceso que está en posesión del sistema operativo. De estos elementos, el programa y los datos, se encuentran contenidos en un rango de direcciones de memoria virtual que va desde 0 hasta el máximo valor que soporte el sistema operativo donde se ejecuta. La información que tiene el sistema operativo sobre el proceso se denomina atributos del proceso.

Entre los atributos de un procesose encuentran:

l Identificador (PID)

l Identificador del proceso padre (PPID)

l Valor de los registros

l Identidades del usuario y grupos.

l Prioridad del proceso con respecto a otros.

l Recursos consumidos por el proceso.

l Ficheros abiertos, mecanismos IPC y cualquier otro tipo de recurso que suministre el sistema operativo.

l Estado del proceso.

Pasemos a explicar de forma breve cada uno de estos conceptos antes de entrar en el tema principal del artículo, orientado a la planificación de procesos.

El atributo que define unívocamente a cada proceso es el identificador de proceso (o process id), más conocido por su acrónimo sajón PID. Se trata de un número único que el sistema asigna a cada proceso de tal forma que aunque los PID se puedan repetir en el sistema, nunca pueden hacerlo en el mismo momento. Este PID es el único valor que tendremos para dar órdenes a los procesos, ya sea mediante funciones y llamadas al sistema desde un programa, o desde la línea de comandos de la shell.

Una característica de los sistemas Unix (y por tanto de Linux) consiste en que todos los procesos son arrancados por otros procesos, por lo que se establece una jerarquía que se puede observar mediante el comando ps con el parámetro f, tal y como se muestra a continuación:

ps f[otros parámetros]

Las características que tiene un proceso dependen en parte de las propias características que tenga el sistema operativo, así por ejemplo tanto Windows 95 como OS/2 son sistemas multiproceso y monousuario, mientras que Linux se caracteriza por ser un sistema multiproceso y multiusuario. Los sistemas multiusuario permiten asignar a cada recurso del sistema un comportamiento diferenciado para cada usuario, de tal forma que parece lógico suponer que este comportamiento sea extendido a los procesos arrancados por los usuarios. Así pues los procesos tienen dos usuarios: el real y el efectivo. El usuario real es aquél que ha lanzado el proceso, mientras que el efectivo es el que utiliza el sistema para controlar el acceso a los recursos. La forma de actuar con respecto a los grupos es análoga a la comentada para los usuarios.

El estado de un proceso en un momento determinado indica qué actividad está realizando en ese momento. Los posibles estados en que puede estar un proceso son:

l Ejecutándose: Los registros generales de la CPU están cargados con los valores correspondientes a este proceso y la CPU está ejecutando una instrucción del mismo.

l Preparado: El proceso puede ser ejecutado, pero existe otro proceso en ejecución en ese momento.

l En espera: El proceso está a la espera de un recurso, por ejemplo en espera de una entrada/salida.

l Parado: El propio proceso ha solicitado su parada.

l Zombi: El proceso ha finalizado pero el sistema operativo sigue manteniendo toda la información relativa al mismo.

Obviamente en un sistema con un único procesador solamente puede haber un proceso en ejecución, mientras que los demás se encuentran en estado Preparado formando una cola de espera. Más adelante se tratará el tema de la organización de los programas colocados en la cola de espera.

LOS PROCESOS DESDE LA SHELL

Los comandos más utilizados para la gestión de los procesos son ps y kill. Mediante el primero de ellos se puede obtener una visión general del estado de los procesos arrancados en el sistema, aunque presenta una gran variedad de salidas dependiendo de los parámetros que se le suministren, siendo los más habituales:

l l: formato largo, muestra PID, PPID, tiempo de CPU consumido, etc.

l f: formato en árbol jerárquico.

l x: muestra los procesos que no tienen terminal de control.

Con el comando kill es posible mandar una señal al programa. Una señal es una técnica mediante la cual es posible alterar la secuencia normal del programa desde el exterior. En Linux existen 32 señales cada una de ellas con un uso muy específico, pero aunque es posible mandar cualquier señal desde la shell con el comando kill, lo más usual es utilizar este comando para finalizar un proceso mediante las señales 15 (SIGTERM) o 9 (SIGKILL) de la forma:

kill 15 PID_proceso

Las propiedades que tiene un proceso dependen en parte de las propias características que tenga el sistema operativo

La forma correcta de finalizar un proceso mediante kill consiste en utilizar la señal 15, pues utilizando esta señal se da la posibilidad al proceso de realizar un cierre ordenado del mismo, mientras que si se manda la señal 9 las posibilidades del proceso para realizar un cierre ordenado son bastante pocas. Si después de enviar la señal 15, y tras un tiempo prudencial (este tiempo dependerá de cada proceso y sólo la experiencia puede servir de guía), el proceso en cuestión no ha finalizado, entonces se deberá recurrir a la señal 9.

LOS PROCESOS DESDE LA PROGRAMACIÓN

Desde un programa y usando diversas llamadas al sistema se pueden realizar las siguientes tareas con procesos:

l Arrancar un proceso hijo.

l Esperar la finalización de uno o varios procesos hijos.

l Obtener y modificar los atributos de los procesos.

l Obtener y modificar las prioridades de planificación.

De todo este abanico de posibilidades se va a explicar únicamente el proceso de creación de un proceso hijo mediante la llamada al sistema fork, así como las estructuras que definen a los procesos dentro del sistema operativo con el fin de sentar las bases necesarias para comprender el funcionamiento del planificador de procesos del sistema operativo. De esta manera se podrán seguir los programas de la práctica con el máximo aprovechamiento.

Para crear un proceso se emplea la llamada al sistema fork. Cuando un proceso ejecuta esta llamada, el sistema operativo duplica el proceso que la ha ejecutado, tanto la parte de código, memoria estática, memoria dinámica y la pila, de tal forma que una finalizada la llamada ambos procesos están listos para tomar el control de la CPU.

Según lo expuesto tenemos dos procesos que pueden coger el control de la CPU, y ambos son idénticos, con lo cual cuando se ejecuten ambos procesos continuarán haciendo lo mismo. Si estos procesos estuvieran leyendo un fichero ambos leerían el mismo registro, realizarían el mismo proceso sobre los registros, y escribirían el mismo resultado sobre el mismo fichero, no hay que olvidar que al duplicarse el proceso se duplican los descriptores de ficheros y por lo tanto apuntan a los mismo ficheros. No parece tener mucho sentido, ya que lo más lógico es que si se crea un proceso hijo éste sirva para realizar una tarea distinta.

La forma de controlar esta situación después de una llamada al sistema fork consiste en determinar qué proceso de los dos es el padre y cuál es el hijo. Esta distinción se consigue basándose en el código de retorno de la llamada al sistema utilizada, que puede ser uno de los tres siguientes:

l Igual 0: Este código lo recibe el proceso hijo.

l Mayor que 0: Este código lo recibe el proceso que ha ejecutado la llamada al sistema, es decir el proceso padre. Y su valor es el número de proceso que identifica al proceso hijo obtenido como resultado de la llamada.

l Menor que 0: La llamada al sistema no ha podido crear un proceso. Este código lo obtiene obviamente el proceso que ejecuta la llamada.

Basándonos es estos códigos la codificación que se debe realizar debe tener un aspecto similar al siguiente:

rc = fork()
if (rc < 0) {

/* Llamada incorrecta */

exit;}

if (rc > 0)
{

/* Llamada correcta bloque de programa que ejecuta el proceso padre rc contiene el número de proceso de hijo */

{else

{/* Parte del programa que ejecuta el proceso hijo */}

Otro punto que puede ser tan importante como la creación de procesos hijos es el control de la finalización de los mismos. Un proceso puede esperar la finalización de un proceso hijo de dos formas: síncrona o asíncrona.

Con la forma síncrona el proceso está esperando la finalización de un proceso hijo determinado o un proceso hijo cualquiera mediante la llamadas al sistema: wait y waitpid. La primera alternativa espera la finalización de un proceso hijo cualquiera, mientras que la segunda resulta más interesante, ya que permite esperar a la finalización de un proceso hijo determinado según los valores que se le suministre a esta llamada. El prototipo de la función es el siguiente:

waitpid(pid_t pid, int statsup, int options)

Según los valores que se suministre en el parámetro pid, la llamada se comporta de la siguiente manera:

l pid > 0: Se espera a la finalización del proceso cuyo identificador corresponde al parámetro.

pid = - 1: Se espera a la finalización del primer proceso hijo, por lo que resulta similar a wait.

l pid = 0: Espera a la terminación de todos los procesos hijos que pertenecen al mismo grupo de procesos que el invocante.

l pid < -1: Espera a la finalización de todos los procesos hijos cuyo identificador de grupo es igual al valor absoluto del parámetro.

Esta llamada devuelve dos valores:

l El código de retorno que puede ser:

- El PID del proceso que ha

finalizado.

- 1 en caso de fallo.

- El estado del proceso hijo en la variable en el campo apuntado por el segundo parámetro.

LOS DEMONIOS

Se trata de unos procesos que se caracterizan porque se ejecutan haciendo que su proceso padre sea el proceso init, cuyo identificador de proceso es el 1, y no tienen como terminales asociados de entrada y salida la pantalla y el teclado respectivamente.

Si se arranca un proceso normal, bien desde la consola del sistema o un terminal tty, el terminal que ha lanzado el proceso queda bloqueado hasta que finalice dicho proceso, a no ser que este proceso se arranque en segundo plano (background). En cualquier caso, el proceso siempre queda ligado a la shell desde la que se ha arrancado por medio de una relación padre/hijo por un lado, y por otro al terminal desde el que se arrancó dicho proceso. Esta situación no es deseable para cierto tipo de procesos como pueden ser aquellos procesos que están manteniendo una sesión con otro proceso mediante un socket TCP/IP para controlar un dispositivo en tiempo real 24 horas al día, con lo que no es lógico tener un terminal tty bloqueado por esta razón, sin mencionar el peligro que puede suponer que alguien pulse las teclas Ctrl-C por error y finalice el proceso.

Un proceso para convertirse en demonio deben realizar dos tareas:

l Independizarse del padre y "engancharse" del init.

l Independizarse del terminal.

El siguiente código muestra como se realizan estas tareas:

if((pid = fork()) < 0)

{

perror("\nError en fork");

exit(0);

}

else

if (pid != 0)

{

sprintf(areaMensaje,

"Saliendo proceso padre PID: %d",getpid());

printf("%s\n",areaMensaje);

escribirMensaje(areaMensaje);

exit(0);

}

setsid();

chdir(dirlog);

/* Lógica del programa */

exit(0);

El esquema general de este código ya ha sido explicado anteriormente y ahora tan sólo debemos observar que el proceso padre ya finaliza. Cuando un proceso ya ha finalizado todos los procesos hijos que se siguen ejecutando van a pasar a depender en el orden de la jerarquía del proceso número 1, que no es otro que el proceso init.

La siguiente acción consiste en independizarse de los terminales, lo que se logra creando una nueva sesión para el proceso mediante la función setsid.

Este último paso, si bien no es necesario realizarlo para la creación de un demonio, resulta bastante conveniente con el fin de poder asegurarnos de que el proceso va a comenzar a trabajar en el directorio de trabajo deseado, y no desde el directorio en que se había situado la shell cuando se arrancó el proceso.

PLANIFICACIÓN DE PROCESOS

Una vez que se han visto las propiedades más notables de los procesos, así como la forma en que se pueden crear mediante llamadas al sistema, vamos a ver cómo gestiona el sistema operativo el acceso de los mismos al control del procesador. El encargado de realizar esta tarea es un componente del núcleo llamado Scheduler (Planificador).

Los procesos que están listos para poder ser ejecutados se colocan en una de las tres colas que tiene el Planificador. Dos de estas colas son para los llamados procesos en tiempo real y la otra para el resto de procesos, tales como los habituales de usuario.

En un sistema con un único procesador solamente puede haber un proceso en ejecución, mientras que los demás están en estado Preparado

Dentro de cada cola los procesos se colocan en un orden determinado en función de un valor que determina la prioridad, es decir en cada cola los procesos no se colocan según llegan, sino en función de un atributo del proceso que marca las prioridades. La prioridad de un proceso es un valor formado por otros dos, por un lado la propiedad fija que es adjudicada a cada proceso por medio de una llamada al sistema y la prioridad variable que se ajusta de manera dinámica.

La prioridad dinámica es gestionada por el sistema operativo en función de los recursos consumidos por el proceso, penalizando en alguna forma a aquellos procesos que consumen más recursos. También es posible modificarla desde la shell por medio del comando nice, aunque este tipo de prioridad está definida para los sistemas de planificación que no son de tiempo real. En este artículo nos vamos a centrar en la prioridad estática así como en las colas del planificador.

Tal y como se ha mencionado arriba existen tres colas para el planificador:

l SCHED_FIFO

l SCHED_RR

l SCHED_OTHER

La política del planificador es la siguiente:

l Se elige el proceso de la cola SCHED_FIFO de más alta prioridad y se ejecuta a menos que se de una de las siguientes condiciones:

l Existe otro proceso en dicha cola de más alta prioridad.

l El proceso genera una interrupción.

l Si no existen procesos en la cola SCHED_FIFO se selecciona un proceso de la cola SCHED_RR, y se ejecuta y no se interrumpe hasta que se dé una de las siguiente condiciones:

l Existe un proceso de cualquier prioridad en la cola SCHED_ FIFO.

l Existe un proceso de prioridad igual o superior en la cola SCHED_RR.

l El proceso genera una interrupción.

l Si no existen procesos en las colas SCHED_FIFO y SCHED_RR se selecciona el proceso de más alta prioridad de la cola SCHED_ OTHER, y sólo se interrumpe cuando exista un proceso de más alta prioridad en cualquiera de las tres colas.

Las llamadas al sistema que permiten modificar la política de planificación y las prioridades de un proceso son:

int sched_setscheduler (pid_t pid, int politica,

const struct sched_param *parametros);

int sched_getscheduler (pid_t pid);

siendo los parámetros que las definen los siguientes:

pid: identificador del proceso al que se desea cambiar la prioridad.

politica: es un valor entero que puede admitir los valores: SCHED_ FIFO (1), SCHED_RR (2) ó SCHED_ OTHER (0).

parametros: se trata de una estructura sobre la que se pueden modificar otros parámetros de la planificación del proceso.

Los prototipos de las funciones que gestionan la prioridad estática son:

int setpriority (int grupo, int ident, int prio);

int getpriority (int grupo, int ident);

EJEMPLO PRÁCTICO

Para poder ver un efecto práctico de lo se ha explicado en estas páginas se adjuntan tres programas: monit, mod y crea, siendo sus fuentes respectivos: monit.c, mod.c y crea.c. Además hay un fichero include con los valores que requieren los programas.

En el fichero Makefile se encuentran las sentencias para compilars, lo que se realiza con el comando: make instala. Esta sentencia compila y enlaza los tres programas y además crea los ficheros semkey y mq.log en el directorio /tmp. Si se desea cambiar estos nombres además de modificarlos en el Makefile será necesario cambiar los nombres de los ficheros en el fichero valores.h. Si en vez de make instala se ejecuta make todo se compilan y enlazan los tres programas.

El código de retorno de la llamada al sistema fork permite saber si se está en el proceso padre o en el hijo

En la figura 3 se puede observar un diagrama de bloques de los programas, siendo el principal monit, que se convierte en demonio una vez ha sido arrancado mediante la sentencia del mismo nombre. Un tema que tener en cuenta consiste en cómo se puede realizar la comunicación con este proceso una vez que se ha convertido en demonio, ya que una de las características de los demonios es su independencia del teclado. Las repuestas son varias: colas de mensajes, sockets, pipes con nombres, de por sí los mecanismos de comunicación entre programas justificarían varios artículos, únicamente decir que el programa utiliza para comunicarse las colas de mensajes. Los mensajes de salida realizan formateados sobre el fichero /tmp/mq.log, que puede ser cambiado tal y como se indica en las instrucciones que vienen en el fichero Makefile.

El programa una vez ha inicializado la cola de mensajes y antes de convertirse en demonio se establece con la política de prioridad SCHED_FIFO y con la máxima prioridad.

El programa crea, utilizando la cola de mensajes da una orden al programa monit para que cree un nuevo proceso (haciendo uso de la llamada al sistema fork), y según el parámetro que reciba le asigna una de las tres políticas de planificación. La sintaxis del comando crea es la siguiente:

crea [fifo | rr | otro ]

Los procesos son creados con una prioridad de 2 y son bucles simples.

Una vez que se ha arrancado el demonio monit ya estamos en condiciones de empezar a crear procesos y ver cómo se van ejecutando. Pero antes de proceder hay que plantearse la siguiente pregunta: si se arranca un proceso que tiene un bucle y además está en la cola SCHED_FIFO, ¿cómo afecta esto a la shell desde la que estoy trabajando?. La respuesta se resume en una palabra: fatal. Si una vez arrancado monit se crea un proceso mediante los comandos: crea rr, o crea fifo, la shell que está en la cola SCHED_OTHER difícilmente podrá acceder a la CPU para ejecutarse. Conclusión: todas las shell se quedan bloqueadas. El autor de este artículo no ha encontrado otra solución que pulsar el botón de reset del equipo para salir de esta situación.

La solución para evitar esta situación pasa por poner al menos una shell en la cola SCHED_FIFO con más prioridad que los procesos que se van a arrancar. El programa mod pone la shell en que se ejecuta en dicha cola con una prioridad de 99.

El atributo que define unívocamente a cada proceso es el identificador de proceso

(o process id), más conocido por PID

Las llamadas al sistema que realiza este programa y que están relacionadas con la gestión de procesos son: getppid para obtener el identificador del proceso padre, que es el que interesa cambiar de política de planificación. Es decir, hay que obtener el identificador de la shell que ejecuta el programa mod, ya que este programa es en sí mismo un proceso y una vez obtenido el identificador cambiar la política y prioridad por medio de la función sched_setscheduler.

El propio programa, si todo va correctamente, indica con unos mensajes la nueva prioridad y planificación de la shell. Además todo esto debe realizarse desde las pantallas locales y en modo texto puro, pues ciertos procesos de gestión del entorno X, así como de comunicaciones pueden ejecutarse en una política de prioridades diferente y quedar por debajo de las rutinas de monit.

Una buena configuración para poder trabajar con las rutinas consiste en tener varias sesiones en los terminales virtuales (Alt+Fn), dejando la shell Alt+F1 con la máxima prioridad y ejecutar la sentencia crea fifo, cambiar a otra sesión Alt+Fn (n>1), y observar como las demás sesiones han quedado congeladas, como si el sistema estuviera colgado. Pero desde la sesión Alt+F1 se puede trabajar normalmente, siendo posible comprobar como trabaja el proceso con el comando ps xf | grep monit (El parámetro x del comando ps lista los procesos demonios). Si ejecutamos damos varios de estos comandos podemos observar como el tiempo de CPU aumenta considerablemente. Matando el proceso, se puede acceder a las demás sesiones y ver como estas funcionan normalmente e incluso han respondido a las peticiones pendientes. Lo que parecía un "cuelgue" del sistema, no era más que una planificación de tareas poco acertada, y únicamente nos ha salvado de tener que hacer un al equipo la precaución de haber cambiado la prioridad de una shell. Si no se hubiera tomado esta precaución, todas las apariencias: procesos bloqueados, la luz de acceso al bus de datos apagada, etc., apuntan a un "cuelgue".

Una vez que se ha trabajado con este ejemplo y todo se ha realizado correctamente (fácil de comprobar, pues si todo va bien no hay que pulsar el botón reset), se pueden arrancar varios procesos. A modo de ejemplo se puede ejecutar la siguiente sentencia de comandos:

crea fifo

Tomar nota del número de proceso con:

ps xf | grep monit

crea fifo

Tomar nota del número de proceso de la misma forma.

En este punto se puede observar como solamente está trabajando el primer proceso. Esto es coherente con lo dicho anteriormente: un proceso en SCHED_FIFO sólo cede la CPU a otro proceso SCHED_FIFO con más prioridad; como ambos procesos tienen la misma prioridad el que tiene control de la CPU no la suelta, a excepción del proceso monit padre y nuestra propia shell que tiene una prioridad superior y la misma política de planificación.

A continuación se ejecutan los siguientes comandos:

crea rr

Tomar nota del número de proceso.

crea rr

Tomar nota del número de proceso.

Observamos la evolución de consumos de procesos y la situación sigue igual ya que los procesos en SCHED_RR se ejecutan después de los procesos en SCHED_FIFO.

Lo siguiente que hacemos es matar el primer proceso monit, observamos los recursos y vemos que el siguiente proceso con SCHED_FIFO toma control la CPU sobre los otros dos procesos SCHED_RR.

Si ahora arrancamos otro proceso SCHED_FIFO mediante crea fifo y matamos el que está activo, el nuevo proceso toma el control de la CPU y los procesos SCHED_RR siguen parados. Por último mataremos el proceso recién creado y observaremos la evolución de los consumos de los dos procesos que quedan en máquina. En este caso ambos progresan en el consumo de recursos, pues en la política SCHED_RR el sistema va balanceando el consumo de recursos entre los procesos con igual prioridad.

CONCLUSIÓN

A lo largo de este artículo se ha visto de forma general el mecanismo de prioridades de Linux. Comprendiendo y utilizando bien estos mecanismos es posible convertir un equipo Linux desde una máquina con usuarios en tiempo compartido y una política de penalización por consumo de recursos hasta un sistema de tiempo real con proceso críticos, e incluso hacer cohabitar ambos tipos de sistemas en una misma máquina.

   

<//!--fin tabla3-->

 
  Banner 468x60
  Explorer 4.0, Netscape 4.0. Resolución 800 x 600.
©Tower Communications 1.998.
Diseño: GRUPO ALBERTINA DE COMUNICACION.