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.