Universidad de Buenos Aires Facultad de Ingenierı́a Tesis de Grado de Ingenierı́a Electrónica Diseño, Implementación y Evaluación de un procesador multi-núcleo Alumno: Sr. Federico Giordano Zacchigna Director: Dr. Ing. Ariel Lutenberg 31 de julio de 2012 i A mi familia ii iii Resumen y motivación del presente trabajo En este trabajo se presenta una implementación de un procesador multinúcleo. El mismo esta basado en el procesador Plasma, que es un procesador de código libre, simple que ha sido utilizado para la realización de varios proyectos y pruebas, y que esta basado en la arquitectura de instrucciones MIPS. El procesador ha sido objeto de estudio en varios trabajos, entre los que se cuentan trabajos realizados por integrantes de nuestro grupo en dónde se estudia su funcionamiento bajo efectos de radiación e interferencia electromagnética. En la actualidad se desea continuar con esta lı́nea de investigación sobre procesadores multi-núcleos y por eso surge la necesidad de implementar este procesador. Las caracterı́sticas antes nombradas sobre el procesador Plasma, son ideales para la realización de este trabajo. El sistema plasma multi-núcleo tiene distintas aplicaciones directas, entre ellas: Sistemas tolerantes a fallas utilizables en ambientes de alta interferencia electromagnética y expuestos a radiación no ionizante, para aplicaciones en sistemas de seguridad de reactores nucleares y sistemas de control y navegación de satélites y vehı́culos espaciales. Este tema es de especial interés hoy en dı́a para instituciones como CONAE, CNEA e INVAP. Investigaciones cientı́ficas relacionadas con el estudio de los efectos de la radiación y la interferencia electromagnética sobre los FPGA’s donde se utilice una versión funcional de un softcore que sirva para realizar mediciones y sacar conclusiones sobre los efectos que estos fenómenos tienen sobre los dispositivos. Desarrollos de sistemas multi-núcleos parametrizables mixtos (Asymetric Multicore Architecture) para aplicaciones de alto rendimiento, donde se implementan micros óptimos para distintos procesos, por ejemplo, ejecución de RTOS, procesamiento de señales, GPU’s, comunicaciones, etc. Un objetivo importante en la implementación del procesador multi-núcleo que se realiza en este trabajo es que sea de forma tal que se puedan instanciar un número genérico de núcleos durante el proceso de sı́ntesis, y las partes más relevante del diseño de la arquitectura del procesador son la arquitectura de la memoria, la memoria cache y la comunicación entre los distintos núcleos. A lo largo del trabajo se detallan los principales factores que influyen sobre el diseño del mismo. También se muestran las principales complicaciones que aparecen y las soluciones a las mismas. Se muestran los detalles de su implementación en VHDL. Se realizan pruebas del mismo en un kit Nexys2 de Digilent, basado en una FPGA Xilinx Spartan3E-1200. Finalmente se presentan los resultados obtenidos. iv v Agradecimientos En primer lugar, agradezco al Laboratorio de Sitemas Embebidos de la Universidad de Buenos Aires, lugar en el cual fue llevado a cabo el presente trabajo. Agradezco a su director y mi tutor, el Dr. Ing. Ariel Lutenberg, por el apoyo brindado. Agradezco también a Lucas Chiesa por la ayuda que me brindó al realizar el trabajo. Agradezco especialmente a los miembros del jurado, el Ing. Juan Manuel Cruz, el Ing. Nicolás Alvarez, y el Ing. Fabián Vargas miembro de la Pontifı́cia Universidade do Rio Grande do Sul (PUCRS), por ceder parte de su tiempo para evaluar el presente trabajo. Parte del desarrollo de este trabajo de tesis tuvo lugar en el Institut für Datentechnik und Kommunikationsnetze (IDA) de la Universidad Técnica Braunschweig. Agradezco a su director Peter Rüffer, y a mis compañeros del laboratorio Mark y Mustafa. Agradezco al DAAD y al Ministerio de Educación de la República Argentina por brindarme esta posibilidad, al darme una beca. Agradezco a los integrantes del programa ALE-ARG del DAAD, Agustin Rosembaum, Alejando Rodriguez, Eliseo Rocchetti, Federico Beltzer, Lucas Claramonte, Mauro Calabria, Ariel Malawka, Sofı́a Carolina Visintini. Agradezco a todas las otras personas que formaron parte de mi vida mientras estuve en Alemania, Jose Alejandro Diaz Vides, Ismael Holgueras de Lucas, Dani Umpierrez Corona, Mariana Almeida Ribeiro, Audrey Segura Medina, Valerio Roger Lasso, José Rivero Rodrı́guez, Maria y Angela Cildoz Guembe. Y agradezco especialmente a aquellos con quienes logre tener una afinidad especial durante mi estadı́a en Alemania, personas que me ayudaron mucho con su compañia y apoyo en los momentos difı́ciles, Sergio Medina, con quien compartı́ un tiempo en Parı́s, Mariel Figueroa, Bruno Strappa, Bruno Emmanuel Rossi, Mari Antber, Ana Rodriguez, Juan José Baena Castillo, Belen Kistner, Miguel Mamani. Durante los primeros cinco años y medio de carrera tuve el agrado de conocer a muchas personas con quienes trabajé en MAN Ferrostaal, a todos ellos quiero agradecerles, Leandro Feniello, Olga Hiczuk, Alicia Hiczuk, Martin Kent, Eduardo Kenda, Daniel Morales, Sergio Acri, Walter Allaltune, Osvaldo Preiti, Lucas De La Canal, Marta, Romina Lepore, Daniela Islas y al resto de los integrantes. Agradezco a todos los profesores que formaron parte de mi formación académica, a aquellos de la facultad y a aquellos del mi colegio, el Instituto Hölter Schule, pero especialmente a mis profesores de electrónica a quienes aprecio mucho, Ing. Rubén Saclier, Ing. Norberto Muiño, Ing. Carlos Siganotto y al Ing. Charly, y a mis profesores de alemán especialmente a Diana, Geraldine Lorenzo, Fedor Pellmann y muy de corazón a Juana Dartsch, quien fue por lejos mi mejor profesora. Agradezco a las personas que forman parte de la materia Dispositivos Semiconductores, de la cual formo parte hace ya más tres años, Mariano Garcı́a Inza, Sebastián Carbonetto, Diego Martin, Claudio Pose, Luciano César Natale, Gabriel Sanca y en su momento Daniel Rus. Agradezco a todos mis compañeros de la facultad de ingenierı́a entre ellos, Fernando Chouza, Diego Martı́n, Fabricio Alcalde, Gabriel Gabian, Pablo Delgado, Enzo Lanzelotti, Fernando Berjano, Paola Pezoimburu, Federico Roasio, Ezequiel Espósito, Claudio Pose, Andres Manikis y a todos aquellos que no recuerde en este momento. También agradezco a aquellos que dejaron ser com- vi pañeros de la facultad, para pasar a ser amigos, Luciano César Natale, Germán Acosta, Diego Vilaseca, Gustavo Dı́az, Matı́as Weber, Claudio Lupi, Manuel Fernández, Lucas Sambuco, Quiero agradecer a todas las personas importantes que pasaron por mi vida durante este perı́odo, ya que no sólo estoy terminando mi carrera sino culminando una gran etapa en mi vida, y todos estas personas formaron parte de ella, Lis Weiss, Jose Spillmann, Marcelo Razeto, Sandra Canepuccia, Santiago Razeto, Natalia y Carla Belén Rossi, Sebastián Salles, Alejandro sansalone, Glenda Busch, Sarah Collins, Ana Belén Garcı́a, Barbara Beutel, Ignacio Unrrein, Lucas Sakalis, Mauro Garcı́a Alena, Alejandro Mauch, Sebastián Deter, Gaston Santana, Pablo Falcioni, Martı́n Burgueño, Alejandro Sappracone, Hernán Goncalves. Agradezco de también a mis más grandes amigos, a quienes siempre voy a querer, quienes fueron compañeros en buenos y malos momentos, quienes siempre estuvieron, con quienes compartir dı́as y noches de estudio, salidas e infinidad de cosas y por eso son especiales para mi, Jesica Lazart, Mariana Ende, Barbará y Daniela Spillmann Weiss, Agustina Cánepa, Franco Gallardi, Hernán Pedros, Juan Sist, Patricio Helfrich, Leonardo Unger, Mauricio Koller, Fernando Perez, Matias Schwabauer e Ignacio Razeto. No puedo dejar de agradecer a la familia Hoffmann, a quienes personalmente me gusta llamar ‘mi familia alemana’, a quienes quiero muchı́simo, ellos son Michael, Martina, Ulrich y Bernhard. Por último quiero agradecerles mucho mucho mucho a mi familia, quienes me apoyaron durante toda mi vida e hicieron esto posible, a ellos los amo incondicionalmente y son Nélida Nazarre, Rodolfo Parrondo, Oscar Zacchigna, Ana Marı́a D’Ambrosio, y Julia Zacchigna. Índice general 1. Introducción 1.1. Ventajas de la paralelización . . . . . . . . . . . 1.2. Clasificación de los procesadores . . . . . . . . 1.3. Evolución de los procesadores . . . . . . . . . . 1.3.1. Inicio . . . . . . . . . . . . . . . . . . . 1.3.2. Paralelismo a nivel de instrucción . . . . 1.3.3. Limitaciones en el paralelismo a nivel de 1.3.4. Paralelismo a nivel de tarea . . . . . . . 1.4. Multi-Threading . . . . . . . . . . . . . . . . . 1.5. Multi-procesadores . . . . . . . . . . . . . . . . 1.6. Sistemas operativos y programación distribuida 1.6.1. Sistemas operativos . . . . . . . . . . . 1.6.2. Procesos e hilos . . . . . . . . . . . . . . 1.6.3. Planificación de hilos . . . . . . . . . . . 1.6.4. Programación distribuida . . . . . . . . 1.7. Consumo y frecuencia de trabajo . . . . . . . . 1.8. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . instrucción . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 2 3 3 3 9 9 10 12 13 13 14 14 15 16 16 2. Teorı́a y diseño 19 2.1. Procesador Plasma y arquitectura MIPS . . . . . . . . . . . . . . 19 2.2. Comunicación entre los procesadores . . . . . . . . . . . . . . . . 20 2.3. Bus de interconexión . . . . . . . . . . . . . . . . . . . . . . . . . 22 2.4. Arquitectura de la memoria . . . . . . . . . . . . . . . . . . . . . 24 2.4.1. Principio de localidad . . . . . . . . . . . . . . . . . . . . 24 2.4.2. Jerarquı́a de la memoria . . . . . . . . . . . . . . . . . . . 24 2.4.3. Memoria Cache . . . . . . . . . . . . . . . . . . . . . . . . 25 2.4.4. Arquitectura de la memoria en procesadores multi-núcleo 30 2.4.5. Protocolos y algoritmos de coherencia de cache . . . . . . 32 2.5. Manejo de interrupciones . . . . . . . . . . . . . . . . . . . . . . 33 2.6. Operaciones atómicas . . . . . . . . . . . . . . . . . . . . . . . . 34 2.7. Caracterización a priori del procesador . . . . . . . . . . . . . . . 39 3. Implementación y resultados obtenidos 3.1. Herramientas utilizadas . . . . . . . . . 3.2. Estructura del procesador multi-núcleo . 3.3. Controlador de memoria . . . . . . . . . 3.3.1. Descripción . . . . . . . . . . . . 3.3.2. Puertos de la entidad . . . . . . vii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 41 42 42 42 43 ÍNDICE GENERAL viii 3.4. Plasma multi-núcleo . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . . 3.4.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . . 3.5. Árbitro del bus . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . . 3.5.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . . 3.6. Manejador de interrupciones . . . . . . . . . . . . . . . . . . . . . 3.6.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . . 3.6.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . . 3.7. Núcleo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.7.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . . 3.7.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . . 3.7.3. Descripción del algoritmo de coherencia de cache . . . . . 3.8. Unidad de control del núcleo . . . . . . . . . . . . . . . . . . . . 3.8.1. Descripción . . . . . . . . . . . . . . . . . . . . . . . . . . 3.8.2. Puertos de la entidad . . . . . . . . . . . . . . . . . . . . 3.9. CPU plasma . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3.10. Caracterización del procesador . . . . . . . . . . . . . . . . . . . 3.10.1. Tamaño del procesador . . . . . . . . . . . . . . . . . . . 3.10.2. Desempeño en función del trabajo de las tareas . . . . . . 3.10.3. Desempeño en función de la utilización del bus . . . . . . 3.10.4. Tiempo de procesamiento en función del número de núcleos 3.10.5. Importancia de la memoria cache en procesadores multinúcleo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 46 50 51 51 53 54 54 54 55 55 60 62 63 63 65 68 68 70 70 70 72 73 4. Conclusiones y trabajos futuros 75 4.1. Conclusiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 4.2. Trabajos Futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 Bibliografı́a 77 A. Implementaciones de spinLocks 79 A.1. Con soporte de hardware . . . . . . . . . . . . . . . . . . . . . . . 79 A.2. Solución propuesta por Peterson . . . . . . . . . . . . . . . . . . 80 A.3. Solución propuesta por el creador del PlasmaOS . . . . . . . . . 80 Capı́tulo 1 Introducción En este capı́tulo se hace una breve introducción a la paralelización en el contexto de los procesadores, para luego poder clasificar a los procesadores según este criterio. Luego se realiza descripción de la evolución de los procesadores a lo largo de la historia. Se muestran también las limitaciones que fueron apareciendo y los cambios en el enfoque de los diseños de los mismos. 1.1. Ventajas de la paralelización El concepto de paralelismo es de gran importancia para este trabajo y lo definiremos en nuestro caso como la forma de realizar dos acciones simultáneamente. Aprovechar el paralelismo es uno de los métodos más importantes para mejorar el rendimiento en la ejecución de cualquier tarea, en particular en procesadores. Por ejemplo, al transferir información de un disco rı́gido, la velocidad de transferencia está dada por el ancho de banda del disco, pero si se se colocan dos discos y los cuales se acceden simultáneamente, se logra duplicar el ancho de banda, y ası́ mejorar el rendimiento. A nivel de ejecución de código hay distintos tipos de paralelismos y distintas formas de explotarlo. Un primer ejemplo del uso del paralelismo es a nivel de sistema, para lo que se pueden utilizar múltiples discos y/o múltiples procesadores. La carga de trabajo para atender las solicitudes de los clientes puede ser repartida entre los procesadores y discos, y como consecuencia se obtiene un mejor rendimiento. Cuando en un sistema es posible expandir la memoria y el número de procesadores y discos se lo llama sistema escalable, y es una propiedad muy valorada en los servidores. Por otro lado a nivel de procesadores individuales, sacar provecho del paralelismo a nivel de instrucción es crı́tico para mejorar la capacidad de procesamiento, se explica con profundidad en la sección 1.3.2. Una de las formas más simples de lograr esto es a través del uso de pipelining, que fue la técnica utilizada en los primero micros para explotar el paralelismo. La idea básica del pipeline, que se explica con mayor detalle en la sección 1.3.2, es solapar la ejecución de las instrucciones y ası́ reducir el tiempo total de ejecución para una 1 CAPÍTULO 1. INTRODUCCIÓN 2 secuencia de instrucciones. Un factor clave que da lugar al pipelining es que no todas las instrucciones dependen de la anterior inmediata y por ello ejecutar la instrucción parcialmente o completamente en paralelo es posible. 1.2. Clasificación de los procesadores A lo largo de la historia han surgido varias clasificaciones para los sistemas computacionales. Muchas de ellas quedaron rápidamente obsoletas con el paso del tiempo. Sin embargo, la clasificación propuesta por Flynn en el año 1966 [1] sigue siendo válida. La misma se basa en un modelo de dos dimensiones: Stream de datos y Stream de instrucciones. Cada una de estas dimensiones tiene dos posibles valores: Simple o Múltiple (en paralelo). Se obtienen ası́ cuatro combinaciones posibles: SISD: En inglés Single Instruction Single Data. Es el caso de un procesador simple como el plasma. SIMD: En inglés Single Instruction Multiple Data. Es el caso de las llamadas computadoras vectoriales o aquellas llamadas VLIW o EPIC. MISD: En inglés Multiple Instruction Single Data. Son de uso poco frecuentes, algunos computadoras que entran en esta categorı́a son aquellas que deben ser tolerantes a fallas, en las cuales existen múltiples procesadores trabajando sobre el mismo stream de datos, y que a la vez deben de coincidir en el resultado. MIMD: En inglés Multiple Instruction Multiple Data. Es el caso de los multiprocesadores que se tratan en este trabajo. Los SIMD y MISD son sistemas diseñados para usos especı́ficos, y no serán tratados con mucha profundidad en este trabajo. Los SISD son los más simples de los procesadores, y no sacan provecho ni de la paralelización de los datos ni de las instrucciones, aunque si hacen uso de técnicas como pipelininig. Las computadoras MIMD son las más flexibles dentro de esta clasificación, es posible ejecutar dos streams de datos e instrucciones diferentes en simultáneo, sean o no independientes entre sı́. Es claro también que deben existir al menos la misma cantidad de tareas (o streams de instrucciones y datos) que de núcleos de procesamiento para poder aprovechar las ventajas de una computadora MIMD. Esto se explica en las siguientes secciones y de allı́ se concluye que el surgimiento de procesadores MIMD es consecuencia del: La necesidad de aumentar el rendimiento en la ejecución de tareas. La limitación de los procesadores SISD, y de la paralelización posible en las instrucciones. La posibilidad de dividir las tareas en diferentes procesos independientes, o poco correlacionados, lo que se llama paralelización a nivel de tareas. A lo largo de este trabajo se tratan consecuencias y problemáticas de esta paralelización. CAPÍTULO 1. INTRODUCCIÓN 1.3. 1.3.1. 3 Evolución de los procesadores Inicio Los primeros procesadores, surgidos en la decada del 60, ejecutaban secuencialmente cada una de las instrucciones del código. La ejecución de cada instrucción esperaba la finalización de la instrucción predecesora antes de comenzar. No hacı́an uso de ningún tipo de paralelización. El método utilizado para aumentar su rendimiento consistı́a en aumentar la frecuencia del reloj, y los diseños de las arquitecturas se apuntaban a reducir tiempo de propagación de las señales en el procesador (es decir, cantidad de compuertas conectadas en cascada), y de esa manera poder aumentar la frecuencia de trabajo. Otro método que se utilizaba para llegar a ese mismo objetivo era mejorar la tecnologı́a del proceso de fabricación de los circuitos integrados, reduciendo cada vez más el tamaño de los transistores y ası́ poder aumentar su velocidad de trabajo [2]. Rápidamente se alcanzaron los lı́mites máximos para las tecnologı́as de la época, y se comenzaron a estudiar otro tipo de maneras de seguir mejorando el rendimiento, basadas en la arquitectura del procesador en vez del proceso de fabricación. La evolución en las arquitecturas se apuntaba a mejorar los rendimientos pero siempre teniendo en cuenta los tipos de programas que se pretendı́a correr en los procesadores. Para medir el rendimiento de un procesador se utilizan programas llamados benchmark. Existen distintos benchmarks para evaluar distintos aspectos de procesador. Por ejemplo, algunos evalúan operaciones en punto fijo, otras en punto flotante, ancho de banda de la memoria, etc. Además existen distintos benchmarks según el contexto para el que fueron diseñados, como pueden ser computadoras de escritorio, servidores, sistemas embebidos, etc. Tras la ejecución de un benchmark, un cierto número de instrucciones han sido procesadas, y una cierta cantidad de ciclos de reloj y un perı́odo de tiempo determinado han transcurrido. A partir de estos tres valores surgen dos parámetros muy utilizados para evaluar a los procesadores, el CPI (en inglés Cicles Per Instruction) que no tiene en cuenta el tiempo de ejecución y a la vez es independiente de la frecuencia de reloj y el IPS (en inglés Instructions Per Second ) que no tiene en cuenta la cantidad de ciclos que le lleva finalizar la ejecución de una instrucción. La frecuencia de trabajo del procesador es a la vez el factor que vincula estos dos valores. En las siguiente secciones de este capı́tulo se muestra la evolución de las arquitecturas, junto con las mejoras y las problemáticas que implican. Finalmente se muestran las nuevas tendencias en los procesadores, donde en algunos casos especiales dejan de ser importantes los rendimientos individualmente y pasa a ser fundamental la relación rendimiento/consumo. 1.3.2. Paralelismo a nivel de instrucción El paralelismo a nivel de instrucción o ILP (en inglés Instruction Level Paralelism) se refiere a la posibilidad de superponer la ejecución de más de una instrucción en un programa. Estas instrucciones pueden ser consecutivas o no. Un ejemplo de dos instrucciones que pueden ejecutarse en paralelo es: 1 2 add $ 1 , $ 2 , $3 sub $ 4 , $ 2 , $3 CAPÍTULO 1. INTRODUCCIÓN 4 Donde el resultado de sumar los regitros $2 y $3, se guarda en $1 y por otro lado la resta de esos dos mismos registros se guarda en el registro $4. Las dos operaciones pueden ejecutarse al mismo tiempo, sin ningún problema. Distinto es en el siguiente ejemplo: 1 2 add $ 1 , $ 2 , $3 sub $ 4 , $ 1 , $5 Donde se ve que la segunda operación no puede ser ejecutada hasta que el resultado de primer instrucción no esté disponible. A estas instrucciones se las llama dependientes, o también se dice que existe una dependencia entre las instrucciones. Existen varios tipos de dependencias entre instrucciones, pueden ser de datos, de control o de hardware Estas dependencias se detallan en profundidad en este trabajo. Para programas tı́picos en assembler MIPS, en bloques de programa donde no hay saltos ni bucles(salvo por el salto para llegar a ese bloque y el salto para volver) el porcentaje tı́pico de instrucciones paralelizables se encuentra entre un 15 % y un 20 % [2]. Al ejecutar este tipo de bloques en un procesador con capacidad de paralelismo no brinda una mejora significativa. Para bloques de programa donde existen ramificaciones y bucles, este porcentaje de instrucciones paralelizables es mucho mayor y hacer provecho de la paralelización a nivel de instrucción se vuelve una de las maneras más eficientes para mejorar el rendimiento. La ejecución de instrucciones en forma superpuesta implica modificaciones en la arquitectura del procesador: Pipelining: Cuando la lógica utilizada para la ejecución de una instrucción no puede seguir siendo reducida, el pipelining es una técnica que surge naturalmente. Se basa en dividir la ejecución de la instrucción en etapas, donde cada una de estas etapas tiene una cantidad mucho menor de compuertas conectadas en cadena. De este modo se logra bajar el tiempo de propagación en cada etapa, lo cual hace posible el aumento de la frecuencia del reloj. Estas etapas son interconectadas por registros (flip-flops), que tienen la función de guardar los resultados de cada etapa y transferirlo a la siguiente en cada ciclo de reloj [3]. Las cinco etapas tı́picas y los registros utilizados para interconectarlas se muestran en la figura 1.1. Las etapas pueden superponerse. En la figura 1.2(a) se ve un ejemplo de un pipeline de una arquitectura RISC, donde la ejecución de la instrucción se divide clásicamente en cinco etapas: • IF Instruction Fetch • ID Instruction Decode • EX Execution • MEM Memory Access • WB Write Back En este tipo de arquitecturas existe lo que se llaman stalls: son momentos donde alguna etapa del pipeline debe ser pausada por existir alguna dependencia entre dos instrucciones consecutivas. Este tipo de arquitecturas no muestra mejoras en el CPI, de hecho en general lo empeoran, pero al lograr un aumento en la frecuencia de reloj, se puede mejorar el IPS. El CPI empeora debido a los Stalls que se generan en el procesador. EX MEM REGISTROS 5 REGISTROS ID REGISTROS IF REGISTROS IF CAPÍTULO 1. INTRODUCCIÓN WB CLK Figura 1.1: Pipeline de cinco etapas interconectadas con registros. En esta figura se muestran las cinco etapas en las que se divide clásicamente una instrucción de una arquitectura RISC, y como se interconectan mediante registros para poder reducir los tiempo de propagación en las etapas individualmente y ası́ poder aumentar la frecuencia de trabajo. En arquitecturas más evolucionada se utilizan técnicas como forwarding y bypassing, que permiten evitar algunos de los stall. Cómo se producen los stalls y las maneras que existe de evitarlos son temas ampliamente explicados en [3] Multiple Instruction Issue: El procesador ejecuta realmente varias instrucciones simultáneamente. Para ellos algunas partes del Hardware deben ser multiplicada, como por ejemplo las ALU. En la figura 1.2(b), se ve como se realiza la ejecución de las instrucciones. Se muestra la diferencia con el ejemplo de la figura 1.2(a), donde en cada ciclo de reloj se ejecutan dos veces cada una de las etapas del pipeline. El uso de este recurso mejora el CPI. En el ejemplo de la figura 1.2(b) el CPI puede llegar a ser reducido a la mitad, al ser posible completar hasta dos instrucciones por ciclo. De esta manera se vuelve posible también alcanzar valores de CPI menores a la unidad. En los pipelines más mas evolucionados los stalls debidos a dependencias entre instrucciones son prácticamente eliminados, lo que mejora notablemente el ILP. Como se dijo este tipo de soluciones permiten la ejecución superpuesta de instrucciones, pero a veces la superposición no es posible debido a las dependencias existentes entre las instrucciones. A veces estas dependencias dependen sólo del orden de las instrucciones, por ejemplo en el siguiente código: 1 2 3 add $ 1 , $ 2 , $3 sub $ 4 , $ 1 , $5 sub $ 6 , $ 2 , $3 las instrucciones 1 y 2 son dependientes. Un procesador con dos issue slots 1 tiene dos ramas de ejecución de instrucciones. La rama que ejecute la segunda instrucción, deberá ser pausada en algún momento debido a la dependencia existente. Para evitar la pausa que se produce en la ejecución se puede modificar el orden de las instrucciones por el siguiente: 1 2 add $ 1 , $ 2 , $3 sub $ 6 , $ 2 , $3 1 No tomamos como ejemplo un procesador que sólo posee un pipeline, porque como se dijo antes las dependencias entre instrucciones pueden ser evitadas en esas arquitecturas. CAPÍTULO 1. INTRODUCCIÓN 6 INSTRUCCIONES Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo 1 3 4 5 6 7 8 9 2 INSTRUCCIÓN 1 INSTRUCCIÓN 2 INSTRUCCIÓN 3 INSTRUCCIÓN 4 INSTRUCCIÓN 5 IF ID IF EX MEM WB ID EX MEM WB IF ID EX MEM WB IF ID EX MEM WB IF ID EX MEM WB TIEMPO (a) Arquitectura con pipeline de cinco etapas. INSTRUCCIONES Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo Ciclo 1 2 3 4 5 6 7 8 9 INSTRUCCIÓN 1 INSTRUCCIÓN 2 INSTRUCCIÓN 3 INSTRUCCIÓN 4 INSTRUCCIÓN 5 INSTRUCCIÓN 6 INSTRUCCIÓN 7 INSTRUCCIÓN 8 INSTRUCCIÓN 9 INSTRUCCIÓN 10 IF IF ID ID IF IF EX MEM WB EX MEM WB ID EX MEM WB ID EX MEM WB IF ID EX MEM WB IF ID EX MEM WB IF ID EX MEM IF ID EX MEM IF ID EX IF ID EX WB WB MEM WB MEM WB TIEMPO (b) Arquitectura con pipeline de cinco etapas y dos issue. Figura 1.2: En la figura 1.2(a) se puede apreciar como las etapas de las diferentes instrucciones se suporponen. En la figura 1.2(b) se muestran como se superponen las etapas e instrucciones de un procesador con el mismo pipeline de cinco etapas y dos issue slots. 3 sub $ 4 , $ 1 , $5 esta porción de código tiene mayor ILP que la anterior. Para aumentar el ILP de un programa se puede entonces reordenar las instrucciones. Para poder hacer un reordenamiento y ası́ hacer uso eficiente del ILP, existen dos enfoques claramente diferenciables: Basado en Software: Este enfoque encuentra posibles instrucciones paralelizables en forma estática, en tiempo de compilación de un programa. Es la forma más básica de hacerlo. Esto depende del compilador, que básicamente reacomoda instrucciones de forma tal de bajar el porcentaje de instrucciones con dependencias. Como se dijo antes en la ISA de MIPS no se especifica la implementación, por lo que el compilador no tiene porque saber la arquitectura del mismo, pero si puede explicitarse, de manera que tenga en cuenta la arquitectura del microprocesador, como puede ser si cuenta o no con unidades de multiplicación de punto fijo, unidades de multiplicación de punto flotante, etc. Basado en Hardware: Este enfoque encuentra posibles instrucciones paralelizables en forma dinámica, en tiempo de ejecución de un programa. Es decir, las instrucciones no necesariamente ingresan en el orden en el CAPÍTULO 1. INTRODUCCIÓN 7 que se encuentran en el código, sino que el procesador puede ‘ver’, la instrucción a la que apunta el contador de programa y un número dado de instrucciones posteriores a esa. A estas instrucciones se las llama ventana de programa. De entre todas las instrucciones que se encuentran en esa ventana del programa, el procesador elige las que son mejores para su ejecución en paralelo. En un caso general estos dos enfoques pueden ser explotados en forma simultánea, logrando ası́ mejores resultados en los valores de CPI e IPS. A modo de ejemplo se presentan brevemente dos técnicas simples utilizadas para la planificación estática: Loop unrolling: Incrementa el número de operaciones que se ejecutan dentro de un bucle y a la vez reduce el número de iteraciones. En los bucles es donde en general se encuentra un mayor número de instrucciones independientes. El ejemplo clásico es el de la suma de dos vectores, donde en vez de sumar una componente, se suman dos o más de las componentes por iteración y ası́ se reduce la cantidad de veces que se ejecutan las instrucciones del bucle. Una consecuencia de realizar esto es que el código aumenta su tamaño ocupando mayor espacio en la memoria. Pipeline Scheduling: Dado que hay distintos tipos de dependencias, existen pares de instrucciones que tienen dependencias reales y otros que pueden ser evitados. Las instrucciones pueden ser planificadas teniendo en cuenta este factor y de esa manera evitar pausas en la ejecución. Esta técnica depende de la arquitectura. Existen otras técnicas de hardware que se utilizan para mejorar el rendimiento, reduciendo los stalls, mejorando la planificación dinámica, aumentando el número de instrucciones paralelizables, prediciendo posibles saltos y especulando que instrucciones se ejecutarán. Se explican a continuación algunas brevemente: A continuación describimos brevemente algunas de las técnicas más conocidas para mejorar el rendimiento. Los objetivos son evitar stalls y pausas en en los procesadores, mejorar la planificación dinámica, aumentar el número de instrucciones paralelizables, y predecir el flujo del programa correctamente: Forwarding and bypassing: Se utiliza en los pipelines para evitar stalls al ejecutar instrucciones que poseen dependencias de datos. Se basa en extraer los datos antes de que terminen de atravesar el pipeline. Por ejemplo, se extrae el resultado de una operación aritmética apenas termina la etapa de ejecución en vez de esperar a que se escriba en un registro. Branch prediction (local and global): Cuando en un programa hay un salto condicional (o bifurcación) las instrucciones que se deben de ejecutar cambian según se cumpla o no la condición de salto. Esta técnica se basa en ir cargando en el procesador las instrucciones más probables y ellas dependen de la probabilidad de saltar o no. En un bucle lo más usual es que al llegar al final se salta de nuevo al principio. Este salto se realiza N veces, de las cuales N − 1 veces se salta y sólo una vez se sigue de largo. Para todo bucle con N mayor a uno las probabilidades de salto son mayores a las de no saltar. CAPÍTULO 1. INTRODUCCIÓN 8 Register renaming: Se generan registros extra aparte de los definidos en la ISA MIPS, estos registros no pueden ser accedidos por los usuarios, en cambio sı́ internamente por el procesador y en algunos casos se logra mejorar el rendimiento. Un ejemplo simple serı́a el siguiente: 1 2 3 add $ 1 , $ 2 , $3 sw $ 1 , 0 x10 ( $ sp ) li $ 1 , 0 x1 Las instrucciones dos y tres tienen una dependencia, de modo que la instrucción tres no puede ser ejecutada antes que la dos, ya que se estarı́a guardando un valor erróneo, pero el registro $1 es cargado con un valor distinto en la tercer instrucción. Si fuese posible cambiar $1 por $4 en las dos primeras instrucciones, entonces sı́ serı́a posible ejecutar las instrucciones dos y tres en paralelo. Esto ocurre a veces por falta de registros en la arquitectura, que es una de las condiciones del procesador perfecto: infinito número de registros en el procesador. A veces no se puede simplemente diseñar un procesador con mayor número de registros, ya que esta caracterı́stica puede estar definida en la arquitectura de instrucciones (como es el caso de MIPS), pero los procesadores sı́ pueden tener un número mayor de registros en su arquitectura e internamente utilizarlos como se explico antes, renombrándolos para lograr mayor ILP. Teniendo en cuenta lo dicho anteriormente se puede hacer una clasificación de los núcleos de procesamiento a partir de la arquitectura del procesador, sin especificar si posee un núcleo o varios de ellos. En ésta clasificación también se identifica a categorı́a pertenece de la clasificación mostrada anteriormente en la sección 1.2: Procesador escalar: Los procesadores más simples, pueden estar implementados con un pipeline o no. Clasificado como SISD. Procesador super-escalar con planificación estática: Procesadores que utilizan Multiple Instruction Issue, en general utilizan una arquitectura con pipeline. La planificación de las instrucciones es estática, o sea que solo se realiza a nivel de compilación. Clasificado como MIMD. Procesadores vectoriales que son clasificados como SIMD, utilizados principalmente en aplicaciones cientı́ficas, que generalmente realizan muchas veces la misma operación entre distintos datos, por ejemplo al hacer operaciones entre vectores de gran tamaño. Procesador de arquitectura VLIW (en inglés Very Long Instruction Word ) o EPIC (en inglés Explicit-Parallel Instruction Computer ): Este tipo de procesadores son diferentes a los que se han presentado en este trabajo. Las instrucciones que ejecutan los mismos indican en forma explı́cita la operación que debe realizar cada una de las unidades funcionales del procesador. Tienen dos ventajas frente a los procesadores de planificación dinámica: Una alta reducción en el hardware necesario para su implementación y que los compiladores pueden generar código de manera que se aproveche al máximo las ventajas del paralelismo. Este tipo de procesadores son del tipo de MIMD. CAPÍTULO 1. INTRODUCCIÓN 9 Procesador super-escalar con planificación dinámica: Procesadores que utilizan Multiple Instruction Issue, en general utilizan una arquitectura con pipeline. Aprovecha tanto las planificación estática lograda a nivel compilación y la planificación dinámica realizada a nivel de ejecución. La operación que ejecuta cada unidad funcional del procesador se define luego de decidir que instrucciones se ejecutan. Este tipo de procesadores se clasifican como MIMD. La mayorı́a de los núcleos de los procesadores que se utilizan hoy en dı́a son superescalares aunque no todos tienen planificación dinámica de instrucciones, debido a la gran complejidad que esto presenta. 1.3.3. Limitaciones en el paralelismo a nivel de instrucción En un procesador ideal que aprovecha completamente el ILP presente en un programa, los lı́mites en el rendimiento son impuestos por los flujos de datos a través de los registros o memoria. Un procesador ideal contarı́a con infinitos registros, una ventana de programa infinita, perfectas predicciones en el flujo de ejecución del programa, un perfecto análisis de aliasing de memoria2 y accesos a memoria que solo necesiten un ciclo de reloj. En un procesador real estas condiciones no se cumplen, y por eso se alcanza el lı́mite en el rendimiento de un mono-procesador, cuando el costo de hardware es demasiado, y la mejora que su complejización proporciona no es significativa. 1.3.4. Paralelismo a nivel de tarea En ocasiones se habla de paralelismo a nivel de tarea o hilo (TLP, en inglés Thread Level Paralelism). La definición precisa de hilos, y la de procesos también, se puede encontrar en [10]. Para explicar TLP basta con tener una pequeña noción de lo que es un hilo o un proceso, para este caso utilizaremos el nombre tarea independientemente de que sea un hilo o un proceso. Una tarea es una porción de programa que cumple con un objetivo especı́fico. Hay tareas que son independientes entre sı́ y tareas que poseen dependencias con otras. El ejemplo clásico para dos tareas pseudo-independientes lo encontramos en un servidor al cual acceden N usuarios y hacen N pedidos distintos. En general estas tareas pueden ser ejecutadas todas por separado y en cualquier orden. Un corolario de poder ejecutar las tareas por separado y en cualquier orden es que también pueden ser ejecutadas en paralelo. Los procesadores MIMD pueden explotar las ventajas del TLP y hay dos enfoques para explotarla: Los multi-procesadores: El enfoque se basa en multiplicar el número de núcleos e interconectarlos de manera eficiente, sin que importe la arquitectura del núcleo en sı́ (ver sección 1.5). Pueden clasificarse en dos grandes 2 Es una técnica para reducir accesos a memoria durante el proceso de optimización en tiempo de compilación. Detecta uno de los dos casos posibles, si existe o no aliasing entre punteros, es decir punteros que apuntan a una misma posición de memoria. Una vez detectado es posible realizar las optimizaciones necesarias, si no se puede asegurar ninguno de los dos casos, las optimizaciones no pueden ser realizadas. CAPÍTULO 1. INTRODUCCIÓN 10 grupos, los procesadores simétricos y los asimétricos. La principal diferencia entre los dos grupos es la forma en la que se interconectan los distintos procesadores, como se comunican y como se distribuye la memoria, como se ve en la sección 2.2. Los procesadores que soportan multi-threading(MT) y simultaneous-multithreading(SMT): Es un enfoque en el que un único núcleo soporta la ejecución de más de una tarea en simultáneo, o que tiene la capacidad de intercambiar tareas de forma muy rápida, ya que almacena contextos de más de una tarea [2]. En la sección 1.4 se profundiza el tema. 1.4. Multi-Threading El concepto de MT o Multi-Threading y SMT del inglés Simultaneous MultiThreading tiene lugar sólo si existen varias tareas en ejecución, que poseen poca dependencia entre sı́. Durante la ejecución de una tarea se puede llegar a tener un stall que dure una cantidad significativa de ciclos de reloj, pero no tan grande como para que convenga realizar un cambio de contexto3 . Es el caso de una instrucción de multiplicación/división entera, o en punto flotante, que suele tomar varios ciclos de reloj antes de entregar el resultado. La situación anterior da lugar a lo que se llama multi-threading, lo que implica almacenar el contexto de múltiples tareas en el núcleo, duplicando hardware necesario, tal como los registros y el contador de programa. De esta manera un cambio de contexto entre estas dos tareas tiene costo nulo y se vuelve provechoso cambiar de tarea cuando existen estos stalls de corto tiempo. Visto de otra manera, lo que se tiene son varias ventanas de programas en tareas distintas y se toman instrucciones o bien de una o de la otra. Hay dos tipos de multi-threading, el grueso y el fino. En el grueso, se realiza un cambio de contexto cada vez que hay un stall que lo amerite. En el fino, se cambia de tarea en cada ciclo. La desventaja del grueso, es que espera a que exista un stall antes de cambiar de tarea, con lo que se pierde un ciclo. Durante este ciclo de reloj se identifica el stall y un ciclo posterior se procede al cambio de contexto. En el fino se cambia de tarea en cada ciclo, lo cual no impide que puedan ocurrir stalls, pero sı́ ocurren con menor frecuencia. Una desventaja del fino es que la latencia de ejecución de una instrucción en una tarea aumenta: En el ejemplo de la figura 1.3, al ejecutar las tareas A y B en un MT grueso, las seis primeras instrucciones comienzan su ejecución entre los ciclos uno y tres, mientras que si se utiliza un MT fino, las seis primeras instrucciones recién comienzan a ejecutarse entre el primero y el noveno ciclo. En multi-threading se pueden tomar instrucciones únicamente de una sola ventana de programa (o sea de una sola tarea) en cada ciclo de reloj. En cambio en Simultaneous multi-threading, en cada ciclo de reloj pueden ser tomadas instrucciones de distintas ventanas, esto se muestra en la figura 1.3 en comparación con el MT fino y grueso. En general existe una dependencia notablemente menor entre instrucciones de distintas tareas, de esta manera se puede sacar mayor 3 Un cambio de contexto es el proceso que realiza el sistema operativo al cambiar de una tarea a otra, para ello debe guardar el estado de la tarea en ejecución y cargar el estado de la tarea que será ejecutada. Se explica con mayor detalle en la sección 1.6.1. CAPÍTULO 1. INTRODUCCIÓN 11 TAREA B TAREA C TAREA D ISSUE SLOTS ISSUE SLOTS ISSUE SLOTS ISSUE SLOTS TIEMPO TAREA A (a) Threads. MT FINO SMT ISSUE SLOTS ISSUE SLOTS ISSUE SLOTS TIEMPO MT GRUESO (b) Multi-Threading. Figura 1.3: Como cuatro tareas usan los issue slots de un procesador superescalar en diferentes enfoques. La figura 1.3(a) muestra como cada tarea utiliza los issue slots en un procesador superescalara estándar sin soporte para MT. Los ejemplos de la figura 1.3(b) muestran tres opciones de MT. Se observa que las tareas se ejecutan en conjunto. El eje horizontal representa los issue slots disponibles en cada ciclo de reloj, en este caso cuatro. El vertical el tiempo en ciclos de reloj. Coarse-MT cambia la tarea en ejecución al producirse un stall (pierde un ciclo en cada cambio). Fine-MT cambia la tarea en ejecución en cada ciclo (aumenta la latencia de ejecución entre dos instrucciones de una misma tarea). SMT utiliza instrucciones de las cuatro tareas en cada ciclo de reloj, haciendo mejor uso del núcleo (aumenta la latencia como en fine-MT pero en menor meida). CAPÍTULO 1. INTRODUCCIÓN 12 provecho de la habilidad del procesador de ejecutar instrucciones en paralelo. La dependencia entre las instrucciones es el factor que limita la paralelización de instrucciones y con esta técnica se logran grandes mejoras. Entonces se puede decir que el multi-threading es una técnica que aprovecha ILP y TLP en conjunto. Teniendo en cuenta este técnica a los procesadores super-escalares se los puede subclasificar en: Superscalar con multithreading grueso. Superscalar con multithreading fino. Superscalar con multithreading simultáneo. Este último tipo de procesadores son usualmente los más poderosos y más grandes en tamaño. No se utilizarán para el análisis de multi-procesadores en este trabajo por una limitación en la capacidad de las FPGAs. 1.5. Multi-procesadores Sin importar si el núcleo saca provecho de ILP, TLP, MT fino o grueso o SMT, los procesadores múlti-nucleo tienen la ventaja de aprovechar el paralelismo a nivel de tarea que se encuentra en las aplicaciones. Es decir que se aprovecha la TLP independientemente de la arquitectura del núcleo de procesamiento, ya que se tiene más de un núcleo de procesamiento, cada uno con sus registros, contador de programa y corriendo una tarea especı́fica, cada uno con un contexto distinto. Los distintos núcleos, o mejor dicho las tareas que corren en ellos, deben comunicarse entre sı́ y además compartir otros recursos de hardware. Existen distintas arquitecturas para lograr esto, que se tratan en el desarrollo de este trabajo. En la figura 1.4 se contrasta un procesador mono-núcleo con uno de múltiples núcleos, que son conectados a través de un bus simple. El hecho de compartir la memoria trae varias complicaciones. Por ejemplo, resulta necesario una entidad que la administre de forma correcta. Cada núcleo se comunicará con esta entidad antes de poder realizar accesos a memoria. En casos donde haya más de un procesador queriendo hacer uso de los recursos uno de ellos deberá ser pausado hasta que alguno de ellos finalice y la entidad que los administra brinde acceso al siguiente núcleo. Este problema se intensifica a medida que se aumenta el número de núcleos y que los programas tienen mayor cantidad de instrucciones que solicitan acceso a los recursos de hardware. Dado que uno de los recursos más solicitado es el acceso a memoria, la implementación de una memoria cache brinda una técnica para disminuir el tráfico que se genera. El uso de memorias cache trae otros beneficios y complicaciones que se detallaran más adelante en la sección 2.4.3. Por último, se deberá diseñar un controlador de interrupciones distinto al utilizado en procesadores con un único núcleo, porque los procesadores pueden en principio ser interrumpidos por separado. La tarea de este controlador será identificar a que procesador corresponde enviar cada solicitud de interrupción y esto se presentará en la sección 2.5. CAPÍTULO 1. INTRODUCCIÓN 13 Figura 1.4: Procesadores de con un solo núcleo vs procesadores con varios núcleos 1.6. 1.6.1. Sistemas operativos y programación distribuida Sistemas operativos Un sistema informático consiste en uno o mas procesadores, una memoria principal, y dispositivos de entrada/salida. Todos estos dispositivos forman un sistema complejo. Escribir un programa que realice un buen seguimiento de todos estos componentes, y que a su vez los utilice en forma correcta y de una manera eficiente, es un trabajo de extrema dificultad. Por estas razones la tendencia actual es equipar a estos sistemas con una capa de software llamada sistema operativo, cuyo trabajo es administrar todos los dispositivos y brindar a los programas escritos por los usuarios una interfaz simplificada con el hardware. Los sistemas operativos se diseñan prestando especial atención al sistema en el que van a correr, servidores, computadoras de escritorio, sistemas embebidos, etc. Se tienen en cuenta muchos factores para su diseño: flujo de datos de entrada/salida, carga de procesamiento, simetrı́a o asimetrı́a de los distintos núcleos de procesamiento, etc. Un factor que interesa nombrar para este trabajo es que existen sistemas operativos de tiempo real o RTOS(en inglés Real-Time Operating Sistems). Entre ellos se pueden diferenciar dos tipos: Hard RTOS, en los cuales las respuestas deben ocurrir en un momento exacto y los Soft RTOS, donde es aceptable no cumplimiento ocasional de algún plazo. Un ejemplo de los Soft RTOS son los sistemas operativos multimedia, que reproducen audio y/o video, mientras que uno para los hard RTOS es un sistema operativo que corre en un sistema computacional que controla una lı́nea de producción, en donde se interactúa con espacios fı́sicos y los movimientos deben de estar perfectamente coordinados. CAPÍTULO 1. INTRODUCCIÓN 1.6.2. 14 Procesos e hilos Se llama proceso a una tarea que debe ejecutar el sistema operativo. Es una porción de código con un fin en particular. Puede funcionar en conjunto con otros procesos o no. Un proceso puede ser dividido a su vez en hilos, que siguen siendo una porción de código con un fin en particular, pero este conjunto de hilos que forman el proceso tiene la particularidad de compartir el mismo espacio de memoria, a veces llamado también contexto. Estos hilos trabajan entonces en un mismo rango de memoria de programa y de datos. Al intercambiar un proceso que se ejecuta en un núcleo, el contexto también debe cambiar, mientras que al intercambiar entre hilos, no. Un cambio de contexto de memoria suele tener un gran costo computacional, y esta es la principal diferencia entre los hilos y procesos. Un cambio en el hilo que se ejecuta tiene un costo computacional mucho menor al cambio de proceso. Un ejemplo del costo de un cambio de contexto está relacionada con las memorias cache (ver sección 2.4). Como dos procesos suelen trabajar en espacios de memoria distintos, los datos e instrucciones deben ser cacheados nuevamente, generando muchos misses los cuales suelen tener un costo grande de tiempo. El contexto incluye también a los registros del procesador, ellos sı́ deben de ser almacenados antes de cambiar de hilo o proceso. 1.6.3. Planificación de hilos Una de las capacidades de los OS es poder planificar las tareas en ejecución. Hay dos protocolos populares: Planificación Round-Robin: Reparte el tiempo de ejecución equitativamente entre todas las tareas. Primero se corre la primer tarea un perı́odo de tiempo preestablecido, luego la segunda tarea la misma cantidad de tiempo, ası́ hasta llegar a la última y se vuelve a empezar con la primera. Planificación preventiva: A cada tarea se le asigna una prioridad y cada una de ellas tiene un estado: ‘lista’, ‘bloqueada’ o ‘en ejecución’. En la figura 1.5, se ven los estados y transiciones posibles de las tareas. Cuando se decide cambiar la tarea en ejecución se selecciona a la tarea en estado ‘lista’ que tenga mayor prioridad, esta será la tarea que pasará a estar Figura 1.5: Estados y transiciones posibles para los hilos de un sistema operativo. CAPÍTULO 1. INTRODUCCIÓN 15 en el estado de ejecución4 . Para sacar una tarea en ejecución existen dos motivos: que el OS decida que ya estuvo suficiente tiempo en ejecución o que la tarea se bloquee. Una tarea sólo puede pase al estado ‘bloqueada’ si está en estado ‘en ejecución’. Las razones para bloquear una tarea son: • La tarea se bloquea por ‘voluntad’ propia, cuando decide que ya realizó suficiente procesamiento libera el CPU para que otra tarea pueda ejecutarse. • La tarea se bloquea al querer utilizar un recurso de hardware ocupado. El OS es entonces una pieza fundamental en todo sistema computacional que trabaje con múltiples tareas ya que es quien planifica las distintas tareas según prioridad y en función de los recursos disponibles, de esta manera el usuario/programador ocuparse solamente de programar las tareas en sı́ y no en como se debe alternar su ejecución. 1.6.4. Programación distribuida La programación distribuida se basa en dividir las tareas que se deben ejecutar en la mayor cantidad de subtareas independientes. Este paradigma/enfoque de programación es impulsado por la aparición de los multi-procesadores, que como vimos tiene la capacidad de correr tareas en paralelo y cuanto mayor sea la independencia entre tareas mayor será el aumento en el rendimiento. Este tipo de enfoque es el que se le está dando hoy en dı́a a la programación. El principal problema de este estilo de programación es que no ha habido grandes avances en este área y los compiladores no brindan el soporte suficiente para hacer este tipo de programación en forma eficiente. El trabajo de subdividir las tareas y lograr la menor dependencia posible queda pura y exclusivamente a cargo de los programadores. Si se lograra hacer aplicaciones que sean cada vez más distribuidas, la eficiencia de los procesadores aumentarı́a significativamente, simplemente incrementando el número de núcleos en un procesador. Como se explicó anteriormente, en aplicaciones de servidores sı́ es posible distribuir eficientemente la carga de trabajo en múltiples tareas. Los sistemas computacionales orientados a servidores suelen tener un número grande de núcleos comparados con otro tipo de sistemas. En ese caso se habla de sistemas de multi-procesadores de gran escala. La particularidad de estas aplicaciones es que pueden ser divididas en tareas muy poco acopladas, es decir intercambian poca información entre sı́. Existen otro tipo de aplicaciones más acopladas, donde el intercambio de datos es mayor. En este tipo de tareas es donde los programadores tienen un desafı́o mayor, que es el de dividir procesos en hilos independientes. Estas aplicaciones son comunes en sistemas de escritorio y también en sistemas embebidos. Los procesadores multi-núcleo son los más populares en este área, y como diremos en breve las tendencias apunta a seguir aumentando en número los núcleos de un procesador. Los programadores se ven obligados a implementar la programación distribuida en caso de querer implementar aplicaciones con un buen 4 En un procesador con un núcleo de procesamiento y sin multi-threading hay una única tarea en estado ‘en ejecución’, para uno con dos núcleos hay dos, para los procesadores con MT depende de la cantidad de contextos que pueda almacenar cada núcleo. CAPÍTULO 1. INTRODUCCIÓN 16 desempeño, y éste es uno de los principales problemas de la informática hoy en dı́a. 1.7. Consumo y frecuencia de trabajo El consumo de un circuito digital esta dado por [5]: P otdisipada ∝ f × CL × V 2 Donde CL es una capacidad asociada a los transistores, V es la tensión de alimentación y f es la frecuencia de trabajo. La limitación para aumentar la frecuencia de trabajo se basa en la potencia que se puede disipar. Años atrás alcanzaba con reducir el tamaño de los transistores, y de esa manera se reducı́a la capacidad y también se bajaba la tensión de alimentación y eso permitı́a aumentar la frecuencia. Hoy en dı́a existen grandes dificultades para seguir reduciendo el tamaño de los transistores, lo que hace que sea difı́cil aumentar la frecuencia de trabajo sin que aumente el consumo. Entonces resulta más conveniente aumentar el número de núcleos de procesamiento y hacerlos funcionar en conjunto. Otra lı́nea de trabajo que existe hoy en dı́a busca bajar el consumo de los procesadores al mı́nimo. Por ello tampoco siempre se utiliza la máxima frecuencia, dejando el rendimiento de lado y el consumo pasa a ser la principal preocupación. Los procesadores más nuevos son diseñados con la capacidad de cambiar la frecuencia de trabajo según el uso que se le este dando al mismo. Cuando se requiere mayor rendimiento se aumenta la frecuencia de trabajo, mientras que cuando se quiere un ahorro de energı́a se disminuye. Otro método que se utiliza para que el rendimiento y consumo de los procesadores sea parametrizable (controlable), es a través del aprovechamiento de los múltiples núcleos que se tiene en un procesador multi-núcleo. Cuando se quiere un consumo bajo, se apagan los núcleos que no son necesarios, y cuando se quieren un rendimiento alto se los enciende. En ocasiones se prefiere tener un número mayor de núcleos de bajo consumo en vez una menor cantidad de núcleos de alto rendimiento. Otro caso especial que se da es implementar procesadores con núcleos asimétricos, con distinto poder de procesamiento y consumo. Dependiendo de la necesidad se utilizan núcleos de alto, mediano o bajo consumo/rendimiento en conjunto dentro del mismo procesador. La utilización de todas estas técnicas en conjunto (cambio en la frecuencia de trabajo, asimetrı́a y apagado y encendido de los núcleos) dan lugar a procesadores de alto rendimiento y muy bajo consumo al mismo tiempo. 1.8. Conclusiones Históricamente los enfoques apuntaban a mejorar la arquitectura del núcleo de procesamiento: Se aumentó la frecuencia de trabajo, se mejoró la tecnologı́a de fabricación de transistores. Luego se empezó a explotar ILP. Se desarrollo el pipeline, luego procesadores con multiple issue slots. Posteriormente multithreading y SMT. Las tendencias de hoy en dı́a apuntan a mejorar los multiprocesadores y las técnicas de programación distribuida, lo que a la vez permite CAPÍTULO 1. INTRODUCCIÓN 17 aumentar el número de núcleos de un procesador, y ésto último impulsa el desarrollo en mejores técnicas de programación distribuida. Se forma un ciclo que se realimenta, e impulsa a los procesadores multi-núcleo. Por otro lado el diseño del núcleo en sı́ ha alcanzado niveles, en los cuales se vuelve difı́cil seguir avanzando, multiplicar el número de núcleos en vez de rediseñarlos desde el comienzo resulta conveniente. El consumo de los procesadores se vuelve un factor muy importante y fomenta también el diseño enfocado a los multi-procesadores. Y como se dijo antes utilizando este enfoque se puede lograr un bajo consumo y un gran rendimiento, por eso esta tesis se centra en los multi-procesadores. CAPÍTULO 1. INTRODUCCIÓN 18 Capı́tulo 2 Teorı́a y diseño En este capı́tulo se realiza una introducción al procesador plasma junto con la arquitectura de instrucciones que soporta. Se evalúan las distintas posibilidades para el diseño del multi-procesador. El tema principal del diseño es la arquitectura de la memoria y la comunicación entre los procesadores. Otros temas secundarios que se tratan son las interrupciones en multi-procesadores y la dificultad de implementar operaciones atómicas en los mismos. Se fundamentan las decisiones tomadas, las cuales a lo largo del trabajo apuntan a lograr un diseño lo más simple posible. 2.1. Procesador Plasma y arquitectura MIPS El micro-procesador plasma es un procesador diseñado en VHDL1 , sintetizable, RISC de 32 bits, ejecuta todas las instrucciones de modo usuario de la ISA MIPS(TM) salvo las de acceso desalineado a memoria2 . En la página http://opencores.org/project,plasma se puede descargar la última versión de este procesador. Esta implementación incorpora también algunos periféricos. Fue probado en distintas FPGAs de Xilinx y Altera, pero sobretodo en FPGAs Spartan-3E de Xilinx, en el kit de Digilent ”Spartan-3E Starter Kit”. La arquitectura del procesador plasma implementa la unidad de procesamiento en dos o tres etapas de pipeline a elección, una memoria cache opcional de 4kB. La frecuencia de trabajo al implementarlo en FPGAs de Xilinx y Altera ha alcanzado los 25MHz y 50MHz dependiendo del modelo de FPGA. Al presente no fue implementado a nivel de silicio, o al menos no ha sido reportado en la bibliografı́a consultada. 1 VHDL es el acrónimo que representa la combinación de VHSIC y HDL, donde VHSIC es el acrónimo de Very High Speed Integrated Circuit y HDL es a su vez el acrónimo de Hardware Description Language. Es un lenguaje definido por el IEEE (Institute of Electrical and Electronics Engineers) (ANSI/IEEE 1076-1993) usado por ingenieros para describir circuitos digitales 2 La implementación de las instrucciones de acceso desalineado a memoria no fue posible, ya que éstas estaban patentadas cuando fue diseñado 19 CAPÍTULO 2. TEORÍA Y DISEÑO 20 MIPS (en inglés Microprocessor without Interlocked Pipeline Stages) es una ISA (en inglés instructions set architecture) RISC (En inglés reduced instruction set computer ). Están definidas instrucciones para procesadores de 32 y 64 bits, y diferentes versiones del set de instrucciones: MIPS I, MIPS II, MIPS III, MIPS IV y MIPS V. El set de instrucciones no define en ningún momento la arquitectura del hardware del procesador, de hecho existen muchas implementaciones distintas de los mismos, con distintas frecuencias de trabajo, con distinto tamaño de cache, incluso con pipelines más o menos profundos. 2.2. Comunicación entre los procesadores Como se dijo en el capı́tulo 1, surge la necesidad de que los núcleos de los multi-procesadores se comuniquen entre sı́ para intercambiar datos, lo cual no sucede en procesadores de un sólo núcleo o mono-procesadores. La comunicación entre los diferentes procesadores se realiza en general a través de la memoria que comparten. Existen varias formas de compartir la memoria, lo que depende de la forma en la que esta está distribuida. Pueden utilizarse buses o una redes de comunicación, a los cuales se conectan los procesadores y la memoria. En la figura 2.1 muestran las dos arquitecturas clásicas para la interconexión de multi-procesadores: En la arquitectura de shared memory o memoria compartida se observa que existe una única memoria principal que esta conectada directamente al bus. Todos los núcleos acceden a la memoria a través del bus de interconexión. En la arquitectura distributed shared memory o memoria compartida distribuida se observa que la memoria no es única, sino que esta particionada en bloques, uno por cada procesador, y a la vez se encuentra conectada a la red de interconexión. Esto implica que dependiendo de la posición de memoria a la que se quiera acceder un núcleo puede necesitar hacerlo a través del bus o no y esto difiere núcleo a núcleo. Las arquitecturas de memoria compartida tiene la particularidad de que el tiempo de acceso a cualquier posición es independiente del núcleo que quiera accesarla. Esto se denomina: UMA en inglés Uniform Memory Access. En una arquitectura de memoria compartida, los núcleos están fuertemente acoplados, lo que significa que pueden comunicarse entre sı́ a velocidades muy altas, esta es su principal ventaja. Por otro lado la principal desventaja que presenta, son las colisiones que se generan en el bus, por ser un bus compartido entre todos los núcleos. Las colisiones se generan cuando varios núcleos pretendan acceder al bus simultáneamente. Esto no es posible, ya que mientras un núcleo tiene acceso, el resto de ellos deben esperar. Las colisiones se incrementan junto con el aumento de núcleos conectados al bus. Por esta razón el número de núcleos que pueden compartir un bus, tiene un lı́mite y generalmente es inferior al de una arquitectura de memoria compartida distribuida. En las arquitecturas de memoria distribuida compartida los tiempos de acceso a una posición de memoria depende del núcleo que quiera accesarla. Si el núcleo debe acceder a través del bus o red de interconexión este tiempo CAPÍTULO 2. TEORÍA Y DISEÑO 21 (a) Arquitectura Shared Memory. (b) Arquitectura Distributed Shared Memory. Figura 2.1: Arquitectura Shared Memory - Los procesadores tienen todos el mismo tiempo de acceso a cualquier posición de memoria, UMA (Uniform Memory Access). La comunicación puede ser a través de uno o varios buses, o también a través de un switch.Arquitectura Distributed Shared Memory Los procesadores tienen distintos tiempos de acceso a distintas zonas de memoria, dependiendo si deben acceder a través del bus o no, NUMA (Non Uniform Memory Access). La comunicación es normalmente a través de una red de comunicación. es mayor. A este tipo de arquitecturas se las denomina NUMA en inglés Non Uniform Memory Access. Por otra parte en las arquitecturas de memoria compartida distribuida si el sistema computacional corre un OS, en el cual se van reprogramando las procesos que se ejecutan en cada procesador, el planificador de procesos del sistema operativo se vuelve mucho más complejo . Al no ser uniformes los tiempos de acceso, el rendimiento al ejecutar las tareas se ve afectado dependiendo de que núcleo sea el que ejecute el proceso, lo cual implica que el sistema operativo no sólo debe decidir que proceso ejecutar, sino también en cuál de los núcleos. Esta problemática también aparece en los sistemas nombrados en los sistemas asimétricos nombrados en la introducción, donde los núcleos de procesamiento pueden ser distintos entre sı́. Para esos sistemas también debe utilizarse un OS complejo que conozca el tipo de procesador que correrá la tarea. Surge entonces otra clasificación para los procesadores multi-núcleo: SMP (en inglés Symmetric MultiProcessors): Donde los núcleos son inter- CAPÍTULO 2. TEORÍA Y DISEÑO 22 cambiables, es decir, se consiguen los mismos resultados al ejecutar una tarea en uno o en otro núcleo de procesamiento. AMP (en inglés Asymmetric MultiProcessors): Se obtienen distintos resultados dependiendo de la distribución de las tareas en los distintos procesadores. En general los SMP tienen una arquitectura de memoria UMA, donde la memoria es compartida y centralizada, y se conectan a través de un bus. Este tipo de arquitectura se utiliza para número bajo de procesadores ya que al aumentar el número de procesadores aumentan las colisiones en el bus. Tienen la gran ventaja de tener un acoplamiento fuerte entre los procesadores, lo cual significa una gran velocidad de comunicación. Por el contrario las arquitecturas AMP, cuando la asimetrı́a es producto de la distribución de la memoria, suelen tener una arquitectura NUMA, donde la memoria es compartida y distribuida y se interconectan a través de una red de comunicación. Los procesadores están mucho menos acoplados entre sı́, lo cual implica una disminución en la velocidad con la que se pueden comunicar, se utiliza para aplicaciones donde los procesadores no deben compartir una gran cantidad de datos y cuando se necesita aumentar el ancho de banda del sistema hacia componentes externos, o sea discos rı́gidos o la conectividad a una red, ya que cada núcleo puede tener asociado dispositivos de entrada/salida locales. Esta arquitectura se observa en la figura 2.1(b). Este tipo de arquitecturas son las que se utilizan generalmente, pero todo tipo de combinaciones puede ser válida, como por ejemplo tener una única memoria compartida pero dispositivos de entrada/salida locales a los núcleos, etc. En el diseño del plasma multi-núcleo se adopta una arquitectura tipo SMP, en el cual las unidades de procesamiento se conectan a través de un bus, y comparten una memoria principal, que es accedida a través del mismo. Todos los núcleos son iguales y están basados en el procesador plasma. Se adopta esta arquitectura por ser simple y adecuada para las aplicaciones embebidas a las que apunta el procesador. 2.3. Bus de interconexión Una vez decidida la utilización de un bus para la comunicación se debe evaluar las distintas posibilidades para su diseño. Existen diferentes estructuras para los buses de comunicación, entre ellas las más conocidas: Single bus en inglés o bus simple. Switched bus en inglés o bus switcheado. En la figura 2.2 se pueden observar estas dos posibilidades. Si se utiliza un bus simple(figura 2.2(a)), cuando un núcleo accede al bus tiene acceso a todos los periféricos conectados al mismo,ya sea memoria principal, o cualquier otro periférico. Mientras un núcleo tiene el acceso, el resto de los núcleo que quieran tomar el control del mismo deberán esperar que el núcleo en cuestión lo libere. Por el contrario un bus switcheado(figura 2.2(b)) tiene la caracterı́stica de poder brindar acceso a dos o más núcleos a la memoria principal o a los periféricos, siempre y cuando no haya dos de ellos tratando de acceder a una misma entidad. CAPÍTULO 2. TEORÍA Y DISEÑO NÚCLEO 23 NÚCLEO NÚCLEO NÚCLEO MEMORIA PRINCIPAL (a) Bus simple NÚCLEO NÚCLEO NÚCLEO NÚCLEO SWITCHED BUS BANCO 0 BANCO 1 BANCO 0 BANCO 1 MEMORIA PRINCIPAL (b) Bus switcheado Figura 2.2: Bus simple y switcheado. En un bus simple, sólo un núcleo puede tener acceso a memoria. Si dos núcleos intentan acceder a memoria, se produce una colisión. Por el contrario, en un bus switcheado se pueden generar accesos en paralelo entre los núcleo y los distintos bancos de memoria (si dos núcleos intentan acceder al mismo banco de memoria, también se produce una colisión). El resultado es un aumento en el ancho de banda entre los núcleos y la memoria. En la figura se muestra una de las posibles conexiones entre los núcleos y los bancos. En algunos casos la memoria principal se puede dividir en bloques y se implementa un controlador de memoria por cada bloque. De esta manera se puede aumentar el ancho de banda del bus, ya que permite acceso de dos o más núcleos (hasta un máximo que depende de la cantidad de bloques o controladores de memoria) en simultáneo, la restricción para ello es que no quieran acceder al mismo bloque de memoria. Este bus es considerablemente más complejo, sin embargo es una de las maneras más eficientes de aumentar el rendimiento del bus. Al aumentar en gran cantidad los núcleos de procesamiento, el bus de conexión se vuelve un cuello de botella, limitando el rendimiento, en estos casos puede ser de extrema utilidad cambiar el bus a un bus switcheado y dividir la memoria principal en bloques. Al hacer uso de esta técnica se aumenta el ancho de banda entre la memoria y los núcleos y la arquitectura sigue siendo tipo UMA, lo cuál no es un detalle menor. CAPÍTULO 2. TEORÍA Y DISEÑO 24 En el plasma multi-núcleo se opta por utilizar un bus simple, ya que en principio no es necesaria otra estructura más compleja, ya que en este trabajo se pretende implementar procesadores de número relativamente bajo de núcleos(hasta ocho). Un bus simple tiene la ventaja de permitir mensajes de broadcast (multidestino), más adelante en este trabajo se discute el beneficio que brinda utilizar esta estructura al algoritmo de coherencia de cache (que se ve en la sección 3.7.3). Al existir muchas entidades que acceden al bus, en necesario controlar los accesos al mismo. Para nuestro caso particular estas entidades son los distintos núcleos. Idealmente el acceso al bus deberı́a ser repartido equitativamente entre todos los procesadores y ninguno de ellos deberı́a tener prioridad, de esta manera que la ejecución de una tarea sea lo más independiente posible del núcleo en el que se ejecuta. En el plasma multi-núcleo se implementa un árbitro de bus, que será el encargado de la tarea de controlar el acceso al bus y repartirlo lo más equitativamente posible. Su implementación y funcionamiento se describe en la sección 3.5. 2.4. 2.4.1. Arquitectura de la memoria Principio de localidad El principio de localidad surge de observar una caracterı́stica muy usual: los programas tienden a reutilizar datos e instrucciones que han sido usadas recientemente. Una regla empı́rica es que los programas pasan un 90 % del tiempo de ejecución con sólo un 10 % del código [2]. De este modo, a partir del pasado reciente se puede predecir con bastante precisión que instrucciones y datos un programa utilizará en un futuro cercano. El principio de localidad es mucho más fuerte cuando se refiere a instrucciones o código, y no tanto al referirse a datos. Existen dos tipos de localidad: Localidad temporal: Es probable acceder en un futuro cercano nuevamente a posiciones de memoria que han sido accesados recientemente. Localidad espacial: Posiciones de memoria situadas cercanas a otras accedidas recientemente, tienden a ser accesados en un futuro cercano también. 2.4.2. Jerarquı́a de la memoria Al programar siempre se desea disponer de una gran cantidad de memoria de acceso rápido. Existen memorias rápidas, que tienen un costo es elevado, y también existen memorias lentas, que tienen un bajo costo, y por otro lado, el principio de localidad se basa en que el código no es accedido uniformemente, algunas porciones son utilizadas con mayor frecuencia que otras. Lo que se busca al jerarquizar la memoria, es utilizar la memorias rápidas y las de bajo costo en conjunto, procurando que las posiciones de memoria que tengan mayor probabilidad de ser accedidas se encuentren ubicadas en las memorias más rápidas. El objetivo es brindar al usuario del sistema la ilusión de poseer un memoria tan grande como la más económica y tan rápida como la más cara. En la figura 2.3 se pueden ver los distintos niveles tı́picos de una memoria jerarquizada. Para aprovechar el costo y el desempeño de los distintos tipos de CAPÍTULO 2. TEORÍA Y DISEÑO CPU MEMORIA 25 Velocidad Tamaño RÁPIDA PEQUEÑA Costo Ejemplo COSTOSA SRAM DRAM MEMORIA MEMORIA LENTA GRANDE ECONÓMICA DISCO RIGIDO MAGNÉTICO Figura 2.3: La esctructura básica de memoria jerarquizada. Se muestra una memoria dividida en tres niveles, y se caracteriza cada nivel de memoria según tamaño, costo, velocidad de acceso y una tecnologı́a posible para su fabricación. A medida que la memoria se aleja del CPU, la velocidad y el costo bajan, mientras que el tamaño aumenta. Al utilizar una memoria jerarquizada, el programador tiene la ilusión de estar trabajando con una memoria tan grande como la más económica y tan rápida como la más cara. memorias se diseñan niveles de memoria que a medida que la memoria se aleja del CPU, el tamaño aumenta, mientras que la velocidad y el costo disminuyen. Históricamente, jerarquizar la memoria se ha vuelto más importante a medida que el desempeño de los procesadores se incrementó, ya que la velocidad de trabajo de los procesadores aumentó más rápido que la velocidad de acceso a la memoria principal. La cantidad y el tamaño de los niveles de memoria puede variar en cada caso. Una de las estructuras clásicas para estos niveles es: cacheL13 , cacheL2, cacheL3(sólo en procesadores de alto rendimiento), memoria principal, dispositivos de entrada/salida (discos rı́gidos, memory stick, etc); En el plasma multi-núcleo no se tienen discos rı́gidos ni otros dispositivos de entrada/salida del estilo. Se elije implementar sólo dos niveles de memoria: cacheL1 y memoria principal. Por la velocidad de trabajo de la FPGA, del procesador y de la memoria externa, no vale la pena utilizar una jerarquı́a más compleja que esta, y con estos dos niveles es suficiente. 2.4.3. Memoria Cache El CPU es quien realiza pedidos de datos e instrucciones en la memoria y lo hace a la memoria más rápida que a su vez es la más cercana. Como se dijo en la sección anterior esta memoria es también la más pequeña, lo cual la imposibilita a tener almacenados todos los datos e instrucciones existentes. Llamaremos items a cada una de las posiciones de memoria, ya sean datos o instrucciones. 3 El nombre cacheL1 viene del inglés cache level one. CAPÍTULO 2. TEORÍA Y DISEÑO 26 Esta memoria entonces tiene un subconjunto de los items presentes en la memoria principal. Este tipo de memorias son llamadas memorias cache. Cuando el CPU realiza el pedido de algún item y éste no es encontrado en una memoria cache, traspasa el pedido al siguiente nivel de memoria hasta que se alcanza la memoria principal. En memoria principal el item tiene que estar obligatoriamente. Cuando no se encuentra un item en cache se dice que se produjo un miss. Por el contrario cuando si se lo encuentra se habla de un hit. Como se dijo antes, a medida que se baja de nivel en la jerarquı́a de memoria, estas se vuelven más rápidas. Esta es la manera en la cual se aprovecha la localidad temporal de un item. Para aprovechar la localidad espacial de los items se utiliza otra técnica, que es la de trabajar con bloques de memoria de tamaño mayor al de una palabra. Llamamos una palabra a una única instrucción o dato, en el caso del plasma están formadas por 32 bits. Si se produce un miss en una cache, en vez de sólo copiar ese item en la cache, se aprovecha para copiar un bloque de tamaño mayor que contenga el item en esa cache. Al tener que copiar un nuevo bloque en una cache, inevitablemente se tiene que escribir sobre algún otro bloque ya existente. Hay distintas maneras de mapear la memoria principal en cache, mapeo directo, asociativo de N vı́as o full asociativo. En algunos casos dada una dirección de memoria el bloque a desplazar es fijo (es el caso de mapero directo), y en otras formas de mapeo existen distintas posibilidades. En caso de existir más de un posible bloque a reemplazar, el rendimiento depende de cual se elije. Para ello existen distintas polı́ticas de reemplazo. Dependiendo de las polı́ticas puede variar el rendimiento de la cache. Los tamaños de los bloques varı́an según el diseño, y también varı́an según el nivel de la memoria. El tamaño de la cache, el tamaño de los bloques, el mapeo de memoria en cache y la polı́tica de reemplazo afectan el desempeño de la cache. Más adelante en esta sección se explica el mapeo de bloques de memoria en cache y las polı́ticas de reemplazo principales. Por otro lado, se pueden clasificar los accesos a memoria de un CPU en, datos e instrucciones. Como se explicó en la sección 2.4.1 al analizar la ejecución de los programas se observan tendencias de accesos a memoria, que es en lo que luego se basa el principio de localidad. Este principio es distinto para los accesos a datos y para los accesos a instrucciones, y es un factor significativo. Otro factor a tener en cuenta es que al copiar un bloque de instrucciones en una memoria cache, la probabilidad de reutilizarlas es relativamente alta. Por lo tanto se prefiere no tener que reemplazar un bloque de instrucciones recientemente copiado por un bloque de datos. En estas dos razones está basada la idea de dividir la memoria cache en instrucciones y datos. Sin embargo, en ánimo de simplificar el diseño, en este trabajo se utiliza una memoria unificada. Mapeo de bloques y polı́ticas de reemplazo en memorias cache. A modo de ejemplo tomemos una memoria cache con las siguientes caracterı́sticas: Tamaño de cache: 8kB. Tamaño de bloque: 1kB. Ancho de la dirección: 32bits. CAPÍTULO 2. TEORÍA Y DISEÑO 27 Figura 2.4: Distribución de bit de las direcciones de memoria. Los bits menos significativos de la dirección son para direccionar las palabras dentro del bloque, en este caso son diez. Luego existen una cierta cantidad de bits para el mapeo de bloques en cache, en este caso tres, dos, unos o cero, dependiendo de la cantidad de vı́as disponibles en la memoria cache. El resto de los bits de la dirección, los más significativos, junto con los que se utilizan para el mapeo, son para direccionar el bloque en memoria principal. Los bits que le siguen a los utilizados para mapear el bloque en cache son utilizados para el tag, y la cantidad de bits de tag puede variar. De estas condiciones se desprende que la memoria cache tiene una capacidad para albergar ocho bloques de memoria. Existen entonces dos maneras de mapear los bloques de memoria principal en memoria cache: directo o asociativo. En ocasiones se habla también de un tercero que es el full asociativo, pero sigue siendo un caso particular del asociativo. En mapeo directo la posición de ubicación de un nuevo bloque queda totalmente determinada por la dirección en la que se encuentra en memoria principal. Para este ejemplo los últimos 10bits direccionan una palabra dentro de algún bloque. Con los siguientes 22bits se selecciona un bloque en memoria principal, algunos de estos bits también se utilizan para determinar la posición del bloque en la cache, la cantidad de bits utilizados depende del tipo de mapeado utilizado. En la figura 2.4 se pueden ver en detalle los bits utilizados en cada caso y en la figura 2.5 un ejemplo de como los bits de la dirección de memoria influyen en la posición que toma el bloque en una memoria cache. Al copiar ese bloque en una memoria cache resultan los siguientes escenarios posibles: mapeo directo: la posición del bloque queda determinada por los 3bits (12 a 10, ver 2.4). En la figura 2.5 se ve un ejemplo de como diferentes bloques de memoria se ubican en memoria cache. En la figura 2.6(a) se muestra como se organizan los bloques para este caso. asociativa de dos vı́as: la posición del bloque queda determinada en primer lugar por los 2bits (11 a 10, ver 2.4), luego puede ubicarse en cualquiera de los dos lugares (dos vı́as, ver figura 2.6(b)) disponibles. La vı́a en la que queda ubicado queda determinada por la polı́tica de reemplazo. asociativa de cuatro vı́as: la posición del bloque queda determinada en primer lugar por un sólo bit(bit10, ver 2.4, luego puede ubicarse en cualquiera de los cuatro lugares (cuatro vı́as, ver figura 2.6(c)) disponibles. La vı́a en la que queda ubicado queda determinada por por la polı́tica de reemplazo. CAPÍTULO 2. TEORÍA Y DISEÑO 28 Figura 2.5: Mapeo directo asociativa de ocho vı́as, este caso es que a también se lo llama full asociativa, donde el bloque puede ser ubicado en cualquiera de las ocho (ocho vı́as, ver figura 2.6(d)) posiciones de la cache, lo cual quedará determinado por la polı́tica de reemplazo. Para los casos donde existe más de una posición de memoria en donde ubicar el bloque, esta posición queda determinada por las polı́ticas de reemplazo: Aleatoria: El bloque es reemplazado de forma aleatoria y en general es la alternativa de menor eficiencia. FIFO: Se usa un algoritmo First In First Out FIFO (primero en entrar es el primero en salir) para determinar qué bloque debe abandonar la caché. Este algoritmo generalmente es poco eficiente. Menos recientemente usado (LRU): El bloque que se copia en cache desplaza al bloque que ha pasado más tiempo en calle sin ser utilizado. Menos frecuencias usadas (LFU): Se sustituye el bloque que ha tenido menor frecuencia de acceso. En el plasma multi-núcleo se utiliza una cache de mapeo directo y con un tamaño de bloque de una palabra. Que el bloque tenga el tamaño de una única palabra es malo en cuanto al rendimiento de la cache, pero presenta una gran ventaja en cuanto a la simplicidad del diseño de la cache. Otra consecuencia de esto es que se deja de aprovechar la localidad espacial, y sólo se hace uso de la localidad temporal. Por otro lado se utiliza una polı́tica de escritura en cache y memoria principal en cada escritura de datos. En la sección 2.4.4 se presentan las distintas polı́ticas de escritura que existen. CAPÍTULO 2. TEORÍA Y DISEÑO (a) Mapeo directo. 29 (b) Mapeo asociativo de dos vı́as. (c) Mapeo asociativo de cuatro vı́as. (d) Mapeo asociativo de ocho vı́as o full asociativa. Figura 2.6: Alternativas de mapeos. En el eje vertical se ven las distintas entradas de la cache que quedan determinadas por la dirección del bloque. En el horizontal se muestran las vı́as disponibles para cada caso. En la memoria de mapeo directo (2.6(a)) 3 bits seleccionan la entrada y sólo existe una único bloque a reemplazar. En asositiva de dos vı́as (2.6(b)) 2 bits seleccionan la entrada y existen dos bloques posibles para reemplazar. En asositiva de cuatro vı́as (2.6(c)) 1 bits seleccionan la entrada y existen cuatro bloques posibles para reemplazar. En full asositiva (2.6(d)) la entrada es única y cualquier bloque puede ser reemplazado para albergar al nuevo. En los casos que hay más de una posibilidad, las polı́ticas de reemplazo se utilizan para decidir cual será reemplazado. CAPÍTULO 2. TEORÍA Y DISEÑO 2.4.4. 30 Arquitectura de la memoria en procesadores multinúcleo Importancia de la memoria cache Como se explico en la sección 2.4.2 jerarquizar la memoria tiene la importante ventaja de brindar la ilusión de una memoria de gran tamaño que a la vez es rápida y barata. Esto lo logra almacenando datos e instrucciones en memorias cache. En procesadores mono-núcleos esa es la única función de este tipo de memoria, pero para procesadores multi-núcleo que utilizan SMP, donde los núcleos son interconectados por un bus, es indispensable implementar bloques de memoria cache para evitar el elevado número de colisiones de acceso al bus que se producirı́an sin ellas. Para explicar esto se supone un sistema de dos núcleos, en dos versiones: una que posee solamente memoria principal y otro que además tiene al menos un nivel de memoria cache. En el caso donde los dos núcleos ejecutan un programa que está en memoria principal, ambos núcleos intentarán acceder a la primer posición de memoria del programa indicada por el contador de programa. Es imposible que los dos núcleos accedan simultáneamente a memoria, entonces uno de ellos será pausado inevitablemente, mientras que el segundo accederá a la primer posición de memoria para buscar la instrucción a ejecutar. Una vez que el primer núcleo finalice con la utilización del bus, recién en ese momento se cederá acceso al segundo núcleo, lo cual sucederá en el siguiente ciclo de reloj. Pero en este nuevo ciclo, el primer procesador ya habrá finalizado con la ejecución de la instrucción anterior y deseará acceder a la segunda instrucción. Ahora la situación será la inversa: el segundo procesador será quien tendrá acceso al bus y a la memoria, y el primero será quien deberá esperar. Esto sucederá constantemente, y básicamente el acceso al bus limitará a los procesadores. Independientemente de la cantidad de núcleos que se tengan, sólo se podrá acceder a una instrucción del código por ciclo de reloj. Entonces, cada uno de los dos núcleos se encontrará pausado uno de cada dos ciclos de reloj, de tener cuatro núcleos cada uno se encontrará pausado tres de cada cuatro ciclos, etc. En el caso de no ser una instrucción lo que quiere acceder uno de los núcleos, es exactamente lo mismo, ya que visto desde afuera, el núcleo pide acceso a una posición de memoria, y del núcleo hacia afuera, no interesa si pretende leerla, escribirla, si es dato o instrucción. Para el caso en el que se tiene una memoria cache, la situación cambia completamente. En principio se consideran programas sin instrucciones de acceso a memoria (en la ISA MIPS estas serı́an por ejemplo LW, SW, LB, SB, etc.), más adelante se analizarán los casos en los que se accede a datos. Cuando los dos procesadores ejecuten un programa que esté en memoria principal, ambos núcleos intentarán acceder a la primer posición de memoria del código. El primero recibirá acceso al bus y a la memoria, mientras que el segundo será pausado hasta que el primero termine con la utilización del bus. Luego el segundo tendrá acceso a bus, pero a diferencia del caso anterior el primer núcleo ya no solicita acceso al bus, debido a que ya posee también la siguiente posición de memoria que necesita, porque al copiar la primera, copió todo un bloque de memoria que incluı́a al resto de las posiciones de memoria cercanas. De esa manera se aprovecha la localidad espacial del código. La localidad temporal del código se aprovecha en los casos que existen bucles CAPÍTULO 2. TEORÍA Y DISEÑO 31 de ejecución, o llamadas reiteradas a una misma función o sub-rutina, donde se ejecutan una y otra vez las mismas instrucciones. En ocasiones particulares, para ciertos tamaños bloque, ciertos tamaños de cache y accesos a posiciones de memoria de cierta distancia especı́fica, se pueden producir misses consecutivos, aumentando mucho los tiempos de acceso. Se concluye entonces que una memoria cache es necesaria para procesadores multi-núcleo, y que puede ser una memoria unificada de instrucciones o datos, pero en caso de estar dividida la de instrucciones es obligatoria, mientras que la de datos no, ya que el porcentaje de accesos a memoria para buscar instrucciones es siempre mucho mayor al de los accesos a datos. Polı́ticas de escritura En general los datos no sólo son leı́dos, sino que en ocasiones también se modifican y se vuelven a escribir en memoria. Al tener en cuenta las escrituras de los datos otros aspectos entran en juego, al diseñar las memorias cache. Por ejemplo, al escribir un dato, existen varias posibilidades: Escribir el dato en memoria cache únicamente. Escribir el dato en memoria principal y cache. Escribir el dato únicamente en memoria principal. La forma en la que se escribe la cache y la memoria principal toman los nombres de polı́ticas de escritura en cache y polı́ticas de escritura en memoria principal. Las polı́ticas de escritura en memoria principal son: Write-Through: La memoria principal se escribe en cada instrucción de escritura. Write-Back : La memoria principal se escribe cuando un bloque que ha sido modificado es desalojado de la memoria cache. Las polı́ticas de escritura en memoria cache son: Write-Allocate: La memoria cache se escribe en las instrucciones de escritura. Write-No-Allocate: La memoria cache no se escribe en las instrucciones de escritura. la polı́tica de escritura es entonces una combinación de las polı́ticas de escritura de memoria cache y de memoria principal: Write-Through Write-Allocate: Poco común, pero muy simple. Write-Through Write-No-Allocate: Poco común, también simple como la anterior. Write-Back Write-Allocate: La más utilizada. Write-Back Write-No-Allocate: Sin sentido, ya que el dato no se escribe en ninguna memoria y se perderı́a. CAPÍTULO 2. TEORÍA Y DISEÑO 32 En general la polı́tica Write-Through es poco utilizada, ya que se debe acceder a memoria en cada escritura, sin embargo su implementación es mucho más simple. En esta primera versión del plasma multi-núcleo se utiliza una polı́tica Write-Through Write-Allocate, que es poco común y algo ineficiente en algunos casos, pero que en este caso particular, al tener un bus simple y un bloque del tamaño de una palabra se vuelve aceptable y por sobre todas las cosas mucho más simple para implementar. En el capı́tulo de implementación (sección 3) se explicará con mayor detalle las razones que fundamentaron su adopción. 2.4.5. Protocolos y algoritmos de coherencia de cache Como se explicó anteriormente, no tiene sentido tener SMP si no se implementa una memoria cache. En esta subsección se retoma el análisis sobre las memorias cache de datos. La diferencia que estas tienen frente a las caches de instrucciones es que las posiciones de memoria que contienen datos, no sólo se leen, sino que también se escriben, como se mencionó anteriormente. El problema aparece cuando dos caches distintas tienen un mismo dato. Si el dato solo es leı́do no hay ningún problema, como sucede con las instrucciones, pero si el dato se escribe sı́. Como se dijo, el dato se puede escribir en cache, en memoria principal o en ambas. En cualquiera de estos casos, el dato que queda albergado en la cache del núcleo que no realiza la escritura queda desactualizado. Entonces, si éste procesador realiza una lectura del dato, leerá un valor incorrecto. La ‘coherencia de cache’se refiere justamente a este problema. En el cuadro 2.1 se presenta un ejemplo con dos memorias cache, y con una polı́tica de escritura en memoria write-through write-allocate Se ve que al final de las cuatro acciones que realizan los procesadores los datos en las dos cache difieren y en este caso el CPU1 realiza una lectura incorrecta del dato. En estos casos se dice que la cache no es coherente, lo cual no debe suceder, porque los datos leı́dos deben ser siempre correctos. Que una memoria cache sea coherente implica que los datos que están almacenados en ella son válidos y que al realizar una lectura siempre se lee un dato correcto, es decir se lee el último valor escrito en esa posición de memoria. Para lograr la coherencia existen diferentes protocolos, que se pueden clasificar en dos grupos: Protocolos basados en un directorio: Existe un directorio que almacena los estados de los bloques de memorias compartidos entre todas las caches Acción Estado inicial CPU0 lee el dato CPU1 lee el dato CPU0 escribe el dato CPU1 lee el dato Consecuencia Valor en Cache0 Valor en Cache1 Miss en Cache0 Miss en Cache1 Write en Cache0 y en memoria principal Lee 0 cuando el data real es 1 0 0 1 0 0 Valor real del dato 0 0 0 1 1 0 1 Cuadro 2.1: Ejemplo de memorias cache no coherentes CAPÍTULO 2. TEORÍA Y DISEÑO 33 del sistema. Las caches se comunican con el directorio al solicitar un dato, el directorio es quien sabe si la cache del CPU que solicita el dato tiene un dato actualizado o no. Es más popular en multi-procesadores de gran escala. Protocolos Snooping: No es centralizado. Cada cache guarda una copia del bloque y una copia del estado del mismo. Cada memoria cache escucha a las señales de snooping de otras memorias caches. Las señales de snooping dan información a cada meoria cache acerca de los cambios en los estados de los bloques. El snooping requiere un mecanismo de broadcast, que puede ser implementado con un bus simple. En casos donde se tiene un bus switcheado no es posible realizar broadcast. Muchas veces se utiliza un bus adicional, exclusivamente para implementar los algoritmos de coherencia. Existen varios algoritmos para mantener la coherencia entre ellos los más populares son: MSI, MESI, MOSI, MOESI [2] [7]. Este tipo de protocolo está presente en la mayoria de los procesadores actuales. En el plasma multi-núcleo se utiliza un protocolo de snooping, por ser más simple y adecuado para la arquitectura. Independientemente del protocolo que se utilice para mantener la coherencia de cache también se debe de tener en cuenta el algoritmo que se utiliza. En el protocolo de snooping, como se dijo antes, se debe de enviar información sobre las acciones que se realizan sobre los bloques a cada una de las caches. Para ellos se necesitan señales extras en el bus local del procesador. En el capı́tulo 3 se detalla el protocolo y el algoritmo implementado y que señales se utilizan para lograr la coherencia. 2.5. Manejo de interrupciones Otro factor a tener en cuenta al pasar de sistemas de un sólo núcleo a SMP, es el manejo de interrupciones. En procesadores mono-núcleo la interrupción de un núcleo depende de los siguientes elementos: El estado de cada una de las fuentes de interrupción. La máscara de interrupciones. Si las mismas están habilitadas o no. Básicamente se realiza una operación and bit a bit entre la máscara y el estado de interrupciones, y si este resultado es distinto de cero y las mismas están habilitadas, el procesador es derivado a alguna tarea que sea capaz de manejar la excepción. En un procesador multi-núcleo las posibilidades aumentan ya que existen varios núcleos que pueden ser interrumpidos. Cada uno de los núcleos a su vez puede habilitar y deshabiltar las interrupciones por separado. Algunos sistemas implementan también las interrupciones inter-procesadores. Esto no será incluido en este trabajo de tesis. Para el plasma multi-núcleo se quiere un manejador de interrupciones que distribuya los pedidos de interrupciones en forma pareja entre todos los procesadores, de modo tal que no sea siempre el mismo núcleo el encargado de manejarlas, y ası́ lograr que todos lo núcleos se comporten igual. No se tiene en CAPÍTULO 2. TEORÍA Y DISEÑO 34 cuenta si el procesador tiene bloqueadas o no las interrupciones, esto podrı́a implementarse en alguna versión futura del plasma de modo de mejorar la latencia que tiene una interrupción en ser procesada. La latencia de la interrupción es el tiempo que tarda desde que da una excepción hasta que se ejecuta la tarea indicada. 2.6. Operaciones atómicas Una operación atómica hace referencia a la lectura-modificación-escritura de una posición de memoria. En ningún caso es posible resolverlo en una única instrucción, por lo tanto se implementa mediante un conjunto de instrucciones. Este conjunto de instrucciones debe de ser ejecutado en forma secuencial y sin ser interrumpidas. El efecto buscado con estas instrucciones puede ser afectado si esto no se cumple. Se utiliza la exclusión mutua, que es un mecanismo para evitar que dos o mas procesadores entren en una sección crı́tica al mismo tiempo. La exclusión mutua se logra a su vez utilizando secciones crı́ticas. Una sección crı́tica es una sección de código en la cual el procesador tiene acceso exclusivo a los recursos compartidos y bajo ningún punto de vista debe ser interrumpido. Si proceso se encuentra en una sección crı́tica y un segundo proceso quiere entrar a una sección crı́tica, este último proceso es bloqueado hasta que el primero sale de la sección crı́tica, ver figura 2.7. Ası́ se evita que haya dos procesos utilizando recursos compartidos. Se dice entonces que los recursos compartidos son protegidos mediante secciones crı́ticas, y ası́ se logra la correcta utilización de los recursos. Lograr la exclusión mutua entre procesos es simple en procesadores mononúcleo. Basta con deshabilitar las interrupciones. De esta manera el proceso que corre en el CPU en ese momento no puede ser interrumpido, por lo tanto tampoco puede ser reemplazado, y al no existir otro procesador corriendo otro proceso en paralelo, éste es el único proceso que corre y por lo tanto tiene acceso exclusivo a los recursos. Para salir de una sección crı́tica basta con restaurar el estado anterior de las interrupciones. En procesadores multi-núcleo la implementación no es trivial. Para entrar en una sección crı́tica, un proceso debe asegurarse que ningún otro proceso se encuentra en una sección crı́tica. Lo primero que hace un proceso que quiere entrar en una sección crı́tica es deshabilitar las interrupciones del núcleo en el que corre de manera que evite ser reemplazado por otro proceso. Luego hay que asegurar que ningún otro proceso, que esté corriendo en otro núcleo se encuentre en una sección crı́tica. Si otro proceso se encuentra en una sección crı́tica, el nuevo proceso que quiere entrar se bloquea. La manera que se tiene de hacer esto es utilizando los llamados locks, que es algún tipo de registro o variable compartida entre los procesadores. Se dice que un proceso tiene un lock cuando es el que tiene el permiso para acceder a los recursos, es decir se encuentra en una sección crı́tica. En este caso se puede hablar de proceso o núcleo indistintamente, ya que al deshabilitar las interrupciones el proceso queda ligado al núcleo. Al salir de una sección crı́tica el proceso libera el lock. La manera de implementar un lock es guardando su estado en un registro. Un ejemplo serı́a cuando ningún proceso tiene el lock, el registro tiene el valor numérico menos uno almacenado. Cuando algún proceso se adueña del lock el registro guarda el número del CPU que corre el proceso que tiene el lock. De esta manera un proceso antes de entrar en una sección critica se asegura que el CAPÍTULO 2. TEORÍA Y DISEÑO 35 TIEMPO PROCESO A SECCIÓN CRÍTICA de A B está bloqueada PROCESO B T0 T1 SECCIÓN CRÍTICA de B T2 T3 T0: A entra a una sección critica. T1: B intenta entrar a una sección crítica, pero no puede y se bloquea. T2: A deja la sección crítica y B entra a una sección crítica. T3: B deja la sección crítica. Figura 2.7: Exclusión mutua, evita que dos procesos ingresen en una sección crı́tica simultáneamente. registro del lock este en menos uno. Le asigna al registro el valor del CPU donde esta corriendo y entra en la sección crı́tica. Cuando algún otro proceso quiera entrar en una sección crı́tica y revise el valor del registro, el mismo no será cero entonces no podrá entrar en la sección crı́tica y deberá volver a intentar nuevamente hasta que se libere el lock (o lo que es lo mismo que el registro del lock valga cero) (ver figura 2.8). Este proceso se lo llama spinLock [10]. El proceso de obtención de un lock para entrar en una sección crı́tica se muestra en la figura 2.8. Se ve que antes de empezar a competir por el lock, los procesos guardan el estado de las interrupciones (habilitado/deshabilitado) del CPU donde están corriendo, para poder restaurarlos al finalizar con la sección crı́tica. Una caracterı́stica que deben tener las secciones crı́ticas protegidas por locks es ser lo más acotadas que sea posible, ya que al entrar a una sección crı́tica bloquean no sólo a otros procesos que quieran entrar en estas secciones, sino también al CPU en el que corren, ya que no es posible cambiar interrumpir al CPU para cambiar el proceso que esta corriendo en él. Este tipo de implementación lleva el nombre de mutual exclusion with busy waiting [10]. Existen otros artilugios utilizados por los sistemas operativos para proteger recursos compartidos y que a la vez pueden ser accedidos por un CPU por un largo perı́odo sin bloquear a los otros CPUs, como son lo Mutex, Semaphores, entre otros [10]. Presentado de esta manera el problema, parecerı́a que la exclusión mutua funciona sin inconvenientes, pero mediante el ejemplo del cuadro 2.2 se demuestra que no. En el ejemplo se toma un sistema con dos núcleos. Al final del quinto ciclo los dos procesos (el que corre en el CPU0 y el que corre en el CPU1) se encuentran simultáneamente en secciones crı́ticas, lo cual es un estado indeseado, ya que no cumple con la exclusión mutua. El problema al querer implementar un lock se muestra en la figura 2.9: para querer implementar un lock es necesario realizar operaciones atómicas, y para realizar operaciones atómicas es necesario tener un lock. CAPÍTULO 2. TEORÍA Y DISEÑO 36 Figura 2.8: Exclusión mutua a través de la utilización de un spinLock. El diagrama en bloques de arriba muestra el proceso para obtener el lock, entrar a una sección crı́tica, hacer uso de algún recurso compartido, salir de la sección crı́tica y liberar el lock. Si un proceso ya posee el lock es imposible que otro lo obtenga al mismo tiempo. El proceso que quiera obtenerlo quedará en un bucle, hasta que el lock sea liberado, y pueda obtenerlo. El lock es quien permite a los procesos acceder o no a las secciones crı́ticas. CAPÍTULO 2. TEORÍA Y DISEÑO 37 Ciclo CPU0 CPU1 0 Quiere entrar a una sección crı́tica Lee registro del lock Compara que sea -1 Escribe el resgistro del lock con 0 Entra en la sección crı́tica Quiere entrar a una sección crı́tica Sin acceso al bus Lee registro del lock Compara que sea -1 1 2 3 4 5 Escribe el resgistro del lock con 1 Entra en la sección crı́tica Registro del lock −1 −1 −1 −1 → 0 0→1 1 Cuadro 2.2: Ejemplo con dos núcleos de la problemática que aparece al querer implementar un lock en forma directa. Las siguientes son las operaciones atómicas que deben realizarse para obtener un lock: Lectura del registro de estado del lock. Evaluar si el valor corresponde al de lock liberado. Si se encuentra liberado guardar un nuevo estado en el mismo, de lo contrario no hacer nada. En lenguaje assembler de MIPS I, esto lleva al menos tres instrucciones y resulta imposible asegurar su atomicidad. Si se considera que $2 apunta al registro del lock y que $3 posee el número del CPU que ejecuta el proceso, entonces las instrucciones serı́an: 1 2 3 4 5 $spinLock : lw $1 , 0($2) bnez $ 1 , $ s p i n L o c k nop #Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o s e toma o no . sw $3 , 0($2) O con una versión equivalente: Si se agrega la condición de que $1 sea distinto de cero se puede reescribir el código de la siguiente manera ocupe tres instrucciones: 1 2 3 4 5 li $1 , 0 $spinLock : bnez $ 1 , $ s p i n L o c k lw $1 , 0($2) #Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o s e toma o no . sw $3 , 0($2) Durante el perı́odo de tiempo que existe entre las instrucciones dos y cinco del primer caso, o tres y cinco del segundo ningún otro núcleo deberı́a de realizar una lectura o escritura del registro del lock (apuntado por 0($2)). Al resultar imposible comprobar esto se puede caer en el problema mostrado en el cuadro 2.2. Para evitar este problema, en MIPS II se han definido dos nuevas instrucciones especialmente diseñadas para realizar éstas operaciones atómicamente: CAPÍTULO 2. TEORÍA Y DISEÑO 38 Figura 2.9: Problemática de las operaciones atómicas. LL - Load Link : SC - Store Conditional : El código cambiarı́a por: 1 2 3 4 5 $spinLock : ll $1 , 0($2) bnez $ 1 , $ s p i n L o c k nop #Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o s e toma o no . sc $3 , 0($2) Y se agregarı́an las siguiente instrucciones para comprobar que las operaciones LL y SC sobre la posición de memoria apuntada por 0($2) hayan sido atómicas, y en caso de no ser atómicas se vuelve a intentar: 6 7 bez $3 , $spinLock #S i l a s i n s t r u c c i o n e s no f u e r o n a t o m i c a s e n t r e l l y s c e n t o n c e s $3 e s i g u a l a 0 nop #Se e j e c u t a siempre , s i n i m p o r t a r s i e l s a l t o s e toma o no . Como se comenta en el código si las instrucciones entre LL y SC no fueron atómicas, entonces $3 termina con valor cero después del SC, sino con valor uno. Al ejecutar LL, un flag es seteado y se mantiene ası́ a menos que: Otro CPU realice algún Store en la misma dirección fı́sica de memoria utilizada en el LL. Que ocurra una excepción entre LL y SC en el CPU que las está ejecutando. El funcionamiento de estas instrucciones se define en la ISA de MIPS [6]. La ISA es más especı́fica en cuanto a la explicación y pone como condición que CAPÍTULO 2. TEORÍA Y DISEÑO 39 la dirección debe ser cacheable, y los stores pueden ser realizados por otras entidades (no sólo CPUs). Los núcleos del plasma no soportan las instrucciones de MIPS II por eso es que se debe buscar otra solución a este problema. Algunos autores proponen soluciones por software a este problema como en [10]. Éstas soluciones son poco eficientes y en la bibliografı́a sólo se muestran ejemplos funcionales para dos núcleos. En el apéndice A se muestra el código para una implementación en software de spinLock. En el procesador plasma multi-núcleo no se utilizarán las soluciones de software, ya que no se han estudiado en profundidad y no se puede asegurar nada acerca de su rendimiento ni que funcionen sin caer en un deadlock, término utilizado para identificar una situación en la cual dos CPU (o más) están esperándose mutuamente a que terminen alguna operación, y ninguno puede hacerlo. En cambio se brinda soporte extra por hardware para las operaciones atómicas, la implementación es muy simple y se ve en la sección 3. Vale aclarar que en caso de que no se quiera utilizar simplemente se puede implementar una de las soluciones propuestas por software sin ningún problema. Otro soporte que debe brindar el hardware para sistemas multi-núcleo es el indice del CPU que corre el proceso. En el plasma multi-núcleo se implementa en un registro fijo por núcleo que almacena este valor. 2.7. Caracterización a priori del procesador Para finalizar este capı́tulo se presenta un resumen de las decisiones de diseño tomadas. En particular implementaremos el diseño en un kit Nexys2 de Digilent que posee una Spartan-3E-1200, el diseño cumplirá con las siguientes caracterı́sticas: Multi-procesador genérico: Se pueden implementar N núcleos. Procesadores: • Escalar: O sea un sólo Issue Slot. • Pipeline de 2 o 3 etapas a elección. Frecuencia de trabajo: 25M Hz. Cache: • 512kB de tamaño. • Tamaño de bloque de 32bits (una palabra). • Compartida para instrucciones y datos. • Polı́ticas de escritura Write-Through Write-Allocate. • Mapeo directo. • Protocolo de snooping para la coherencia de cache. Memoria RAM: 16M B de memoria para instrucciones y datos. Memoria interna en cada procesador: 1KB Conexión serie UART. CAPÍTULO 2. TEORÍA Y DISEÑO 40 GPIO: • 1 puerto de salida de 32bits. • 1 puerto de entrada de 32bits. Interrupciones: Las interrupciones son emascarables globalmente, y se pueden habilitar en cada núcleo por separado. Las fuentes de interrupciones son: • Interrupción por nivel en uno de los pines del puerto de entrada. • Temporizador: cada 10ms aproximadamente. • UART: Nuevos datos recibidos. • UART: Disponible para el envı́o de datos. 1 contador de 32 bits. 2 registros de propósito general, para comunicación de los procesadores. Soporte adicional de hardware: • Para identificación del número núcleo. • Para implementación de locks. Capı́tulo 3 Implementación y resultados obtenidos En este capı́tulo se hace primero una breve descripción de las herramientas utilizadas y luego se detalla la forma en la que se implementó cada bloque del plasma multi-núcleo. Se presenta luego la implementación de la capa superior y se avanza hacia las capas inferiores. De esta manera queda ordenado el trabajo y se comprende claramente la arquitectura diseñada. 3.1. Herramientas utilizadas El diseño total del sistema se realizó en un entorno de un sistema operativo linux, que es un sistema operativo libre y gratuito. Las principales herramientas utilizadas para la implementación del procesador fueron las siguientes: Eclipse+Sigasi: Eclipse es una interfaz de desarrollo genérica. El plugin Sigasi brinda soporte para el desarrollo de código de VHDL y Verilog. Kate: Es un editor de texto que se utilizó en algunos casos que resultaba más cómodo que el eclipse. GHDL: Es un compilador y simulador del lenguaje VHDL. gtkwave: Es un visualizador de señales, que permite ver resultado de las simulaciones realizadas mediante GHDL. IseWebPack: Es una herramienta de Xilinx gratuita que se utilizó para sintetizar los circuitos y generar los archivos necesarios para programar la FPGA. La herramienta brinda muchas mas prestaciones, como compilar y simular VHDL, visualizador de señales y interfaz para programar las FPGAs, pero sólo se utilizó para la sı́ntesis de los circuitos. DigilentAdept: Es un set de herramientas utilizadas para comunicarse con los kits de desarrollo de Digilent. Brinda la interfaz para programar las FPGAs de estos kits. 41 CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 42 Vale destacar que todas las herramientas y software utilizada para su realización son de carácter libre o bien gratuito. 3.2. Estructura del procesador multi-núcleo La estructura general del procesador se ve en la figura 3.1. Se ven cuatro capas principales: 1. La capa superior, que incluye el procesador plasma multi-core y el controlador de memoria. Este último es dependiente del kit en el que se implementa. 2. La arquitectura interna del procesador multi-núcleo, que incluye entre otras entidades al árbitro del bus y a los núcleos. 3. La arquitectura interna del núcleo, que incluye a los núcleos de procesamiento del plasma original y también toda la arquitectura de la memoria cache. 4. La capa inferior para nosotros serán las unidades de procesamiento del plasma original. 3.3. Controlador de memoria Como se dijo anteriormente, se comienza por presentar el controlador de memoria que es la capa superior del sistema. El controlador de memoria funciona como interfaz entre el plasma y la memoria externa (ver figura 3.2). La memoria externa es fabricada por Micron y el modelo es MT45W8MW16BGX. Su función es adaptar las diferencias que pueda haber en ancho de palabras, ancho de direcciones, tiempos de acceso, etc. El plasma posee dos puertos uno de entrada de datos (dataR) y otro de salida datos (dataW), cada uno con un ancho de 32bits y está diseñado para una memoria asincrónica. Por otro lado la memoria posee un único puerto de entrada/salida de 16bits. 3.3.1. Descripción El controlador de memoria es una simple máquina de estados, que arranca en un estado de inicialización. Como la memoria viene configurada por defecto para operar en modo asincrónico, lo único que se hace en este estado es esperar una cierta cantidad de ciclos hasta que la memoria se encuentre operativa. El número de ciclos que debe esperar es parametrizable, de esta manera se puede cambiar fácilmente la frecuencia de trabajo del mismo. Una vez finalizada la inicialización se pasa a un estado ocioso a la espera de alguna solicitud de escritura o lectura. Por cada solicitud de escritura/lectura del CPU se realizan dos escrituras/lecturas en memoria, esto se debe a la diferencia del ancho de palabra, la del CPU es de 32bits mientras que la memoria 16bits. Nuevamente los ciclos de espera para una lectura o escritura son parametrizables. El controlador de memoria se puede hacer funcionar a una frecuencia igual o mayor a la del CPU, y de esta manera se podrı́an obtener pequeñas mejoras en CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 43 Figura 3.1: Estructura jerárquica del plasma multi-núcleo. los tiempos de acceso. En esta implementación y haciendo trabajar al controlador a 25M Hz, igual que el procesador, se obtiene una lectura de 32bits en ocho ciclos y una escritura de 32bits en diez ciclos. Esto nos brinda tiempos de escritura de 400ns y en caso de haber un miss en cache la penalidad será de 320ns. En la figura 3.3 se pueden ver simulaciones de las operaciones de escritura y lectura. 3.3.2. Puertos de la entidad clk i (entrada): Para sincronizar. reset (entrada)i: Para reiniciar la unidad. Interfaz plasma multi-núcleo: • Salida 30bits de dirección: El procesador direcciona con 30 bits palabras de 32 bits (o 4 bytes). • Salida 32bits de datos a escribir: Los datos que el procesador escribe en memoria. • Entrada 32bits de datos a leer: Los datos que el procesador lee desde la memoria. 32 bits-dataW 32 bits-dataR 4 bits-byteWE busy_o request_i reset_i clk_i 44 23bits-dirección 16 bits-dataIO memCE_o memWE_o memLB_o memUB_o memOE_o memClk_o memAdV_o memCRE_o memWait_i Memoria ram externa Plasma multi-núcleo 22bits-dirección Controlador de memoria CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS Figura 3.2: Controlador de memoria para el plasma multi núcleo. El mismo depende del kit en el que se implementa. Las señales en van del controlador a la memoria externa y las verdes del controlador al plasma multi-núcleo • Salida 4bits de habilitación por byte para la escritura: • Salida solicitud de memoria: Indica cuando las señales son válidas, es decir cuando comenzar una escritura o lectura de memoria. • Entrada memoria ocupada: Esta señal indica al plasma cuando se ha finalizado el acceso a memoria solicitado. Si la solicitud es de escritura entonces cuando esta señal lo indique podrán ser leı́dos los datos y no antes. Un cero lógico indica que los datos son válidos, un uno lógico que el controlador sigue trabajando y por lo tanto que los datos no son válidos. Interfaz de la memoria: • Entrada 23 bits de dirección: Para direccionar las 223 posiciones de memoria de 16 bits, el procesador direcciona con 22 bits efectivos palabras de 32 bits (Memoria: 2(23bitsdireccion) × 16bits de datos = 128M bits - CPU: 2(22bitsdireccion) × 32bits de datos = 128M bits)). • Entrada/Salida 16bits de datos a escribir/leer: La memoria utiliza un única puerto de 16bits para escribir y leer datos. • Entrada memClk (Clock ): Se utiliza para los modos sincrónicos, en este caso tiene el valor constante cero. • Entrada memAdV (Address Valid ): Se utiliza para los modos sincrónicos, en este caso tiene el valor constante cero. • Entrada memCRE (Configuration Register Enable): Se utiliza para configurar el modo de funcionamiento de la memoria, en este caso tiene valor constante cero, ya que la memoria viene configurada por defecto para operar en modo asincrónico. • Entrada memCE (Chip enable): Se utiliza para habilitar o deshabilitar la memoria. En este caso tiene un valor constante cero, lo cual indica que la memoria está siempre habilitada. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 45 (a) Operación de lectura. (b) Operación de escritura. Figura 3.3: Operaciones de escritura y lectura. En la figura 3.3(a) se visualizan las señales del controlador de memoria al realizar una operación de lectura, se ve como la lectura de 32bits del CPU requiere dos lecturas de memoria de 16bits. El dato leı́do (dataR o) cambia primero los 16bits menos significativos y luego el resto. Una vez que se tiene el dato válido la señal de busy o toma el valor cero. En la figura 3.3(b) se visualizan las señales del controlador de memoria al realizar una operación de escritura. En este caso se realiza la escritura del byte más significativo de la palabra de 32bits (bits31 1 24). La primera de las dos escrituras en memoria es en los bits15 a 0 y la segunda en los bits31 a 16. Como se escribe el byte más significativo sólo se habilita la señal memUB o (se habilita la escritura dejando la señal cero) en la segunda escritura en memoria. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 46 • Entrada memOE (Output Enable): Habilita la salida de la memoria. Tendra valor cero cuando se realicen lectura de datos y uno al escribir la memoria. • Entrada memWE (Write Enable): Habilita la escritura de la memoria. • Entrada memLB (Lower Byte enable): Habilita la escritura del byte bajo (bits7 a 0) de la palabra (de 16bits) que se escribe. • Entrada memUB (Upper Byte enable): Habilita la escritura del byte alto (bits15 a 8) de la palabra (de 16bits) que se escribe. • Salida memWait (Wait): Provee información relevante del estado de la memoria al operar en modo burst, en este caso su valor será ignorado. 3.4. Plasma multi-núcleo Siguiendo con la descripción jerárquica de las entidades, aquı́ se aborda el plasma multi-núcleo. Esta es la entidad que alberga a todo el resto del diseño. A continuación se muestra el detalle de su arquitectura y luego se realiza un listado de los puertos de entrada y salda de la entidad (ver figuras 3.4 y 3.5). 3.4.1. Descripción Se listan a continuación los distintos bloques que presenta el plasma multinúcleo en su arquitectura interna. Aquellas simples serán descriptas directamente en esta sección otras más complejas se detallarán en secciones posteriores. 1. N núcleos que se quieran instanciar: La arquitectura de cada núcleo se explica en la sección 3.7. 2. Árbitro del bus: Su arquitectura se explica en la sección 3.5. clk_i reset_i addres_o (30 bits) we_o (4 bits) dataW_o uart_RX_i Entrada uart_TX_o / Salida GPIO0_o (32 bits) Plasma multi-núcleo dataR_i Al controlador de memoria memBusy_i busBusy_i GPIOA_i (32 bits) Figura 3.4: Puertos de entrada y salida del plasma multi-núcleo. Aparte del reset i y el clk i tenemos dos tipos de pines en el procesador: aquellos que son de la interfaz con la memoria y los de entra/salida hacia el usuario. Señales (de izq. a der.): - busSnoop_i - memRequest_o - cpuPause_i - busRequest_o - busAccessEnable_i - busBusy_i - irq_i - lockRequest_o Señales hacia el controlador de memoria externo: - busDataR_s - busDataW_s - busAddress_s - busWe_s - memRequest_s Señales (de izq. a der.): - cpuDataR_s - cpuDataW_s - cpuAddress_s - cpuWe_s Las señales de clk_i y reset_i van a todos los bloques Árbitro de bus Núcleo Núcleo Manejador de interrupciones Multiplexor memRequest_s cpuPause_s busSnoop_s Generador de señales irqStatus_s irqMask_s busValid_s Soporte de HW para locks Núcleo busAddress_s busWe_s memRequest_s Regs. de intercom. UART read UART write Máscara de IRQ Estado de IRQ Contador GPIO (E/S) Componentes varios CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 47 Figura 3.5: Arquitectura del plasma multi-núcleo, agrupa a todo el resto de las entidades del procesador. memRequest_s CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 48 3. Manejador de interrupciones: Su arquitectura se explica en la sección 3.6. 4. Multiplexor del bus: Este sistema recibe todas las señales que pueden ser leı́das por el CPU y también la señal de escritura proveniente del CPU. Mediante las señales de dirección y de habilitación de escritura por byte (cpuAddress s y cpuWE s), rutea las señales a los destinos correspondientes. El dato de lectura del núcleo (la señal cpuDataR s) toma valores de alguna de las siguientes fuentes: memoria interna la memoria externa (directo de memoria externa si se produce un miss o dato proveniente de cache durante un hit). el puerto de lectura de UART (datos recibidos). el puerto de entradas de propósito general (GPIO puerto A). el puerto de salida (GPIO puerto 0). los registros de comunicación inter-procesador, son registros de propósito general. el registro de estado de interrupciones. el registro de máscara de interrupciones. el registro del lock, se . el registro del contador. El CPU por su parte la señal de escritura del CPU (señaL cpuDataW s) puede ser enviada a varios destinos: los registros de intercomunicación. el puerto de salida de datos de la UART (datos para ser transmitidos). el puerto de salidas de propósito general (GPIO puerto 0). el registro de máscara de interrupción. la memoria interna. la memoria externa (se escribe memoria externa y cache al mismo tiempo, por la polı́tica de escritura write-allocate). Todos los registros especiales son mapeados en direcciones de memoria. En la sección 3.8 se detalla la distribución de las direcciones de memoria. 5. Manejador del lock : Es una porción de hardware para el soporte de locks, el resto del hardware necesario para su implementación se encuentra en cada uno de los núcleo. Por su parte, cada núcleo envı́a una señal de solicitud a esta entidad, quien las chequea y entrega el lock al núcleo que corresponda (únicamente a uno). La forma que tiene de entregar el lock a uno u otro núcleo, es asignando el valor correspondiente a ese núcleo en el registro del lock 1 . 1 El valor correspondiente al núcleo i es 2i , un valor nulo en el registro del lock indica que ningún núcleo lo posee. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 49 Esta entidad analiza las solicitudes de los núcleos, una en cada ciclo de reloj. Las solicitudes son realizadas por los núcleos a través de la señal de solicitud(lockRequest s). Si el núcleo que es chequeado solicita el lock, esta entidad se lo entrega. En cambio si no hay una solicitud, se procede a chequear la señal de solicitud del siguiente núcleo. Cuando el lock ya se encuentra asignado a un núcleo, la entidad deja de chequear las solicitudes del resto de los núcleos y espera a que el núcleo que posee el lock lo libere. El mecanismo que implementan los núcleos para liberar el lock es borrar la solicitud (señal que recibe esta entidad). Una vez liberado el lock es posible seguir evaluando las siguientes solicitudes. La implementación en VHDL se muestra a continuación: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 u2 l o c k H a n d l e r : process ( c l k i , r e s e t i , plasmaCoreLockRequest s ) variable checkCpu v : i n t e g e r ; begin i f r e s e t i = ’ 1 ’ then l o c k R e g i s t e r s <= ZERO( 3 1 downto 0 ) ; checkCpu v : = 0 ; e l s i f r i s i n g edge ( c l k i ) then i f plasmaCoreLockRequest s ( checkCpu v ) = ’ 1 ’ then l o c k R e g i s t e r s ( checkCpu v ) <= ’ 1 ’ ; else l o c k R e g i s t e r s <= X” 00000000 ” ; checkCpu v := ( checkCpu v + 1 ) mod numberOfCores ; end i f ; end i f ; end process ; Esta implementación soporta hasta treinta y dos núcleos y a la vez debe ser igual a alguna potencia de dos debido a las operaciones en módulo (lı́nea 12 del código VHDL). Otra deficiencia del diseño es que al controlar uno a uno los cores, se puede tener una latencia entre la solicitud del lock y la obtención del mismo de hasta N ciclos de reloj en el peor de los casos, con N = numero de nucleos. Esto último no es de gran importancia ya que en general el tiempo que tarda el CPU en chequear el registro del lock (para saber si lo obtuvo o no), es mayor que esta latencia, e inclusive en el peor de los casos tiene mejor rendimiento que la implementación por software propuesta en el sistema operativo del plasma. Esta implementación es simple, requiere poco hardware, brinda un reparto equitativo de los locks entre los núcleos, se desempeña mejor que la implementación por software y se asegura que no habrá deadlocks entre los núcleos que compitan por el recurso. 6. Generador de señales: No es una entidad real, es una forma de agrupar tres procesos presentes en el plasma multi-núcleo: a) busSnoop s: Es una señal necesaria para la impementación del algoritmo de snooping para la coherencia de cache. La señal es repartida a todos los núcleos, en la sección 3.7.3 se explica como cada núcleo la utiliza esta señal para mantener la coherencia de cache. La señal toma un valor lógico alto cada vez que alguno de los núcleos realiza una escritura en memoria, y se mantiene en ese valor durante un solo ciclo de reloj. En las escrituras en memoria es en el único momento en el que una posición de memoria puede ser modificada. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 50 b) memRequest s: Es la señal que se envı́a al controlador de memoria para solicitar una lectura escritura de un dato. Esta señal toma el valor lógico uno cuando el núcleo que tiene accesos al bus realiza una solicitud de memoria, el resto de las solicitudes de los núcleos sin acceso al bus son ignoradas. c) corePause s: Esta es la señal de pausa que recibe cada uno de los núcleos. Un núcleo es pausado si: Si el núcleo que posee acceso al bus quiere acceder a memoria y el controlador indica que esta ocupada. Si se intenta escribir en el registro de la UART y ésta se encuentra ocupada enviando un dato previo. El núcleo internamente puede pausarse a sı́ mismo, esto sucede por ejemplo en los casos donde no consiguen el acceso al bus. 7. Componentes varios: Al igual que en el item anterior, este entidad no existe realmente, es una manera de agrupar varios componentes que están relacionados: a) UART: Es una controlador de UART estándar, que se accede a través de dos registros, uno para leer datos recibidos y otro para enviar datos. La velocidad del mismo es configurable durante el proceso de sı́ntesis, pero luego queda fija. Los datos son de 8 bits y los datos se envı́an y reciben sin bit de paridad. b) Regitros para las solicitudes de interrupción (IRQ del inglés Interrupt ReQuest): posee dos registros, para leer el estado de las interrupciones y la máscara de interrupción. El registro de máscara también puede ser escrito. c) Registro del contador: Tiene 32bits y se incrementa en cada ciclo. Sólo es posible leerlo, para saber la cantidad de ciclos que transcurrieron desde que se alimentó el circuito. El bit 18 del contador funciona como fuente de interrupción del procesador. d ) Registros del GPIO: Son dos registros que representan los puertos de entrada y salida del procesador. El registro del puerto de entrada (32bits) puede ser leı́do. El registro del puerto de salida (32bits) puede ser escrito o leı́do. Las escrituras en el registro del puerto de salida se pueden realizar son bit a bit. e) Registros para comunicación inter-procesadores: Dos registros de propósito general que se comparten entre todos los procesadores. Brindan una forma de comunicación entre los núcleos sin necesidad de recurrir a la memoria externa. Son útiles a la hora de ejecutar programas de de inicialización de memoria o durante el boot de los procesadores antes de que se inicie el sistema operativo. 3.4.2. Puertos de la entidad clk i (entrada): Para sincronizar. reset (entrada)i: Para reiniciar la unidad. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 51 Interfaz con la memoria externa: todas las señales se envı́an hacia el controlador de memoria o se reciben desde el mismo. • address o (salida 30 bits): Para direccionar la memoria externa. Esta señal se envı́a al controlador de memorial. • we o (salida 4 bits): para indicar escritura selectiva de los 4bytes de las palabras de 32bits. • dataW o (salida 32 bits): dato de escritura en memoria principal a través del controlador de memoria. • dataR i (entrada 32 bits): dato de lectura provenientes de la memoria principal a través del controlador. • memBusy i (entrada): señal que indica que la memoria esta ocupada, por lo tanto los datos de lectura que entrega no son válidos. • memRequest o (salida): señal para solicitar un acceso a memoria, las señales dataW o y address o deben tener valores válidos. Entrada/Salida: • uart TX o (salida): señal de transmisión de datos de la UART. • uart RX i (entrada): señal de recepción de datos de la UART. • gpio0 o (salida 32 bits): puerto de salida de propósito general. • gpioA i (entrada 32 bits): puerto de entrada de propósito general. 3.5. Árbitro del bus Esta entidad se encarga de arbitrar la utilización al bus: recibe las distintas solicitudes de acceso al bus de los núcleos y decide cual de ellos tendrá control sobre el mismo. Ası́ los núcleos acceden de a uno al bus, y esta entidad se encarga de que lo hagan de manera equitativa. 3.5.1. Descripción A continuación se presenta el algoritmo que fue implementado para brindar el acceso al bus de manera equitativa. Si el bus se encuentra liberado, entonces se brinda acceso al primer núcleo que lo solicite. Los pedidos posteriores (mientras el bus siga ocupado) se listan en un buffer FIFO en el orden que suceden. En caso de que haya solicitudes simultáneas, se le da prioridad según el número de núcleo, el 0 tiene prioridad frente al 1, el 1 frente al 2 y ası́ sucesivamente. Cuando el núcleo que está controlando el bus lo libera, el siguiente valor del buffer es procesado. Este valor indica cual será el siguiente núcleo en controlar el bus. Si en el momento el que el bus es liberado, el buffer se encuentra vacı́o, significa que ningún otro núcleo pretende controlar el bus. El bus pasa deja de estar ocupado y pasa a un estado liberado, como se encontraba inicialmente. La utilización de un buffer FIFO es fundamental para brindar control del bus a los núcleos en forma equitativa La simulación del funcionamiento de esta entidad se ve en la figura 3.6. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 52 Figura 3.6: Simulación del árbitro del bus, donde se observa que el la utilización del bus se reparte en forma equitativa entre los núcleos y el acceso al mismo se va cediendo en el orden en el que llegan las solicitudes. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS busBusy_o clk_i reset_i 53 Árbitro del bus busRequest_i BusAccess Enable_o busValid_o Figura 3.7: Puertos de entrada y salida de la entidad del árbitro del bus. El buffer utilizado debe tener al menos la misma cantidad de posiciones que el número de núcleos del procesador, para contemplar el peor caso, que es cuando todos los núcleos solicitan acceso simultáneamente. El buffer se implementa de manera circular, para ello en el código VHDL se utilizaron algunas operaciones en módulo [11]. El sintetizador sólo puede implementar operaciones en módulo de potencias de dos. Estas operaciones al trabajar con números binarios son fácilmente implementables, sólo es cuestión de tomar un cierto rango de bits. Por ejemplo, si se tiene una señal de 32bits, aplicar operaciones en módulo dos, implica quedarse con el último bit, en módulo cuatro con los últimos dos bits y ası́ sucesivamente. Por el contrario operaciones en cualquier otro módulo se vuelven mucho más complejas de implementar. Aquı́ es donde reside la limitación del plasma multi-núcleo de poder ser sintetizado únicamente con un número de núcleos igual a potencias de dos. Modificando un poco el código de VHDL esta limitación puede evitarse. La forma de modificarlo es tomando un buffer siempre de tamaño igual a una potencia de dos, y también mayor al número de núcleos. Por ejemplo si se tienen tres núcleos tomar un buffer de cuatro posiciones, si se tienen cinco, seis o siete tomar uno de ocho, y ası́ análogamente para cualquier otro valor. El buffer sigue cumpliendo perfectamente su función. La contra que implica la modificación es que el buffer circular dejarı́a de tener el número mı́nimo de posiciones (igual al número de núcleos), desperdiciando las posiciones adicionales. Esto no representa un gran costo a nivel de hardware y la contra se vuelve insignificante. Esta modificación queda pendiente para el futuro, y no tiene mayor importancia para este trabajo. Las señales busAccessEnable o, busBusy o y busValid o son redundantes, ya que la señal es de habilitación del bus, es cero cuando busValid s es cero y es busBusy negado cuando es uno. Cada núcleo podrı́a saber deducirla internamente, pero se deja de esta manera pensando en cambiar el bus por uno switcheado, donde dejan de ser redundantes. 3.5.2. Puertos de la entidad La entradas y salida listadas a continuación son para el procesador plasma multi-núcleo con N núcleos (ver figura 3.7): clk i (entrada): para sincronizar. reset i (entrada): para resetear la entidad. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 54 busRequest i (entrada N bits): una señal por cada núcleo, esta es la manera que tiene lo núcleos de realizar la solicitud del bus. busBusy o (salida N bits): una señal hacia cada núcleo, para indicar cuando el bus esta ocupado. busAccessEnable o (salida N bits): una señal hacia cada núcleo, para indicar cuando el núcleo tiene el acceso. busValid o (salida): indica cuando los datos en bus son válidos, o lo que es lo mismo cuando uno de los núcleos tiene el control del mismo. En el caso en que ningún núcleo tiene control sobre el bus, el mismo tiene datos inválidos. 3.6. Manejador de interrupciones Como todo procesador necesita un controlador de interrupciones, este en particular está diseñado para el plasma multi-núcleo y se encarga de identificar cuando existe una interrupción y a la vez de repartir estas interrupciones de manera equitativa entre los distintos núcleo. 3.6.1. Descripción La entidad identifica las interrupciones haciendo una operación lógica AND entre la máscara de interrupciones y el vector de estado de interrupciones. En caso de ser distinto de cero implica que existe una solicitud de interrupción que no fue enmascarada y alguno de los núcleos debe atenderla. La entidad traslada la solicitud al primer núcleo. Al identificar una nueva interrupción la solicitud es trasladada al siguiente núcleo. La entidad reparte las interrupciones equitativamente entre todos los núcleos. Una desventaja de esta implementación es que el manejador de interrupciones no puede identificar si el núcleo al que traslada la solicitud tiene las interrupciones habilitadas o no. La solicitud no podrá ser atendida hasta que el núcleo en cuestión no habilite las interrupciones, lo cual puede provocar un aumento en la latencia del procesador para atender una interrupción. Usualmente un procesador no deberı́a tener interrupciones deshabilitadas por un largo tiempo, por lo que no serı́a un gran problema. Por el contrario, si se pretende generar un sistema que pueda asegurar una latencia máxima para atender las interrupciones, este manejador deberı́a ser modificado, de modo que sı́ identifique que núcleo puede ser interrupido. 3.6.2. Puertos de la entidad En la figura 3.8 se muestra la entidad del manejador de interrupciones. clk i (entrada): Para sincronizar. reset i (entrada): Para reiniciar la unidad. irqMask i (entrada8 bits): Recibe la el valor del registro de la máscara de las interrupciones, ver cuadro 3.1. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS clk_i reset_i Manejador de interrupciones 55 irqMask_i (8 bits) irqStatus_i (8 bits) plasmaCoreIrq_o (N bits) Figura 3.8: Puertos del manejador de interrupciones. Bit del registro 0 1 2 3 4 5 6 7 Fuente de interrupción UART - dato disponible UART - disponible para escribir bit 18 del contador bit 18 del contador negado No asignado, valor constante 0 No asignado, valor constante 0 bit 31 del GPIA bit 31 del GPIA negado Cuadro 3.1: Mapeo de los bits del registro de estado y máscara de interrupciones. Los dos primeros son para la comunicación UART. Se utilizan el bit 18 del contador y este mismo negado, para proporcionar interrupciones cada 10ms aproximadamente. El bit 31 del puerto de A (entradas) del procesador brinda una fuente de interrupción externa por nivel alto (bit 6) o bajo (bit 7 ). irqStatus i (entrada8 bits): Recibe la el valor del registro de los estados de las interrupciones, ver cuadro 3.1. plasmaCoreIrq o (salida 8 bits): Las señales de interrupción hacia cada uno de los núcleos. 3.7. Núcleo En esta sección se da una descripción del núcleo. Es la unidad más importante del sistema. En su interior alberga varias entidades, entre ellas la memoria cache, el soporte de hardware necesario para locks y su propia unidad de control. La unidad de control del núcleo es la más compleja del todo el diseño. En esta sección también se describe el algoritmo de coherencia de cache. 3.7.1. Descripción En la figura 3.9 se muestra un diagrama de la arquitectura y a continuación se listan los componentes del mismo: CPU: Es la unidad de procesamiento original del plasma. Ningún cambio fue realizado en la misma. Esta unidad es quien impone las direcciones de memoria que necesita leer o escribir, impone el valor del dato y la busAccEn busSnoop memBusy busBusy busRequest memRequest delayedBusAccessEnable_s busAccessEnable_s Buffer Tri-state Unidad de control cpuIndex CPU Memoria cache de tags Soporte para locks lockRequest Memoria cache de datos BUS (dataW, dataR, address, byteWE) clk_i reset_i cpuPause_s irq_i Puerto A NÚCLEO Memoria Ram Interna DataR (32bits) DataW (32bits) Address (30bits) byteWE (4bits) reset_i reset Regisitros clk_i clk CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 56 Puerto B Figura 3.9: Diagrama en bloque de la arquitectura de cada núcleo. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 57 habilitación por byte de una escritura. Es a quien se debe entregar los datos/instrucciones leı́dos. El dato puede provenir de memoria interna, externa, cache, y otros componentes, que se ven con detalle a continuación. Bus transceivers: Son utilizados para acoplarse al bus de interconexión. Cuando el núcleo controla el bus impone los valores propios de dirección, de dato a escribir y de habilitación por byte en el bus, el dato de lectura presente en el bus es el correspondiente a la dirección del bus. Cuando el núcleo no tiene acceso al bus esta entidad procura presentar una alta impedancia hacia el bus, de esta manera otro núcleo puede imponer los valores deseados. Se muestran en la figura 3.10. Memoria ram Interna: Es una memoria sincrónica interna exclusiva de cada núcleo. Al ser sincrónica es direccionada con las señales de addressNext s y weNext s. Por su parte la señal dataW s siempre tiene con un ciclo de anticipación el valor correcto del dato que desea escribir. El contador de programa de los núcleos se inicializa en la primer posición de esta memoria, de esta manera cada núcleo puede correr un programa independiente. Se realizaron dos implementaciones para esta memoria: una genérica y otra utilizando las librerı́as de Xilinx llamadas Unisims. Las librerı́as proveen una forma eficiente de implementar memorias en FPGAs de Xilinx, aprovechando espacio y tiempo de sı́ntesis. Soporte de hardware para el ı́ndice de CPU: Es un registro constante que almacena el ı́ndice del núcleo. El multiplexor, que se explica a continuación, direcciona este valor hacia la señal de lectura de datos del CPU (cpuDataR s). Multiplexor del dato de lectura del CPU (cpuDataR s): Esta unidad selecciona el dato que sera leı́do por el CPU. El dato a leer es seleccionado por la señal proveniente de la unidad de control del núcleo, cpuDataRSelection s, y en un caso especial por la dirección del CPU. Los posibles valores pueden verse en el cuadro 3.2. Memoria cache: Esta compuesta por dos memorias sincrónicas de dos puertos cada una, A y B. La primera de las memorias guarda los datos y la segunda los indicadores (tags en inglés). La memoria de datos tiene una ancho de palabra de 32 bits y la memoria de indicadores un ancho de diez bits. Son nueve bits de tag y uno para indicar la validez del dato. El puerto A se utiliza para atender solicitudes del CPU y el B para la implementación del algoritmo de coherencia de cache, de esta manera la implementación es más simple. Se deben considerar los casos en donde la dirección del puerto A y B coinciden. El caso donde sea una lectura en ambos puertos no existe ningún problema. En los casos donde haya una escritura la única a tener en cuenta es cuando el puerto B realiza una escritura y el A una lectura, los otros dos casos, donde A realiza una escritura, son problemas a resolver por el programador, ya que no se puede hilar tan fino sobre un recurso compartido, o puede ser que se está accediendo a recursos compartidos sin protegerlos por locks. El único caso que debe considerarse es cuando se lee un dato a través del puerto A y al mismo tiempo el puerto B trata de escribir un nuevo dato. Se permite CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 58 Condición Fuente del dato leı́do por el CPU (cpuDataR s) Comentario Dirección del CPU apunta a 0X1000 cpuDataRSelection s = "00" cpuDataRSelection s = "01" cpuDataRSelection s = "01" Indice del CPU (coreNumber) Datos de la memoria interna (internalRamDataR s) Datos de memoria cache (cacheDataRA s(31 downto 0)) Datos del bus (busDataR i) cpuDataRSelection s = "01" No interesa Soporte para la identificar del número de CPU (reg. constante) Para toda lectura de datos o inst. de la memoria interna. Cuando la lectura en cache es un hit. Al leer memoria externa (cacheable o no cacheable), cuando se produce un miss en una lectura o se leen componentes externos. Los datos no son leı́dos por el CPU, por lotanto no interesa. Cuadro 3.2: Posibles fuentes para la señal cpuDataR s . la escritura del dato a través del puerto B y el puerto A en vez de leer el dato almacenado en cache entrega el dato de escritura del puerto B. Cuando el CPU realiza una escritura que no es de 32 bits a través del puerto A se invalida esa posición de memoria, para evitar malas lecturas en el resto de la palabra. Por ejemplo: si un dato se encuentra almacenado en cache y se realiza la escritura de uno sólo de sus bytes, todo funciona correctamente. Lo mismo sucede con un cache update, ya que el dato es escrito en las caches de otros núcleos sólo si si el dato ya se encontraba albergado en las mismas. El problema aparece al realizar una escritura de un dato que no se encuentra en cache. Sólo es posible a través del puerto A, ya que el puerto B sólo realiza escrituras de datos que sı́ se encuentran en cache (actualiza datos, nunca reemplaza los bloques que ya están en cache). Si se escribe el byte cero de una palabra que no está cacheada, la polı́tica de escritura en cache write-allocate, obliga a escribir el dato en cache, y sólo se escribe el byte cero y el resto queda con valores no necesariamente válidos. Al realizar una lectura de cualquier otro de los bytes de esa palabra, el dato leı́do es entonces erróneo. Tampoco es posible simplemente no escribir el dato, ya que si dato sı́ se encuentra en memoria cache, al realizar la escritura de un byte de una palabra, el dato tampoco es actualizado. Dicho esto se plantean tres soluciones: 1. Si se realiza una escritura que no sea de 32 bits en un dato que no se encuentre albergado en memoria cache, primero se busca el dato en memoria y se lo copia en cache Luego se escribe el byte correspondiente (en cache y memoria). 2. Invalidar el dato en una escritura que no sea de 32 bits: El bit más significativo del tag es en realidad el bit de dato válido. Al colocar éste último bit en un estado lógico uno, el dato se invalida. 3. Al realizar una escritura que no sea de 32 bits se comprueba si el CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 59 dato se encuentra en cache o no. En caso de que sı́ se encuentre se escribe, y en caso de que no se encuentre no se realiza nada. Serı́a la solución más efectiva, ya que no invalida datos innecesariamente y actualiza los bytes de un dato cacheado si es necesario. El problema que tiene esta solución es que es necesario acceder dos veces a cache, primero para leer el indicador y compararlo con el del dato a escribir, y luego, en caso de detectar que el dato sı́ se encuentra en cache, realizar la escritura del byte correspondiente. Al ser la memoria de lectura sincrónica es posible realizarlo en un sólo ciclo de reloj. Si fuera asincrónica el tag puede ser leı́do, luego comparado con el otro tag y finalmente el resultado de la comparación puede ser utilizado para habilitar la escritura en memoria cache, todo en el mismo ciclo de reloj. Al utilizar una memoria sincrónica son entonces necesarios al menos dos ciclos de reloj y su implementación se vuelve algo más complicada. La opción (1) se descarta por ser totalmente ineficiente, y la (3) también es descartada, porque al utilizar memoria sincrónica se vuelve más complicada que la (2), que es la implementación adoptada. Registros sincrónicos: Son un conjunto de registros sincrónicos que retrasan algunas señales un ciclo de reloj, para poder conservar sus valores. Las señales que se almacenan en estos registros y la necesidad de los mismos se detalla en la sección 3.7.3. Soporte de hardware para los locks: En la dirección de memoria 0x200000A0 se mapea un registro virtual, que al escribirlo con cualquier valor distinto de cero, provoca que la señal lockRequest o tome un valor lógico uno, y toma un estado lógico cero al ser escrito con un valor nulo. Su implementación se muestra a continuación: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 l o c k R e q u e s t : process ( c l k i , r e s e t i ) begin i f r e s e t i = ’ 1 ’ then l o c k R e q u e s t o <= ’ 0 ’ ; e l s i f r i s i n g edge ( c l k i ) then i f cpuAddress s & ” 00 ” = X” 200000A0” and cpuWe s / = ” 0000 ” then i f cpuDataW s = X” 00000000 ”then l o c k R e q u e s t o <= ’ 0 ’ ; else l o c k R e q u e s t o <= ’ 1 ’ ; end i f ; end i f ; end i f ; end process ; La señal lockRequest o se envı́a al manejador de locks del plasma multinúcleo, quien luego decide que núcleo obtiene el lock. Multiplexor del puerto A de la cache(dirección, habilitación por byte, dataW): mediante de este multiplexor se seleccióna qué señales controlan al puerto A de la cache. Las señales que se multiplexan son la dirección de lectura/escritura, el dato a escribir, y la habilitación por byte. Hay dos posibles fuentes para estas señales: CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 60 • Al realizar un write-through o una lectura de cache, los datos provienen del CPU: ◦ Dirección de cache: cpuAddresNext s, es la única señal que nos interesa en una lectura. ◦ Habilitación por byte: cpuWeNext s. ◦ Dato a escribir en cache: cpuDataW s, el dato a escribir por el CPU se encuentra disponible con un ciclo de anticipación, por eso se puede utilizar la señal cpuAddresNext s y cpuWeNext s. ◦ Tag: Son los bits 19 a 11 de la dirección de escritura, es decir cpuAddressNext s(19 downto 0). • Al producirse un miss de lectura, los datos provienen del bus: ◦ Dirección de cache: busAddres io. ◦ Habilitación por byte: busWe io. ◦ Dato a escribir en cache: busDataW io. ◦ Indicador: Son los bits 19 a 11 de la dirección de escritura, es decir busAddress s(19 downto 0). Unidad de control: es la unidad de control del núcleo y sin duda la parte más importante del mismo. Se detalla su funcionamiento en la sección 3.8. 3.7.2. Puertos de la entidad En la figura 3.10 se muestran los puertos listados a continuación junto con los tranceivers del bus: clk i (entrada): para sincronizar. reset i (entrada): para reiniciar la unidad. irq i (entrada): señal para generar una excepción en el núcleo. Señales de solicitud de recursos compartidos: • lockRequest o (salida): señal que utiliza el núcleo para solicitar el lock, descripta en la sección 3.4. Esta señal es recibida por la entidad que administra el lock entre todos los núcleos. Un estado lógico ‘uno’es una solicitud y ‘cero’es una no solicitud o liberación del lock. • busRequest o (salida): señal de solicitud de uso del bus, se envı́a a la entidad árbitro del bus descripta en la sección 3.5. • busAccessEnable i (entrada): señal recibida desde el árbitro del bus que indica cuando se tiene acceso al bus. • busBusy i (entrada): señal recibida desde el árbitro del bus que indica cuando el bus se encuentra ocupado. • memRequest o (salida): señal de solicitud de acceso a memoria principal. El plasma multi-núcleo toma las solicitudes de memoria de todos los núcleos y genera una única señal de solicitud que es enviada al controlador de memoria. De todos los núcleos que soliciten la memoria el que tiene acceso es el mismo que tiene acceso al bus. Esta señal funciona en conjunto con la de solicitud del bus, o bien el núcleo solicita acceso al bus (Por ejemplo al acceder a los periféricos) o solicita acceso al bus y a memoria simultáneamente. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 61 clk_i reset_i busAddres_io irq_i busDataW_io memBusy_i BUS busAccessEnable_i Núcleo busSnoop_i busWE_io busBusy_i busRequest_o busDataR_i lockRequest_o memRequest_o Figura 3.10: Puertos de entrada y salida de cada uno de los núcleos del plasma multi-núcleo. Se muestra también como tres de las salida tiene buffers tri-state, necesarios para conectarse al bus. A su vez se tienen entradas directas desde el bus, éstas son necesarias para recibir mensajes de broadcast, necesarios para la implementación del algoritmo de coherencia de cache elegido. • memBusy i (entrada): indica cuando la memoria se encuentra ocupada. Esta señal puede ser activada por varias razones, por ejemplo durante la inicialización de la memoria, o cuando algún núcleo realiza un acceso a memoria principal como se explicó en la sección 3.3, o podrı́a ser en el caso de que se implemente algún dispositivo DMA2 . Señales del bus de datos/instrucciones y de coherencia de cache: • busSnoop i (entrada): Señal para la implementación de la coherencia de cache, que indica que los datos del bus deben ser leı́dos para luego verificar si se debe actualizar la cache o no. • busAddress io (entrada/salida 30 bits): Señal para acceder a memoria y para la coherencia de cache. Es la dirección que se impone en el bus. • busDataW io (entrada/salida 32 bits): Señal para acceder a memoria y para la coherencia de cache. Cuando es salida es el dato que escribe en memoria. Cuando es entrada es el dato escrito por otro núcleo en memoria. 2 En inglés Direct Memory Access, es la manera que tienen entidades que no son núcleos de procesamiento de acceder a la memoria directamente sin necesidad de hacerlo a través de un CPU, de esa manera no le quitan tiempo de procesamiento. Estas entidad realizan únicamente movimiento de datos sin procesamiento alguno. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 62 • busWe io (entrada/salida 4 bits): Señal para acceder a memoria y para la coherencia de cache. Es la habilitación por byte del dato a escribir puede ser propia o de otro núcleo. • busDataR i (entrada 32 bits): Sólo para el acceso a memoria en particular cuando se realiza una lectura. 3.7.3. Descripción del algoritmo de coherencia de cache El plasma multi-núcleo brinda cierto soporte al algoritmo, como ser las señales de snooping. Pero es núcleo quien alberga al resto del hardware necesario para su implementación. La unidad de control del núcleo es quien lógicamente controla el algoritmo. Se utiliza un protocolo de snooping (sección 2.4.5). El algoritmo de coherencia es muy simple, y se detalla a continuación. Hay una serie de elementos importantes que dan lugar a este algoritmo tan sencillo: 1. La interconexión de los núcleos se realiza a través de un bus simple. Los núcleos se conectan al bus a través de buffers tri-state. Las señales cpuDataW s, cpuAddress s y cpuWe s pueden ser impuestas en el bus si el núcleo posee el acceso. Y cuando no tiene el acceso el núcleo puede leer los datos impuestos por el núcleo que sı́ controla al bus. Esto último sumado a la señal busSnooping permiten el envı́o de mensajes de broadcast entre los núcleos. 2. La señal busSnooping indica cuando hay un mensaje de broadcast en el bus al ponerse en un estado lógico uno. 3. La polı́tica de escritura en memoria write-throug obliga a escribir los datos directamente en memoria principal. En una polı́tica algo ineficiente, pero de ésta manera se logra que la memoria principal siempre tenga los datos válidos y más importante aún, obliga a que la información sobre la modificación de algún dato viaje siempre por el bus. Éstos items permiten que al modificar cualquier dato en memoria se envı́e a la vez un mensaje de broadcast a todos los núcleos, con la información del nuevo dato. Cada núcleo lee este mensaje y en caso de tener almacenado el dato en cache lo actualiza con el nuevo dato. A un nivel más bajo, esto implica la lectura del tag en memoria cache cada vez que se recibe la señal busSnooping . Como la memoria es sincrónica, el resultado de la lectura se obtiene un ciclo después. Nada asegura que un ciclo después el dato siga estando en el bus para realizar la comparación3 , por lo que los datos del bus necesarios son guardados en registros. Las señales necesarias son busAddress io, busWe io, busDataW io y busAccessEnable i, son almacenadas en delayedBusAddress s, delayedBusWe s, delayedBusDataW s y delayedBusAccessEnable s respectivamente. Una vez se obtiene el indicador de la cache se compara con el indicador del registro (delayedBusAddress io(19 downto 11). Si los tags coinciden y el núcleo no 3 En esta implementación se utiliza la memoria del kit Nexys2 y el acceso a memoria tarda más de un ciclo. El dato se mantendrı́a en el bus por más de un ciclo y no serı́an necesarios los registros, pero si la memoria a la que se accede es interna (sea la memoria principal o una cache de nivel dos o inclusive un buffer de escritura), la lectura puede durar un ciclo y los registros se vuelven necesarios. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 63 fue el que realizó la escritura, entonces la señal cacheUpdate s toma un valor lógico ‘uno’, sino ‘cero’. No tiene sentido actualizar el dato en cache del núcleo que realiza la escritura, ya que el dato que se encuentra en cache es obligatoriamente el correcto por la polı́tica de write-allocate. La señal delayedBusAccessEnable s indica si el núcleo realizó o no la escritura del dato, si en el ciclo anterior tuvo acceso al bus, es quien realizó la escritura. Mediante la señal cacheUpdate s se le indica al puerto B que debe escribir el dato guardado en los registros. El indicador no es escrito a través del puerto B, ya que si se actualiza la cache, es porque el indicador de cache igual al de los registros. El banco de indicadores sigue estando disponible para la lectura en caso de que se produzcan escrituras en ciclos de clock consecutivos, en este caso no es posible eso por lo anteriormente mencionado, que la escritura se realiza en una memoria lenta, y no se realiza en más de un ciclo de reloj. En la figura 3.11 se muestra una simulación del proceso de actualización de cache. En la simulación los cuatro núcleos realizan una operación de escritura de una misma posición de memoria. 3.8. 3.8.1. Unidad de control del núcleo Descripción La unidad controlador del núcleo es una máquina de estados con ocho estados (ver figura 3.12). Los estados dependen del tipo de memoria a la que se esta accediendo, sea interna, externa o cache. Todos los periféricos y registros especiales del núcleo están mapeados en memoria, por lo que también son considerados como una memoria más. El estado también depende del tipo de acceso, o sea si es una escritura o o una lectura. En total se tienen ocho estados: 0. INTERNAL RAM: lectura/escritura en memoria interna. 1. CHECKING: Lectura en cache. Durante este estado también se comprueba el indicador leı́do, y en caso de coincidir se considera la lectura un hit, de lo contrario un miss. 2. WT0 (write-through): escritura en memoria externa, también se escribe en memoria cache, debido a la polı́tica de escritura write-allocate. 3. WT1 (write-through): estado necesario si se pretende volver a realizar una lectura en cache después de un WT0, se explica más adelante. 4. MISC COMPONENTS: lectura/Escritura de registros especiales, en el cuadro 3.3 se muestran los mapeos en memoria. 5. NON CACHEABLE: lectura/Escritura de la porción de memoria que no puede ser cacheada. 6. MISS0: luego de un CHECKING, si se produce un miss el dato debe ser leı́do de memoria externa. Ésto es lo que indica este estado. 7. MISS1: Si luego de un miss se pretende leer cache nuevamente, se debe pasar por este estado, según se explica a continuación. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 64 Figura 3.11: Simulación del proceso de actualización de cache. En la figura se ve una simulación de un procesador con cuatro núcleos. En esta sección del código ejecutan los cuatro una instrucción de escritura en la misma posición de memoria. Los momentos en los que cada uno de los núcleos inicia la escritura de memoria se pueden ver en la señal de busSnoop s. La primer escritura no genera ninguna actualización, señal cacheUpdate s. En la segunda se actualiza el dato recientemente cacheado en el núcleo uno, en la tercera se actualizan los datos en los núcleos uno y dos, y por último cuando el núcleo cuatro es quien escribe en memoria los núcleos uno, dos y tres actualizan la cache. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 65 Figura 3.12: Estados y transiciones de la máquina de estados de la unidad de control. Al existir un miss o un write-thorugh se debe escribir un dato en la memoria cache. La escritura de este dato se hace durante los estados WT0 y MISS0. Si luego de estar en alguno de estos estados se pretende ir al estado CHECKING, se produce un conflicto, la señal de dirección de memoria cache del puerto A debe tomar dos valores al mismo tiempo, la dirección de escritura del dato y la dirección de lectura de la siguiente instrucción. Para solucionar este conflicto se introducen los estados WT1 y MISS1. Durante WT0 y MISS0 la señal de dirección del puerto A tiene la dirección correspondiente al dato a escribir en en memoria cache y en los estados WT1 y MISS1 la señal de dirección del puerto A tiene la dirección del dato/instrucción a leer, para chequear el tag en el siguiente ciclo, mientras se está en el estado CHECKING. Dentro de la unidad de control hay dos señales llamadas state s y stateNext s, que tienen información del estado actual y del estado siguiente respectivamente. state s es una señal sincrónica, mientras que stateNext s se obtiene con lógica combinacional a partir de las entradas y del estado actual. Las salidas de esta entidad son las señales de control de todo el procesador. Las salida pueden depender de state s y de stateNext s. En este caso al depender del estado siguiente, que a su vez depende de las entradas, esta máquina es una máquina de Mealey. Durante el diseño de esta entidad se tuvo que prestar especial atención a las dependencias entre las entradas y las salidas, para evitar generar lazos combinacionales. 3.8.2. Puertos de la entidad En la figura 3.13 se muestra esta entidad. clk i (entrada): Para sincronizar. reset i (entrada): Para reiniciar la unidad. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS Dirección de Memoria 0x00000000 ... 0x00000FFC 0x00001004 66 Mapeo Memoria interna Índice del CPU 0x00001004 ... 0x0FFFFFFC 0x10000000 ... 0x100FFFFF 0x10100000 ... 0x103FFFFF 0x10400000 ... 0x1FFFFFFF 0x20000000 0x20000010 0x20000020 0x20000030 0x20000040 0x20000050 0x20000060 0x20000080 0x200000A0 0x20000000 ... 0xFFFFFFFC No asignada Memoria externa cacheable Memoria externa no-cacheable No asignada Lectura/escritura UART Máscara de interrupciones Estado de interrupciones GPI0 set-bits/lectura GPI0 clear-bits/lectura GPIA lectura Contador Registro de comunicación Registro del lock No asignada, salvo las direcciones antes nombradas Cuadro 3.3: Mapeo de la memoria. Las direcciones apuntan a cada byte de la memoria. Como utilizamos palabras y registros de 32 bits, las direcciones van de cuatro en cuatro. Ası́ se define en la ISA de MIPS. cpuAddress i (entrada30 bits): Proveniente del mliteCpu. cpuWe i (entrada4 bits): Proveniente del mliteCpu. cpuAddressNext i (entrada30 bits): Proveniente del mliteCpu. cpuWeNext i (entrada4 bits): Proveniente del mliteCpu. busSnoop i (entrada): Señal necesaria para implementar el algoritmo de coherencia de cache. Indica cuando hay un mensaje de broadcast en el bus. busAccessEnable i (entrada): Provenientes del árbitro del bus. Indica cuando se tiene acceso al bus. busBusy i (entrada): Provenientes del árbitro del bus. Indica cuando el bus esta ocupado. memBusy i (entrada): Provenientes del controlador de memoria externa. Indica cuando la memoria esta ocupada. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 67 clk_i reset_i cacheUpdate_o cpuAddres_i cpuWe_i cacheAddressASource_o cacheWEASource_o cacheDataRASource_o cacheEnableA_o cpuAddresNext_i CpuWeNext_i busSnoop_i busAccessEnable_i busBusy_i memBusy_i cacheTagA_i cacheTagB_i delayedBusAddress_i delayedBusAccessEnable_i delayedBusWe_i Unidad de control cacheEnableB_o internalRamEnable_o cpuPause_o cpuDataRSource_o busRequest_o memRequest_o busTransceiversEnable_o Figura 3.13: Unidad de control que posee cada uno de los núcleos de plasma multi-núcleo cacheRamTagA i (entrada10 bits): Proveniente de la cache, es el indicador leı́do de la cache a través del puerto A, incluye el bit de validez. cacheRamTagB i (entrada10 bits): Proveniente de la cache, es el indicador leı́do de la cache a través del puerto B, incluye el bit de validez. delayedBusAddress i (entrada30 bits): Proveniente de los registros del núcleo. Brinda la dirección presente en el bus con un ciclo de retraso. delayedBusWe i (entrada(30 bits): Proveniente de los registros del núcleo. Brinda la habilitación de escritura por byte presente en el bus con un ciclo de retraso. delayedBusAccessEnable i (entrada): Proveniente de los registros del núcleo. Brinda información del acceso al bus de éste núcleo con un ciclo de retraso. cacheUpdate o (salida): Señal que indica que se debe actualizar la cache con los valores de los registros. cacheRamAddressASource o (salida2 bits): Se envı́a al multiplexor del puerto A, selecciona la dirección del puerto. cacheRamWeASource o (salida3 bits): Se envı́a al multiplexor del puerto A, selecciona la habilitación por byte del puerto. cacheRamDataWASource o (salida2 bits): Se envı́a al multiplexor del puerto A, selecciona el dato a escribir del puerto. cacheRamEnableA o (salida): Es la habilitación del puerto A de la cache. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 68 address clk byte_we reset_in data_r intr_in mem_pause mliteCpu data_w address_next byte_we_next Figura 3.14: Puertos de entrada y salida de CPU del plasma original. cacheRamEnableB o (salida): Es la habilitación del puerto B de la cache. cpuPause o (salida): Es la señal de pausa para el mliteCpu. cpuDataRSelection o (salida): Se envı́a al multiplexor del mliteCpu. Selecciona la fuente del dato de lectura del CPU. internalRamEnable o (salida): Es la habilitación de la memoria interna del CPU. busRequest o (salida): Se envı́a al árbitro del bus. El la solicitud para la utilización del bus. busTransceiverEnable o (salida): Se envı́a a los buffers tri-state, es la habilitación para que impongan la señal en el bus. memRequest o (salida): Se envı́a al controlador de memoria. Es la solicitud para acceso a memoria. 3.9. CPU plasma Como se dijo anteriormente el CPU no se rediseñó desde cero, sino que se lo utiliza tal cual se extrajo del plasma original. En la figura 3.14 se muestra la interfaz de conexión del CPU. No se analiza su estructura interna, sólo su interfaz. Todas las salida de este bloque son secuenciales, salvo las salidas address next y byte we next, cuyo valor dependen del valor del la entrada mem pause, puede verse en la figura 3.15. Es importante tener esto en cuenta, ya que el valor de la entrada mem pause no puede depender directamente ni del valor de address next ni byte we next, ya que se generarı́a un lazo combinacional. 3.10. Caracterización del procesador En esta sección se evalúan caracterı́sticas de tamaño y desempeño del procesador multi-núcleo. Se muestran los resultados de las implementaciones y mediciones realizadas y se hace un análisis de los mismos. El procesador multi-núcleo muestra mejoras en el rendimiento al ejecutar los programas de prueba, respecto a la implementación mono-núcleo del plasma. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 69 Figura 3.15: Interfaz del plasma original. Al analizar su interfaz se encontró que las salidas address next y byte we next dependen convinacionalmente del valor de la entrada mem pause. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS Número de núcleos 1 2 4 70 Slices Utilizadas 2635 4624 8030 Cuadro 3.4: Tamaño del procesador plasma 3.10.1. Tamaño del procesador En el cuadro 3.4 se puede ver el tamaño en slices que ocupa el plasma multinúcleo. En estas condiciones se pueden implementar hasta cuatro núcleos en una FPGA Spartan3E-1200. Las evaluaciones de rendimiento que se muestran en este trabajo se realizan en procesadores de uno a cuatro núcleos. 3.10.2. Desempeño en función del trabajo de las tareas Los siguientes resultados surgen de ejecutar distintos programas de pruebas en el procesador, en cada programa de prueba se aumentaba la cantidad de instrucciones. La figura 3.16 muestra como mejora el rendimiento en el procesamiento a medida que aumenta la cantidad de trabajo que realizan las tareas de prueba, el trabajo que realizan las tareas se mide en número de instrucciones que ejecutan. Los resultados mostrados son para tres tareas distintas, donde la fracción de instrucciones que acceden a memoria cambia. En estas gráficas en particular se ven tareas con un 15, 00 %, 4, 17 % y 3, 22 % de tareas que acceden al bus del procesador. Las figuras 3.16(a) y 3.16(b) muestran los resultados al comparar el procesador de cuatro núcleos frente al de uno y el de dos núcleos frente al de uno respectivamente. Se ve que en todos los casos el rendimiento aumenta a medida que aumenta el trabajo que realizan las tareas en sı́. Esto se debe a que el porcentaje de tiempo que le toma al sistema operativo reescalonar las tareas es cada vez menor. A partir de cierto valor, este tiempo se vuelve despreciable frente al tiempo de procesamiento efectivo. También se aprecia que siempre se llega a un lı́mite en la mejora, el cual depende del porcentaje de instrucciones que acceden al bus. En el mejor caso de la figura 3.16(a) este valor se acerca a 4, mientras que en la 3.16(b) a 2, que son los respectivos valores teórico máximos que se pueden alcanzar en casos ideales. El caso ideal es cuando no existen colisiones en el bus, y la única manera de asegurar que las colisiones sean nulas es que ningún núcleo acceda al bus en ningún momento, lo cual es imposible. El aumento en las colisiones provoca que el rendimiento disminuya. Las colisiones en el bus generan un lı́mite en el rendimiento de procesadores multi-núcleo. A continuación se evalúa el rendimiento de un procesador en función de los accesos a memorias de los núcleo. 3.10.3. Desempeño en función de la utilización del bus Los siguientes resultados surgen de realizar pruebas en el procesador, ejecutando programas en los cuales se aumenta la utilización del bus progresivamente. Las colisiones en el bus aumentan al aumentar el número de núcleos y al aumentar el porcentaje de utilización del bus de cada uno de ellos. Se evalúa como mejora la eficiencia del procesadores al disminuir las colisiones. En la figura 3.17, se muestra que la mejora en el tiempo de procesamiento de las tareas disminuye CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 71 (a) Al pasar de uno a cuatro núcleos. (b) Al pasar de uno a dos núcleos. Figura 3.16: Mejora en el tiempo de ejecución al pasar de uno a cuatro núcleos. Muestra la relación en los tiempos de ejecución en función de la cantidad de procesamiento de las tareas que se corren. La cantidad de procesamiento se mide instrucciones que ejecutan las tareas. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 72 Figura 3.17: Mejora en el tiempo de ejecución para cuatro y dos núcleos, frente a un núcleo, en función del porcentaje de instrucciones que acceden al bus en las tareas. junto con el aumento del porcentaje de instrucciones que acceden al bus. También se ve un resultado indeseado, que es que para porcentajes muy bajos, la mejora vuelve a disminuir. Esto se debe a que para estos valores tan pequeños, el tiempo de procesamiento efectivo de las tareas disminuye y comienza a influir el tiempo necesario para reescalonar las tareas, es decir deja de ser despreciable. Para poder despreciarlo se deben elegir tareas con mayor cantidad de procesamiento, como se mostró en los resultados expuestos en las figuras 3.16(a) y 3.16(b). 3.10.4. Tiempo de procesamiento en función del número de núcleos La figura 3.18 muestra resultados del tiempo de procesamiento para procesadores de uno a cuatro núcleos para tareas con distinta utilización del bus. Nuevamente se observa como el porcentaje de instrucciones que acceden al bus es crı́tico. Dependiendo de la frecuencia de utilización del bus la mejora es más o menos marcada al aumentar el número de núcleos. A medida que disminuye la utilización los tiempos mejoran en mayor medida al aumentar los núcleos de procesamiento. En algunos casos solo se ve que existe una mejora significativa al pasar de uno a dos núcleos y no de dos a cuatro. Esto se debe al lı́mite impuesto por las colisiones en el bus, al aumentar la cantidad de núcleos, las colisiones aumentan. En esta gráfica también tenemos un resultado indeseado para los casos de 2 % y 4 %, también debido al hecho de que el tiempo efectivo de procesamiento disminuye y empieza a influir el tiempo de replanificación de las tareas. Este efecto no se notarı́a si se ejecutasen tareas que realicen mayor trabajo, como se mostró en los resultados de la sección 3.10.2 CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 73 Figura 3.18: Tiempo de ejecución para distintas tareas en función del número de núcleos de procesamiento. Las tareas difieren en el porcentaje de instrucciones que acceden al bus. 3.10.5. Importancia de la memoria cache en procesadores multi-núcleo Se quiere evaluar el rendimiento al correr un programa en el procesador plasma multi-núcleo sin las correspondientes memorias cache, para ellos se corren programas de prueba en implementaciones sin la memoria cache. El resultado es la nula mejora en el rendimiento, independientemente del número de núcleos y del tipo de tarea, ya sea que tenga o no gran porcentaje de instrucciones de acceso al bus. Este resultado es esperado, ya que todos los núcleos acceden al bus por el solo hecho de tener que leer la instrucción que ejecutan. El arbitro del bus permite el acceso al mismo de a un núcleo a la vez pausando al resto. Tener más de un núcleo carece de sentido, ya que el procesamiento efectivo sin memoria cache es igual o inclusive peor. CAPÍTULO 3. IMPLEMENTACIÓN Y RESULTADOS OBTENIDOS 74 Capı́tulo 4 Conclusiones y trabajos futuros 4.1. Conclusiones Se diseñó e implementó un sistema multi-núcleo sobre el cual se realizaron testeos de eficiencia. Los resultados presentados muestran la dependencia del rendimiento con el número de núcleos ,el tipo de tarea que se tenga y la memoria cache. Los factores más determinantes para la obtención o no de una mayor eficiencia al procesar distintas tareas son la utilización del bus y la carga de procesamiento que éstas tengan. Se mostró también que existe un lı́mite en la posible mejora al rendimiento y que puede estar muy alejado del caso teórico ideal. Existen otros factores a evaluar en un futuro, entre ellos como se ve modificado el lı́mite de una posible mejora al implementar otro tipo de memorias cache con otras polı́ticas y que no sea compartida entre instrucciones y datos. Otros trabajos futuros pueden ser la implementación del plasma multi-núcleo, que soporte una cantidad de núcleos que no se necesariamente potencia de dos. También la implementación del monitor del sistema operativo, para controlar la correcta distribución de las tareas que se ejecutan, en ambientes de gran exposición electromagnética, que fue uno de los factores que impulsó este trabajo. Este trabajo presenta una primer implementación del plasma multi-núcleo. Hay una gran cantidad de posibilidades para seguir mejorando el diseño. 4.2. Trabajos Futuros En primer lugar listaremos trabajos futuros que requieren pocas modificaciones a la arquitectura y serı́an casi inmediatas: Modificar el código en las secciones que sean necesarias para poder implementar un número de núcleo que no deba ser necesariamente potencia de dos. 75 CAPÍTULO 4. CONCLUSIONES Y TRABAJOS FUTUROS 76 Cambiar las polı́ticas de escritura de memoria cache, de write-allocate a write-no-allocate, y luego cambiar levemente el algoritmo de coherencia de cache, de modo que el mismo núcleo observe sus propios mensajes de broadcast y actualice la los datos almacenados en memoria cache utilizando el puerto B de la misma, como hacen el resto de los núcleo. Se lograrı́a una leve mejora al no invalidar datos de memoria cache durante escrituras que no sean de 32 bits. Una vez hecho el cambio realizar mediciones y compara el rendimiento frente a la versión actual. Modificar la memoria cache y hacerla únicamente de instrucciones. De esta manera los algoritmos de coherencia de cache pueden ser simplificados, reduciendo ası́ el hardware necesario. Comparar la diferencia de tamaño del procesador y su eficiencia con los de la versión anterior. Modificación de la memoria interna de cada núcleo. La cantidad de núcleos que pueden ser implementados en en las FPGAs, están limitados hoy en dı́a por los bloques de memoria ram diponibles en las mismas. Reduciendo la memoria interna posibilitarı́a incrementar el número de núcleos. Realizar nuevas mediciones. Siguiendo la lı́nea de trabajo que se mencionó durante este trabajo, la implementación de un task scheduler monitor para el plasma multi-núcleo. No precisa ninguna modificación al plasma. Luego realizar las experimentaciones en ambientes de alta interferencia electromagnética y/o bajo radiación nuclear. Otro tipo de desarrollos posibles basados en estre trabajo pueden ser: Implementación de un bus switcheado, incluyendo más de un controlador de memoria. Modificación de la estructura de la cache para que el tamaño de bloque sea parametrizable. Modificación de la estructura de la cache para que sea parametrizable la asociatividad. Implementación de polı́tica de escritura en memoria del tipo Write Back. Separación de memoria caché en instrucciones y datos. Implementación de la MMU (Memory Management Unit). Implementación de la FPU (Floating Point Unit). Mejoras y vectorización del manejo de interrupciones y excepciones (NVIC). Hoy en dı́a hay al menos otros cuatro estudiantes que siguen la lı́nea de trabajo del plama múlti-núcleo en sus trabajos de tesis de grado. Ellos tratarán algunos de los temas anteriormente nombrados. Bibliografı́a [1] Michael J. Flynn, Computer Architecture. Jones & Bartlett Learning, 1995. [2] John L. Hennessy, David A. Patterson Computer Architecture - A Quantitative Approach, 4th Edition. Morgan Kaufman Publishers, 2007. [3] David A. Patterson, John L. Hennessy Computer Organization and Design - The Hardware/Software Interface, 3rd Edition. Morgan Kaufman Publishers, 2005. [4] Procesador Plasma y PlasmaOS: http://opencores.org/project,plasma [5] Jan M. Rabaey, Anantha Chandrakasan, and Borivoje Nikolic Digital Integrated Circuits, A Design Perspective, 2nd Edition, Prentice-Hall [6] Arquitectura MIPS: http://www.mips.com/ [7] David E. Culler, Jaswinder Pal Singh, Parallel Computer Architecture: A Hardware/Software Approach 1998. [8] J. Tarrillo, L. Bolzani, F. Vargas, A Hardware-Scheduler for Fault Detection in RTOS-Based Embedded Systems 2009. [9] D. Silva, K. Stangherlin, L. Bolzani, F. Vargas, A Hardware-Based Approach to Improve the Reliability of RTOS-Based Embedded Systems 2011. [10] Andrew S. Tanenbaum, Modern Operating Systems, 2nd edition. Prentice Hall, 2002. [11] Aritmética modular, http://en.wikipedia.org/wiki/Modular arithmetic, Wikipedia. [12] Benchmarking, http://es.wikipedia.org/wiki/Benchmark, Wikipedia. 77 BIBLIOGRAFÍA 78 Apéndice A Implementaciones de spinLocks A.1. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Con soporte de hardware u i n t 3 2 OS SpinLock ( void ) { uint32 state ; u i n t 3 2 cpuIndex = OS CpuIndex ( ) ; s t a t e = OS A sm I nt er r up t En ab l e ( 0 ) ; // d i s a b l e i n t e r r u p t s i f ( MemoryRead ( 0 x200000A0 ) == (1<<cpuIndex ) ) return ( u i n t 3 2 ) −1; MemoryWrite ( 0 x200000A0 , 0 x f f f f f f f f ) ; // l o c k R e q u e s t f o r ( ; ; ) // w a i t f o r l o c k { i f ( MemoryRead ( 0 x200000A0 ) == (1<<cpuIndex ) ) break ; } a s s e r t ((1<<OS CpuIndex ( ) )==MemoryRead ( 0 x200000A0 ) ) return s t a t e ; } void OS SpinUnlock ( u i n t 3 2 s t a t e ) { a s s e r t ((1<<OS CpuIndex ( ) )==MemoryRead ( 0 x200000A0 ) ) i f ( s t a t e == ( u i n t 3 2 ) −1) return ; // n e s t e d l o c k c a l l MemoryWrite ( 0 x200000A0 , 0 x0 ) ; // c l e a r l o c k R e q u e s t OS A sm I nt e rr up t En ab l e ( s t a t e ) ; // r e s t o r e i n t e r r u p t s } 79 APÉNDICE A. IMPLEMENTACIONES DE SPINLOCKS A.2. 80 Solución propuesta por Peterson Es una solución por software mostrada en [10], propuesta por G. L. Peterson en el año 1981. La solución es más simple que las que existente en esa época. El código presentado a continuación es identico al del libro, a pesar de que parezca genérico sólo funciona para dos núcleos. Cada proceso debı́a llamar a la función entre region, antes de utilizar un recurso compartido, y a la función leave region al terminar la utilización del mismo. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #DEFINE FALSE 0 #DEFINE TRUE 1 #DEFINE N 2 /∗ number o f p r o c e s s e s ∗/ int turn ; i n t i n t e r e s t e d [N ] ; /∗ whose t u r n i s t i ? ∗/ /∗ a l l v a l u e s i n i t a l l y 0 (FALSE) ∗/ 16 17 18 19 20 } 21 void e n t e r r e g i o n ( i n t p r o c e s s ) /∗ p r o c e s s i s 0 o r 1 ∗/ { int other ; /∗ number o f t h e o t h e r p r o c e s s ∗/ o t h e r = 1− p r o c e s s ; /∗ t h e o p p o s i t e o f p r o c e s s ∗/ interested [ process ] /∗ show t h a t you a r e i n t e r e s t e d ∗/ turn = p r o c e s s ; /∗ s e t f l a g ∗/ while ( t u r n == p r o c e s s && i n t e r e s t e d [ o t h e r ] == TRUE) ; /∗ n u l l s t a t e m e n t ∗/ void l e a v e r e g i o n ( i n t p r o c e s s ) { i n t e r e s t e d [ p r o c e s s ] = FALSE ; c r i t i c a l r e g i o n ∗/ } A.3. /∗ p r o c e s s : who i s l e a v i n g ∗/ /∗ i n d i c a t e s d e p a r t u r e from Solución propuesta por el creador del PlasmaOS Aseguro que no halla más de un proceso en una regione crı́tica y pero no se puede asegurar nada sobre su rendimiento y tampoco de que no caiga en un deadlock. Un ejemplo propuesto es el siguiente: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 u i n t 3 2 OS SpinLock ( void ) { u i n t 3 2 s t a t e , cpuIndex , i , ok , d e l a y ; v o l a t i l e u i n t 3 2 keepVar ; cpuIndex = OS CpuIndex ( ) ; s t a t e = OS A sm I nt er r up t En ab l e ( 0 ) ; i f ( SpinLockArray [ cpuIndex ] ) return ( u i n t 3 2 ) −1; d e l a y = ( 4 + cpuIndex ) << 2 ; // d i s a b l e i n t e r r u p t s // a l r e a d y l o c k e d // Spin u n t i l o n l y t h i s CPU has t h e s p i n l o c k for ( ; ; ) { ok = 1 ; SpinLockArray [ cpuIndex ] = 1 ; APÉNDICE A. IMPLEMENTACIONES DE SPINLOCKS 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 f o r ( i = 0 ; i < OS CPU COUNT; ++i ) { i f ( i != cpuIndex && SpinLockArray [ i ] ) ok = 0 ; //Another CPU has t h e s p i n l o c k } i f ( ok ) return s t a t e ; SpinLockArray [ cpuIndex ] = 0 ; OS A sm I nt er r up tE n ab l e ( s t a t e ) ; // re −e n a b l e i n t e r r u p t s f o r ( i = 0 ; i < d e l a y ; ++i ) // w a i t a b i t ++ok ; keepVar = ok ; //don ’ t o p t i m i z e away t h e d e l a y l o o p i f ( delay < 128) d e l a y <<= 1 ; s t a t e = OS A sm I nt e rr up t En ab l e ( 0 ) ; // d i s a b l e i n t e r r u p t s } } v o i d OS SpinUnlock ( u i n t 3 2 s t a t e ) { u i n t 3 2 cpuIndex ; i f ( s t a t e == ( u i n t 3 2 ) −1) return ; cpuIndex = OS CpuIndex ( ) ; SpinLockArray [ cpuIndex ] = 0 ; } // n e s t e d l o c k c a l l 81