Cambiar a contenido.

OCW UNED

Secciones
Herramientas personales
Acciones de documento
Saltar a contenido principal

Planificación tradicional en UNIX

En esta sección se va a describir el diseño y la implementación del planificador utilizado en BSD4.33 .La política de planificación que utiliza este planificador es del tipo round robin con colas multinivel. Cada proceso tiene asignada una prioridad de planificación4 que cambia con el tiempo. Dicha prioridad le hace pertenecer a una de las múltiples colas de prioridad que maneja el planificador.

El planificador siempre selecciona al proceso que encontrándose en el estado preparado en memoria principal para ser ejecutado o en el estado expropiado tiene la mayor prioridad. En el caso de los procesos de igual prioridad (se encuentran en la misma cola) lo que hace es ceder el uso de la CPU a uno de ellos durante un cuanto, cuando finaliza dicho cuanto le expropia la CPU y se lo cede a otro proceso. El planificador varía dinámicamente la prioridad de los procesos basándose en su tiempo de uso de la CPU. Si un proceso de mayor prioridad alcanza el estado preparado en memoria principal para ser ejecutado, el planificador expropia el uso de la CPU al proceso actual incluso aunque éste no haya completado su cuanto.

El núcleo tradicional de UNIX es estrictamente no expropiable. Es decir, si el proceso actual se encuentra en modo núcleo (debido a una llamada al sistema o a una interrupción), no puede ser forzado a ceder la CPU a un proceso de mayor prioridad.

Dicho proceso cederá voluntariamente la CPU cuando entre en el estado dormido. En caso contrario sólo se le podrá expropiar la CPU cuando retorne a modo usuario.

Prioridades de planificación de un proceso

La prioridad de planificación de un proceso es un valor entre 0 y 127. Numéricamente los valores más bajos corresponden a las prioridades más altas. Las prioridades entre 0 y 49 están reservadas para el núcleo, mientras que los procesos en modo usuario tienen las prioridades entre 50 y 127.

La entrada asociada a un proceso en la tabla de procesos posee los siguientes campos que contienen información relacionada con la prioridad de planificación:

  • p_pri. Contiene la prioridad de planificación actual.
  • p_usrpri. Contiene la prioridad de planificación actual en modo usuario.
  • p_cpu. Contiene el tiempo transcurrido desde que el proceso utilizó por última vez la CPU, también denominado uso reciente de la CPU.
  • p_nice. Contiene el factor de amabilidad, que es controlable por el usuario.

Los campos p_pri y p_usrpri se utilizan de modo diferente. El planificador consulta p_pri para decidir que proceso debe planificar. Cuando un proceso se encuentra en modo usuario, su valor p_pri es idéntico a p_usrpri. Cuando el proceso despierta después de haber entrado en el estado dormido durante una llamada al sistema, su prioridad es temporalmente aumentada para dar preferencia al procesamiento en modo núcleo. Por este motivo el planificador utiliza p_usrpri para salvar la prioridad que debe ser asignada al proceso cuando éste retorne al modo usuario, y p_pri para almacenar su prioridad en modo núcleo.

El núcleo asocia una prioridad de dormir en función en función del evento por el que el proceso entró en el estado dormido. Ésta es una prioridad en modo núcleo, y por tanto su valor está comprendido entre 0 y 49. Por ejemplo (ver Figura 4.2), la prioridad de dormir para un proceso esperando por la entrada en un terminal es 28, mientras que para un proceso esperando por una operación de E/S con el disco es 20. Cuando un proceso despierta, el núcleo configura su valor p_pri a la prioridad de dormir del evento o recurso. Puesto que las prioridades en modo en modo núcleo son más altas que las prioridades en modo usuario, estos procesos son planificados antes que aquellos que ejecutan código de usuario. Esto permite que las llamadas al sistema se puedan completar apropiadamente, lo que es deseable puesto que los procesos pueden tener bloqueado algún recurso clave del núcleo mientras ejecutan la llamada al sistema.

Cuando un proceso completa la llamada al sistema y va a retornar a modo usuario, su prioridad de planificación es configurada a su prioridad en modo usuario actual, es decir, al valor que se encontraba almacenado en p_usrpri. Si esta prioridad es más baja que la de otros procesos planificables, entonces el núcleo realizará un cambio de contexto.

La prioridad de ejecución en modo usuario depende de dos factores: el factor de amabilidad (p_nice) y el uso reciente de la CPU (p_cpu).

El factor de amabilidad es un número entero entre 0 y 39. Su valor por defecto es 20. Se denomina factor de amabilidad, porque un usuario incrementando este valor está disminuyendo la prioridad de sus procesos y en consecuencia le está cediendo el turno de uso de CPU a los procesos de otros usuarios. A los procesos en segundo plano el núcleo les asigna de forma automática un factor de amabilidad elevado.

El factor de amabilidad de un proceso también puede ser disminuido y en consecuencia se estaría aumentando su prioridad. Esta acción solamente la puede realizar el superusuario.

La llamada al sistema nice permite aumentar o disminuir el factor de amabilidad actual del proceso que la invoca. Por lo tanto un proceso no puede modificar el factor de amabilidad de otro proceso. Su sintaxis es:

resultado=nice(incremento);

donde incremento es una variable entera que puede tomar valores entre -20 y 19. El valor de incremento será sumado al valor del factor de amabilidad actual. Sólo el superusuario puede invocar a nice con valores de incremento negativos. Si se produce un error durante la ejecución de nice, entonces resultado contendrá el valor -1.

También es posible modificar el factor de amabilidad de un proceso desde la línea de comandos mediante el uso del comando nice.

Los sistemas de tiempo compartido intentan asignar el procesador de tal forma que las aplicaciones en competición reciban aproximadamente la misma cantidad de tiempo de CPU. Esto requiere monitorizar el uso de la CPU de los diferentes procesos y utilizar esa información en las decisiones de planificación. El campo p_cpu almacena una medida del uso reciente de la CPU por parte del proceso. Este campo se inicializa a 0 cuando el proceso es creado. En cada tic, la rutina de tratamiento de la interrupción del reloj incrementa p_cpu para el proceso actualmente en ejecución, hasta un máximo de 127. Además, cada segundo, el núcleo invoca a una rutina denominada schedcpu que reduce el valor de p_cpu de un proceso mediante un factor de disminución (decay). SVR3 utiliza un factor de disminución fijo de 1/2, mientras que BSD4.3 utiliza la siguiente fórmula:

decay=(2*load_average)/(2*load_average+1); (1)

donde load_average es el número medio de procesos preparados para ejecución en el último segundo. Luego al cabo de un segundo el nuevo valor de p_cpu vendrá dado por la fórmula:

p_cpu=decay*p_cpu; (2)

La rutina schedcpu también recalcula las prioridades de usuario de todos los procesos usando la fórmula:

p_usrpri=PUSER+(p_cpu/4)+(2*p_nice); (3)

donde PUSER es la prioridad de usuario base, que vale 50. Este valor es la prioridad más alta que puede tomar un proceso ejecutándose en modo usuario, y es justamente el límite con respecto a las prioridad en modo núcleo.

Como resultado, si un proceso ha acumulado recientemente un gran cantidad de tiempo de CPU, su factor p_cpu aumentará. Ello producirá un mayor valor de p_usrpri, y por tanto una prioridad de ejecución más baja. Cuanto más tiempo está esperando un proceso en ser planificado, más disminuirá el factor de disminución su p_cpu, y en consecuencia su prioridad irá aumentando.

Este esquema evita que los procesos de baja prioridad nunca lleguen a ser ejecutados. También favorece a los procesos limitados por E/S (procesos que requieren muchas operaciones de E/S, por ejemplo, las consolas de comandos y los editores de texto) en contraposición a los procesos limitados por la CPU (procesos que requieren mucho uso de la CPU, por ejemplo, los compiladores). Un proceso limitado por E/S, mantiene una alta prioridad ya que su p_cpu es pequeño, y recibe tiempo de CPU rápidamente cuando la necesita. Por contra, los procesos limitados por la CPU tienen valores de p_cpu altos y por tanto una baja prioridad.

El factor de uso de la CPU suministra justicia y paridad en la planificación de los procesos de tiempo compartido. La idea básica es mantener la prioridad de todos estos procesos en un rango aproximadamente igual durante un periodo de tiempo. Los procesos subirán o bajarán dentro de un cierto rango dependiendo de cuanto tiempo de CPU hayan consumido recientemente. Si las prioridades cambian demasiado lentamente, los procesos que comenzaran con una prioridad más baja, permanecerían así durante largos periodos de tiempo, por lo que su ejecución se demorará demasiado.

El efecto del factor de disminución es suministrar un promedio ponderado exponencialmente de uso de la CPU a los procesos durante todo su tiempo de vida. La formula usada en el SVR3 conduce a un promedio exponencial simple, que tiene como efecto indeseable la elevación de las prioridades cuando la carga del sistema aumenta. Esto es así porque en un sistema con mucha carga cada proceso recibe poco uso de la CPU y en consecuencia su valor de uso de la CPU se mantiene bajo, y el factor de disminución lo reduce aún más. Como resultado, el uso de la CPU no tiene mucho impacto en la prioridad, y los procesos que comienzan con una prioridad más baja se quedan sin usar la CPU durante un tiempo desproporcionado.

La aproximación BSD4.3 fuerza al factor de disminución a depender de la carga del sistema. Cuando la carga es elevada el factor de disminución es pequeño. Consecuentemente, procesos que reciben ciclos de CPU verán rápidamente disminuida su prioridad.

Implementación del planificador

El planificador mantiene un array denominado qs de 32 colas de ejecución (ver Figura 6.2). Cada cola se corresponde a cuatro prioridades adyacentes. Así, la cola 0 es utilizada por las prioridades 0-3, la cola 1 por las prioridades 4-7, etc. Cada cola contiene la cabecera de la lista doblemente enlazada de entradas de la tabla de procesos. La variable global whichqs contiene un mapa de bits con un bit asociado a cada cola. El bit está activado si hay al menos un proceso en la cola. Solamente procesos planificables son mantenidos en estas colas del planificador. Esto simplifica la tarea de selección de un proceso para ser ejecutado. El algoritmo del núcleo que implementa el cambio de contexto (swtch en BSD4.3), examina whichqs para encontrar el índice del primer bit activado. Este índice identifica la cola del planificador que contiene al proceso ejecutable de más elevada prioridad. El algoritmo swtch borra al proceso de la cabeza de la cola, y realiza el cambio de contexto.

  • Ejemplo 6.4:

En la Figura 6.2 se muestra el array qs y la variable global whichqs. Se observa que la cola 3 (se comienza a contar desde 0) contiene 3 procesos cuyas prioridades se encuentran en el rango 12-15. En consecuencia, en la variable whichqs el cuarto bit desde la izquierda, que es el asociado a la cola 3, se encuentra activado.

Asimismo se observa que la cola 5 contiene 2 procesos cuyas prioridades se encuentran en el rango 20-23. Por lo tanto, en la variable whichqs el sexto bit desde la izquierda, que es el asociado a la cola 5, se encuentra activado.

Cuando el algoritmo del núcleo que implementa el cambio de contexto (swtch), examine whichqs comenzando por la izquierda el primer bit que encontrará activado será el asociado a la cola 3. El proceso que será planificado será el que se encuentre en la cabeza de la cola. Así que el algoritmo swtch borra al proceso de la cabeza de la cola, y realiza el cambio de contexto.

     
  Figura 6.2: Estructuras que usa el planificador en el UNIX BSD4.3  


Figura 6.2: Estructuras que usa el planificador en el UNIX BSD4.3

Puesto que tanto BSD4.3 y SVR2 y SVR3 tenían a la arquitectura VAX-11 como máquina objetivo, la implementación del planificador está fuertemente influenciado por esta arquitectura.

Manipulación de las colas de ejecución

El planificador sigue las siguientes reglas para manipular las colas de ejecución:

  • El proceso de más alta prioridad siempre se ejecuta, excepto si el proceso actual se está ejecutando en modo núcleo.
  • Un proceso tiene asignado un tiempo de ejecución fijo denominado cuanto (100 ms en 4.3BSD). Esto solamente afecta a la planificación de los procesos pertenecientes a la misma cola. Cada 100 milisegundos, el núcleo invoca (usando un callout) una rutina denomina roundrobin para planificar al siguiente proceso de la misma cola.
  • Si un proceso de más alta prioridad fuese puesto en el estado listo para ejecución, éste sería planificado de forma preferente sin esperar por roundrobin.
  • Si los procesos en el estado preparado en memoria para ser ejecutado o en el estado expropiado pertenecen a una cola de prioridad más baja que el proceso actual, éste continuará ejecutándose incluso aunque su cuanto haya expirado.

La rutina schedcpu recalcula la prioridad de cada proceso una vez por segundo. Puesto que la prioridad no puede cambiar mientras el proceso está en la cola de planificados, schedcpu borra al proceso de la cola, cambia su prioridad, y lo vuelve a colocar, quizás en una cola de prioridad distinta. La rutina de tratamiento de la interrupción del reloj recalcula la prioridad del proceso actual cada cuatro tics.

El núcleo configura un indicador denominado runrun, que indica que un proceso (B) de mayor prioridad que el actual (A) está esperando para ser planificado. Cuando el proceso A retorne a modo usuario, el núcleo comprueba el indicador runrun, si está activado, transfiere el control a la rutina de cambio de contexto, para iniciar un cambio de contexto y planificar a B.

  • Ejemplo 6.5:

Supóngase que en un sistema UNIX el tic de reloj es de 10 ms y que el cuanto es de 100 ms. En consecuencia en un cuanto se producirán 10 tics.

Supóngase también que tres procesos A, B y C han sido creados de forma simultánea con una prioridad inicial p_usrpri=90. El factor de amabilidad para todos ellos es p_nice=20. La prioridad de usuario base es PUSER=50. El tiempo de uso de la CPU (en tics) es p_cpu=0 para los tres procesos. Se va a utilizar la siguiente notación p_usrpri(X) y p_cpu(X) denotan la prioridad de usuario y el tiempo de uso de la CPU, respectivamente, para el proceso X.

Supóngase además que los procesos durante su ejecución no invocan a ninguna llamada al sistema, y que no existe ningún otro proceso en el sistema en el estado preparado para ejecución.

En el modelo de planificador descrito la rutina de tratamiento de la interrupción de reloj (se ejecuta cada tic) recalcula usando la ecuación (3) la prioridad del proceso actual cada 4 tics (es decir, 40 ms). Al finalizar un cuanto se dispara a la rutina roundrobin que planifica al siguiente proceso de la misma cola de ejecución. Cada segundo se dispara a la rutina schedcpu que reduce el tiempo de uso de la CPU p_cpu de todos los procesos planificables mediante un factor de disminución decay=1/2 usando la ecuación (2) y recalcula la prioridad de usuario de todos los procesos planificables usando la ecuación (3).

Por simplificar la descripción se va a suponer que la rutina del núcleo asociada al tratamiento de la interrupción de reloj, la rutina roundrobin y la rutina schedcpu se ejecutan de manera prácticamente instantánea.

En el rango de tiempo entre 0 y 100 ms se ejecuta el proceso A. Durante este tiempo la rutina de tratamiento de la interrupción de reloj recalcula la prioridad del proceso actual dos veces en 40 ms y 80 ms usando la ecuación (3). En 40 ms p_cpu(A)=4 tics, luego

p_usrpri(A)=PUSER+(p_cpu(A)/4)+(2*p_nice(A))= 50+(4/4)+(2*20)=91

En 80 ms p_cpu(A)=8 tics, luego

p_usrpri(A)= 50+(8/4)+(2*20)=92

Al finalizar el cuanto en 100 ms se dispara roundrobin que planifica al proceso B. Este se ejecuta en el rango entre 100 y 200 ms. La rutina de tratamiento de la interrupción de reloj recalcula la prioridad del proceso actual dos veces en 140 y 180 ms. En 140 ms p_cpu(B)=4 tics, luego

p_usrpri(B)= 50+(4/4)+(2*20)=91

En 180 ms p_cpu=8 tics, luego

p_usrpri(B)= = 50+(8/4)+(2*20)=92

Al finalizar el cuanto en 200 ms se dispara roundrobin que planifica al proceso C. Este se ejecuta en el rango entre 200 y 300 ms. La rutina de tratamiento de la interrupción de reloj recalcula la prioridad del proceso actual dos veces en 240 y 280 ms. En 240 ms p_cpu(C)=4 tics, luego

p_usrpri(C)= 50+(4/4)+(2*20)=91

En 280 ms p_cpu=8 tics, luego

p_usrpri(C)= 50+(8/4)+(2*20)=92

Este esquema de funcionamiento se iría repitiendo hasta llegar a 1s. Así se tiene que:

  • En el rango [300, 400] ms se ejecuta el proceso A. Su tiempo de CPU al finalizar el cuanto es p_cpu(A)=16 y su prioridad de usuario es p_usrpri(A)= 94.
  • En el rango [400, 500] ms se ejecuta el proceso B. Su tiempo de CPU al finalizar el cuanto es p_cpu(B)=16 y su prioridad de usuario es p_usrpri(B)= 94.
  • En el rango [500, 600] ms se ejecuta el proceso C. Su tiempo de CPU al finalizar el cuanto es p_cpu(C)=16 y su prioridad de usuario es p_usrpri(C)= 94.
  • En el rango [600, 700] ms se ejecuta el proceso A. Su tiempo de CPU al finalizar el cuanto es p_cpu(A)=24 y su prioridad de usuario es p_usrpri(A)= 96.
  • En el rango [700, 800] ms se ejecuta el proceso B. Su tiempo de CPU al finalizar el cuanto es p_cpu(B)=24 y su prioridad de usuario es p_usrpri(B)= 96.
  • En el rango [800, 900] ms se ejecuta el proceso C. Su tiempo de CPU al finalizar el cuanto es p_cpu(C)=24 y su prioridad de usuario es p_usrpri(C)= 96.
  • En el rango [900, 1000] ms se ejecuta el proceso A. Su tiempo de CPU al finalizar el cuanto es p_cpu(A)=32 y su prioridad de usuario es p_usrpri(A)= 98.

Al cabo de 1 s se dispara la rutina schedcpu que disminuye el tiempo de uso de CPU de todos los procesos usando la fórmula (2)

p_cpu(A)=decay*p_cpu(A)=(1/2)*32=16

p_cpu(B)=decay*p_cpu(B)=(1/2)*24=12

p_cpu(C)=decay*p_cpu(C)=(1/2)*24=12

Asimismo la rutina schedcpu recalcula la prioridad en modo usuario de todos los procesos usando la fórmula (3)

p_usrpri(A)= 50+(16/4)+(2*20)=94

p_usrpri(B)= 50+(12/4)+(2*20)=93

p_usrpri(C)= 50+(12/4)+(2*20)=93

Además quita a los tres procesos de la cola de ejecución en la que habían sido colocados al ser creado, la asociada al rango 88-91, y los coloca en la cola de ejecución asociada al rango de prioridades 92-95.

Si no existe otro proceso más prioritario el próximo proceso en ser planificado será el proceso B.

Análisis

El algoritmo de planificación tradicional es simple pero efectivo. Es adecuado para un sistema de tiempo compartido con una mezcla de trabajos interactivos y batch. El cálculo dinámico de las prioridades previene el abandono de cualquier proceso. Esta implementación favorece a los trabajos limitados por E/S que requieren de forma poco frecuente ciclos de CPU.

El planificador tiene varias limitaciones que lo hacen poco adecuado para su uso en una amplia variedad de aplicaciones comerciales:

  • No está bien escalado, si el número de procesos es muy grande resulta poco eficiente para calcular todas las prioridades cada segundo.
  • No hay forma de garantizar un determinado tiempo de uso de la CPU a un proceso o a un grupo de procesos en concreto.
  • Las aplicaciones tienen poco control sobre sus prioridades. El mecanismo del factor de amabilidad es demasiado simple y resulta inadecuado.
  • Puesto que el núcleo no es expropiable, los procesos de mayor prioridad quizás tengan que esperar una cantidad de tiempo significativa incluso después de estar en el estado preparado para ejecución. A este fenómeno se le denomina inversión de prioridades.

Los sistemas UNIX modernos son utilizados en una amplia gama de entornos. En particular, hay una fuerte necesidad para que el planificador soporte aplicaciones en tiempo real que requieren un comportamiento más predecible y tiempos de respuesta limitados. Por ello los sistemas UNIX modernos (SVR4, Solaris 2.x, etc) tuvieron que rediseñar por completo el planificador.