Diseño e Implementación de un Planificador para un Sistema de Virtualización basado en Minix Prinsich Bernz, Emilio Quaglia, Constanza Director: Pessolani,Pablo Universidad Tecnológica Nacional Facultad Regional Santa Fe Abstract El sistema operativo de microkernel Minix [1] brinda un entorno adecuado para desarrollos experimentales, incluso para incorporarle funciones de hipervisor y poder gestionar así Máquinas Virtuales (del inglés VMs). El presente trabajo fue realizado en el marco de la catedra “Diseño e Implementación de Sistemas Operativos” del 4° nivel de la Carrera de Ingeniería en Sistemas de Información. El mismo busca diseñar y evaluar un planificador jerárquico que permita seleccionar, en primera instancia, la máquina virtual en función de los recursos disponibles en una reserva y luego el próximo proceso a ejecutar de esa máquina virtual. El proyecto propone un algoritmo de planificación de tipo token bucket (cubeta de fichas) [2] para la selección de las máquinas virtuales y administración de los tiempos de CPU para cada una. Palabras Clave Minix, Sistema Operativo, hipervisor, planificación de procesos, Máquinas Virtuales, paravirtualización, Token Bucket. Introducción La virtualización [3] consiste en un software que emula al hardware y puede ejecutar programas como si fuera una computadora real. Dicho software se denomina hipervisor. Esta tecnología permite que una sola computadora contenga múltiples máquinas virtuales (VMs), denominadas huéspedes (en inglés Guests). En particular, la paravirtualización es un tipo de virtualización en la cual el código fuente de los huéspedes es modificado de manera que en lugar de ejecutar instrucciones sensibles que provocan excepciones que son atrapadas por el hipervisor, realicen explícitamente llamadas al hipervisor (hypervisor calls). Minix es un OS basado en microkernel y estratificado, desarrollado por Andrew Tanenbaum con fines académicos. En Minix, todos los procesos y tareas son aislados y se comunican a través de mensajes. El microkernel se encarga de planificar procesos, de las transiciones entre estados, de atender interrupciones, excepciones y fallos, y de la transferencia de mensajes. Compartiendo el espacio de direcciones del microkernel se encuentran dos procesos con privilegios especiales: la CLOCK task y la System task (SYSTASK). La CLOCK task es el proceso que se encarga de gestionar aquellas operaciones relacionadas con el reloj y el tiempo del sistema. La SYSTASK es el proceso representante del kernel que brinda servicios a servidores y tareas a través de una serie de llamadas al kernel (kernel calls) para que puedan realizarse operaciones privilegiadas. En este sentido, el microkernel de Minix posee un mecanismo similar al de la paravirtualización. Se puede afirmar que este actúa como hipervisor paravirtualizado de una única VM. El propósito de este artículo es exponer el trabajo realizado acerca del diseño, modelado, implementación y evaluación de un planificador jerárquico para un sistema de virtualización basado en Minix, denominado MHyper. MHyper puede dar soporte a múltiples VMs con Minix como huesped. Se trabajó sobre un algoritmo de tipo token bucket para planificar las VMs y sus respectivos procesos. Elementos del Trabajo y metodología Contexto Una máquina virtual debe contar con las siguientes características: Duplicado: Debería comportarse de forma idéntica a la máquina real, excepto por la existencia de menos recursos disponibles y diferencias de temporización al tratar con dispositivos; Aislamiento: Se pueden ejecutar varias VMs sin interferencias. El aislamiento debe ser: o De rendimiento: El rendimiento de una VM no debe afectar al de otra VM; o De fallo: Un fallo de una VM no debe provocar ningún efecto en las demás; o De seguridad: No debe ser posible acceder a ningún recurso de una VM desde otra; Eficiencia: La VM debería ejecutarse a una velocidad cercana a la del HW real. Existen varios tipos de virtualización: Hipervisor tipo I (full virtualization): El monitor se ejecuta directamente sobre el hardware en modo kernel y los huéspedes sobre el monitor en modo usuario; Hipervisor tipo II (indirecto): El monitor se ejecuta sobre un OS en modo usuario y los huéspedes sobre el monitor; Paravirtualización: Consiste en modificar el código fuente del OS huésped para que en vez de ejecutar instrucciones sensibles realicen llamadas al hipervisor. De esta forma se logra un rendimiento cercano a tener máquinas reales. Minix es un sistema operativo de tipo cliente/servidor basado en microkernel. En su versión 3 los procesos se dividen en 4 capas, como se muestra en la figura 1. Los procesos de las capas 2, 3 y 4 se ejecutan en modo usuario y comprenden las siguientes categorías: Procesos de usuarios; Procesos servidores; Drivers o tasks. Debido a la similitud mencionada anteriormente entre Minix y un sistema de paravirtualización, el proyecto MHyper busca adaptar a Minix para convertir a la SYSTASK de Minix en un hipervisor paravirtualizado de múltiples VMs. Figura 1. Estructura de Minix 3 MHyper posee una arquitectura similar a la de los contenedores de Linux [4]. La VM0 es una VM privilegiada que permite administrar las demás VMs. Al iniciarse, posee todos los recursos del sistema, por lo que si no se inicia ninguna VM trabaja de igual forma que un Minix normal. Fuera del espacio del microkernel, se añadió un nuevo servidor para administrar VMs llamado Virtual Machine Manager (VMM), exclusivo de la VM0. También se creó una herramienta de administración para cargar, iniciar, detener, reanudar y terminar las VMs utilizando las llamadas al sistema atendidas por VMM. Las demás VMs son versiones de Minix que no ejecutan su propio kernel, pero poseen todos las demás tareas, drivers y servidores. Para mantener el aislamiento de seguridad, recursos y fallos, el espacio de direcciones de memoria asignado a una VM no puede ser accedido por ninguna otra, incluyendo la VM0. El Process Manager (PM) de la VM0 no tiene bajo su gestión el rango de direcciones de memoria de una VM hasta que ésta termina. Además, la SYSTASK mantiene la rango del espacio de direcciones para cada VM para poder controlar que las llamadas al kernel solicitadas por un proceso operan dentro del área de memoria asignado a su VM (es decir, la copia de bloques de memoria entre procesos). La arquitectura de procesos de MHyper se muestra en la figura 2. Figura 2. Arquitectura de MHyper La mayor parte de este proyecto ya fue implementado, pero el planificador con el que cuenta (heredado de Minix) utiliza un algoritmo de planificación por prioridades que no distingue entre procesos de diferentes VMs. Por lo tanto, si los procesos de una VM cambian su prioridad a una mayor, éstos serán privilegiados por el planificador frente a los procesos de otras VMs. Es necesario entonces implementar un mecanismo que administre de forma justa los recursos disponibles, y particularmente el tiempo de CPU, entre los procesos de las diferentes VMs. Para poder llevar adelante esta tarea, es necesario reemplazar el actual planificador de MHyper. En el planificador de Minix cada proceso tiene una prioridad inicial, relacionada a la arquitectura que se muestra en la figura 1, donde los procesos de capas inferiores tienen mayor prioridad que los de los superiores. El planificador mantiene 16 colas de procesos en estado de listo, una por prioridad, como se muestra en la figura 3. Dentro de cada cola se aplica un algoritmo de round robin. Cuando a un proceso que está ejecutando se le termina el quántum, es movido al final de la cola. Cuando un proceso pasa de bloqueado a listo, es colocado al principio de la cola. Finalmente, cuando un proceso se bloquea o recibe una señal kill es removido de la cola. El algoritmo de planificación consiste entonces en encontrar la cola de mayor prioridad no vacía y elegir el primer proceso de esa cola. El proceso IDLE siempre está listo y es el de menor prioridad, es decir que será elegido cuando las otras colas se encuentre vacías. Para encolar y desencolar procesos, Minix se basa en las funciones enqueue() y dequeue(). Enqueue() coloca un proceso en la cola que corresponda y llama a pick_proc(), que determina cuál será el próximo proceso a ejecutar. El planificador de Minix sólo distingue procesos. Sin embargo, para planificar un sistema de virtualización es necesario distinguir a qué VM pertenece cada proceso. En caso contrario, podría darse un escenario como el que se describe a continuación. Supóngase que se tienen dos VMs, corriendo procesos con igual prioridad. VM1 y VM2 tienen 8 y 2 procesos en estado de listo, respectivamente. En este caso, la VM1 tiene una probabilidad del 80% de ocupar la CPU, y la VM2 el 20%, y cuantos más procesos ponga la VM1 a ejecutar, mayor será la proporción de CPU que obtendrá. Figura 3. Cola de prioridades de Minix (obtenida en [1]) Este comportamiento afecta el aislamiento de rendimiento que toda VM debería tener. Por otro lado, tampoco es suficiente con distinguir solamente las diferentes VMs. Un algoritmo que no distinga entre los tipos procesos, podría por ejemplo planificar un proceso de usuario de una VM, mientras está en espera un driver con restricciones temporales correspondiente a otra VM. Generalmente los drivers son procesos sensibles al tiempo, con lo cual deberían mantener su prioridad frente al resto de los procesos con el fin de reducir su tiempo de respuesta. Solución propuesta Para cumplir con las condiciones de aislamiento de rendimiento exigidas por la virtualización, no es suficiente con planificar procesos. Se propone adoptar un algoritmo jerárquico que tenga en consideración los diferentes tipos de procesos y los recursos consumidos por todos los procesos de cada VM. En primer lugar, se divide a los procesos en tres grupos: Kernel (incluyendo SYSTASK y CLOCK TASK): pertenecientes a la capa 1 de la arquitectura de Minix; Tareas (tasks): aquellos procesos pertenecientes a la capa 2 que están relacionados con la gestión de dispositivos y por lo tanto tienen restricciones temporales; Procesos: pertenecientes a las capas 3 y 4. Luego, a cada VM se le asigna una cubeta de fichas o token bucket de tamaño dado. Periódicamente esta cubeta es completada de fichas (tokens) por el microkernel. Una ficha simboliza un tick del timer de MHyper. Cada vez que se produce un tick, CLOCK task sustrae una ficha de la cubeta de la VM a la que pertenece el proceso interrumpido. Este mecanismo permite limitar el tiempo de ejecución de procesos de cada VM a fin de lograr una distribución más equitativa de la CPU. Cuando se inicia el sistema, arranca VM0 a la que se le asignan 256 tokens. Al iniciar una VM, el sistema extrae una cantidad vm_bsize de tokens de la VM0 y se los asigna a dicha VM. El tamaño de bucket vm_bsize se obtiene como parámetro de inicio y no puede superar la cantidad de tokens que tenga la VM0 disponibles en ese momento. Cuando una VM se detiene, estos tokens se devuelven a la VM0. La asignación de tokens a la VM0 se realiza con el único fin de ser distribuidos a las demás VMs. Por ser una VM con características particulares, la VM0 está exenta del control del consumo de tokens, ya que sus procesos son prioritarios para el funcionamiento del sistema. Cuando el planificador es invocado, la selección de VMs se realiza rastreando las diferentes colas de prioridad buscando procesos de acuerdo al siguiente orden: Tareas y procesos de la VM0; Tareas de VMs con tokens en sus buckets; Procesos de VMs con tokens en sus buckets; Tareas de VMs con sus buckets vacíos; Procesos de tareas con sus buckets vacíos; IDLE. Cada cierto tiempo el sistema se refresca y todos los buckets se vuelven a su estado inicial. El tiempo de refresco 𝑡_𝑟𝑒𝑓𝑟𝑒𝑠𝑐𝑜 se calcula como la cantidad de ticks de temporizador que tomaría consumir todos los tokens en el sistema: 𝑡_𝑟𝑒𝑓𝑟𝑒𝑠𝑐𝑜 = ∑ 𝑣𝑚_𝑏𝑠𝑖𝑧𝑒[𝑖] 𝑖 ∈ 𝑉𝑀_𝑅𝑈𝑁𝑁𝐼𝑁𝐺 Metodología de trabajo En primera instancia se optó por realizar una serie de simulaciones del algoritmo propuesto y otras variantes, a fin de evaluarlas y compararlas. Para ello se utilizó un software de simulación de procesos llamado SimSo[5] que permite realizar un prototipo de planificador, definir procesos, simularlos y obtener estadísticas del tiempo de procesamiento y rendimiento del hardware. Dichas simulaciones arrojaron una distribución más equitativa de la CPU entre las VMs, como se puede observar en la figura 4. Figura 4. Distribución de CPU Además se observó que entregándole más tokens a una VM en particular se le estaría entregando mayor prioridad que a las demás como se observa en la figura 5, por lo que se concluyó que el algoritmos no solo daba equitatividad sino también la posibilidad de dar diferentes prioridades a las VMs. servers, usuario), se agregaron nuevos bits en un campo del descriptor (p_misc_flags) de procesos. Luego se procedió a modificar el algoritmo de planificación, particularmente la función pick_proc(), encargada de elegir el próximo proceso listo para ejecutarse (proc_ptr apunta al descriptor de ese proceso). El algoritmo realiza múltiples búsquedas en las colas de prioridad de procesos listos (ready_q). A continuación se presenta el pseudocódigo que describe su funcionamiento: pick_proc(){ //Tareas o procesos de la VM0 foreach q in ready_q: proc_ptr = head[q]; if(proc_ptr != NULL && proc_ptr->vm == 0) return; //Tareas de las VMs con tokens foreach q in ready_q: proc_ptr = head[q]; if(proc_ptr != NULL && proc_ptr->level == TASK && proc_ptr->vm->tokens > 0) return; //Procesos de las VMs con tokens foreach q in ready_q: proc_ptr = head[q]; if(proc_ptr != NULL && proc_ptr->vm->tokens > 0) return; Figura 5. Priorización de las VMs Implementación Con los resultados positivos durante la etapa de simulación se inició la implementación del nuevo algoritmo. En primer lugar, se modificó la estructura de datos que describe las VMs. A dicha estructura se le agregaron los campos vm_bucket y vm_tokens que corresponden al tamaño total del bucket y la cantidad actual de tokens del mismo. El vm_bucket es pasado a la VM como un parámetro de inicio. Para que el algoritmo pueda distinguir entre los diferentes tipos o niveles (level) de procesos (kernel, tareas, //Tareas de las VMs sin tokens foreach q in ready_q: proc_ptr = head[q]; if(proc_ptr != NULL && proc_ptr->level == TASK) return; //Procesos de las VMs sin tokens foreach q in ready_q: proc_ptr = head[q]; if(proc_ptr != NULL) return; //IDLE proc_ptr = IDLE; return; Se puede observar que primero se buscan procesos o tareas correspondientes a la VM0. Seguido de estas se buscan tareas de aquellas VMs que posean tokens. De no haber ninguna se buscan procesos en la misma condición. Luego, para evitar la posibilidad de que el procesador este ocioso hasta el próximo refresco habiendo tareas o procesos pendientes, se da la posibilidad de ejecución a los procesos de VMs que no posean tokens. Por último, si no hay ningún proceso listo para ejecutarse, se ejecuta IDLE. La otra función modificada fue clock_handler(),que se ejecuta en cada tick del temporizador. En primer lugar se encarga de hacer el refresco de todos los bucket de las VMs. Cada vez que se inicializa una VM se actualiza la variable global t_refresco, que contiene el tiempo de refresco de los buckets (vm_bsize). Cada t_refresco ticks todos los buckets son llenados nuevamente, independientemente de la situación de la VM. t_counter++; if(t_counter >= t_refresco) t_counter = 0; for(i = 0; i < NR_VMS; i++) VM[i].vm_tokens=VM[i].vm_bsize; En segundo lugar, se encarga de restarle un token a la VM que se encuentra en ejecución en ese instante. clock_handler(), junto con pick_proc() conforman el núcleo de la implementación del algoritmo de planificación token bucket. Para poder verificar que el algoritmo refleja similares resultados a los obtenidos en las simulaciones previas, se implementó un sistema de métricas para evaluar el dessempeño del algoritmo. Para registrar las mediciones se creó un vector de estructuras vm_metric. struct vm_metric{ clock_t timestamp; int kernel[NR_VMS]; int task[NR_VMS]; int proc[NR_VMS]; int idle; }; Cada 300 ticks de reloj (5 segundos dado que se producen 60 ticks/s) se almacena en un elemento del vector la cantidad de ticks en las que clock_handler() detectó en ejecución un tipo de proceso dado de una VM (kernel, task y proc) y un timestamp del momento en que se registra esta información. Minix ofrece la facilidad de poder acceder a cualquier dirección de memoria a través de su sistema de archivos. El dispositivo /dev/kmem es un dispositivo que representa toda la memoria del computador. Se utilizó esta facilidad para acceder desde un programa de usuario al vector de registración de métricas y así poder recolectar y registrar en un archivo de disco los datos de medición. Resultados Se realizó una serie de pruebas utilizando MHyper con ambos planificadores, el de Minix y el propuesto. Se crearon dos programas en C, uno para ser ejecutado en el hipervisor (testVM0.c) y otro para ser ejecutado en las VMs (test.c). El programa que se ejecuta en el hipervisor simplemente ejecuta un bucle infinito de manera de ocupar la CPU al 50%. El programa que se ejecuta en las VMs también es un bucle infinito para ocupar la CPU y además lee datos de un archivo y los escribe en otro. Con estos programas se corrieron diversos escenarios, con la finalidad de observar el comportamiento y comparar ambos planificadores. Priorización de la VM0 En primer lugar, se puso a correr testVM0 en la VM0. Luego de unos minutos se inician VM2 y VM3, las cuales ejecutan el proceso test. En este caso, como muestran la figura 6, el comportamiento cuando sólo está corriendo la VM0 es igual para los dos planificadores, dicha VM ocupa aproximadamente el 50% de la CPU. Sin embargo, cuando se inician las otras dos, el planificador de Minix, al no distinguir a qué VM pertenece cada proceso, le da mayor importancia a la VM2 que a la VM0. En el planificador de token bucket esto no ocurre, sino que se atiende a las VMs, pero sin relegar a la VM0 (figura 7). Figura 6. Priorización VM0 sin VM Priorización de las VMs El tercer conjunto de benchmarks consitió en ejecutar 2 VMs con diferentes asignaciones de tokens: 32 y 32, 64 y 64, 128 y 128 y 32 y 128, respectivamente. Como se puede ver en la figura 9, se pudo comprobar que el uso de CPU es proporcional a la cantidad de tokens que cada VM tiene asignados. Es decir que si a dos VMs se le asigna el mismo tamaño de bucket, la distribución de CPU será equitativa, mientras que si a una se le asigna un bucket más grande, ésta tendrá mayor prioridad. Figura 7. Priorización VM0 con 3 VMs Distribución de CPU En segundo lugar, se iniciaron dos VMs con igual cantidad de tokens. En cada una de ellas se corrió el mismo proceso, sólo que con diferentes prioridades. En VM2 se ejecutó nice -n 10 test y en VM3 nice -n 20 test. La figura 8 muestra que el planificador de Minix realiza una planificación desigual entre las dos VMs, atendiendo a la prioridad de cada proceso, mientras que el planificador de token bucket planifica la CPU de forma equitativa. Figura 8. Distribución de la CPU. Figura 9. Priorización de las VMs Priorización de tareas sobre procesos. Con los datos obtenidos en el benchmark anterior, se realizó una comparación del comportamiento cuando se asignan 32 y 32 tokens, y 32 y 128. En la figura 10 puede verse cómo la VM3 al bajar su proporción de tokens, baja su porcentaje de CPU ocupado por procesos (P) en un 11% (de 31% a 20%), mientras que sus tareas (T) bajan en 6% (de 18% a 12%). Es decir, que al perder porcentaje de CPU debido a un cambio en el tamaño de bucket, la VM3 cede más tiempo de procesos que de tareas. Figura 10: Priorización de Tareas sobre Procesos Discusión Tanto las simulaciones como las mediciones realizadas sobre el sistema arrojaron resultados que confirman la efectividad del algoritmo propuesto. En la figura 8 se observa un uso equitativo de la CPU por parte de todas las VMs, priorizando las tareas sin descuidar los procesos, como se ve en la figura 10. En la figura 9 se hace evidente cómo asignándole más tokens a una VM se logra darle prioridad por encima de las demás. La utilización de un software de simulación previo a la implementación es ventajosa, ya que da la posibilidad de probar diferentes algoritmos de una forma sencilla, y poder compararlos y encontrar errores o defectos antes de comenzar a implementar. Otro punto a destacar es la implementación del sistema de métricas que ayudó a ver de forma fehaciente el comportamiento del algoritmo y comprobar que el sistema funciona de la manera que esperada. Conclusión En este trabajo se presentó un sistema de planificación basado Minix, para el cual se propone un algoritmo de planificación alternativo, de manera de lograr una distribución equitativa de la CPU entre diferentes VMs. El trabajo muestra dos aspectos importantes sobre el diseño de un planificador para un sistema de virtualización. En primera instancia, la planificación de procesos de un sistema de virtualización no es una tarea trivial y mucho menos si se pretende el aislamiento de rendimiento requerido por las VMs. Existen un sin número de factores a tener en cuenta y el más mínimo cambio puede desestabilizar el sistema. Por otro lado Minix, al ser un sistema de microkernel y estratificado, brinda una excelente oportunidad para su modificación y la libertad de trabajar sin la preocupación de que algún driver o tarea interfiera o altere la planificación. Los conocimientos adquiridos al trabajar con este sistema operativo no se limitan solamente al mismo, sino que son un punto de partida para el aprendizaje de cuestiones relacionadas a los sistemas operativos en general, con lo cual pueden extrapolarse a futuros proyectos. Agradecimientos A nuestros docentes por el apoyo y permitirnos ser parte de este proyecto. Referencias [1] Tanenbaum, A. S., Woodhull, A. (2006). Operating Systems: Design and Implementation- 3o edición. Prentice Hall. [2] Tanenbaum, A. S. (2003). Redes Computadoras - 4o edición. Prentice Hall. de [3] Tanenbaum, A. S. (2009). Sistemas Operativos Modernos - 3o edición. Prentice Hall. [4] LXC - Linux Containers https://linuxcontainers.org [5] SimSo - Simulation of Multiprocessor Scheduling with Overheads. http://homepages.laas.fr/mcheramy/simso/ [6] XEN Credit Scheduler. http://wiki.xen.org/wiki/Credit_Scheduler [7] MINIX 3 Documentation http://www.minix3.org/documentation/index.html