Introducción a los SO en tiempo real: Planificación de tareas

En entradas anteriores anteriores introducimos el concepto de Sistema Operativo en tiempo real y echamos a andar nuestro primer programa para hacer parpadear un LED con FreeRTOS y la ESP32. En esta entrada vamos a profundizar un poco en la planificación de tareas y la prioridad que da el sistema a cada una para que puedan funcionar de forma ordenada.

Recordemos por un momento la estructura multitarea que planteamos en la primer entrada. En ella nuestro RTOS ejecutaba una porción de código de configuración y luego pasaba a ejecutar las tareas individuales de forma cíclica. Suponiendo que trabajamos con un microcontrolador, puede que usemos interrupciones adicionales al código principal. Sin embargo, el asunto no es tan simple como lo mostramos en el diagrama y en realidad las tareas se ejecutan en intervalos concretos de tiempo con un criterio de priorización fijo. Vamos a analizar un caso particular para expandir esta idea.

Control de prioridades y uso de procesador

En el primer ejercicio de Blink explicamos que las tareas tienen un valor asignado de prioridad que le permite al administrador de tareas asignarle un turno de ejecución en el procesador, dependiendo de que tan alta sea su prioridad. Algo que coordina el tiempo de ejecución es el denominado ciclo de ejecución o tick. Cada ciclo de ejecución tiene una duración de 1ms, dependiendo cómo se configure la versión de RTOS y lo que hace es interrumpir la tarea en ejecución para llamar al administrador de tareas.

Primer escenario: solo una tarea a ejecutarse

Si no hay una tarea de mayor prioridad se sigue ejecutando la tarea A por un segundo ciclo de ejecución. En este caso la tarea A termina de ejecutarse por lo que para el siguiente ciclo de ejecución no hay una tarea que ejecutar, lo que mantiene al procesador inactivo.

Segundo escenario: dos tareas de mayor prioridad

En un segundo escenario, la Tarea A se ejecuta, pero en vez de terminar de ejecutarse en el segundo ciclo, se pasa a ejecutar la tarea B, de mayor prioridad y cuando el ciclo de ejecución se termina, la tarea B permanece pausada hasta que el administrador de tareas le asigne el ciclo de ejecución. Como la tarea C tiene la misma prioridad que la tarea B, pasa a ejecutarse en el siguiente ciclo y, terminado el ciclo, se vuelve a llamar al administrador de tareas. El administrador vuelve a ejecutar la Tarea B desde el punto en que se pausó, pero en este caso una interrupción externa interrumpió la ejecución, por lo que debe resolverse antes de continuar. Las tareas B y C se turnarán el uso del procesador hasta que terminen de ejecutarse y el administrador de tareas asigne el turno a una tarea de mayor o menor prioridad.

Diagrama temporal que muestra el uso del procesador por distintas tareas A, B y C. Cada fracción de tiempo, llamada «tick» le asigna la tarea al procesador dependiendo la prioridad de la tarea. La tarea A tiene prioridad 0, la más baja y las tareas B y C, 1. La tarea A se ejecuta hasta que tiene que ejecutarse la B y la C. Si se da una interrupción, tienen prioridad sobre todas las tareas hasta que se resuelva la subrutina de servicio. Vía: Digi Key Electronics

Como se puede observar, la ejecución de las tareas no es técnicamente simultánea, como se haría en un procesador multinúcleo. En cambio, se van repartiendo el uso del procesador dependiendo de su prioridad. Su ejecución se va llevando a cabo de forma pausada hasta que se termina la tarea. Esto permite administrar recursos entre varias tareas, evitando que se ejecuten de forma progresiva, haciendo más eficiente el programa.

Los estados que puede tomar una tarea

Una tarea puede tener distintos estados que le indican al administrador de tareas si debe ejecutarla o no. Si debe esperar al siguiente ciclo de ejecución, o debe bloquearse para que no se ejecute en dicho ciclo. Esto permite administrar el uso del procesador entre las distintas tareas, dependiendo de la prioridad que se le haya asignado.

  • Ready (Lista): Cuando se crea una tarea se obtiene este estado por defecto. Si no existe una tarea con mayor prioridad se ejecuta esta tarea. En caso que exista una tarea de mayor prioridad se mantiene como «Lista» hasta que pueda ser ejecutada.
  • Running (Corriendo): Cuando la prioridad de la tarea lo permite, el administrador pasa la tarea de «Lista» a Corriendo. A medida que evoluciona el programa puede volver al estado de «Lista». Si el administrador ejecuta un API que bloquee la tarea, se cambia a «Bloqueada»
  • Blocked (Bloqueada): En este estado la tarea no se ejecuta en el ciclo de programa. La tarea espera hasta que se desbloquee y pase al estado «Lista» para poder ejecutarla.
  • Suspended (Suspendida): Este estado es propio de FreeRTOS y a diferencia del estado «Bloqueada» puede llamarse a la función vTaskSuspend(). Otra diferencia es que permite poner a «dormir» tareas sin depender de un temporizador de bloqueo, con solo usar la función vTaskResume()

El cambio te contexto

Otro aspecto importante que debe cumplir el administrador de tareas es guardar el estado en el que se encontraba la tarea una vez se terminó de ejecutar para que, cuando se ejecute de nuevo, no inicie desde cero. Es decir, debe guardar las variables estados de registros y valores en RAM. A esto lo llamamos «cambio de contexto» o context switching.

Conclusiones:

En este tutorial revisamos como se ejecutan las tareas y como el administrador las selecciona para pasar al siguiente ciclo de ejecución. Es importante identificar ese proceso de selección y turnado de tareas para que el diseño del programa sea adecuado. Las necesidades del dispositivo a implementar determinarán la prioridad de cada tarea.

Referencias:

Introduction to RTOS Part 3 – Task Scheduling | Digi-Key Electronics