Un sistema operativo en tiempo real ( RTOS ) es un sistema operativo (OS) para aplicaciones informáticas en tiempo real que procesa datos y eventos que tienen restricciones de tiempo definidas críticamente. Un RTOS es distinto de un sistema operativo de tiempo compartido , como Unix, que administra el uso compartido de los recursos del sistema con un programador, búferes de datos o priorización de tareas fijas en entornos multitarea o multiprogramación. Todas las operaciones deben completarse de manera verificable dentro de las restricciones de tiempo y recursos dadas o, de lo contrario, fallarán de manera segura . Los sistemas operativos en tiempo real están impulsados por eventos y son preventivos , lo que significa que el SO puede monitorear la prioridad relevante de las tareas en competencia y realizar cambios en la prioridad de la tarea. Los sistemas impulsados por eventos cambian entre tareas según sus prioridades, mientras que los sistemas de tiempo compartido cambian la tarea según las interrupciones del reloj . [1]
Una característica clave de un sistema operativo en tiempo real (RTOS) es el nivel de su consistencia en relación con la cantidad de tiempo que lleva aceptar y completar la tarea de una aplicación ; la variabilidad es " jitter ". [2] Un sistema operativo en tiempo real "duro" (hard RTOS) tiene menos jitter que un sistema operativo en tiempo real "suave" (soft RTOS); una respuesta tardía es una respuesta incorrecta en un RTOS duro, mientras que una respuesta tardía es aceptable en un RTOS blando. El objetivo principal del diseño no es un alto rendimiento , sino más bien una garantía de una categoría de rendimiento blando o duro . Un RTOS que puede cumplir habitualmente o en general con una fecha límite es un SO en tiempo real blando, pero si puede cumplir con una fecha límite de manera determinista, es un SO en tiempo real duro. [3]
Un sistema operativo en tiempo real tiene un algoritmo avanzado para la programación . La flexibilidad del programador permite una orquestación más amplia de las prioridades de los procesos por parte del sistema informático, pero un sistema operativo en tiempo real se dedica con más frecuencia a un conjunto reducido de aplicaciones. Los factores clave de un sistema operativo en tiempo real son la latencia mínima de interrupción y la latencia mínima de cambio de subproceso ; un sistema operativo en tiempo real se valora más por la rapidez o la previsibilidad con la que puede responder que por la cantidad de trabajo que puede realizar en un período de tiempo determinado. [4]
Un RTOS es un sistema operativo en el que el tiempo necesario para procesar un estímulo de entrada es menor que el tiempo transcurrido hasta el siguiente estímulo de entrada del mismo tipo.
Los diseños más comunes son:
Los diseños de tiempo compartido cambian de tareas con más frecuencia de lo estrictamente necesario, pero ofrecen una multitarea más fluida , generando la ilusión de que un proceso o usuario tiene uso exclusivo de una máquina.
Los primeros diseños de CPU necesitaban muchos ciclos para cambiar de tarea durante los cuales la CPU no podía hacer nada más útil. Como el cambio llevaba tanto tiempo, los primeros sistemas operativos intentaron minimizar el desperdicio de tiempo de CPU evitando el cambio de tareas innecesario.
En diseños típicos, una tarea tiene tres estados:
La mayoría de las tareas están bloqueadas o listas la mayor parte del tiempo porque, generalmente, solo se puede ejecutar una tarea a la vez por núcleo de CPU . La cantidad de elementos en la cola de tareas listas puede variar en gran medida, según la cantidad de tareas que el sistema necesita realizar y el tipo de programador que utiliza el sistema. En sistemas más simples, no preemptivos pero que aún realizan múltiples tareas, una tarea tiene que ceder su tiempo en la CPU a otras tareas, lo que puede hacer que la cola de tareas listas tenga una mayor cantidad de tareas generales en el estado listo para ejecutarse ( falta de recursos ).
Generalmente, la estructura de datos de la lista de tareas listas en el programador está diseñada para minimizar el tiempo de espera en el peor de los casos en la sección crítica del programador, durante el cual se inhibe la preempción y, en algunos casos, se deshabilitan todas las interrupciones, pero la elección de la estructura de datos depende también del número máximo de tareas que pueden estar en la lista de tareas listas.
Si nunca hay más de unas pocas tareas en la lista de tareas listas, entonces una lista doblemente enlazada de tareas listas es probablemente la mejor opción. Si la lista de tareas listas generalmente contiene solo unas pocas tareas pero ocasionalmente contiene más, entonces la lista debe ordenarse por prioridad, de modo que encontrar la tarea con mayor prioridad para ejecutar no requiera recorrer la lista. En cambio, insertar una tarea requiere recorrer la lista.
Durante esta búsqueda, no se debe inhibir la prelación. Las secciones críticas largas se deben dividir en partes más pequeñas. Si se produce una interrupción que hace que una tarea de alta prioridad esté lista durante la inserción de una tarea de baja prioridad, esa tarea de alta prioridad se puede insertar y ejecutar inmediatamente antes de que se inserte la tarea de baja prioridad.
El tiempo crítico de respuesta, a veces denominado tiempo de retorno, es el tiempo que se tarda en poner en cola una nueva tarea lista y restaurar el estado de ejecución de la tarea de mayor prioridad. En un sistema operativo en tiempo real bien diseñado, preparar una nueva tarea requerirá de 3 a 20 instrucciones por entrada en la cola de tareas listas, y restaurar la tarea lista de mayor prioridad requerirá de 5 a 30 instrucciones.
En sistemas avanzados, las tareas en tiempo real comparten recursos informáticos con muchas tareas que no son en tiempo real, y la lista de tareas listas puede ser arbitrariamente larga. En tales sistemas, una lista de tareas listas del planificador implementada como una lista enlazada sería inadecuada.
Algunos algoritmos de programación RTOS comúnmente utilizados son: [5]
Un sistema operativo multitarea como Unix es deficiente en tareas en tiempo real. El programador otorga la máxima prioridad a los trabajos con menor demanda de la computadora, por lo que no hay forma de garantizar que un trabajo crítico en términos de tiempo tenga acceso a suficientes recursos. Los sistemas multitarea deben administrar el uso compartido de datos y recursos de hardware entre múltiples tareas. Por lo general, no es seguro que dos tareas accedan al mismo dato específico o recurso de hardware simultáneamente. [6] Existen tres enfoques comunes para resolver este problema:
Los sistemas operativos de propósito general no suelen permitir que los programas de usuario enmascaren (desactiven) las interrupciones , porque el programa de usuario podría controlar la CPU durante el tiempo que se le pida. Algunas CPU modernas no permiten que el código de modo de usuario desactive las interrupciones, ya que dicho control se considera un recurso clave del sistema operativo. Sin embargo, muchos sistemas integrados y sistemas operativos en tiempo real permiten que la propia aplicación se ejecute en modo kernel para lograr una mayor eficiencia de las llamadas al sistema y también para permitir que la aplicación tenga un mayor control del entorno operativo sin requerir la intervención del sistema operativo.
En sistemas de un solo procesador, una aplicación que se ejecuta en modo kernel y enmascara las interrupciones es el método con menor sobrecarga para evitar el acceso simultáneo a un recurso compartido. Si bien las interrupciones están enmascaradas y la tarea actual no realiza una llamada de bloqueo del sistema operativo, la tarea actual tiene uso exclusivo de la CPU ya que ninguna otra tarea o interrupción puede tomar el control, por lo que la sección crítica está protegida. Cuando la tarea sale de su sección crítica, debe desenmascarar las interrupciones; las interrupciones pendientes, si las hay, se ejecutarán. El enmascaramiento temporal de las interrupciones solo se debe realizar cuando la ruta más larga a través de la sección crítica es más corta que la latencia de interrupción máxima deseada . Por lo general, este método de protección se usa solo cuando la sección crítica tiene solo unas pocas instrucciones y no contiene bucles. Este método es ideal para proteger registros de mapa de bits de hardware cuando los bits están controlados por diferentes tareas.
Cuando el recurso compartido debe reservarse sin bloquear todas las demás tareas (como esperar a que se escriba en la memoria Flash), es mejor utilizar mecanismos que también están disponibles en sistemas operativos de propósito general, como un mutex y mensajes entre procesos supervisados por el SO. Estos mecanismos implican llamadas al sistema y, por lo general, invocan el código de despachador del SO al salir, por lo que suelen requerir cientos de instrucciones de CPU para ejecutarse, mientras que el enmascaramiento de interrupciones puede requerir tan solo una instrucción en algunos procesadores.
Un mutex (no recursivo) puede estar bloqueado o desbloqueado. Cuando una tarea ha bloqueado el mutex, todas las demás tareas deben esperar a que su propietario (el hilo original) desbloquee el mutex. Una tarea puede establecer un tiempo de espera para un mutex. Existen varios problemas bien conocidos con los diseños basados en mutex, como la inversión de prioridad y los interbloqueos .
En la inversión de prioridad, una tarea de alta prioridad espera porque una tarea de baja prioridad tiene un mutex, pero a la tarea de menor prioridad no se le da tiempo de CPU para terminar su trabajo. Una solución típica es hacer que la tarea que posee un mutex "herede" la prioridad de la tarea que espera más alta. Pero este enfoque simple se vuelve más complejo cuando hay múltiples niveles de espera: la tarea A espera un mutex bloqueado por la tarea B , que espera un mutex bloqueado por la tarea C . Manejar múltiples niveles de herencia hace que otro código se ejecute en un contexto de alta prioridad y, por lo tanto, puede causar la inanición de subprocesos de prioridad media.
En un interbloqueo , dos o más tareas bloquean un mutex sin tiempos de espera y luego esperan eternamente el mutex de la otra tarea, lo que crea una dependencia cíclica. El escenario de interbloqueo más simple ocurre cuando dos tareas bloquean alternativamente dos mutex, pero en el orden opuesto. El interbloqueo se evita mediante un diseño cuidadoso.
El otro enfoque para compartir recursos es que las tareas envíen mensajes en un esquema organizado de paso de mensajes . En este paradigma, el recurso es administrado directamente por una sola tarea. Cuando otra tarea desea interrogar o manipular el recurso, envía un mensaje a la tarea administradora. Aunque su comportamiento en tiempo real es menos preciso que el de los sistemas de semáforos , los sistemas simples basados en mensajes evitan la mayoría de los peligros de bloqueo de protocolo y, en general, se comportan mejor que los sistemas de semáforos. Sin embargo, es posible que surjan problemas como los de los semáforos. La inversión de prioridad puede ocurrir cuando una tarea está trabajando en un mensaje de baja prioridad e ignora un mensaje de mayor prioridad (o un mensaje que se origina indirectamente de una tarea de alta prioridad) en su cola de mensajes entrantes. Los bloqueos de protocolo pueden ocurrir cuando dos o más tareas esperan que la otra envíe mensajes de respuesta.
Dado que un controlador de interrupciones bloquea la ejecución de la tarea de mayor prioridad y que los sistemas operativos en tiempo real están diseñados para mantener la latencia de los subprocesos al mínimo, los controladores de interrupciones suelen ser lo más breves posible. El controlador de interrupciones posterga toda interacción con el hardware si es posible; normalmente, todo lo que se necesita es reconocer o deshabilitar la interrupción (para que no vuelva a ocurrir cuando el controlador de interrupciones regrese) y notificar a una tarea que es necesario realizar un trabajo. Esto se puede hacer desbloqueando una tarea del controlador mediante la liberación de un semáforo, la configuración de una bandera o el envío de un mensaje. Un programador a menudo proporciona la capacidad de desbloquear una tarea del contexto del controlador de interrupciones.
Un sistema operativo mantiene catálogos de objetos que administra, como subprocesos, mutexes, memoria, etc. Las actualizaciones de este catálogo deben controlarse estrictamente. Por este motivo, puede resultar problemático que un controlador de interrupciones llame a una función del sistema operativo mientras la aplicación también lo está haciendo. La función del sistema operativo llamada desde un controlador de interrupciones podría encontrar que la base de datos de objetos está en un estado inconsistente debido a la actualización de la aplicación. Existen dos enfoques principales para abordar este problema: la arquitectura unificada y la arquitectura segmentada. Los RTOS que implementan la arquitectura unificada resuelven el problema simplemente deshabilitando las interrupciones mientras se actualiza el catálogo interno. La desventaja de esto es que aumenta la latencia de las interrupciones, lo que potencialmente hace que se pierdan interrupciones. La arquitectura segmentada no realiza llamadas directas al sistema operativo, sino que delega el trabajo relacionado con el sistema operativo a un controlador independiente. Este controlador se ejecuta con una prioridad más alta que cualquier subproceso, pero menor que los controladores de interrupciones. La ventaja de esta arquitectura es que agrega muy pocos ciclos a la latencia de las interrupciones. Como resultado, los sistemas operativos que implementan la arquitectura segmentada son más predecibles y pueden lidiar con tasas de interrupción más altas en comparación con la arquitectura unificada. [ cita requerida ]
De manera similar, el modo de administración del sistema en hardware compatible con x86 puede tardar mucho tiempo antes de devolver el control al sistema operativo.
La asignación de memoria es más crítica en un sistema operativo en tiempo real que en otros sistemas operativos.
En primer lugar, por razones de estabilidad, no puede haber fugas de memoria (memoria que se asigna pero no se libera después de su uso). El dispositivo debe funcionar indefinidamente, sin necesidad de reiniciarlo. [ cita requerida ] Por este motivo, la asignación dinámica de memoria está mal vista. [ cita requerida ] Siempre que sea posible, toda la asignación de memoria requerida se especifica de forma estática en el momento de la compilación.
Otra razón para evitar la asignación dinámica de memoria es la fragmentación de la memoria. Con la asignación y liberación frecuente de pequeños fragmentos de memoria, puede ocurrir una situación en la que la memoria disponible se divida en varias secciones y el RTOS no pueda asignar un bloque continuo de memoria lo suficientemente grande, aunque haya suficiente memoria libre. En segundo lugar, la velocidad de asignación es importante. Un esquema de asignación de memoria estándar escanea una lista enlazada de longitud indeterminada para encontrar un bloque de memoria libre adecuado, [7] lo cual es inaceptable en un RTOS ya que la asignación de memoria debe ocurrir dentro de una cierta cantidad de tiempo.
Debido a que los discos mecánicos tienen tiempos de respuesta mucho más largos e impredecibles, el intercambio de archivos de disco no se utiliza por las mismas razones que la asignación de RAM analizada anteriormente.
El algoritmo simple de bloques de tamaño fijo funciona bastante bien para sistemas integrados simples debido a su baja sobrecarga.