06 julio 2012

Interrupciones rutinarias

Eres un máquina. Has fabricado un robot autónomo tú solito, y lo has enviado a Marte a explorar la geografía marciana. Programaste el robot con una serie de instrucciones sencillas, para asegurarte de que todo funciona correctamente. El robot está programado para hacer lo siguiente:

  1. Cada segundo comprueba los sensores de distancia en la parte delantera
    1. Si no hay obstáculo enfrente, continúa hacia delante.
    2. Si hay obstáculo, detente y realiza maniobra de evasión del obstáculo.
  2. Cada minuto guarda información en la memoria sobre la orografía marciana.

Felizmente, el robot se pasea por los cráteres de Marte durante unos 10 minutos, constantemente mirando los sensores y cada minuto actualizando la información recién descubierta sobre el planeta rojo. De repente, mientras el robot está guardando los datos en su disco duro, ¡zas! Se come una piedra. El sensor de distancia delantero ha quedado inservible, así que el robot intenta continuar con su ruta, pero cada pocos metros se choca contra otra piedra, porque el pobre ni las ve venir. Después de varias colisiones a toda velocidad los daños mecánicos son irreparables, y llegado cierto punto el robot no es capaz de continuar su tarea. Imagínate que, después de todo, sí que hay vida en Marte; lo último que quieres es que se hagan con nuestra más avanzada tecnología para después mejorarla y usarla contra nosotros. Incapaz de servir su cometido, entonces, la única posibilidad para el robot es el suicidio. Comienza la secuencia de autodestrucción y... ¡Boom! La maquinaria vuela por los aires. Después de semejante explosión, los habitantes nativos de Marte que, vista la aparentemente pacífica conducta de los humanos en Marte hasta el momento, se limitaban a esconderse de nosotros, ven el evento como un claro acto bélico y comienzan una guerra interplanetaria que acaba destruyendo la Tierra. El fin de nuestra civilización tal y como la conocemos.

Así es como acaban miles de años de civilización. Licencia CC Wikimedia Commons.

Pues sí que te ha cundido el robot, campeón... Te preguntarás, ¿qué ha pasado? Me alegro de que te hagas semejante pregunta, ¡deja que te refresque la memoria! El problema comenzó cuando el robot se estrelló contra la primera piedra. A partir de ahí, sin sensor de distancia, no había nada que el robot pudiera hacer para evitar ser víctima de la catástrofe. Pero, ¿por qué se chocó contra la piedra en primer lugar, si está constantemente revisando el sensor de distancia para detectar obstáculos? Como el robot estaba guardando la información en la memoria, no podía simultáneamente revisar los sensores y actualizar la información en la memoria. Por eso, el corto espacio de tiempo que transcurre hasta que la información se ha guardado pudo ser suficiente para no detectar el obstáculo y merendárselo. En este momento, puede que tengas la genial idea de, en la próxima versión del robot, incluir un procesador que soporte varios hilos de ejecución. Usando multitarea real, pensarás, no puede darse semejante problema.

Lo primero, ya has provocado la destrucción masiva de la Tierra, so inútil, así que ya me contarás cómo vas a fabricar otro robot si, en el mejor de los casos, no estás muerto sino eres esclavo de los marcianos. Lo segundo, tener varios hilos de ejecución no te garantiza la ejecución a tiempo de ninguno de los hilos. Si el procesador se queda colgado mientras guarda la información en memoria y el sistema deja de responder, o el guardar la información usa más de un hilo de ejecución, la colisión sería también inevitable. Entonces, ¿hay alguna manera de evitar la colisión y la consecuente destrucción de nuestro planeta? La respuesta es sí, y se puede resumir en una palabra: interruptores.

Interruptor de un procesador visto bajo un microscopio de alta resolución. Licencia CC Wikimedia Commons.

Una interrupción es una operación realizada por el hardware del procesador capaz de parar la ejecución de todas las tareas para atender dicha interrupción. Por ejemplo, si estás trabajando en tu escritorio puedes estar completamente concentrado en una tarea en concreto, como lo estaba el robot cuando guardaba información en la memoria pero, de repente, recibes una interrupción, como una urgente llamada telefónica. No importa cómo de concentrado estuvieras, serás capaz de atender la llamada y responder consecuentemente. Una vez haya terminado la llamada, podrás continuar la tarea que estabas haciendo anteriormente. El concepto detrás de las interrupciones es el mismo, el procesador detendrá cualquier ejecución de código en un determinado momento para atender la interrupción. Basados en este modelo, el robot podría tener las siguientes instrucciones:

  1. Cada segundo, interrumpe la ejecución de código para comprobar los sensores.
    1. Si no hay obstáculo enfrente, continúa hacia delante.
    2. Si hay obstáculo, detente y realiza maniobra de evasión del obstáculo.
  2. Cada minuto actualiza la información en la memoria interna sobre la orografía marciana.

Con esa configuración, no importa cómo de colgado se quede el robot mientras accede la memoria, tenemos garantizado que, al saltar la interrupción, parará lo que esté haciendo y ejecutará la lógica correspondiente a la interrupción.

Sólo nos queda un último problema por resolver, ¿qué pasa si la lógica del interruptor no es algo trivial? En otras palabras, ¿qué pasa si para responder la llamada urgente tenemos que mantener el teléfono descolgado mucho tiempo? Puede que durante el tiempo que está el teléfono descolgado recibas otra llamada, pero no puedas contestarla porque tienes la línea ocupada. La solución lógica, entonces, sería guardar en memoria la información mínima necesaria para saber qué tarea debemos ejecutar para responder a la interrupción, y ejecutar dicha tarea después de salir de la ejecución. En términos del anterior ejemplo, sería como anotar las acciones requeridas para responder la llamada urgente, colgar el teléfono, y entonces trabajar en lo que sea que requiera la urgencia. ¡Pero no puedes ponerte a trabajar  en algo diferente así como así! Recuerda que tienes el escritorio ocupado con lo que estabas trabajando antes y no querrías perder el progreso hecho. Pues el procesador también tiene su escritorio particular, los registros mantienen información esencial de la tarea siendo ejecutada.

Simulación del Spirit explorando marte. Licencia CC Wikimedia Commons.

Es como estuvieses trabajando en algún proyecto de investigación y tuvieras un montón de libros abiertos encima del escritorio, cada uno en una página distinta, con anotaciones en los márgenes y notas adhesivas por todas partes. Si cerrases todos los libros y los guardaras, volver a la tarea en cuestión sería bastante ineficiente y cabe la posibilidad de que pierdas el hilo de por dónde ibas, así que debes encontrar algún método para cambiar de tarea (también llamado cambio de contexto) más eficiente y sin perder el progreso. Hay que guardar la información de los registros antes de que sean reusados por la tarea urgente.

Una metodología muy común es usar la pila de llamadas para guardar toda la información. Por orden, recogerías todos los libros del escritorio y, sin cerrarlos, los apilarías en algún sitio que no molesten; o en una jerga más técnica, empujas el valor de todos los registros a la pila de llamadas y así, mientras recuerdes dónde está la pila, te puedes olvidar temporalmente de ellos. Una vez termines con la tarea urgente, puedes retirar los valores de la pila y devolverlos a los registros para volver al mismo contexto donde estabas antes de atender la interrupción. Si la pila no es lo suficientemente grande para la cantidad de información de los registros entonces se produce el muy temido desbordamiento de pila, pero eso es un caso aparte. La imagen a continuación ilustra el funcionamiento de la pila de manera simplificada:

Representación de una pila de llamadas. Licencia CC Wikimedia Commons.

En resumen, lo que haces es salvar lo que hay en todos los registros en un sitio seguro, para luego devolverlos a su estado original cuando vuelvas a la tarea. Un concepto tan sencillo como ése es lo que permite la creación de un sistema operativo de tiempo real (RTOS por sus siglas en inglés). No importa cómo de ocupado esté el procesador, comenzar una tarea urgente será tan rápido como lo que tardes en empujar los registros a la pila y comenzar la ejecución de la nueva tarea. Ése tiempo será considerado la latencia de interrupción y es una de las partes más críticas a tener en cuenta en un sistema operativo de tiempo real. Con una latencia suficientemente baja, ya puede ser tu robot la máquina más torpe de todos los tiempos que no será pillado de imprevisto por una piedra en su camino; puede que de tan torpe que es no pueda esquivarla, pero seguro que no le pillará por sorpresa. Si no se cumple un requisito mínimo de latencia, se dirá que el sistema ha fallado, puesto que, como bien indica la wikipedia, se espera que un sistema operativo de tiempo real sea determinista bajo cualquier interrupción. Típicamente, no se dará ningún tipo de concurrencia en las tareas. De esa manera, el comportamiento del procesador para responder a cada tarea es mucho más predecible, lo cual es una característica bastante deseable para sistemas de importancia crítica como equipamiento médico, centrales nucleares o la nave espacial recientemente enviada para abastecer a la tripulación de la ISS, el SpaceX Dragon; todos ellos ejemplos de la importancia que los sistemas operativos de tiempo real tienen en nuestras vidas, aunque nunca hayas oído hablar de ellos antes.

No hay comentarios:

Publicar un comentario