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.
|