Quels sont les avantages d'un système d'exploitation non préemptif? et le prix de ces avantages?

14

Pour un microcontrôleur métallique nu, par rapport au code maison avec boucle d'arrière-plan et architecture d'interruption du minuteur, quels sont les avantages d'un système d'exploitation non préemptif? Quels sont ces avantages suffisamment intéressants pour qu'un projet adopte un système d'exploitation non préemptif, plutôt que d'utiliser du code maison avec une architecture de boucle d'arrière-plan?
.

Explication de la question:

J'apprécie vraiment tous ceux qui ont répondu à ma question. Je sens que la réponse est presque là. J'ajoute cette explication à ma question ici qui montre ma propre considération et peut aider à affiner la question ou à la rendre plus précise.

Ce que j'essaie de faire, c'est de comprendre comment choisir le RTOS le plus approprié pour un projet en général.
Pour y parvenir, une meilleure compréhension des concepts de base et des avantages les plus attractifs des différents types de RTOS et du prix correspondant aidera, car il n'y a pas de meilleur RTOS pour toutes les applications.
J'ai lu des livres sur OS il y a quelques années mais je ne les ai plus avec moi. J'ai cherché sur Internet avant de poster ma question ici et j'ai trouvé cette information très utile: http://www.ustudy.in/node/5456 .
Il existe de nombreuses autres informations utiles telles que les introductions sur le site Web de différents RTOS, des articles comparant la planification préventive et la planification non préemptive, etc.
Mais je n'ai trouvé aucun sujet mentionné quand choisir un RTOS non préemptif et quand il est préférable d'écrire votre propre code en utilisant une interruption de minuterie et une boucle d'arrière-plan.
J'ai certaines réponses, mais je n'en suis pas assez satisfait.
J'aimerais vraiment connaître la réponse ou le point de vue de personnes plus expérimentées, en particulier dans la pratique de l'industrie.

Jusqu'à présent, ma compréhension est la suivante:
peu importe que vous utilisiez ou non un système d'exploitation, certains types de codes de planification sont toujours nécessaires, même sous forme de code comme:

    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;  
    }  

Avantage 1:
un système d'exploitation non préemptif désigne le chemin / style de programmation pour le code de planification, afin que les ingénieurs puissent partager la même vue même s'ils n'étaient pas dans le même projet auparavant. Ensuite, avec la même vue sur la tâche conceptuelle, les ingénieurs peuvent travailler sur différentes tâches et les tester, les profiler de manière indépendante autant que possible.
Mais combien pouvons-nous vraiment gagner de cela? Si les ingénieurs travaillent dans le même projet, ils peuvent trouver un moyen de bien partager la même vue sans utiliser un système d'exploitation non préemptif.
Si un ingénieur appartient à un autre projet ou entreprise, il en bénéficiera s'il connaissait le système d'exploitation auparavant. Mais s'il ne l'a pas fait, encore une fois, cela ne semble pas faire une grande différence pour lui d'apprendre un nouveau système d'exploitation ou un nouveau morceau de code.

Avantage 2:
si le code du système d'exploitation a été bien testé, cela permet de gagner du temps du débogage. C'est vraiment un bon avantage.
Mais si l'application n'a que 5 tâches, je pense que ce n'est pas vraiment compliqué d'écrire votre propre code en utilisant une interruption de minuterie et une boucle d'arrière-plan.

Un système d'exploitation non préemptif est ici référencé à un système d'exploitation commercial / gratuit / hérité avec un planificateur non préemptif.
Lorsque j'ai posté cette question, je pense principalement à certains systèmes d'exploitation comme:
(1) KISS Kernel (A Small NonPreemptive RTOS - revendiqué par son site Web)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (RTOS léger - revendiqué par son site Web)
(3) FreeRTOS (C'est un RTOS préemptif, mais si je comprends bien, il peut également être configuré comme un RTOS non préemptif)
(4) uC / OS (similaire à FreeRTOS)
(5 ) code OS / planificateur hérité dans certaines entreprises (généralement créé et géré par l'entreprise en interne)
(Impossible d'ajouter plus de liens en raison de la limitation du nouveau compte StackOverflow)

Si je comprends bien, un système d'exploitation non préemptif est une collection de ces codes:
(1) un planificateur utilisant une stratégie non préemptive.
(2) des installations pour la communication entre les tâches, le mutex, la synchronisation et le contrôle du temps.
(3) gestion de la mémoire.
(4) autres installations / bibliothèques utiles comme système de fichiers, la pile réseau, l' interface graphique et etc. (FreeRTOS et uC / OS offre ces, mais je ne sais pas s'ils encore du travail lorsque le planificateur est configuré comme non préemptif)
Certains ils ne sont pas toujours là. Mais l'ordonnanceur est un incontournable.

hailang
la source
C'est à peu près tout en un mot. Si vous avez une charge de travail qui doit être multithread et que vous pouvez vous permettre la surcharge, utilisez un système d'exploitation de thread. Sinon, un simple «planificateur» temporel ou basé sur les tâches suffit dans la plupart des cas. Et pour déterminer si le multitâche préemptif ou coopératif est le meilleur ... Je suppose que cela se résume aux frais généraux et au degré de contrôle que vous souhaitez avoir sur le multitâche que vous devez faire.
akohlsmith

Réponses:

13

Cela sent un peu hors sujet, mais je vais essayer de le ramener sur la bonne voie.

Le multitâche préventif signifie que le système d'exploitation ou le noyau peut suspendre le thread en cours d'exécution et passer à un autre en fonction de l'heuristique de planification qu'il a en place. La plupart du temps, les threads en cours d'exécution n'ont aucune idée qu'il se passe d'autres choses sur le système, et ce que cela signifie pour votre code, c'est que vous devez faire attention à le concevoir de sorte que si le noyau décide de suspendre un thread au milieu d'un opération en plusieurs étapes (par exemple, changer une sortie PWM, sélectionner un nouveau canal ADC, lire l'état d'un périphérique I2C, etc.) et laisser un autre thread s'exécuter pendant un certain temps, afin que ces deux threads n'interfèrent pas l'un avec l'autre.

Un exemple arbitraire: disons que vous êtes nouveau dans les systèmes embarqués multithread et que vous avez un petit système avec un ADC I2C, un écran LCD SPI et une EEPROM I2C. Vous avez décidé que ce serait une bonne idée d'avoir deux threads: un qui lit à partir de l'ADC et écrit des échantillons dans l'EEPROM, et un qui lit les 10 derniers échantillons, les fait la moyenne et les affiche sur l'écran LCD SPI. Le design inexpérimenté ressemblerait à quelque chose comme ça (grossièrement simplifié):

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);
    }
}

Ceci est un exemple très grossier et rapide. Ne codez pas comme ça!

Rappelez-vous maintenant, le système d'exploitation multitâche préventif peut suspendre l'un de ces threads à n'importe quelle ligne du code (en fait à n'importe quelle instruction d'assemblage) et donner à l'autre thread le temps de s'exécuter.

Pensez-y. Imaginez ce qui se passerait si le système d'exploitation décidait de suspendre adc_thread()entre le réglage de l'adresse EE pour écrire et l'écriture des données réelles. lcd_thread()s'exécutait, se promenait avec le périphérique I2C pour lire les données dont il avait besoin, et lorsque adc_thread()son tour recommençait, l'EEPROM ne serait pas dans le même état qu'elle était laissée. Les choses ne fonctionneraient pas très bien du tout. Pire, cela pourrait même fonctionner la plupart du temps, mais pas tout le temps, et vous deviendriez fou en essayant de comprendre pourquoi votre code ne fonctionne pas quand il A l'air comme il le devrait!

C'est un meilleur exemple; le système d'exploitation pourrait décider de préempter i2c_write()du adc_thread()contexte de et recommencer à l'exécuter à partir lcd_thread()du contexte de! Les choses peuvent devenir vraiment désordonnées très rapidement.

Lorsque vous écrivez du code pour travailler dans un environnement multitâche préventif, vous devez utiliser des mécanismes de verrouillage pour vous assurer que si votre code est suspendu à un moment inopportun, tout l'enfer ne se déchaîne pas.

Le multitâche coopératif, d'autre part, signifie que chaque thread contrôle quand il abandonne son temps d'exécution. Le codage est plus simple, mais le code doit être conçu avec soin pour s'assurer que tous les threads ont suffisamment de temps pour s'exécuter. Un autre exemple artificiel:

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();
    }
}

Ce code ne fonctionnera pas comme vous le pensez, ou même s'il semble fonctionner, il ne fonctionnera pas lorsque le débit de données du fil d'écho augmente. Encore une fois, prenons une minute pour l'examiner.

echo_thread()attend qu'un octet apparaisse dans un UART, puis le récupère et attend qu'il y ait de la place pour l'écrire, puis l'écrit. Après cela, cela donne un tour aux autres threads.seconds_counter()incrémentera un nombre, attendra 1000 ms, puis donnera aux autres threads une chance de s'exécuter. Si deux octets entrent dans l'UART pendant ce temps sleep(), vous risquez de ne pas les voir car notre hypothétique UART n'a pas de FIFO pour stocker des caractères pendant que le CPU est occupé à faire d'autres choses.

La bonne façon de mettre en œuvre ce très mauvais exemple serait de mettre yield_cpu() où que vous ayez une boucle occupée. Cela aidera les choses à avancer, mais pourrait causer d'autres problèmes. Par exemple, si le timing est critique et que vous cédez le CPU à un autre thread qui prend plus de temps que prévu, vous pourriez avoir votre timing désactivé. Un système d'exploitation multitâche préventif n'aurait pas ce problème car il suspend de force les threads pour s'assurer que tous les threads sont correctement planifiés.

Maintenant, qu'est-ce que cela a à voir avec une boucle de minuterie et d'arrière-plan? Le minuteur et la boucle d'arrière-plan sont très similaires à l'exemple multitâche coopératif ci-dessus:

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;
        }
    };
}

Cela ressemble assez à l'exemple de filetage coopératif; vous avez une minuterie qui configure les événements et une boucle principale qui les recherche et agit sur eux de manière atomique. Vous n'avez pas à vous soucier des «fils» ADC et LCD qui interfèrent l'un avec l'autre car l'un n'interrompra jamais l'autre. Vous devez toujours vous soucier d'un "fil" trop long; par exemple ce qui se passe siget_adc_data() prend 30 ms? vous manquerez trois occasions de vérifier un personnage et de le faire écho.

L'implémentation loop + timer est souvent beaucoup plus simple à implémenter qu'un micro-noyau multitâche coopératif car votre code peut être conçu plus spécifiquement pour la tâche à accomplir. Vous n'êtes pas vraiment multitâche autant que de concevoir un système fixe où vous donnez à chaque sous-système un certain temps pour effectuer ses tâches d'une manière très spécifique et prévisible. Même un système multitâche coopératif doit avoir une structure de tâches générique pour chaque thread et le prochain thread à exécuter est déterminé par une fonction de planification qui peut devenir assez complexe.

Les mécanismes de verrouillage pour les trois systèmes sont les mêmes, mais les frais généraux requis pour chacun sont assez différents.

Personnellement, je code presque toujours sur cette dernière norme, l'implémentation loop + timer. Je trouve que le filetage est quelque chose qui devrait être utilisé avec parcimonie. Non seulement il est plus complexe à écrire et à déboguer, mais il nécessite également plus de surcharge (un micro-noyau multitâche préemptif sera toujours plus gros qu'un minuteur stupidement simple et un suiveur d'événements de boucle principale).

Il y a aussi un dicton que toute personne travaillant sur des threads appréciera:

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

:-)

akohlsmith
la source
Merci beaucoup pour votre réponse avec des exemples détaillés, akohlsmith. Cependant, je ne peux pas conclure de votre réponse pourquoi vous choisissez une horloge simple et une architecture de boucle d'arrière-plan plutôt que le multitâche coopératif . Ne vous méprenez pas. J'apprécie vraiment votre réponse, qui fournit de nombreuses informations utiles sur les différents horaires. Je n'ai tout simplement pas compris.
hailang
Pourriez-vous s'il vous plaît travailler un peu plus sur ce sujet?
hailang
Merci, akohlsmith. J'aime la phrase que tu mets à la fin. Il m'a fallu un certain temps pour le reconnaître :) Revenons au point de votre réponse, vous codez presque toujours sur l'implémentation boucle + minuterie. Ensuite, dans les cas où vous avez abandonné cette implémentation et vous êtes tourné vers un système d'exploitation non préemptif, qu'est-ce qui vous a poussé à le faire?
hailang
J'ai opté pour des systèmes multitâches coopératifs et préemptifs lorsque j'utilisais le système d'exploitation de quelqu'un d'autre. Soit Linux, ThreadX, ucOS-ii ou QNX. Même dans certaines de ces situations, j'ai utilisé la boucle simple et efficace timer + event ( poll()vient immédiatement à l'esprit).
akohlsmith
Je ne suis pas un fan du threading ou du multitâche en embarqué, mais je sais que pour les systèmes complexes, c'est la seule option sensée. Les systèmes de micro-exploitation en conserve vous offrent un moyen rapide de faire fonctionner les choses et fournissent souvent des pilotes de périphérique.
akohlsmith
6

Le multitâche peut être une abstraction utile dans de nombreux projets de microcontrôleurs, bien qu'un véritable planificateur préemptif soit trop lourd et inutile dans la plupart des cas. J'ai réalisé plus de 100 projets de microcontrôleurs. J'ai utilisé des tâches coopératives un certain nombre de fois, mais le changement de tâche préventif avec ses bagages associés n'a jusqu'à présent pas été approprié.

Les problèmes liés à la tâche préventive par rapport à la tâche coopérative sont les suivants:

  1. Beaucoup plus lourd. Les planificateurs de tâches préventifs sont plus compliqués, prennent plus d'espace de code et prennent plus de cycles. Ils nécessitent également au moins une interruption. C'est souvent une charge inacceptable pour la demande.

  2. Des mutex sont requis autour des structures auxquelles on peut accéder simultanément. Dans un système coopératif, vous n'appelez simplement pas TASK_YIELD au milieu de ce qui devrait être une opération atomique. Cela affecte les files d'attente, l'état global partagé et les modifications dans de nombreux endroits.

En général, dédier une tâche à un travail particulier est logique lorsque le processeur peut prendre en charge cela et que le travail est suffisamment compliqué avec suffisamment d'opérations dépendantes de l'historique pour que le diviser en quelques événements individuels distincts soit lourd. C'est généralement le cas lors de la gestion d'un flux d'entrée de communication. Ces choses sont généralement fortement pilotées par l'état en fonction de certaines entrées précédentes. Par exemple, il peut y avoir des octets d'opcode suivis par des octets de données uniques pour chaque opcode. Ensuite, il y a le problème de ces octets qui vous viennent à l'esprit lorsque quelque chose d'autre a envie de les envoyer. Avec une tâche distincte gérant le flux d'entrée, vous pouvez le faire apparaître dans le code de tâche comme si vous sortiez et obteniez l'octet suivant.

Dans l'ensemble, les tâches sont utiles lorsqu'il y a beaucoup de contexte d'état. Les tâches sont essentiellement des machines à états, le PC étant la variable d'état.

Beaucoup de choses qu'un micro doit faire peuvent être exprimées comme répondant à un ensemble d'événements. En conséquence, j'ai généralement une boucle d'événement principale. Cela vérifie chaque événement possible dans l'ordre, puis revient en haut et recommence. Lorsque la gestion d'un événement prend plus que quelques cycles, je reviens généralement au début de la boucle d'événement après avoir géré l'événement. Cela signifie que les événements ont une priorité implicite en fonction de l'endroit où ils sont vérifiés dans la liste. Sur de nombreux systèmes simples, cela suffit.

Parfois, vous obtenez des tâches un peu plus compliquées. Ceux-ci peuvent souvent être décomposés en une séquence d'un petit nombre de choses distinctes à faire. Vous pouvez utiliser des indicateurs internes comme événements dans ces cas. J'ai fait ce genre de choses plusieurs fois sur des PIC bas de gamme.

Si vous avez la structure d'événement de base comme ci-dessus, mais devez également répondre à un flux de commandes via l'UART, par exemple, il est utile d'avoir une tâche distincte pour gérer le flux UART reçu. Certains microcontrôleurs ont des ressources matérielles limitées pour le multitâche, comme un PIC 16 qui ne peut pas lire ou écrire sa propre pile d'appels. Dans de tels cas, j'utilise ce que j'appelle une pseudo-tâche pour le processeur de commandes UART. La boucle d'événements principale gère toujours tout le reste, mais l'un de ses événements à gérer est qu'un nouvel octet a été reçu par l'UART. Dans ce cas, il passe à une routine qui exécute cette pseudo-tâche. Le module de commande UART contient le code de tâche, et l'adresse d'exécution et quelques valeurs de registre de la tâche sont enregistrées dans la RAM de ce module. Le code sauté par la boucle d'événement enregistre les registres actuels, charge les registres de tâches enregistrés, et passe à l'adresse de redémarrage de la tâche. Le code de tâche appelle une macro YIELD qui fait l'inverse, qui revient ensuite au début de la boucle d'événement principale. Dans certains cas, la boucle d'événement principale exécute la pseudo-tâche une fois par passage, généralement en bas pour en faire un événement de faible priorité.

Sur un PIC 18 et supérieur, j'utilise un véritable système de tâches coopératif car la pile d'appels est lisible et inscriptible par le firmware. Sur ces systèmes, l'adresse de redémarrage, quelques autres éléments d'état et le pointeur de pile de données sont conservés dans une mémoire tampon pour chaque tâche. Pour laisser toutes les autres tâches s'exécuter une fois, une tâche appelle TASK_YIELD. Cela enregistre l'état actuel de la tâche, recherche dans la liste la prochaine tâche disponible, charge son état, puis l'exécute.

Dans cette architecture, la boucle d'événement principale n'est qu'une autre tâche, avec un appel à TASK_YIELD en haut de la boucle.

Tout mon code multitâche pour les PIC est disponible gratuitement. Pour le voir, installez la version des outils de développement PIC à l' adresse http://www.embedinc.com/pic/dload.htm . Recherchez les fichiers avec «tâche» dans leurs noms dans le répertoire SOURCE> PIC pour les PIC 8 bits et dans le répertoire SOURCE> DSPIC pour les PIC 16 bits.

Olin Lathrop
la source
des mutex peuvent encore être nécessaires dans les systèmes multitâches coopératifs, bien que cela soit rare. L'exemple typique est un ISR qui a besoin d'accéder à une section critique. Cela peut presque toujours être évité grâce à une meilleure conception ou au choix d'un conteneur de données approprié pour les données critiques.
akohlsmith
@akoh: Oui, j'ai utilisé à plusieurs reprises des mutex pour gérer une ressource partagée, comme l'accès au bus SPI. Mon point était que les mutex ne sont pas intrinsèquement nécessaires dans la mesure où ils se trouvent dans un système préventif. Je ne voulais pas dire qu'ils ne sont jamais nécessaires ou jamais utilisés dans un système coopératif. De plus, un mutex dans un système coopératif peut être aussi simple que de tourner dans une boucle TASK_YIELD vérifiant un seul bit. Dans un système préventif, ils doivent généralement être intégrés au noyau.
Olin Lathrop
@OlinLathrop: Je pense que l'avantage le plus important des systèmes non préemptifs en ce qui concerne les mutex est qu'ils ne sont nécessaires que lors de l'interaction directe avec des interruptions (qui par nature sont préemptives) ou lorsque le temps nécessaire pour détenir une ressource gardée dépasse le temps que l'on veut passer entre les appels "de rendement", ou on veut maintenir une ressource gardée autour d'un appel qui "pourrait" produire (par exemple "écrire des données dans un fichier"). À certaines occasions, lorsque le rendement d'un appel "écrire des données" aurait été un problème, j'ai inclus ...
supercat
... une méthode pour vérifier la quantité de données pouvant être écrites immédiatement, et une méthode (qui peut probablement rapporter) pour garantir qu'une certaine quantité est disponible (accélérer la récupération des blocs flash sales et attendre qu'un nombre approprié ait été récupéré) .
supercat
Salut Olin, j'aime tellement ta réponse. Ses informations dépassent largement mes questions. Cela comprend beaucoup d'expériences pratiques.
hailang
1

Edit: (Je vais laisser mon précédent post ci-dessous; peut-être que cela aidera quelqu'un un jour.)

Les systèmes d'exploitation multitâche de tout type et les routines de service d'interruption ne sont pas - ou ne devraient pas être - des architectures système concurrentes. Ils sont destinés à différents emplois à différents niveaux du système. Les interruptions sont vraiment destinées à de brèves séquences de code pour gérer des tâches immédiates comme le redémarrage d'un périphérique, éventuellement l'interrogation de périphériques sans interruption, le chronométrage dans le logiciel, etc. On suppose généralement que l'arrière-plan effectuera tout traitement supplémentaire qui n'est plus critique en temps après la les besoins immédiats ont été satisfaits. Si tout ce que vous avez à faire est de redémarrer une minuterie et de basculer une LED ou d'impulser un autre appareil, l'ISR peut généralement tout faire au premier plan en toute sécurité. Sinon, il doit informer l'arrière-plan (en définissant un indicateur ou en mettant un message en file d'attente) que quelque chose doit être fait et libérer le processeur.

J'ai vu des structures de programme très simples dont la boucle arrière - plan est juste une boucle de repos: for(;;){ ; }. Tout le travail a été effectué dans le temporisateur ISR. Cela peut fonctionner lorsque le programme doit répéter une opération constante qui est garantie de se terminer en moins d'une période de temporisation; certains types limités de traitement du signal me viennent à l'esprit.

Personnellement, j'écris des ISR qui nettoient une sortie, et laisse l'arrière-plan prendre le dessus sur tout ce qui doit être fait, même si c'est aussi simple qu'une multiplication et un ajout qui pourrait être fait en une fraction de temps. Pourquoi? Un jour, j'aurai l'idée brillante d'ajouter une autre fonction "simple" à mon programme, et "diable, il faudra juste un court ISR pour le faire" et soudain, mon architecture précédemment simple développe des interactions que je n'avais pas prévues sur et arriver de manière incohérente. Ce n'est pas très amusant à déboguer.


(Comparaison précédemment publiée de deux types de tâches multiples)

Changement de tâche: Pre-emptive MT s'occupe du changement de tâche pour vous, notamment en veillant à ce qu'aucun thread ne soit affamé par le processeur et que les threads de haute priorité s'exécutent dès qu'ils sont prêts. Cooperative MT nécessite que le programmeur s'assure qu'aucun thread ne garde le processeur trop longtemps à la fois. Vous devrez également décider combien de temps est trop long. Cela signifie également que chaque fois que vous modifiez le code, vous devez savoir si un segment de code dépasse maintenant ce quantum temporel.

Protection des opérations non atomiques: avec un PMT, vous devrez vous assurer que les échanges de threads ne se produisent pas au milieu des opérations qui ne doivent pas être divisées. Lecture / écriture de certaines paires de registres de périphériques qui doivent être traitées dans un ordre particulier ou dans un délai maximum, par exemple. Avec CMT, c'est assez facile - ne cédez pas le processeur au milieu d'une telle opération.

Débogage: généralement plus facile avec CMT, car vous planifiez quand / où les commutations de thread se produiront. Les conditions de concurrence entre les threads et les bogues liés aux opérations non thread-safe avec un PMT sont particulièrement difficiles à déboguer car les changements de thread sont probabilistes, donc non répétables.

Comprendre le code: les threads écrits pour un PMT sont à peu près écrits comme s'ils pouvaient être autonomes. Les threads écrits pour un CMT sont écrits sous forme de segments et selon la structure du programme que vous choisissez, il peut être plus difficile à suivre pour un lecteur.

Utilisation d'un code de bibliothèque non thread-safe: vous devrez vérifier que chaque fonction de bibliothèque que vous appelez sous un thread-safe PMT. printf () et scanf () et leurs variantes ne sont presque toujours pas thread-safe. Avec un CMT, vous saurez qu'aucun changement de thread ne se produira, sauf lorsque vous cédez spécifiquement le processeur.

Un système piloté par machine à états finis pour contrôler un appareil mécanique et / ou suivre des événements externes est souvent un bon candidat pour la CMT, car à chaque événement, il n'y a pas grand-chose à faire - démarrer ou arrêter un moteur, définir un drapeau, choisir l'état suivant , etc. Ainsi, les fonctions de changement d'état sont intrinsèquement brèves.

Une approche hybride peut très bien fonctionner dans ces types de systèmes: CMT pour gérer la machine d'état (et donc, la plupart du matériel) fonctionnant comme un thread, et un ou deux threads supplémentaires pour effectuer des calculs plus longs lancés par un état changement.

JRobert
la source
Merci pour votre réponse, JRobert. Mais ce n'est pas adapté à ma question. Il compare le système d'exploitation préemptif au système d'exploitation non préemptif, mais il ne compare pas le système d'exploitation non préemptif au système non OS.
hailang
Bien - désolé. Ma modification devrait mieux répondre à votre question.
JRobert