¿Cuáles son los beneficios de un sistema operativo no preventivo? y el precio de estos beneficios?

En el caso de un MCU básico, en comparación con el código casero con bucle de fondo más arquitectura de interrupción del temporizador, ¿cuáles son los beneficios de un sistema operativo no preventivo? ¿Cuáles de estos beneficios son lo suficientemente atractivos para que un proyecto adopte un sistema operativo no preventivo, en lugar de usar código casero con arquitectura de bucle de fondo?
.

Explicación a la pregunta:

Realmente aprecio a todos los que han respondido a mi pregunta. Siento que la respuesta ha estado casi allí. Agrego esta explicación a mi pregunta aquí, que muestra mi propia consideración y puede ayudar a reducir la pregunta o hacerla más precisa.

Lo que estoy tratando de hacer es entender cómo elegir el RTOS más apropiado para un proyecto en general.
Para lograr esto, ayudará una mejor comprensión de los conceptos básicos y los beneficios más atractivos de los diferentes tipos de RTOS y el precio correspondiente, ya que no existe el mejor RTOS para todas las aplicaciones.
Leí libros sobre SO hace unos años, pero ya no los tengo conmigo. Busqué en Internet antes de publicar mi pregunta aquí y encontré que esta información fue muy útil: http://www.ustudy.in/node/5456 .
Hay mucha otra información útil, como las introducciones en el sitio web de diferentes RTOS, artículos que comparan la programación preventiva y la programación no preventiva, etc.
Pero no encontré ningún tema mencionado sobre cuándo elegir un RTOS no preventivo y cuándo es mejor simplemente escribir su propio código usando la interrupción del temporizador y el bucle de fondo.
Tengo ciertas respuestas propias, pero no estoy lo suficientemente satisfecho con ellas.
Realmente me gustaría saber la respuesta o la opinión de personas con más experiencia, especialmente en la práctica de la industria.

Mi entendimiento hasta ahora es:
no importa si usa o no un sistema operativo, siempre es necesario cierto tipo de códigos de programación, incluso en forma de código como:

    in the timer interrupt which occurs every 10ms  
    if(it's 10ms)  
    {  
      call function A / execute task A;  
    }  
    if(it's 50ms)  
    {  
      call function B / execute task B;  
    }  

Beneficio 1:
un sistema operativo no preventivo designa la forma/estilo de programación para el código de programación, de modo que los ingenieros puedan compartir la misma vista incluso si no estaban en el mismo proyecto antes. Luego, con la misma vista sobre la tarea conceptual, los ingenieros pueden trabajar en diferentes tareas y probarlas, perfilarlas de forma independiente tanto como sea posible.
Pero, ¿cuánto podemos ganar realmente con esto? Si los ingenieros están trabajando en el mismo proyecto, pueden encontrar la manera de compartir bien la misma vista sin usar un sistema operativo no preventivo.
Si un ingeniero es de otro proyecto o empresa, obtendrá el beneficio si conocía el sistema operativo antes. Pero si no lo hizo, de nuevo, parece no hacer una gran diferencia para él aprender un nuevo sistema operativo o una nueva pieza de código.

Beneficio 2:
si el código del sistema operativo se ha probado bien, se ahorra tiempo de depuración. Esto es realmente un buen beneficio.
Pero si la aplicación tiene solo alrededor de 5 tareas, creo que no es realmente complicado escribir su propio código usando la interrupción del temporizador y el bucle de fondo.

Un sistema operativo no preventivo aquí se refiere a un sistema operativo comercial/gratuito/heredado con un programador no preventivo.
Cuando publiqué esta pregunta, pensé principalmente en ciertos sistemas operativos como:
(1) KISS Kernel (un pequeño RTOS no preventivo, reclamado por su sitio web)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (RTOS ligero, reclamado por su sitio web)
(3) FreeRTOS (es un RTOS preventivo, pero según tengo entendido, también se puede configurar como un RTOS no preventivo)
(4) uC/OS (similar a FreeRTOS)
(5 ) SO heredado/código del programador en algunas empresas (generalmente creado y mantenido internamente por la empresa)
(No se pueden agregar más enlaces debido a la limitación de la nueva cuenta de StackOverflow)

Según tengo entendido, un sistema operativo no preventivo es una colección de estos códigos:
(1) un programador que utiliza una estrategia no preventiva.
(2) facilidades para comunicación entre tareas, mutex, sincronización y control de tiempo.
(3) gestión de la memoria.
(4) otras instalaciones/bibliotecas útiles como sistema de archivos, pila de red, GUI, etc. (FreeRTOS y uC/OS las proporcionan, pero no estoy seguro de que sigan funcionando cuando el programador está configurado como no preventivo)
Algunas de ellos no siempre están ahí. Pero el planificador es imprescindible.

Eso es más o menos todo en pocas palabras. Si tiene una carga de trabajo que necesita subprocesos múltiples y puede pagar los gastos generales, use un sistema operativo de subprocesos. De lo contrario, en la mayoría de los casos es suficiente un simple "programador" basado en tiempo o tareas. Y para averiguar si la multitarea preventiva o cooperativa es mejor... Supongo que se trata de gastos generales y cuánto control desea tener sobre la multitarea que necesita hacer.

Respuestas (3)

Esto huele un poco fuera de tema, pero intentaré volver a encarrilarlo.

La multitarea preventiva significa que el sistema operativo o el kernel pueden suspender el subproceso que se está ejecutando actualmente y cambiar a otro en función de las heurísticas de programación que tenga implementadas. La mayoría de las veces, los subprocesos que se ejecutan no tienen idea de que están sucediendo otras cosas en el sistema, y ​​lo que esto significa para su código es que debe tener cuidado al diseñarlo de modo que si el kernel decide suspender un subproceso en medio de un operación de varios pasos (por ejemplo, cambiar una salida PWM, seleccionar un nuevo canal ADC, leer el estado de un periférico I2C, etc.) y dejar que otro subproceso se ejecute durante un tiempo, para que estos dos subprocesos no interfieran entre sí.

Un ejemplo arbitrario: supongamos que es nuevo en los sistemas integrados de subprocesos múltiples y tiene un pequeño sistema con un ADC I2C, una pantalla LCD SPI y una EEPROM I2C. Usted decidió que sería una buena idea tener dos subprocesos: uno que lea desde el ADC y escriba muestras en la EEPROM, y otro que lea las últimas 10 muestras, las promedie y las muestre en la pantalla LCD SPI. El diseño sin experiencia se vería así (muy simplificado):

char i2c_read(int i2c_address, char databyte)
{
    turn_on_i2c_peripheral();
    wait_for_clock_to_stabilize();

    i2c_generate_start();
    i2c_set_data(i2c_address | I2C_READ);
    i2c_go();
    wait_for_ack();
    i2c_set_data(databyte);
    i2c_go();
    wait_for_ack();
    i2c_generate_start();
    i2c_get_byte();
    i2c_generate_nak();
    i2c_stop();
    turn_off_i2c_peripheral();
}

char i2c_write(int i2c_address, char databyte)
{
    turn_on_i2c_peripheral();
    wait_for_clock_to_stabilize();

    i2c_generate_start();
    i2c_set_data(i2c_address | I2C_WRITE);
    i2c_go();
    wait_for_ack();
    i2c_set_data(databyte);
    i2c_go();
    wait_for_ack();
    i2c_generate_start();
    i2c_get_byte();
    i2c_generate_nak();
    i2c_stop();
    turn_off_i2c_peripheral();
}

adc_thread()
{
    int value, sample_number;

    sample_number = 0;

    while (1) {
        value = i2c_read(ADC_ADDR);
        i2c_write(EE_ADDR, EE_ADDR_REG, sample_number);
        i2c_write(EE_ADDR, EE_DATA_REG, value);

        if (sample_number < 10) {
            ++sample_number;
        } else {
            sample_number = 0;
        }
    };
}

lcd_thread()
{
    int i, avg, sample, hundreds, tens, ones;

    while (1) {
        avg = 0;
        for (i=0; i<10; i++) {
            i2c_write(EE_ADDR, EE_ADDR_REG, i);
            sample = i2c_read(EE_ADDR, EE_DATA_REG);
            avg += sample;
        }

        /* calculate average */
        avg /= 10;

        /* convert to numeric digits for display */
        hundreds = avg / 100;
        tens = (avg % 100) / 10;
        ones = (avg % 10);

        spi_write(CS_LCD, LCD_CLEAR);
        spi_write(CS_LCD, '0' + hundreds);
        spi_write(CS_LCD, '0' + tens);
        spi_write(CS_LCD, '0' + ones);
    }
}

Este es un ejemplo muy crudo y rápido. ¡No codifiques así!

Ahora recuerde, el sistema operativo multitarea preventivo puede suspender cualquiera de estos subprocesos en cualquier línea del código (en realidad, en cualquier instrucción de ensamblaje) y dar tiempo al otro subproceso para que se ejecute.

Piénsalo. Imagine lo que sucedería si el sistema operativo decidiera suspender adc_thread()entre la configuración de la dirección EE para escribir y escribir los datos reales. lcd_thread()se ejecutaría, jugaría con el periférico I2C para leer los datos que necesitaba, y cuando adc_thread()llegara su turno para ejecutarse nuevamente, la EEPROM no estaría en el mismo estado en que se quedó. Las cosas no funcionarían muy bien en absoluto. Peor aún, incluso podría funcionar la mayor parte del tiempo, pero no todo el tiempo, y te volverías loco tratando de averiguar por qué tu código no funciona cuando PARECE que debería.

Ese es un ejemplo del mejor de los casos; ¡ el sistema operativo podría decidir anticiparse al contexto i2c_write()de adc_thread()y comenzar a ejecutarlo nuevamente desde lcd_thread()el contexto de! Las cosas pueden complicarse mucho muy rápido.

Cuando está escribiendo código para trabajar en un entorno multitarea preventivo, debe usar mecanismos de bloqueo para asegurarse de que si su código se suspende en un momento inoportuno, no se desate el infierno.

La multitarea cooperativa, por otro lado, significa que cada subproceso tiene el control de cuándo cede su tiempo de ejecución. La codificación es más simple, pero el código debe diseñarse cuidadosamente para asegurarse de que todos los subprocesos tengan suficiente tiempo para ejecutarse. Otro ejemplo inventado:

char getch()
{
    while (! (*uart_status & DATA_AVAILABLE)) {
        /* do nothing */
    }

    return *uart_data_reg;
}

void putch(char data)
{
    while (! (*uart_status & SHIFT_REG_EMPTY)) {
        /* do nothing */
    }

    *uart_data_reg = data;
}

void echo_thread()
{
    char data;

    while (1) {
        data = getch();
        putch(data);
        yield_cpu();
    }
}

void seconds_counter()
{
    int count = 0;

    while (1) {
        ++count;
        sleep_ms(1000);
        yield_cpu();
    }
}

Ese código no funcionará como usted piensa, o incluso si parece funcionar, no funcionará a medida que aumente la velocidad de datos del subproceso de eco. Una vez más, tomemos un minuto para mirarlo.

echo_thread()espera que aparezca un byte en un UART y luego lo obtiene y espera hasta que haya espacio para escribirlo, luego lo escribe. Una vez hecho esto, le da a otros subprocesos un turno para ejecutarse. seconds_counter()incrementará un conteo, esperará 1000 ms y luego dará a los otros subprocesos la oportunidad de ejecutarse. Si llegan dos bytes al UART mientras sleep()transcurre ese tiempo, es posible que no los vea porque nuestro hipotético UART no tiene FIFO para almacenar caracteres mientras la CPU está ocupada haciendo otras cosas.

La forma correcta de implementar este ejemplo muy pobre sería colocar yield_cpu()donde sea que tenga un bucle ocupado. Esto ayudará a que las cosas avancen, pero podría causar otros problemas. Por ejemplo, si la temporización es crítica y cede la CPU a otro subproceso que tarda más de lo esperado, es posible que pierda la temporización. Un sistema operativo multitarea preventivo no tendría este problema porque suspende los subprocesos a la fuerza para asegurarse de que todos los subprocesos estén programados correctamente.

Ahora, ¿qué tiene esto que ver con un temporizador y un bucle de fondo? El temporizador y el bucle de fondo son muy similares al ejemplo de multitarea cooperativa anterior:

void timer_isr(void)
{
    ++ticks;
    if ((ticks % 10)) == 0) {
        ten_ms_flag = TRUE;
    }

    if ((ticks % 100) == 0) {
        onehundred_ms_flag = TRUE;
    }

    if ((ticks % 1000) == 0) {
        one_second_flag = TRUE;
    }
}

void main(void)
{
    /* initialization of timer ISR, etc. */

    while (1) {
        if (ten_ms_flag) {
            if (kbhit()) {
                putch(getch());
            }
            ten_ms_flag = FALSE;
        }

        if (onehundred_ms_flag) {
                    get_adc_data();
            onehundred_ms_flag = FALSE;
        }

        if (one_second_flag) {
            ++count;
                    update_lcd();
            one_second_flag = FALSE;
        }
    };
}

Esto se parece bastante al ejemplo de subprocesamiento cooperativo; tiene un temporizador que configura eventos y un bucle principal que los busca y actúa sobre ellos de forma atómica. No tiene que preocuparse de que los "hilos" de ADC y LCD interfieran entre sí porque uno nunca interrumpirá al otro. Todavía tiene que preocuparse de que un "hilo" tarde demasiado; por ejemplo, ¿qué sucede si get_adc_data()tarda 30 ms? perderá tres oportunidades para buscar un carácter y repetirlo.

La implementación de loop+timer a menudo es mucho más simple de implementar que un microkernel cooperativo multitarea, ya que su código puede diseñarse de manera más específica para la tarea en cuestión. En realidad, no está realizando múltiples tareas tanto como diseñando un sistema fijo en el que le da a cada subsistema algo de tiempo para realizar sus tareas de una manera muy específica y predecible. Incluso un sistema multitarea cooperativo debe tener una estructura de tareas genérica para cada subproceso y el siguiente subproceso que se ejecutará está determinado por una función de programación que puede llegar a ser bastante compleja.

Los mecanismos de bloqueo para los tres sistemas son los mismos, pero la sobrecarga requerida para cada uno es bastante diferente.

Personalmente, casi siempre codifico con este último estándar, la implementación loop+timer. Creo que el enhebrado es algo que debe usarse con moderación. No solo es más complejo de escribir y depurar, sino que también requiere más gastos generales (un micronúcleo multitarea preventivo siempre va a ser más grande que un temporizador estúpidamente simple y un seguidor de eventos de bucle principal).

También hay un dicho que cualquiera que trabaje en hilos apreciará:

if you have a problem and use threads to solve it, yoeu ndup man with y pemro.bls

:-)

Muchas gracias por su respuesta con ejemplos detallados, akohlsmith. Sin embargo, no puedo concluir de su respuesta por qué elige una arquitectura simple de temporizador y bucle de fondo en lugar de la multitarea cooperativa . No me malinterpretes. Realmente aprecio su respuesta, que proporciona mucha información útil sobre diferentes horarios. Simplemente no tengo el punto.
¿Podrías trabajar un poco más en esto?
Gracias, akohlsmith. Me gusta la frase que pones al final. Me tomó un tiempo reconocerlo :) Volviendo al punto de su respuesta, casi siempre codifica la implementación de bucle + temporizador. Entonces, en los casos en que abandonó esta implementación y recurrió a un sistema operativo no preventivo, ¿qué lo hizo hacerlo?
He optado por sistemas multitarea cooperativos y preventivos cuando estaba ejecutando el sistema operativo de otra persona. Ya sea Linux, ThreadX, ucOS-ii o QNX. Incluso en algunas de esas situaciones, he usado el ciclo simple y efectivo de temporizador + evento ( poll()viene inmediatamente a la mente).
No soy un fanático de la creación de subprocesos o la multitarea en sistemas integrados, pero sé que para sistemas complejos es la única opción sensata. Los sistemas microoperativos enlatados le brindan una forma rápida de poner las cosas en funcionamiento y, a menudo, también proporcionan controladores de dispositivos.
Me gusta el modelo multitarea cooperativo de muchas maneras; en muchas situaciones, es bastante fácil esparcir llamadas de "rendimiento" en todo el código. El mayor problema que puedo encontrar es si quiero producir una salida de diagnóstico en un momento en que realmente no quiero un cambio de tarea, y resuelvo ese problema haciendo que mi rutina de salida de diagnóstico guarde la salida si es posible o salte si no.

La multitarea puede ser una abstracción útil en muchos proyectos de microcontroladores, aunque un verdadero planificador preventivo sería demasiado pesado e innecesario en la mayoría de los casos. He hecho más de 100 proyectos de microcontroladores. He usado tareas cooperativas varias veces, pero el cambio de tareas preventivas con su equipaje asociado hasta ahora no ha sido apropiado.

Los problemas con las tareas preventivas en contraposición a las tareas cooperativas son:

  1. Mucho más pesado. Los programadores de tareas preventivas son más complicados, ocupan más espacio de código y requieren más ciclos. También requieren al menos una interrupción. Eso es a menudo una carga inaceptable para la aplicación.

  2. Se requieren exclusiones mutuas alrededor de las estructuras a las que se puede acceder simultáneamente. En un sistema cooperativo, simplemente no llama a TASK_YIELD en medio de lo que debería ser una operación atómica. Esto afecta las colas, el estado global compartido y se crea en muchos lugares.

En general, dedicar una tarea a un trabajo en particular tiene sentido cuando la CPU puede admitir esto y el trabajo es lo suficientemente complicado con suficiente operación dependiente del historial que dividirlo en unos pocos eventos individuales separados sería engorroso. Este suele ser el caso cuando se maneja un flujo de entrada de comunicaciones. Estas cosas suelen estar fuertemente impulsadas por el estado dependiendo de alguna entrada previa. Por ejemplo, puede haber bytes de código de operación seguidos de bytes de datos únicos para cada código de operación. Luego está el problema de que estos bytes te llegan cuando algo más tiene ganas de enviarlos. Con una tarea separada que maneja el flujo de entrada, puede hacer que aparezca en el código de la tarea como si estuviera saliendo y obteniendo el siguiente byte.

En general, las tareas son útiles cuando hay mucho contexto de estado. Las tareas son básicamente máquinas de estado, siendo la PC la variable de estado.

Muchas cosas que tiene que hacer un micro pueden expresarse como respuestas a un conjunto de eventos. Como resultado, normalmente tengo un bucle de evento principal. Esto verifica cada evento posible en secuencia, luego vuelve a la parte superior y lo hace todo de nuevo. Cuando el manejo de un evento lleva más que unos pocos ciclos, generalmente vuelvo al inicio del ciclo del evento después de manejar el evento. En efecto, esto significa que los eventos tienen una prioridad implícita en función de dónde se marcan en la lista. En muchos sistemas simples, esto es lo suficientemente bueno.

A veces te dan tareas un poco más complicadas. Estos a menudo se pueden dividir en una secuencia de un pequeño número de cosas separadas para hacer. Puede usar banderas internas como eventos en esos casos. He hecho este tipo de cosas muchas veces en PIC de gama baja.

Si tiene la estructura de eventos básica como la anterior, pero también tiene que responder a un flujo de comandos a través del UART, por ejemplo, entonces es útil tener una tarea separada que maneje el flujo UART recibido. Algunos microcontroladores tienen recursos de hardware limitados para realizar múltiples tareas, como un PIC 16 que no puede leer ni escribir su propia pila de llamadas. En tales casos, uso lo que llamo una pseudotarea para el procesador de comandos UART. El bucle de eventos principal todavía maneja todo lo demás, pero uno de sus eventos para manejar es que el UART recibió nuevos bytes. En ese caso, salta a una rutina que ejecuta esta pseudotarea. El módulo de comando UART contiene el código de la tarea, y la dirección de ejecución y algunos valores de registro de la tarea se guardan en la RAM de ese módulo. El código saltado por el bucle de eventos guarda los registros actuales, carga los registros de tareas guardados, y salta a la dirección de reinicio de la tarea. El código de la tarea invoca una macro YIELD que hace lo contrario, que finalmente regresa al inicio del ciclo del evento principal. En algunos casos, el bucle de eventos principal ejecuta la pseudotarea una vez por pase, generalmente en la parte inferior para convertirlo en un evento de baja prioridad.

En un PIC 18 y superior, utilizo un verdadero sistema de tareas cooperativas ya que el firmware puede leer y escribir en la pila de llamadas. En estos sistemas, la dirección de reinicio, algunas otras partes del estado y el puntero de la pila de datos se mantienen en un búfer de memoria para cada tarea. Para permitir que todas las demás tareas se ejecuten una vez, una tarea llama a TASK_YIELD. Esto guarda el estado actual de la tarea, busca en la lista la siguiente tarea disponible, carga su estado y luego la ejecuta.

En esta arquitectura, el ciclo de eventos principal es solo otra tarea, con una llamada a TASK_YIELD en la parte superior del ciclo.

Todo mi código multitarea para PIC está disponible de forma gratuita. Para verlo, instale la versión de herramientas de desarrollo de PIC en http://www.embedinc.com/pic/dload.htm . Busque archivos con "tarea" en sus nombres en el directorio SOURCE > PIC para los PIC de 8 bits y en el directorio SOURCE > DSPIC para los PIC de 16 bits.

los mutex todavía pueden ser necesarios en sistemas cooperativos multitarea, aunque es raro. El ejemplo típico es un ISR que necesita acceso a una sección crítica. Esto casi siempre se puede evitar mediante un mejor diseño o eligiendo un contenedor de datos apropiado para los datos críticos.
@akoh: Sí, he usado mutexes en algunas ocasiones para manejar un recurso compartido, como el acceso al bus SPI. Mi punto era que los mutexes no son inherentemente necesarios en la medida en que lo son en un sistema preventivo. No quise decir que nunca se necesitan o nunca se usan en un sistema cooperativo. Además, un mutex en un sistema cooperativo puede ser tan simple como girar en un ciclo TASK_YIELD verificando un solo bit. En un sistema preventivo, generalmente deben integrarse en el kernel.
@OlinLathrop: Creo que la ventaja más significativa de los sistemas no preventivos cuando se trata de mutexes es que solo se requieren cuando interactúan directamente con interrupciones (que por naturaleza son preventivas) o cuando el tiempo que uno necesita para mantener un recurso protegido excede el tiempo que uno quiere pasar entre llamadas de "rendimiento", o uno quiere mantener un recurso protegido alrededor de una llamada que "podría" rendir (por ejemplo, "escribir datos en un archivo"). En algunas ocasiones, cuando tener un rendimiento dentro de una llamada de "escribir datos" hubiera sido un problema, he incluido...
... un método para verificar cuántos datos se pueden escribir de inmediato y un método (que probablemente rinda) para garantizar que haya cierta cantidad disponible (acelerando la recuperación de bloques flash sucios y esperando hasta que se reclame un número adecuado) .
Hola Olin, me gusta mucho tu respuesta. Su información está mucho más allá de mis preguntas. Incluye muchas experiencias prácticas.
¿Puedo concluir su respuesta a mi pregunta de la siguiente manera?: (1) La multitarea puede ser una abstracción útil en muchos proyectos de microcontroladores, aunque un verdadero planificador preventivo sería demasiado pesado e innecesario en la mayoría de los casos. (2) las tareas son útiles cuando hay mucho contexto estatal. Las tareas son básicamente máquinas de estado, siendo la PC la variable de estado. (3) En una MCU que puede leer o escribir su propia pila de llamadas, como PIC 18 y superior, utiliza un verdadero sistema de tareas cooperativas.
Si mi conclusión es correcta, ¿podría explicar "por qué la multitarea puede ser una abstracción útil en muchos proyectos de microcontroladores" ? (Entiendo que la lógica de su respuesta a mi pregunta es: la multitarea es buena -> la programación no preventiva es liviana y suficiente, mientras que la programación preventiva es demasiado pesada e innecesaria en la mayoría de los casos -> por lo que es mejor elegir una programación no preventiva OS en lugar de un código casero con bucle de fondo y arquitectura de interrupción del temporizador) Encontré un buen artículo sobre esto, pero también me gustaría saber cómo lo entiende . Gracias.
Otra pregunta es: en una MCU que puede leer o escribir su propia pila de llamadas, como un PIC 18 y superior, ¿existen casos en la práctica industrial en los que elegirá no usar un sistema de tareas cooperativas sino usar un bucle + temporizador más simple? ¿implementación? Si los hay, ¿por qué elige loop+timer en estos casos?
@hailang: incluso en los procesadores que pueden intercambiar pilas, se deben tener en cuenta las bibliotecas de compiladores u otras rutinas que pueden depender de variables estáticas. Esto es especialmente importante en C++; si uno no tiene cuidado, una excepción lanzada en un subproceso podría quedar "atrapada" en otro.
@hailang: Lo siento, he estado fuera. En realidad, todavía estoy fuera, pero resulta que tengo acceso a Internet. Sí, tus conclusiones 1-3 son lo que estaba tratando de decir. Pensé que muchas de las respuestas podrían ser sobre por qué la multitarea puede ser útil. Básicamente, cuando un trabajo depende en gran medida del estado, puede ser engorroso manejar muchas condiciones de eventos porque habría muchas y son difíciles de mantener. Una tarea hace esto muy bien, saltando a secciones de código según el estado. Esto es lo que quise decir cuando la PC es la variable de estado. Sin embargo, a menudo también hago sistemas de bucle de eventos simples. Lo que sea que encaje.
@supercat: the most significant advantage of non-preemptive systems when it comes to mutexes is that they are only required either when interacting directly with interrupts...Estoy bastante seguro de que no quiere decir que un sistema (preventivo o no) deba usar mutexes (a diferencia de las secciones críticas) para proteger los datos compartidos entre un hilo y una interrupción. Pendiente de un mutex en una interrupción (asincrónica), no es una buena idea. Aunque estoy seguro de que sabes esto, no estoy tan seguro sobre el OP.
@Radian: Lo siento si mi terminología fue descuidada; el tipo de construcción que generalmente usaría implicaría que el código principal establezca una bandera que le diga a la interrupción que deje algo en paz, y/o la interrupción establezca una bandera que le diga al código de la línea principal que "espere" (llamandoyield() hasta que la bandera esté clara ). No es exactamente el patrón normal para un mutex o una sección crítica; No estoy seguro de cómo debería llamarse.

Editar: (Dejaré mi publicación anterior a continuación; tal vez ayude a alguien algún día).

Los sistemas operativos multitarea de cualquier tipo y las rutinas de servicio de interrupción no son, o no deberían ser, arquitecturas de sistemas competidoras. Están destinados a diferentes trabajos en diferentes niveles del sistema. Las interrupciones están realmente destinadas a secuencias de código breves para manejar tareas inmediatas como reiniciar un dispositivo, posiblemente sondear dispositivos que no interrumpen, controlar el tiempo en el software, etc. las necesidades inmediatas han sido satisfechas. Si todo lo que necesita hacer es reiniciar un temporizador y alternar un LED o pulsar otro dispositivo, el ISR generalmente puede hacerlo todo en primer plano de forma segura. De lo contrario, debe informar al fondo (estableciendo un indicador o poniendo en cola un mensaje) que es necesario hacer algo y liberar el procesador.

He visto estructuras de programa muy simples cuyo bucle de fondo es solo un bucle inactivo: for(;;){ ; }. Todo el trabajo se realizó en el temporizador ISR. Esto puede funcionar cuando el programa necesita repetir alguna operación constante que está garantizada para terminar en menos de un período de tiempo; vienen a la mente ciertos tipos limitados de procesamiento de señales.

Personalmente, escribo ISR que limpian y salen, y dejo que el fondo se haga cargo de cualquier otra cosa que necesite hacerse, incluso si eso es tan simple como multiplicar y sumar que podría hacerse en una fracción de un período de tiempo. ¿Por qué? Algún día, tendré la brillante idea de agregar otra función "simple" a mi programa, y ​​"diablos, solo tomará un ISR corto para hacerlo" y de repente mi arquitectura previamente simple desarrolla algunas interacciones que no había planeado. y suceden de manera inconsistente. Esos no son muy divertidos de depurar.


(Comparación publicada anteriormente de dos tipos de tareas múltiples)

Cambio de tareas: la MT preventiva se encarga del cambio de tareas por usted, lo que incluye asegurarse de que ningún subproceso se quede sin CPU y que los subprocesos de alta prioridad se ejecuten tan pronto como estén listos. La MT cooperativa requiere que el programador se asegure de que ningún subproceso mantenga el procesador durante demasiado tiempo. También tendrás que decidir cuánto tiempo es demasiado largo. Eso también significa que cada vez que modifique el código, deberá saber si algún segmento de código ahora excede ese tiempo.

Protección de operaciones no atómicas: con un PMT, deberá asegurarse de que no se produzcan intercambios de subprocesos en medio de operaciones que no deben dividirse. Lectura/escritura de ciertos pares dispositivo-registro que deben manejarse en un orden particular o en un tiempo máximo, por ejemplo. Con CMT es bastante fácil: simplemente no ceda el procesador en medio de una operación de este tipo.

Depuración: generalmente más fácil con CMT, ya que planifica cuándo y dónde se producirán los cambios de subprocesos. Las condiciones de carrera entre subprocesos y errores relacionados con operaciones no seguras para subprocesos con un PMT son particularmente difíciles de depurar porque los cambios de subprocesos son probabilísticos, por lo que no se pueden repetir.

Comprender el código: los subprocesos escritos para un PMT están escritos prácticamente como si pudieran ser autónomos. Los subprocesos escritos para un CMT se escriben como segmentos y, según la estructura del programa que elija, pueden ser más difíciles de seguir para un lector.

Uso de código de biblioteca no seguro para subprocesos: deberá verificar que cada función de biblioteca a la que llame esté bajo un PMT seguro para subprocesos. printf() y scanf() y sus variantes casi siempre no son seguros para subprocesos. Con una CMT, sabrá que no se producirá ningún cambio de subproceso, excepto cuando ceda específicamente el procesador.

Un sistema controlado por una máquina de estado finito para controlar un dispositivo mecánico y/o rastrear eventos externos suele ser un buen candidato para CMT, ya que en cada evento no hay mucho que hacer: arrancar o detener un motor, configurar una bandera, elegir el siguiente estado , etc. Por lo tanto, las funciones de cambio de estado son inherentemente breves.

Un enfoque híbrido puede funcionar muy bien en este tipo de sistemas: CMT para administrar la máquina de estado (y, por lo tanto, la mayor parte del hardware) que se ejecuta como un subproceso, y uno o dos subprocesos más para realizar cálculos más largos iniciados por un estado. cambio.

Gracias por tu respuesta, JRobert. Pero no se adapta a mi pregunta. Compara el sistema operativo preventivo con el sistema operativo no preventivo, pero no compara el sistema operativo no preventivo con el sistema operativo sin sistema operativo.
Correcto, lo siento. Mi edición debería abordar mejor su pregunta.