|
|
Scheduling en UNIX y Linux
Hay una necesidad por procesos en un sistema los cuales
ocasionalmente
piden servicios del kernel. Algunnos sistemas operativos antiguos
tuvieron un estilo citado
de
proveer esos servicios - El proceso requiere un servicio y espera un
punto particular, hasta que la tarea venga del kernel y sirva por parte
del proceso.
UNIX trabaja muy distinto. Mejor que tener tareas del
kernel sirviendo las peticiones de un proceso
, el proceso entra por si mismo a Espacio de Kernel. Esto
significa, mejor que tener
el proceso esperando "fuera" del kernel, este entra al kernel por si
mismo (i.e. el proceso comenzara
a ejecutar codigo de kernel por si mismo).
Esto puede sonar como un recipiente para un desastre,
pero la
capacidad del proceso de entrar a espacio de kernel esta estrictamente
crontrolado (requiere soporte de hardware). Por ejemplo, en x86, un
proceso entra a espacio de kernel a traves de llamadas de sistema -
puntos bien conocidos a los cuales el proceso debe invokar para entrar
al kernel.
Cuando un proceso invoca una llamada de sistema, el
hardware es
cambiado a configuraciones de kernel (por ejemplo,
en x86, entre otras cosas, el nivel de proteccion esta establecido a
ring 0 en vez de ring 3). En este punto, el proceso estara ejecutando
codigo de la imagen del kernel. Tiene poder completo para hacer
estragos
en este punto, no como cuando estuvo en espacio de usuario. Ademas, el
proceso no es mas pre-emptible.
Pre-emptibilidad
Los procesos en espacio de usuario son pre-emptibles
- esto significa que los procesos pueden tomar la CPU arbitrariamente.
Asi es como funcionan las multitarea pre-emptive: la rutina de
scheduling
peridocamente suspendera el proceso actual en ejecucion, y posiblemente
schedule otra tarea para correr esa CPU. Esto significa teoricamente,
que un proceso puede estar en la situacion donde este nunca
devuelve la CPU. En relialidad el codigo de scheduling tiene interes en
la viabilidad y tratara de dar la CPU a cada proceso con un nivel debil
de viabilidad, pero no hay garantias.
En contraste, toda tarea (todos los objetos schedulables
son
referidos como "tareas" para claridad) corriendo en espacio de kernel No
puede ser pre-empted. Esto significa que la CPU nunca sera
scheduled away de la tarea. Este hecho es complicado por dos aspectos :
Interrupciones
A menos que se hayan deshabilitado las interrupciones (y algunas
interrupciones no pueden ser deshabilitadas), una interrupcion puede
ocurrir que durante la cual temporalmente interrumpira la tarea en
ejecucion. Esto puede pasar en tareas en ambos espacios, de usuario y
de kernel. La diferencia es que aqui, para tareas en espacio de kernel,
la interrupcion garantiza retorno de la CPU a la tarea tarde o
temprano. Para tareas en espacio de usuario, la interrupcion puede
causar que otra tarea sea scheduled en la CPU, y puede tambien puede
que se ejecuten otras tareas, - aqui en espacio de usuario
la tarea debe ser escogida de nuevo por el scheduler. Por supuesto que
esta no es la explicacion completa (como es usual)
- Primero, hay subsistemas en espacio de kernel que pueden registrar
codigo para ser ejecutado al volver de una interrupcion, tal como
bottom halves y los tasklets. Esto no cambia el hecho de que, sin
embargo, de que el scheduler no estara envuelto en la interrupcion de
una tarea en espacio de kernel. Segundo, las interrupciones pueden
interrumpir interrupciones -
un ejemplo saliente es la arquitectura ARM, donde las interrupciones
rapidas (FIQs) tienen prioridad mas alta de hardware que las
interrupciones normales (IRQs). Por eso de hecho de una FIQ puede
retornar a otro manejador de interrupciones. Tarde o temprano a traves,
de la interrupcion original se completara y returnara la CPU a la tarea
en espacio de kernel.
Multi-tarea co-operativa
Ya hemos dicho que una tarea en
espacio de kernel space no puede tener la CPU scheduled away desde
esta. Sin embargo, puede escoger ser schedule() con proposito (y por
razones de latencia toda buena llamada de sistema hace esto
usualmente). Notese que el termino "con proposito" es un poco
engañoso
- ya que actualmente significa que una tarea en espacio de kernel,
lleno de propositos incluye codigo que puede causar que pase un
scheduling. Por ejemplo una tarea puede establecer la politica SCHED_YIELD,
entonces se llama volunariamente a la rutina para darle la CPU.
Pero tambien puede causar un schedule() to
happen usando rutinas que puedan dormir. Un ejemplo comun es kmalloc(),
la cual duerme cuando es llamada con una prioridad GFP_KERNEL.
La diferencia aqui pienso es que el codigo en espacio de kernel "sabe"
que esto puede causar que ocurra un schedule, por eso tiene la
oportunidad de mantener o retener la CPU si quiere. Una tarea en
espacio de usuario no tiene oportunidad de eleccion de escoger en el
asunto. Una consecuencias esta descrita mas abajo en la seccion
"Scheduling y Locks".
Contextos de usuario e hilos del kernel
Recuerda, para el
scheduler y para el kernel en grande, todo objecto que sea schedulable
(i.e. cualquiera que pueda ser escogido por la rutina schedule())
es conocido como tarea. No se hace distinction entre alguno de esos
objectos, por eso usualmente son llamados procesos, LWPs, hilos del
kernel , fibras, hilos, etc. son todos solo tareas para el
kernel, cada uno de ellos con sus propias caracteristicas particulares.
Esto es una gran ganancia en terminos de limpieza de kernel -
no hay razon real para separarlos fuera de caso.
Estas caracteristicas particulares estan pensadas
interesantemente . Por ejemplo algunas tareas pueden tener
mapeos de memoria y pila en espacio de usuario - un ejemplo tipico es
un proceso que esta en espacio de usuario. El termino contexto de
procesoes
usado para referirse a una de esas tareas que se ejecutan en espacio de
kernel - ellas tienen de mapeos de espacio, y los (posiblemente
temporales) mapeos de kernel y pila. En este contexto se tiene sentido
de la copia desde/hacia memoria de usuario.
Una vez mas, lo que a veces se conoce como "hilos del
kernel" o
"fibras" no se tratan diferente a las otras tareas.
Ellas pueden tener mapeos de memoria en espacio de usuario solo como
procesos "normales". La unica caracateristica distinguida aqui es el
codigo que se ejecuta por los hilos de kernel, que vienen del kernel o
de la imagen de un modulo, mejor que desde las imagenes de procesos
binarios.
El termino contexto de interrrupcion
se usa usualmente para denotar al codigo que actualmente se ejecuta
como resultado de una interrupcion de hardware. Esto rodea bottom
divide en dos, ISRs, softirqs, y tasklets. Aqui no hay tarea asociada
tal como de menos significado para el schedule (y de hecho panicking
bug). Esto tambien significa que no puedes dormir aqui,
esto implica un schedule.
Algoritmo para Scheduling
El scheduler tiene un problema - hay una contradiccion entre
latencia/igualdad (permitiendo que cada tarea se ejecute tan pronto
como sea posible) y el costo del cambio de contexto (las operaciones
necesarias para cambiar una tarea por otra).
Que se asigne demasiado tiempo a una tarea significa que los procesos
tendrar que esperar mas por la CPU - esto no es bueno
si uno de los procesos esta tratando de brindar facilidades
interactivas, por ejemplo. Hacer demasiados cambios usualmente se toma
demasiado tiempo en este proceso, dejando menos CPU para hacer algo
util en el momento.
Cada tarea pre-emptible es ubicada en un periodo de tiempo - un
pequeñisimo periodo de tiempo durante el cual se puede
ejecutar.
La Interrupcion temporizador es invocada peridicamente y esta decidira
si la tarea deberia ser pre-empted en favor de otra tarea en la lista
de ejecución (la lista de ejecucion es una lista que tiene
todas las
tareas que estan listas para ejecutarse). Adicionalmente, en el
scheduling puede ocurrir cuando sea pedido por el codigo del kernel, y
tambien despues que finalizan las llamadas a sistema, en la ruta de
retorno de la llamada de sistema del codigo de kernel a espacio de
usuario. Mas detalles: aqui.
Algun codigo
FIXME
Scheduling y Locking
Ya hemos mencionado que las tareas del kernel no pueden ser pre-empted
a menos de que ellas escogan perimitirlo, en puntos bien conocidos (tal
como kmalloc() llamada de prioridad GFP_KERNEL).
Pero en sistemas SMP, esto aun significa que muchas tareas pueden
ejecutarse en espacio de kernel al mismo tiempo (ademas se necesita
protección adicional contra interrupciones
, igual en sistemas ARRIBA).
Una consecuencia obia es que las tareas necesitan ser "sincronizadas",
i.e. recursos compartidos deben darse exclusivamente a una tarea
alterando esos resultados. La falta de sincronización
adecuada de
recursos compartidos se conoce como "condicion de carrera" - llamada de
la nocion de que una tarea "esta en carrera" con otra en acceso al
recurso. Esto es imparcialmente obio algo malo. Unos de los mecanismos
de sincronizacion es el spinlock.
Esta es simplemente es una estructura de datos la cual es adquirida
automaticamente, y solo puede ser matenida por una sola tarea al
tiempo. El
spinlock es ya adquirido sobre (la sección de codigo que
modifica el
recurso compartido de un "estado conocido"
a otro). Si la tarea trata de adquirir un spinlock ya ocupado (con spin_lock(&lock)
o funcion similar)
este "girara", i.e. ejecuta un pequeño bucle hasta que
se lanza el spinlock .
Esto es por que es una mala idea (lee: ilegal) llamar a una funcion que
puede dormir mientras un spinlock: Mantendras la CPU para otra tarea
que puede tratar de aquirir el mismo spinlock. Esto puede facilmente
direcciona a un deadlock donde tienes una tarea A esperando por
un spinlock entonces este puede dejar libre un recurso necesitado por
la tarea B, la cual actualmente mantiene el spinlock que
quiere la tarea A.
(Adicionalmente el scheduling con un spinlock ya adquirido significa
que los apuntadores rotos pueden seguirse cuando se manipula la lista
de ejecucion (runqueue)).
Pero, escuche que preguntas, seguramente esto es usualmente necesario
para dormir mientras aun se manrtiene la exclusion mutual en alguna
estructura de datos ?
y en efecto, es. El metodo que generalmente se usa aqui es el semaforo.
Hay mucho esfuerzo de diferentes locking primitives
, tal como los tipos atomic_t. Lee el documento sobre el
kernel-locking en Documentation/DocBook/
en las fuentes del arbol del kernel para mas informacion sobre esto.
Algo menor que notar es que el big kernel lock (BKL), usado por lock_kernel()
y unlock_kernel(), no es un spinlock normal. Puedes
dormir con el BKL ya adquirido, y este se liberara cuando se realize un
schedule.
Glossario
- Pre-emptible
- Una tarea es pre-emptible si la CPU puede ser
scheduled y deja
de ejecutarse y pasa a otra tarea del proceso. Esto es diferente a una
interrupción, donde la interrupcion usa temporalmente la
CPU.
- Espacio de kernel
- Es el codigo que se ejecuta el cual proviene de una
imagen de
Kernel o de una imagen de un modulo. Esto codigo tiene permisos
completos para hacer lo que el quiera (en x86, el codigo esta en ring
0). Esta puede ser una tarea o una interrupcion invocada. Si es una
tarea,
no es pre-emptible. El acceso a memoria en espacio de usuario
usualmente requiere que los datos sean copidados.
- Espacio de Usuario
- El codigo ejecutable que proviene de la imagen de un
proceso
normal. Este se ejecuta en nivel de ring 3 en x86 y tienen derechos
limitados.
Esta protegido para no afectar a otros procesos o la direccion de
espacio del kernel y no tiene acceso directo al hardware (a menos que
sea otorgado por el kernel explicitamente).
- Contexto de proceso
- Codigo ejecutable del kernel por parte de un proceso.
El
codigo puede schedule (durmiendo, o explicitamente) pero aun no es
pre-emptible.
*** Correcciones a moz@compsoc.man.ac.uk.
|
|