Herramientas para la Gestión de la Localidad en Microkernels para

Transcripción

Herramientas para la Gestión de la Localidad en Microkernels para
Herramientas para la Gestión de la Localidad en
Microkernels para Memoria Compartida
Marisa Gil, Xavier Martorell, Yolanda Becerra, Ernest Artiaga, Albert Serra, Nacho Navarro
Departament d'Arquitectura de Computadors
Universitat Politècnica de Catalunya (UPC)
Gran Capità s/n, Campus Nord, D6, 08071, Barcelona, Spain
[email protected]
Abstract: Muchas aplicaciones paralelas se programan hoy en día utilizando paquetes
de threads. El grado de paralelismo que puede explotar eficientemente una aplicación
depende de la sobrecarga de utilizar threads. Hoy, que se trabaja sobre microkernels y
librerías de threads de usuario, el coste de creación, destrucción y cambio de contexto
está muy optimizado. Pero hay un coste que no se ha considerado suficientemente hasta
ahora: el coste de traer los datos y el código a las memorias próximas al procesador y
memoria cache durante la ejecución de un thread, y más aún a la hora de planificar la
elección del siguiente thread. En este trabajo1 presentamos el diseño de un entorno cuya
realización permite la planificación de flujos sobre un microkernel como Mach 3.0
tomando en consideración el particionado de los procesadores y la gestión de la memoria
a nivel de kernel, de librerías y de servidores.
1. Introducción
Muchas aplicaciones paralelas se programan hoy en día utilizando paquetes de threads. La poca sobrecarga que introduce la gestión de threads a nivel usuario mueve a los programadores a explotar el paralelismo natural de las aplicaciones. Pero hay que considerar también la gestión de los threads de kernel
(procesadores virtuales) que soportan a los de usuario y su planificación sobre los procesadores físicos.
El grado de paralelismo que puede explotar eficientemente una aplicación depende de la sobrecarga
de utilizar threads. Hoy, que se trabaja sobre microkernels y librerías de threads de usuario, el coste de
creación, destrucción y cambio de contexto está muy optimizado. La planificación de procesadores
empieza a aparecer explícita en algunos sistemas y se basa en la planificación espacial, que permite a
una aplicación disfrutar de una partición de la máquina y decidir, dentro de su grupo de procesadores,
las políticas de planificación de flujos más adecuadas. En esta línea, en un trabajo anterior [GIL94a],
hemos desarrollado “scheduler-activations” en el kernel de Mach [ACCE86], completado con nuevas
políticas la librería CThreads [COOP88] y dotado al entorno de un servidor de procesadores.
Pero, desafortunadamente, la sobrecarga no se limita al coste de la planificación flujos-procesadores,
sino que hay otro coste que no se ha considerado suficientemente hasta ahora: el de traer los datos y el
código a las memorias próximas al procesador y memoria cache durante la ejecución de un thread, y más
aún a la hora de planificar la elección del siguiente thread. Este coste puede ser substancial tanto en
copias entre módulos de memoria, como en fallos en la cache de un multiprocesador con coherencia de
caches. Más aún hoy que la diferencia de velocidad entre los procesadores y la memoria se va agrandando.
Necesitamos un entorno que facilite la reducción de este coste ejecutando los threads allí donde se
encuentre la memoria a la que van a acceder. Para ello necesitamos controlar la asignación de memoria
y sus políticas de gestión de forma que todos los niveles de gestión cooperen para que la aplicación tenga
cargada en la memoria física más conveniente los datos y códigos de los threads que correrán en el procesador asociado. Las políticas actuales que siguen estas ideas de afinidad se basan en técnicas de “footprint” [BLAC90][GUPT91] y en las propuestas de “memory-conscious scheduling” [MARK92a].
Pero nos encontramos que los microkernels actuales, muy eficientes en la gestión de las abstracciones
que ellos ofrecen, como los threads de kernel que aprovechan los multiprocesadores, dejan la gestión de
1. Este trabajo ha sido subvencionado por la Comisión Interministerial de Ciencia y Tecnología (CICYT), bajo el contrato TIC94-0439 “Cooperación entre el microkernel y las aplicaciones para explotar el paralelismo en sistemas multiprocesadores”.
la creación de trabajos, la entrada/salida, los sistemas de ficheros y la paginación en manos de servidores
externos que provienen del entorno UNIX y que secuencializan los servicios al no estar ellos convenientemente paralelizados. Necesitamos acompañar la ejecución de las aplicaciones paralelas con mecanismos y políticas adecuados dentro y fuera del kernel. En nuestro trabajo, partimos del concepto de
particionado de los recursos y de mecanismos dentro del kernel y políticas en servidores para:
• Ofrecer un entorno completo de ejecución, compuesto de librerías y servidores, que permita
la ejecución de aplicaciones paralelas directamente sobre el microkernel, evitando la
dependencia y sobrecarga actuales debida a la emulación de sistema redireccionando los
servicios hacia el servidor UNIX.
• Gestionar la asignación de procesadores (processor sets) desde un punto de vista global de la
máquina y ofrecer un particionado espacial.
• Cargar convenientemente en el espacio virtual disperso de la task, las regiones de datos y de
código de forma que minimicen las colisiones entre procesadores.
• Gestionar a nivel usuario la asignación de memoria (memory sets) y políticas de acercamiento
de los datos a los flujos y viceversa. Controlar los fallos de páginas y la planificación de la
memoria física.
• Gestionar la entrada/salida, ofreciendo paralelismo y eficiencia. El funcionamiento actual se
hace a través del servidor de UNIX, que considera que los procesos tienen un solo flujo,
provocando mucha contención.
• Instrumentar todos esos niveles de forma que extraigamos información de depuración para las
aplicaciones y estadísticas que permitan la adaptación automática de las políticas de gestión a
los recursos disponibles.
2. Planificación
Con el abaratamiento de los componentes y la aparición de máquinas masivamente paralelas, surge la
tendencia en los multiprocesadores de mecanismos y políticas de planificación espacial, además de la
tradicional planificación temporal. Consideramos conveniente la existencia de un planificador que gestione la asignación de procesadores de la máquina a las aplicaciones que los soliciten. Es el CPU server.
Partiendo de este particionado de la máquina, se puede permitir, y es muy conveniente para la ejecución eficaz de las aplicaciones, el hecho de que el usuario (el programa en ejecución) conozca y utilice
de la forma más adecuada la submáquina que le ha sido asignada. El máximo rendimiento lo conseguirá
si logra que la concurrencia de la aplicación se adapte al paralelismo real (procesadores) que le ha asignado el sistema. Respaldamos las propuestas de mapeo uno a uno de flujos de usuario sobre procesadores
físicos [MARK92b][GOTT92][EDLE92].
Conviene también que la planificación dentro del kernel no vaya en contra de los deseos de la aplicación. Eliminar el tiempo compartido de propósito general, contrario a la planificación necesaria dentro
de una única aplicación. Para ello, los eventos que ocurran dentro del kernel y que necesiten una replanificación de los flujos de ejecución, serán notificados al usuario mediante un mecanismo eficiente: las
upcalls. Hemos añadido los mecanismos necesarios y la política de “scheduler-activations” a la planificación de procesadores del kernel de Mach 3.0 [LOEP93].
Como hemos dicho ya, no podemos dejar que la gestión de la memoria vaya en contra de la óptima
ejecución de los flujos. Por lo tanto, nuestra propuesta es que la aplicación misma pueda gestionar su
memoria. Aparece entonces la necesidad de asegurar una cierta cantidad de memoria de forma que la
aplicación ajuste su ejecución a ese recurso e indique al sistema la forma más correcta de planificarlo.
Nos parece conveniente la aparición de una abstracción nueva de kernel: el memory set, que permita
también una planificación espacial del recurso memoria, hasta ahora gestionado con políticas globales
de paginación y memoria virtual que provoca que las aplicaciones no sólo dependan de sus patrones de
acceso a memoria, sino que estén a expensas de lo que hacen las demás aplicaciones que corren en la
misma máquina, provocando tiempos de acceso a los datos erráticos e impredecibles.
En esta línea, estamos desarrollando los mecanismos a nivel de kernel y a nivel de usuario (librerías
y servidores) para dotar al entorno de las siguientes capacidades:
• Poder cargar las zonas de memoria de la aplicación de forma que esté todo preparado allí
donde correrán los flujos de ejecución (son las técnicas de “memory conscious scheduling”).
• Lanzar la ejecución de los threads allí donde ya estén, o probablemente estén sus datos por
haber corrido anteriormente allí (técnicas de “footprint”).
• En máquinas de arquitectura NUMA, situar los datos en las memorias más rápidas, más
cercanas al procesador.
• Que el programador o el compilador puedan dar pistas (hints) sobre cuál será la gestión futura
más conveniente de la memoria de la aplicación. Por ejemplo, información para que el
paginador pueda hacer “prefetching” de datos y código.
• Que la aplicación pueda dar indicaciones para la selección de páginas a reemplazar de
memoria virtual, y fijar en memoria física aquellas que no interese paginar durante un tiempo,
siempre dentro de su partición de la máquina (memory set), sin poner en peligro a las demás
aplicaciones.
• Permitir la coexistencia de varias políticas de gestión de memoria en una máquina, gracias
siempre al particionado espacial, que da a cada usuario la posibilidad de decidir sobre la
política de su submáquina y los parámetros más adecuados a su aplicación.
En los siguientes apartados mostramos las posibilidades que nos ofrece la nueva tecnología microkernel, en especial el microkernel Mach 3.0, y la propuesta de un entorno eficiente para la ejecución de aplicaciones paralelas.
En el apartado 3 presentamos extensamente la gestión de memoria en un microkernel como Mach,
ya que es un tema poco conocido y nada documentado. Esta gestión es el punto central de nuestro trabajo
actual. En el apartado 4 describimos el entorno que hemos desarrollado. En el Apartado 5 describimos
una aplicación ejemplo. En el apartado 6 presentamos los mecanismos de evaluación de la ejecución de
aplicaciones en nuestro entorno.
3. Gestión de la memoria
3.1. Gestión de la memoria en Mach: 3 niveles
El sistema de gestión de memoria en Mach se basa en paginación y soporta memoria virtual.
Una de las metas de Mach ha sido diseñar un sistema de gestión de memoria realmente portable para
multiprocesadores, intentando reducir al máximo la parte de la gestión dependiente de la arquitectura.
Por ello, la gestión de memoria aparece dividida en tres módulos. El primero, llamado pmap, se ejecuta
en el kernel y es el único que depende de la arquitectura. Se encarga de la gestión de la MMU (configura
sus registros, las tablas de páginas de hardware y recibe todos los fallos de página). El segundo módulo
es código de kernel independiente de la máquina, y se refiere al procesamiento de los fallos de página,
la gestión de los mapeos de direcciones y el reemplazo de páginas. El tercero se ejecuta como proceso
de usuario y se llama gestor de memoria o paginador externo. Se encarga de la parte lógica del sistema
de gestión de memoria, fundamentalmente el soporte en disco. Existe un gestor por defecto (default
memory manager). El kernel y los gestores de memoria se comunican a través de un protocolo bien definido.
3.2. Espacio de direcciones
La gestión del espacio de direcciones virtual se corresponde con el segundo módulo, y es el kernel el que
se encarga de ella.
Un espacio de direcciones virtual está asociado a una task y se crea y se destruye al mismo tiempo
que la task propietaria. Puede verse como un conjunto de direcciones virtuales válidas referenciables por
un thread que se ejecute dentro de la task. Consiste en un conjunto indexado y disperso de páginas de
memoria, que el kernel agrupa internamente en regiones, de forma transparente al usuario. Cada región
está formada por un conjunto de páginas virtuales contiguas que tienen los mismos atributos. Una región
está definida por su dirección base y su tamaño. La agrupación de páginas en regiones permite que la
tabla lógica de páginas no se incremente de forma desmesurada, ya que se puede determinar si una dirección está en uso o no a través de las regiones. Es decir: una dirección virtual sólo es válida si pertenece
a una región definida.
Para cada página se puede especificar un atributo de herencia. Inicialmente, el espacio de direcciones
de una nueva task sólo contendrá aquellas páginas marcadas como heredables en la task que la ha creado.
El resto de este nuevo espacio de direcciones será inválido. La llamada al sistema vm_inherit, puede
modificar los atributos de herencia de un rango de memoria.
Otros atributos asociados a página son los de protección, referentes al tipo de acceso permitido. Viene
especificada como una combinación de los valores de lectura/escritura/ejecución, y necesitan soporte del
hardware para poder ser aplicados. El atributo de máxima protección, especifica la máxima permisividad
que se podrá tener sobre esa página, y, una vez establecido, sólo se podrá modificar hacia valores más
restrictivos. El de protección actual, puede modificarse mientras respete el valor máximo permitido. La
llamada vm_protect modifica estos atributos de protección.
3.3. Memory object
Una abstracción relacionada con el uso del espacio de direcciones virtual es el memory object. Está dentro del tercer nivel, y se encargan de él los gestores de memoria.
Un memory object, es la unidad de soporte para almacenamiento. Lógicamente puede verse como un
recipiente para datos indexado por byte. Puede ser una página o un conjunto de páginas, pero también
puede ser un fichero u otras estructuras de datos más especializadas.
Los memory objects pueden mapearse en una porción del espacio de direcciones que no esté en uso,
formando una nueva región, y de este modo acceder a él como se haría a cualquier dirección de memoria.
La llamada que permite hacer esto es vm_map, y necesita como parámetros las características de la
nueva región (posición, tamaño, protección, herencia, y offset dentro del objeto), así como el port que
representa al memory object que soporta a la región. Si este port es nulo, será el default memory manager
el encargado de esta nueva región, y el que provea las páginas solicitadas, inicializadas con ceros. El
comportamiento de esta llamada en este caso, es equivalente al de la llamada vm_allocate, en la cual, el
kernel decide de forma transparente qué objeto en el default memory manager soportará a la nueva
región. Así, siempre que se establece un nuevo rango de memoria en un espacio de direcciones, se especifica un memory object, ya sea de forma implícita (para ser gestionado por el default memory manager)
o de forma explícita, que dará soporte a ese rango. Otro parámetro de vm_map permite decidir si se
quiere que el acceso a esa porción del objeto pueda ser compartido para lectura y escritura, o si, por el
contrario, se quiere una copia privada, de manera que las modificaciones que se hagan no afecten al
objeto original. En este último caso, el kernel creará un nuevo objeto, gestionado por el default memory
manager, para que soporte la copia, que se hará usando la técnica de optimización copy-on-write.
Cada memory object tiene que estar controlado por un gestor de memoria, que puede ser el default
memory manager del sistema o uno externo. Cada uno puede implementar su propia semántica, que
determine dónde almacenar las páginas que no están en memoria y llevar a cabo sus propias políticas
que implementen, por ejemplo, memoria compartida entre tasks no relacionadas, o aporten sus propias
reglas sobre lo que ocurre con los objetos después de sacarlos de memoria (por ejemplo, podría mantener
una cache para los objetos referenciados últimamente, intentando ahorrar accesos al almacén de
soporte).
La llamada al sistema que sirve para invalidar cualquier rango de memoria es vm_deallocate. El kernel soporta otras manipulaciones explícitas de memoria mediante llamadas como vm_copy, vm_write o
vm_read.
La gestión de las páginas libres del sistema la lleva a cabo un thread de kernel, el pager daemon, que
comprueba periódicamente el estado de la memoria. Cuando no hay suficientes páginas libres, se selecciona una página antigua que, si es necesario, se enviará al gestor encargado del memory object al que
pertenece, con el objetivo de que éste la pase al almacén de soporte.
3.4. Gestor externo de memoria
Los gestores externos de memoria son las tasks de usuario que controlan los memory objects, y permiten
que el usuario elija la política que quiere que se siga en la gestión de una región de su espacio de direcciones. Por ejemplo, a través de estos gestores se puede implementar memoria compartida entre tasks no
relacionadas o también memoria distribuida compartida, siendo estos paginadores los que se encarguen
de mantener la coherencia. El default memory manager ofrece memoria compartida, pero no soporta
memoria distribuida compartida.
El gestor de memoria por defecto del sistema es, en la mayoría de los aspectos, simplemente un gestor
de memoria externo. Provee almacén de soporte para la memoria anónima (vm_allocate, copia de
memoria...). La ejecución del default memory manager tiene como propiedad importante, que no puede
provocar fallos de página, ya que ningún otro gestor de memoria puede ofrecerle paginación. Por ello,
sus datos, su código, y las páginas que recibe a través de mensajes se fijan en memoria.
Para que un proceso pueda mapear un objeto1, debe comunicarse con el paginador que lo gestiona
para obtener el port que representa a ese objeto (abstract memory object). Esta comunicación se debe
realizar a través de funciones ofrecidas por el paginador para establecer la conexión. Entonces, el cliente
ya puede utilizar la llamada vm_map. Se necesita asociar un port más al objeto, para que se pueda llevar
a cabo el protocolo entre el kernel y el gestor de memoria. Este es el memory cache control port, creado
por el kernel cuando un cliente mapea por primera vez el objeto. Será el port a través del cual el kernel
recibirá los mensajes que el gestor le envíe relacionados con el memory object. Una vez creado, el kernel
enviará un mensaje al abstract memory object port, pasándole derechos de envío al memory cache control port, y esperará la respuesta del gestor, que podrá ser de confirmación o de rechazo de la conexión.
Las siguientes llamadas vm_map sobre el mismo objeto en el mismo host no provocarían ninguna
comunicación entre el kernel y el gestor, porque la asociación ya existiría.
Pager
port
Pager
paging
file
memory
object
object_create
(1)
return(object_port)
(2)
Task
Loader
bin
file
vm_write
(6)
address
space
memory object
port
(4)
region
(3)
vm_map
(5)
memory_object_init
memory cache
control port
Mach 3.0
Figura 1: Inicialmente, se crea un objeto vacío que soportará el espacio de direcciones de la task, se mapea una porción del objeto en la región de la task y se llena de
código y datos del fichero binario.
El primer acceso que el cliente haga al objeto, provocará un fallo de página, ya que todavía no hay
ninguna página del objeto en memoria. La rutina de gestión de la excepción hará una petición de datos
al gestor encargado del objeto.
Las peticiones de datos que hace el kernel son a través de mensajes, que el paginador puede contestar
con otro mensaje que contenga los datos, con un mensaje de error, o con uno en el que diga que los datos
no están disponibles, y que debe ser el kernel el que provea las páginas.
Puede ocurrir que el kernel decida limpiar alguna página, es decir, enviar al paginador encargado de
ella las modificaciones, por ejemplo, antes de sacarla de memoria física. En este caso, enviará la página
en un mensaje al gestor como datos out-of-line (que mapeará en su espacio de direcciones). Además hará
que la página física pase a estar asociada con el default memory manager; así, si en un tiempo razonable
el gestor no ha movido las páginas a su destino, el kernel podría sacarlas de memoria a través del default
manager que la llevará al fichero de paginación, aunque el gestor original seguiría teniéndola en su espa1. El protocolo que se describe es el especificado en la versión MK4.
cio de direcciones. Pero, normalmente, el gestor de memoria podrá hacer el tratamiento correspondiente
a la página (posiblemente copiarla en algún dispositivo o sistema de ficheros) y después utilizar la llamada vm_deallocate para liberar el espacio físico que ocupaba en su espacio de direcciones (como cualquier memoria recibida como datos out-of-line).
También puede pasar que sea un gestor el que desee actualizar o recuperar alguna de las páginas gestionadas por él, y entonces pida al kernel que inicie este protocolo.
Cuando ninguna task tenga ya mapeado un memory object determinado, el kernel informará de ello
al gestor que se encarga de él, pero antes sacará de memoria todas las páginas modificadas. También
puede ser un gestor el que decida finalizar el mapeo de uno de sus objetos, enviando un mensaje al kernel, que descartará todas las páginas residentes en memoria de ese objeto.
4. Entorno de ejecución de aplicaciones paralelas
A continuación vamos a presentar el entorno propuesto para la ejecución de aplicaciones paralelas.
Task
Parent
CPU
Server
Pager
OSF/1
Server
Parallel
Application
Application
Manager
Region
Memory
Object
I/O
Server
Mach 3.0
Processor set
Regions
pages
Processors
Page Fault
Figura 2: Servidores del entorno de ejecución de aplicaciones paralelas sobre Mach 3.0.
4.1. Gestor de aplicaciones paralelas
Los sistemas operativos tradicionales proporcionan herramientas para la carga de programas y su posterior control (tratamiento de excepciones, fallos de página, etc.). Con este mismo objetivo, nuestro
entorno incluye un servidor dedicado a la gestión básica de las aplicaciones paralelas.
El primer paso necesario para ejecutar una aplicación paralela directamente sobre un microkernel
consiste en leer el contenido de un fichero ejecutable, construir a partir de él un proceso (task) y ofrecerle
un entorno de ejecución sólido. Dentro de este entorno de ejecución el gestor de aplicaciones dispone
quién se ocupa de la memoria del proceso, quién se ocupa de los procesadores asignados al proceso y
quién se ocupa de la entrada / salida que pueda realizar. Además el mismo gestor se encarga del tratamiento por defecto de las excepciones de la aplicación.
En nuestra implementación actual, el gestor de aplicaciones entiende el formato de ficheros mach
object (macho), con lo que le es posible construir un espacio de direcciones completo conteniendo
código, datos inicializados, datos no inicializados y pila. El gestor utiliza el paginador para crear estas
cuatro regiones de memoria tal y como están descritas en el fichero ejecutable. A continuación mapea la
información contenida en el fichero en las regiones de código y datos inicializados y simplemente pone
a cero las regiones de datos no inicializados y pila.
Como gestor de excepciones por defecto, este servidor indica al sistema que él va a recibir las notificaciones de las excepciones provocadas por las aplicaciones. Queda abierta además la posibilidad de
que una aplicación concreta pueda gestionarse ella misma sus excepciones.
En caso necesario el gestor de aplicaciones puede asignar procesadores y preparar la entrada/salida
estándard a la aplicación. Estos dos últimos pasos, opcionalmente, puede realizarlos también la propia
aplicación.
4.2. Pager
Hemos implementado un paginador externo con el objetivo de poder controlar la memoria del espacio
de direcciones de una task, y poder decidir las políticas que se aplican en cada uno de los niveles de la
gestión de memoria.
El cargador de aplicaciones que hemos implementado, nos permite especificar el proceso que gestionará el espacio de direcciones de una task, ya que utiliza la llamada vm_map para cargar en memoria el
código, los datos y la pila de las tasks. Para poder extender este control a la memoria reservada dinámicamente, las tasks en nuestro entorno no deben utilizar la llamada vm_allocate, ya que ésta asocia la
nueva memoria al default memory manager, sino reservar la memoria con vm_map y utilizar el mismo
gestor que se encarga del resto de su memoria.
Es posible que la aplicación y el kernel colaboren con el gestor del espacio de direcciones para decidir
la política más adecuada en cada caso. La aplicación puede dar pistas sobre el uso que va a hacer de la
memoria, y el kernel, ante un fallo de página, puede detectar quién lo ha producido. De esta manera, el
gestor puede intentar predecir cómo van a ser referenciadas las páginas y aplicar él la política de reemplazo adecuada, incluso utilizando pre-fetching en la paginación. En esta línea, sería útil disponer de una
herramienta que permitiera que el gestor pudiera reubicar dinámicamente el código y los datos, en función de las pistas aportadas por la aplicación y el kernel [ORR92][ORR94].
Para optimizar la ejecución, el paginador debe ser un proceso privilegiado, que pueda fijar su propio
espacio de direcciones en memoria física. De este modo, evitamos que él mismo sufra el proceso de paginación, que podría afectar a páginas gestionadas por él, que, en ese momento, tuviera en su espacio de
direcciones para manipularlas. Esto haría que el gestor perdiera el control sobre ellas, ya que serían paginadas por el encargado del espacio de direcciones del gestor.
También hay que evitar la influencia entre aplicaciones, debida a la paginación provocada por cada
una de ellas.
4.3. Cpu server
Para dar soporte a la posibilidad de una planificación basada en políticas de espacio compartido necesitamos una utilidad para asignar procesadores a aplicaciones y permitir así el particionado del multiprocesador y la coexistencia de varios modelos de programación. Las aplicaciones paralelas que desean
sacar un alto rendimiento a la máquina necesitan saber cuántos procesadores están disponibles, cuántos
pueden pedir en este instante, qué procesadores son, etc.
Hemos llevado a cabo la realización de un servidor de procesadores o CPU server, es decir, un proceso privilegiado (una task) que se encarga de gestionar los procesadores físicos en favor de las aplicaciones que se los pidan. Los clientes de este servidor son aplicaciones paralelas que corren sobre un
multiprocesador de memoria compartida. Los clientes suelen ser una task con varios threads concurrentes (pero en la realización actual está abierto que pueda ser también una aplicación que incumba a varias
tasks).
El interfaz de las llamadas al servidor es al principio muy sencilla; se compone únicamente de tres
llamadas. La primera es para solicitar un determinado número de procesadores, la segunda es para devolverlos y la tercera para pedir información.
Mach ofrece la posibilidad de agrupar procesadores físicos y virtuales para que éstos últimos se ejecuten sobre los primeros; el objeto de kernel que los reúne es el processor set o grupo de procesadores.
La aplicación que necesite procesadores en exclusiva tiene que crear uno o varios processor set propios y a continuación pedir al servidor que le inserte procesadores en uno de esos processor sets. La aplicación paralela decide también qué threads se ejecutarán en cada grupo de procesadores, asignando y
desasignando sus threads a sus processor set.
El servidor mantiene disponibles, para otorgar a cualquier task que le llama, hasta el número máximo
de procesadores de la máquina menos uno, que es el procesador master y que es mejor no asignar en
exclusiva a nadie (de hecho, no puede salir del default processor set). El server no desbanca procesadores a las aplicaciones; son éstas las que los liberan en cuanto ya no los necesitan o acaban.
El CPU server mantiene el estado de los procesadores en una tabla. En ella relaciona los procesadores
físicos con el pset al que están actualmente asignados y qué task solicitó la asignación. Los procesadores
asignados al default pset, sin contar al master, están disponibles para ser reasignados.
Debido a que no todas la peticiones de servicio se pueden atender inmediatamente, el CPU server ha
sido realizado con la posibilidad de creación dinámica de flujos que atiendan peticiones de reasignación
de procesadores.
El usuario tiene la posibilidad de solicitar que no se le sirva su petición hasta que todos los procesadores que ha solicitado le hayan sido asignados.
4.4. I/O server
Para que las aplicaciones se ejecuten en un entorno completo, hace falta proporcionarles entrada/salida.
Nuestra propuesta incluye un servidor de entrada/salida que ofrece los servicios básicos de UNIX (open,
close, read, write, etc.).
Las aplicaciones se montan con una librería de acceso al servidor de entrada/salida. La librería se
comunica con el I/O server a través de paso de mensajes. Cada vez que la aplicación realiza una llamada
a la librería, ésta envía un mensaje al servidor, que accede al fichero o dispositivo y devuelve el resultado
o un error. El servidor puede trabajar sobre el subsistema OSF/1, obteniendo las ventajas de la entrada/
salida de UNIX o puede acceder a los dispositivos directamente a través de Mach.
El hecho de que la librería ofrezca un interfaz de entrada/salida compatible con UNIX trae consigo
la rápida portabilidad de un mayor número de aplicaciones con las que poder evaluar nuestro entorno.
5. Aplicaciones
El objetivo final de todo sistema operativo (y, por supuesto, también de aquellos basados en un microkernel) es ofrecer un entorno para la ejecución de aplicaciones. En concreto, nuestro trabajo está centrado
en la ejecución de aplicaciones paralelas en sistemas multiprocesadores. Así pues, se han escogido
muestras de diversos tipos de aplicaciones con el objetivo de evaluar la bondad del entorno ofrecido por
el microkernel.
El esfuerzo se ha orientado en dos sentidos:
• En primer lugar, procurar que el microkernel, dentro de la sencillez, facilite a la aplicación
todos los recursos que ésta pueda necesitar para su ejecución.
• En segundo lugar, estudiar las modificaciones que conviene hacer a las aplicaciones actuales
con el fin de aprovechar al máximo el nuevo entorno, aumentando así su rendimiento.
Las aplicaciones sobre las que se ha trabajado incluyen un programa de generación de fractales (en
el que hay que realizar cálculos intensivos, pero bastante independientes unos de otros), un servidor (que
combina entrada/salida con cálculo), etc.
5.1. Generación de fractales
El programa fractal genera un conjunto de Mandelbrot y lo muestra por pantalla. El algoritmo es sencillo: consiste en obtener las dimensiones de la ventana donde se va a dibujar (determinadas por el usuario
al redimensionar la ventana de la aplicación, en tiempo de ejecución) y se aplica al punto correspondiente a cada píxel la fórmula iterativa que determina su color.
Los cálculos que afectan a un punto son independientes del resto de puntos, con lo cual son fácilmente paralelizables. La sincronización se necesita en el momento de pintar los puntos. Por motivos de
eficiencia, una vez obtenido el color de un punto, no se dibuja, sino que se almacenan en un buffer; y
sólo cuando esté lleno se pintan los píxels correspondientes. Esto se aprovecha para distribuir los datos
de manera que el sistema pueda explotar la localidad.
En resumen, cada vez que se redimensiona la ventana, el proceso es el siguiente:
obtener las nuevas dimensiones de la ventana
por cada punto de la ventana
calcular su color
acumularlo en el buffer
si el buffer está lleno, vaciarlo pintando los puntos
En el diseño del programa (figura 3) se pueden distinguir tres tipos de threads. El thread principal se
encarga de recibir los eventos de X y lanzar los threads calculadores. Estos realizan los cálculos y pasan
el resultado al thread visualizador, que es el único que llama a las rutinas de dibujo.
Cada vez que se recibe un evento indicando el redimensionado de la ventana, el thread principal crea
cierto número de threads calculadores. Cada uno de estos nuevos threads irá cogiendo fragmentos de
ventana y calculando el color de los píxels. El resultado se irá acumulando en buffers. Cada uno de estos
buffers se reparte por colores, de manera que los puntos del mismo color van a parar a la misma zona.
Cuando una de estas zonas está llena, se cede el control al visualizador, que la vuelca en un buffer propio
y llama a las rutinas de dibujo.
thread principal
Xt loop()
creación de flujo
datos
ventana
buffers
threads calculadores
resize
X event
thread visualizador
XtDrawPoints()
procesadores
Figura 3: Estructura básica de datos y flujos que intervienen en la generación de fractales.
Hay diversas posibilidades para implementar el acceso a buffers. Una de ellas sería tener un único
buffer. De este modo, los threads calculadores deberían utilizar técnicas de exclusión mutua para acceder
a cada zona del buffer. La opción que hemos tomado es tener tantos buffers como procesadores (virtuales
o físicos, dependiendo del entorno). De este modo, cada thread calculador podría acceder al buffer
correspondiente a su procesador sin necesidad de exclusión mutua. El caso representado en la figura
anterior corresponde a una situación en la que cada thread calculador se encuentra sobre un procesador
(virtual o físico). Esta distribución facilita que los datos utilizados por el código que se ejecuta en cada
procesador, vayan situándose en las cache o en memorias locales, facilitando que el sistema aproveche
la afinidad de memoria y se acelere, por tanto, la ejecución de la aplicación.
5.2. Otras aplicaciones
Otro tipo de aplicaciones que consideramos características para la ejecución paralela en nuestro entorno
son los servidores o aplicaciones similares, en las que el número de threads es bastante estático y longevo. En esta línea hemos diseñado una aplicación cuyo comportamiento es el de un productor/consumidor y un servidor sintético que combina threads estáticos con otros que se crean dinámicamente, como
benchmark para tomar medidas. Su descripción se puede leer en [GIL94c][GIL94a].
6. Toma de medidas
Junto con el objetivo de conseguir un buen entorno de ejecución para aplicaciones paralelas, es muy
importante tener en cuenta las posibilidades que ha de ofrecer dicho entorno para evaluar su ejecución.
Por ello, se han estudiado los mecanismos que ofrecen la arquitectura y el sistema operativo, se han
incorporado herramientas de medición y, finalmente, se ha instrumentado el kernel y se han realizado
medidas del interior del sistema operativo.
6.1. Soporte de la arquitectura y el sistema operativo
La versión multiprocesador del microkernel Mach sobre arquitectura Intel permite la medición de tiempos con alta precisión minimizando a la vez la perturbación introducida en el código que se desea medir.
En este sentido al compilar el kernel se puede especificar que uno de los procesadores se utilice única y
exclusivamente como contador. En este caso las aplicaciones van a disponer de un procesador menos
para ejecutarse, pero como contrapartida se podrán tomar medidas bastante precisas (del orden de 0.5
microsegundos de precisión utilizando un procesador i486 a 33 Mhz.). Dicho contador está aislado dentro de una página física en el espacio virtual del kernel y la gran ventaja es que se mapea en los demás
espacios de direcciones de modo que pueden acceder a ella para leerlo sin necesidad de hacer una llamada a sistema.
6.2. Herramientas de medición: Jewel
La técnica de dedicar un procesador a la toma de medidas fue introducida en Mach por el grupo de desarrollo de la herramienta JEWEL (Just a nEW Evaluation tooL) [LANG92]. Esta herramienta ha sido
desarrollada en GMD (German National Research Center for Computer Science).
Utilizando Jewel, las aplicaciones utilizan memoria compartida para consultar el tiempo y dejar sus
trazas. Éstas son generadas por unas macros en C proporcionadas por Jewel y que deben introducirse
dentro del código fuente de las aplicaciones que se desean medir. Jewel está formado por dos procesos
principales: el collector y el evaluator. La aplicación instrumentada comparte un buffer con el collector.
La aplicación deja la traza en el buffer y el collector la va recogiendo y la envía al evaluator. El proceso
evaluator puede ejecutarse en otro ordenador, ya que la comunicación entre collector y evaluator se realiza mediante sockets. Esta característica ayuda también a no introducir alteraciones, en forma de
aumento de carga, en la máquina donde se realizan las mediciones.
El proceso evaluator presenta los datos ordenados según el tiempo en que han ocurrido y en formato
texto. Se puede a su vez utilizar para suministrarlos a otras aplicaciones que permitirían la visualización
gráfica de los resultados e incluso presentar la evolución que sufre la aplicación a lo largo del tiempo.
6.3. Instrumentación del kernel para la toma de medidas
Parte de nuestro trabajo ha consistido en introducir modificaciones en el código del kernel con el objetivo de tomar medidas. Éstas pueden clasificarse en dos tipos: frecuencia con que se ejecuta un código
del kernel o pasa algún evento y tiempo empleado por las operaciones sencillas que ejecuta el kernel,
como los cambios de contexto de bajo nivel o la creación de threads.
En cuanto a las primeras, se ha hecho un estudio para determinar qué contadores de eventos ya existían dentro del kernel y se han introducido nuevos contadores. Por ejemplo, ya existían los contadores
de número de threads creados, número de pilas creadas, etc. A éstos se han añadido contadores por procesador sobre número de veces que se realiza un cambio de contexto en cada procesador, número de
veces que un thread se bloquea en cada procesador, aciertos en footprints, número de fallos de página
por procesador, etc.
Inicialmente los contadores internos del kernel sólo podían consultarse desde el kernel debugger. Sin
embargo, es mucho más cómodo consultarlos mediante una llamada a sistema. Para ello se ha aprovechado la llamada host_info añadiéndole una nueva opción que extrae el valor de los contadores dejándolos en una estructura de usuario (struct host_extended_info).
En cuanto a la toma de medidas de coste de las operaciones internas al kernel, éste se ha modificado
para que tome el tiempo al entrar y salir de la secuencia de código que se quiere medir. Para tomar el
tiempo se utiliza el contador incrementado por el procesador dedicado, con lo cual la alteración en la
ejecución es mínima. Dicha toma de tiempo consiste simplemente en consultar el valor del contador y
depositarlo en una variable global del kernel. Esta variable contiene los valores del contador al entrar y
salir de la secuencia de código. Existe una pareja de valores almacenada por cada secuencia de código
que se quiere medir. La información almacenada en estas variables se extrae del kernel a través de una
llamada a sistema, utilizando un mecanismo muy similar a la llamada host_info. De esta forma se han
tomado medidas del tiempo que consumen la rutina de creación de threads (thread_create), la de recálculo de prioridades de un thread (thread_quantum_update) y la de cambio de contexto (switch_context).
7. Conclusiones y trabajo futuro
Hoy, que se trabaja sobre microkernels y librerías de threads de usuario, el coste de creación, destrucción
y cambio de contexto está muy optimizado. La planificación de procesadores se apoya en la planificación
espacial. Sin embargo, hay un coste que no se ha considerado todavía suficientemente: el coste de traer
los datos y el código a las memorias próximas al procesador. Presentamos un entorno que facilita la
reducción de este coste mediante la planificación de los threads allí donde se encuentre la memoria a la
que van a acceder, basado en técnicas de “footprint” y en las propuestas de “memory-conscious scheduling”.
Respaldamos las propuestas de mapeo uno a uno de flujos de usuario sobre procesadores físicos.
Hemos añadido los mecanismos necesarios y la política de “scheduler-activations” a la planificación de
procesadores del kernel de Mach 3.0, los mecanismos que permitan aplicar “memory conscious scheduling” y “footprint”, situar los datos en las memorias más rápidas, dar pistas (hints) sobre cuál será la gestión futura más conveniente de la memoria de la aplicación, dar indicaciones para la selección de páginas
a reemplazar de memoria virtual, y permitir la coexistencia de varias políticas de gestión de memoria.
Estamos desarrollando y portando aplicaciones reales sobre este entorno e instrumentando el kernel
y los servidores para la extracción de medidas. Las herramientas están listas, el siguiente paso es la definición y evaluación de las políticas.
8. Referencias y bibliografía
[ACCE86] “Mach: A New Kernel Foundation for UNIX Development”, Mike Accetta et al., Proceedings of the Summer 1986 Usenix Conference, July 1986.
[BLAC90] “Scheduling and Resource Management Techniques for Multiprocessors”, David L. Black,
PhD Thesis, Carnegie Mellon University, July 1990.
[BOYK93] “Programming under Mach”, J. Boykin, D. Kirschen, A. Langerman, S. LoVerso, AddisonWesley Publishing Company, 1993.
[COOP88] “CThreads”, Eric C. Cooper and Richard P. Draves, CMU-CS-88-154, School of Computer
Science, Carnegie Mellon University, June 1988.
[CROV91] “Multiprogramming on Multiprocessors”, M. Crovella, P. Das, C. Dubnicki, T. LeBlanc
and E. Markatos, Third IEEE Symposium on Parallel and Distributed Processing, December 1991, Technical Report 385, Computer Science Department, University of Rochester,
New York, February 1991.
[EDLE92] Jan Edler, comunicación personal, junio 1992.
[GIL94a] “Cooperación entre la aplicación y el kernel para la planificación de flujos, en sistemas multiprocesadores, como soporte al paralelismo”, Tesis doctoral, Departament d’Arquitectura
de Computadors, Universitat Politècnica de Catalunya, Mayo 1994.
[GIL94b] “Towards User-level Parallelism with Minimal Kernel Support on Mach”, Marisa Gil, Toni
Cortés, Angel Toribio, Nacho Navarro, DAC/UPC Report RR-94/07, 1994.
[GIL94c] “The Enhancement of a User-Level Thread Package Scheduling on Multiprocessors”, Marisa
Gil, Xavier Martorell, Nacho Navarro, 2nd International Conference on Software for Multiprocessors and Supercomputers, SMS TPE'94, Moscow.
[GOLU91] “Moving the Default Memory Manager out of the Mach Kernel”, David B. Golub, Richard
P. Draves, Proceedings of the Usenix Mach Symposium, Nov. 1991.
[GOTT92] Allan Gottlieb, comunicación personal, junio 1992.
[GUPT91] “The Impact of Operating System Scheduling Policies and Synchronization Methods on the
Performance of Parallel Applications”, Anoop Gupta, A. Tucker and S. Urushibara, ACM
SIGMETRICS, May 1991, Performance Evaluation Review, Vol.19 Num.1, 1991.
[LANG92] “JEWEL: Design and Implementation of a Distributed Measurement System”, F. Lange, R.
Kröger, M. Gergeleit, IEEE Trans. on Parallel and Distributed Systems, Vol. 3, No 6.,
Nov. 1992.
[LOEP93] “OSF Mach Kernel Interfaces”, Keith Loepere, Open Software Foundation and Carnegie
Mellon University. April, 1993.
[LOEP93] "MACH 3 Kernel Principles", Keith Loepere, Open Software Foundation and Carnegie
Mellon University. April, 1993.
[MARK92a] “Memory-Conscious Scheduling in Shared Memory Multiprocessors”, E. P. Markatos, T.
J. Leblanc, 1992.
[MARK92b] Evangelos. P. Markatos, comunicación personal, junio 1992.
[ORR92] “OMOS - An Object Server for Program Execution”, Douglas B. Orr, Robert W. Mecklenburg, UUCS-92-033, July 1992.
[ORR94] “Dynamic Program Monitoring and Transformation Using the OMOS Object Server”,
Douglas B. Orr, Robert W. Mecklenburg, Peter J. Hoogenboom and Jay Lepreau, in “The
Interaction of Compilation Technology and Computer Architecture” , Chapter 1, Kluwer
Academy Publishers, Ed. Lilja and Bird, Feb. 1994.
[RASH87] “Machine-Independent Virtual Memory Management for Paged Uniprocessor and Multiprocessor Architectures”, R. Rashid, A. Tevanian, jr, et al., Technical Report CMU-CS87-140, Oct. 1987.
[TANE92] “Modern Operating Systems”, Andrew S. Tanenbaum, Prentice-Hall International, 1992,
ISBN 0-13-595752-4.
[YOUN90] “The X Window System: Programming and Applications with Xt, Motif Edition”, Douglas
Young, Prentice Hall, 1990, ISBN 0-13-497074-8.

Documentos relacionados