Was sind die Vorteile eines nicht präemptiven Betriebssystems? und der Preis für diese Vorteile?

Was sind die Vorteile eines nicht präemptiven Betriebssystems für eine Bared-Metal-MCU im Vergleich zum hausgemachten Code mit Hintergrundschleife plus Timer-Interrupt-Architektur? Welche dieser Vorteile sind attraktiv genug für ein Projekt, um ein nicht-präemptives Betriebssystem einzuführen, anstatt hausgemachten Code mit Hintergrundschleifenarchitektur zu verwenden?
.

Erklärung zur Frage:

Ich schätze wirklich alle, die auf meine Frage geantwortet haben. Ich habe das Gefühl, die Antwort war fast da. Ich füge diese Erklärung meiner Frage hier hinzu, die meine eigene Überlegung zeigt und helfen kann, die Frage einzugrenzen oder zu präzisieren.

Was ich versuche, ist zu verstehen, wie man das am besten geeignete RTOS für ein Projekt im Allgemeinen auswählt.
Um dies zu erreichen, hilft ein besseres Verständnis der grundlegenden Konzepte und der attraktivsten Vorteile verschiedener Arten von RTOS und des entsprechenden Preises, da es nicht das beste RTOS für alle Anwendungen gibt.
Ich habe vor ein paar Jahren Bücher über OS gelesen, aber ich habe sie nicht mehr bei mir. Ich habe im Internet gesucht, bevor ich meine Frage hier gepostet habe, und fand, dass diese Informationen am hilfreichsten waren: http://www.ustudy.in/node/5456 .
Es gibt viele andere hilfreiche Informationen wie die Einführungen auf der Website verschiedener RTOS, Artikel, die präemptives Scheduling und nicht-präemptives Scheduling vergleichen, und so weiter.
Aber ich habe kein Thema gefunden, das erwähnt wurde, wann man ein nicht präemptives RTOS wählt und wann es besser ist, einfach seinen eigenen Code mit Timer-Interrupt und Hintergrundschleife zu schreiben.
Ich habe gewisse eigene Antworten, aber ich bin nicht zufrieden genug mit ihnen.
Ich würde wirklich gerne die Antwort oder Ansicht von erfahreneren Leuten wissen, insbesondere aus der Industriepraxis.

Mein bisheriges Verständnis ist:
Unabhängig davon, ob Sie ein Betriebssystem verwenden oder nicht, sind bestimmte Arten von Planungscodes immer erforderlich, auch in Form von Code wie:

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

Vorteil 1:
Ein nicht präemptives Betriebssystem legt den Weg / Programmierstil für den Planungscode fest, sodass Ingenieure dieselbe Ansicht teilen können, auch wenn sie zuvor nicht im selben Projekt waren. Dann können Ingenieure mit der gleichen Ansicht über Konzeptaufgaben an verschiedenen Aufgaben arbeiten und sie testen, sie so weit wie möglich unabhängig voneinander profilieren.
Aber wie viel können wir wirklich davon profitieren? Wenn Ingenieure am selben Projekt arbeiten, können sie die gleiche Ansicht gut teilen, ohne ein nicht präemptives Betriebssystem zu verwenden.
Wenn ein Ingenieur aus einem anderen Projekt oder Unternehmen stammt, profitiert er davon, wenn er das Betriebssystem vorher kannte. Aber wenn nicht, dann scheint es für ihn keinen großen Unterschied zu machen, ein neues Betriebssystem oder einen neuen Code zu lernen.

Vorteil 2:
Wenn der OS-Code gut getestet wurde, spart das Zeit beim Debuggen. Das ist wirklich ein guter Vorteil.
Aber wenn die Anwendung nur etwa 5 Aufgaben hat, denke ich, dass es nicht wirklich chaotisch ist, Ihren eigenen Code mit Timer-Interrupt und Hintergrundschleife zu schreiben.

Ein nicht präemptives Betriebssystem wird hier auf ein kommerzielles / kostenloses / Legacy-Betriebssystem mit einem nicht präemptiven Scheduler bezogen.
Wenn ich diese Frage poste, denke ich hauptsächlich an bestimmte Betriebssysteme wie:
(1) KISS Kernel (ein kleines nichtpräemptives RTOS - behauptet von seiner Website)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (leichtes RTOS – wird von seiner Website behauptet)
(3) FreeRTOS (es ist ein präemptives RTOS, aber soweit ich weiß, kann es auch als nicht präemptives RTOS konfiguriert werden)
(4) uC/OS (ähnlich wie FreeRTOS)
(5 ) Legacy OS / Scheduler-Code in einigen Unternehmen (normalerweise vom Unternehmen intern erstellt und gepflegt)
(Kann keine weiteren Links hinzufügen, da die Einschränkung durch das neue StackOverflow-Konto erfolgt)

Soweit ich weiß, ist ein nicht präemptives Betriebssystem eine Sammlung dieser Codes:
(1) ein Scheduler, der eine nicht präemptive Strategie verwendet.
(2) Einrichtungen für Kommunikation zwischen Tasks, Mutex, Synchronisation und Zeitsteuerung.
(3) Speicherverwaltung.
(4) andere hilfreiche Einrichtungen / Bibliotheken wie Dateisystem, Netzwerkstapel, GUI usw. (FreeRTOS und uC/OS bieten diese, aber ich bin mir nicht sicher, ob sie noch funktionieren, wenn der Scheduler als nicht präemptiv konfiguriert ist)
Einige davon sie sind nicht immer da. Aber der Planer ist ein Muss.

Das ist es in Kürze. Wenn Sie eine Arbeitslast haben, die Multithreading erfordert, und Sie sich den Overhead leisten können, verwenden Sie ein Threading-Betriebssystem. Ansonsten reicht für die meisten Fälle ein einfacher zeit- oder aufgabenbasierter „Scheduler“. Und um herauszufinden, ob präventives oder kooperatives Multitasking am besten ist ... Ich denke, es kommt auf den Overhead an und darauf, wie viel Kontrolle Sie über das Multitasking haben möchten, das Sie tun müssen.

Antworten (3)

Das riecht etwas off-topic, aber ich werde versuchen, es wieder auf den richtigen Weg zu bringen.

Preemptives Multitasking bedeutet, dass das Betriebssystem oder der Kernel den aktuell ausgeführten Thread aussetzen und auf der Grundlage der vorhandenen Scheduling-Heuristik zu einem anderen wechseln kann. Meistens haben die laufenden Threads keine Ahnung, dass auf dem System andere Dinge vor sich gehen, und das bedeutet für Ihren Code, dass Sie darauf achten müssen, ihn so zu gestalten, dass, wenn der Kernel entscheidet, einen Thread mitten in a Mehrschrittbetrieb (z. B. Ändern eines PWM-Ausgangs, Auswählen eines neuen ADC-Kanals, Lesen des Status von einem I2C-Peripheriegerät usw.) und einen anderen Thread für eine Weile laufen lassen, damit sich diese beiden Threads nicht gegenseitig stören.

Ein beliebiges Beispiel: Nehmen wir an, Sie sind neu bei eingebetteten Multithread-Systemen und haben ein kleines System mit einem I2C-ADC, einem SPI-LCD und einem I2C-EEPROM. Sie haben entschieden, dass es eine gute Idee wäre, zwei Threads zu haben: einen, der vom ADC liest und Samples in das EEPROM schreibt, und einen, der die letzten 10 Samples liest, sie mittelt und auf dem SPI-LCD anzeigt. Das unerfahrene Design würde in etwa so aussehen (grob vereinfacht):

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

Dies ist ein sehr grobes und schnelles Beispiel. Codieren Sie nicht so!

Denken Sie jetzt daran, dass das präventive Multitasking-Betriebssystem einen dieser Threads in jeder Zeile im Code (eigentlich in jeder Assembler-Anweisung) aussetzen und dem anderen Thread Zeit zum Ausführen geben kann.

Denk darüber nach. Stellen Sie sich vor, was passieren würde, wenn das Betriebssystem entscheiden würde, adc_thread()zwischen dem Setzen der EE-Adresse zum Schreiben und dem Schreiben der eigentlichen Daten zu unterbrechen. lcd_thread()würde laufen, mit dem I2C-Peripheriegerät herumspielen, um die benötigten Daten zu lesen, und wenn adc_thread()es wieder an der Reihe wäre, wäre das EEPROM nicht in demselben Zustand, in dem es verlassen wurde. Die Dinge würden überhaupt nicht sehr gut funktionieren. Schlimmer noch, es könnte sogar die meiste Zeit funktionieren, aber nicht immer, und Sie würden verrückt werden, wenn Sie versuchen würden, herauszufinden, warum Ihr Code nicht funktioniert, wenn er so AUSSIEHT, wie er sollte!

Das ist ein Best-Case-Beispiel; Das Betriebssystem entscheidet sich möglicherweise dafür, den Kontext i2c_write()von adc_thread()zu verlassen und es erneut aus lcd_thread()dem Kontext von auszuführen! Die Dinge können sehr schnell sehr chaotisch werden.

Wenn Sie Code schreiben, der in einer präventiven Multitasking-Umgebung funktioniert, müssen Sie Sperrmechanismen verwenden , um sicherzustellen, dass nicht die Hölle losbricht, wenn Ihr Code zu einem ungünstigen Zeitpunkt ausgesetzt wird.

Kooperatives Multitasking hingegen bedeutet, dass jeder Thread die Kontrolle darüber hat, wann er seine Ausführungszeit aufgibt. Die Codierung ist einfacher, aber der Code muss sorgfältig entworfen werden, um sicherzustellen, dass alle Threads genügend Zeit zum Ausführen erhalten. Noch ein erfundenes Beispiel:

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

Dieser Code wird nicht so funktionieren, wie Sie denken, oder selbst wenn er zu funktionieren scheint, wird er nicht funktionieren, wenn die Datenrate des Echo-Threads zunimmt. Nehmen wir uns noch einmal eine Minute Zeit, um es uns anzusehen.

echo_thread()wartet darauf, dass ein Byte an einem UART erscheint und bekommt es dann und wartet, bis es Platz zum Schreiben gibt, dann schreibt es. Nachdem dies erledigt ist, können andere Threads ausgeführt werden. seconds_counter()erhöht einen Zähler, wartet 1000 ms und gibt dann den anderen Threads die Möglichkeit, ausgeführt zu werden. Wenn während dieser Zeit zwei Bytes in den UART sleep()eingehen, könnten Sie sie übersehen, da unser hypothetischer UART kein FIFO zum Speichern von Zeichen hat, während die CPU mit anderen Dingen beschäftigt ist.

Der richtige Weg, dieses sehr schlechte Beispiel zu implementieren, wäre, dort zu platzieren, yield_cpu()wo immer Sie eine Besetztschleife haben. Dies hilft, die Dinge voranzutreiben, kann aber andere Probleme verursachen. Wenn zB das Timing kritisch ist und Sie die CPU einem anderen Thread überlassen, der länger dauert als erwartet, könnte Ihr Timing abgeworfen werden. Ein preemptives Multitasking-Betriebssystem hätte dieses Problem nicht, da es Threads zwangsweise anhält, um sicherzustellen, dass alle Threads korrekt geplant sind.

Was hat das nun mit einem Timer und einer Hintergrundschleife zu tun? Der Timer und die Hintergrundschleife sind dem obigen kooperativen Multitasking-Beispiel sehr ähnlich:

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

Dies sieht dem kooperativen Threading-Beispiel ziemlich ähnlich; Sie haben einen Timer, der Ereignisse einrichtet, und eine Hauptschleife, die nach ihnen sucht und auf atomare Weise darauf reagiert. Sie müssen sich keine Sorgen machen, dass sich die ADC- und LCD-„Threads“ gegenseitig stören, da einer den anderen niemals unterbricht. Sie müssen sich immer noch Sorgen machen, dass ein "Thread" zu lange dauert; zB was passiert wenn get_adc_data()dauert 30ms? Sie werden drei Gelegenheiten verpassen, nach einem Zeichen zu suchen und es wiederzugeben.

Die Loop+Timer-Implementierung ist oft viel einfacher zu implementieren als ein kooperativer Multitasking-Mikrokernel, da Ihr Code spezifischer für die jeweilige Aufgabe entworfen werden kann. Sie betreiben weniger Multitasking als vielmehr das Entwerfen eines festen Systems, bei dem Sie jedem Subsystem etwas Zeit geben, um seine Aufgaben auf sehr spezifische und vorhersehbare Weise zu erledigen. Sogar ein kooperatives Multitasking-System muss eine generische Aufgabenstruktur für jeden Thread haben, und der nächste auszuführende Thread wird durch eine Scheduling-Funktion bestimmt, die recht komplex werden kann.

Die Verriegelungsmechanismen für alle drei Systeme sind die gleichen, aber der für jedes erforderliche Overhead ist ziemlich unterschiedlich.

Persönlich programmiere ich fast immer nach diesem letzten Standard, der Loop+Timer-Implementierung. Ich finde Threading ist etwas, das sehr sparsam verwendet werden sollte. Es ist nicht nur komplexer zu schreiben und zu debuggen, sondern erfordert auch mehr Overhead (ein preemptiver Multitasking-Mikrokernel wird immer größer sein als ein dumm einfacher Timer und Event-Follower der Hauptschleife).

Es gibt auch ein Sprichwort, das jeder, der an Threads arbeitet, zu schätzen wissen wird:

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

:-)

Vielen Dank für Ihre Antwort mit detaillierten Beispielen, akohlsmith. Ich kann jedoch aus Ihrer Antwort nicht schließen, warum Sie sich für eine einfache Timer- und Hintergrundschleifenarchitektur und nicht für das kooperative Multitasking entscheiden . Versteh mich nicht falsch. Ich weiß Ihre Antwort sehr zu schätzen, die viele nützliche Informationen zu verschiedenen Zeitplänen enthält. Ich habe einfach nicht den Sinn.
Könntest du bitte noch ein bisschen daran arbeiten?
Danke Kohlschmied. Ich mag den Satz, den du am Ende gesetzt hast. Es hat eine Weile gedauert, bis ich es erkannt habe :) Zurück zum Punkt Ihrer Antwort, Sie codieren fast immer für die Loop + Timer-Implementierung. Was hat Sie dann in den Fällen, in denen Sie diese Implementierung aufgegeben und sich einem nicht präemptiven Betriebssystem zugewandt haben, dazu veranlasst?
Ich habe sowohl kooperative als auch präventive Multitasking-Systeme verwendet, als ich das Betriebssystem eines anderen ausgeführt habe. Entweder Linux, ThreadX, ucOS-ii oder QNX. Selbst in einigen dieser Situationen habe ich die einfache und effektive Timer+Event-Schleife verwendet ( poll()kommt mir sofort in den Sinn).
Ich bin kein Fan von Threading oder Multitasking in Embedded, aber ich weiß, dass dies für komplexe Systeme die einzig vernünftige Option ist. Konservierte Mikrobetriebssysteme bieten Ihnen eine schnelle Möglichkeit, Dinge zum Laufen zu bringen, und stellen häufig auch Gerätetreiber bereit.
Ich mag das kooperative Multitasking-Modell in vielerlei Hinsicht; In vielen Situationen ist es ziemlich einfach, "yield"-Aufrufe im gesamten Code zu verteilen. Das größte Problem, auf das ich stoßen kann, ist, wenn ich eine Diagnoseausgabe zu einem Zeitpunkt erzeugen möchte, an dem ich wirklich keinen Taskwechsel möchte, und ich löse dieses Problem, indem ich meine Diagnoseausgaberoutine die Ausgabe nach Möglichkeit puffern oder überspringen lasse es wenn nicht.

Multitasking kann in vielen Mikrocontroller-Projekten eine nützliche Abstraktion sein, obwohl ein echter präemptiver Scheduler in den meisten Fällen zu schwerfällig und unnötig wäre. Ich habe weit über 100 Mikrocontroller-Projekte durchgeführt. Ich habe einige Male kooperatives Tasking verwendet, aber ein präventiver Task-Wechsel mit dem damit verbundenen Ballast war bisher nicht angemessen.

Die Probleme beim präemptiven Tasking im Gegensatz zum kooperativen Tasking sind:

  1. Viel schwerer. Präventive Task-Scheduler sind komplizierter, benötigen mehr Codeplatz und mehr Zyklen. Sie benötigen außerdem mindestens einen Interrupt. Das ist oft eine unzumutbare Belastung für die Anwendung.

  2. Mutexe sind um Strukturen herum erforderlich, auf die möglicherweise gleichzeitig zugegriffen wird. In einem kooperativen System rufen Sie TASK_YIELD einfach nicht mitten in einer eigentlich atomaren Operation auf. Dies wirkt sich auf Warteschlangen, den gemeinsamen globalen Zustand und an vielen Stellen aus.

Im Allgemeinen ist es sinnvoll, eine Aufgabe einem bestimmten Job zuzuweisen, wenn die CPU dies unterstützen kann und der Job mit genügend verlaufsabhängigen Operationen so kompliziert ist, dass es umständlich wäre, ihn in einige wenige separate Einzelereignisse aufzuteilen. Dies ist im Allgemeinen der Fall, wenn ein Kommunikationseingangsstrom verarbeitet wird. Solche Dinge sind normalerweise stark zustandsgesteuert, abhängig von früheren Eingaben. Beispielsweise können Opcode-Bytes gefolgt von Datenbytes vorhanden sein, die für jeden Opcode eindeutig sind. Dann gibt es das Problem, dass diese Bytes auf Sie zukommen, wenn etwas anderes Sie senden möchte. Mit einer separaten Aufgabe, die den Eingabestrom verarbeitet, können Sie ihn im Aufgabencode so erscheinen lassen, als würden Sie hinausgehen und das nächste Byte abrufen.

Insgesamt sind Aufgaben nützlich, wenn viel Zustandskontext vorhanden ist. Tasks sind im Grunde Zustandsmaschinen, wobei der PC die Zustandsvariable ist.

Viele Dinge, die ein Mikro tun muss, können als Reaktion auf eine Reihe von Ereignissen ausgedrückt werden. Infolgedessen habe ich normalerweise eine Hauptereignisschleife. Dies überprüft jedes mögliche Ereignis nacheinander, springt dann zurück an die Spitze und wiederholt alles. Wenn die Behandlung eines Ereignisses mehr als nur ein paar Zyklen dauert, springe ich nach der Behandlung des Ereignisses normalerweise zum Anfang der Ereignisschleife zurück. Dies bedeutet praktisch, dass Ereignisse eine implizite Priorität haben, je nachdem, wo sie in der Liste markiert sind. Auf vielen einfachen Systemen ist dies gut genug.

Manchmal bekommt man etwas kompliziertere Aufgaben. Diese können oft in eine Abfolge einer kleinen Anzahl separater Aufgaben unterteilt werden. In diesen Fällen können Sie interne Flags als Ereignisse verwenden. Ich habe so etwas oft bei Low-End-PICs gemacht.

Wenn Sie die grundlegende Ereignisstruktur wie oben haben, aber beispielsweise auch auf einen Befehlsstrom über den UART antworten müssen, dann ist es nützlich, eine separate Aufgabe zu haben, die den empfangenen UART-Strom verarbeitet. Einige Mikrocontroller haben begrenzte Hardware-Ressourcen für Multitasking, wie ein PIC 16, der seinen eigenen Call-Stack nicht lesen oder schreiben kann. In solchen Fällen verwende ich eine, wie ich sie nenne, Pseudo-Task für den UART-Befehlsprozessor. Die Hauptereignisschleife behandelt immer noch alles andere, aber eines ihrer zu behandelnden Ereignisse ist, dass ein neues Byte vom UART empfangen wurde. In diesem Fall springt es zu einer Routine, die diese Pseudo-Aufgabe ausführt. Das UART-Befehlsmodul enthält den Aufgabencode, und die Ausführungsadresse und einige Registerwerte der Aufgabe werden im RAM in diesem Modul gespeichert. Der Code, zu dem die Ereignisschleife gesprungen ist, speichert die aktuellen Register, lädt die gespeicherten Task-Register, und springt zur Task-Restart-Adresse. Der Aufgabencode ruft ein YIELD-Makro auf, das das Gegenteil tut, das dann schließlich zum Anfang der Hauptereignisschleife zurückspringt. In einigen Fällen führt die Hauptereignisschleife die Pseudo-Aufgabe einmal pro Durchgang aus, normalerweise ganz unten, um sie zu einem Ereignis mit niedriger Priorität zu machen.

Auf einem PIC 18 und höher verwende ich ein echtes kooperatives Tasking-System, da der Aufrufstapel von der Firmware gelesen und beschrieben werden kann. Auf diesen Systemen werden die Neustartadresse, einige andere Zustandselemente und der Datenstapelzeiger für jede Aufgabe in einem Speicherpuffer gehalten. Um alle anderen Aufgaben einmal laufen zu lassen, ruft eine Aufgabe TASK_YIELD auf. Dies speichert den aktuellen Aufgabenstatus, durchsucht die Liste nach der nächsten verfügbaren Aufgabe, lädt ihren Status und führt sie dann aus.

In dieser Architektur ist die Hauptereignisschleife nur eine weitere Aufgabe mit einem Aufruf von TASK_YIELD am Anfang der Schleife.

Mein gesamter Multitasking-Code für PICs ist kostenlos verfügbar. Um es anzuzeigen, installieren Sie die PIC Development Tools- Version unter http://www.embedinc.com/pic/dload.htm . Suchen Sie nach Dateien mit „task“ in ihrem Namen im Verzeichnis SOURCE > PIC für die 8-Bit-PICs und im Verzeichnis SOURCE > DSPIC für die 16-Bit-PICs.

Mutexe können in kooperativen Multitasking-Systemen immer noch erforderlich sein, obwohl dies selten vorkommt. Das typische Beispiel ist ein ISR, der Zugriff auf einen kritischen Abschnitt benötigt. Dies kann fast immer durch ein besseres Design oder die Wahl eines geeigneten Datencontainers für die kritischen Daten vermieden werden.
@akoh: Ja, ich habe bei einigen Gelegenheiten Mutexe verwendet, um eine gemeinsam genutzte Ressource zu handhaben, wie den Zugriff auf den SPI-Bus. Mein Punkt war, dass Mutexe nicht in dem Maße erforderlich sind, wie sie in einem präventiven System vorhanden sind. Ich wollte nicht sagen, dass sie niemals in einem kooperativen System benötigt oder verwendet werden. Außerdem kann ein Mutex in einem kooperativen System so einfach sein wie das Drehen in einer TASK_YIELD-Schleife, die ein einzelnes Bit prüft. In einem präventiven System müssen sie im Allgemeinen in den Kernel eingebaut werden.
@OlinLathrop: Ich denke, der bedeutendste Vorteil von nicht präemptiven Systemen in Bezug auf Mutexe besteht darin, dass sie nur erforderlich sind, wenn sie entweder direkt mit Interrupts interagieren (die von Natur aus präventiv sind) oder wenn entweder die Zeit benötigt wird, um eine geschützte Ressource zu halten die Zeit überschreitet, die man zwischen "yield"-Aufrufen verbringen möchte, oder man möchte eine geschützte Ressource um einen Aufruf herum halten, der "yield" ergibt (z. B. "Daten in eine Datei schreiben"). In manchen Fällen, in denen es ein Problem gewesen wäre, einen Ertrag innerhalb eines "Daten schreiben" -Aufrufs zu haben, habe ich ...
... eine Methode, um zu prüfen, wie viele Daten sofort geschrieben werden können, und eine Methode (die wahrscheinlich ergeben kann), um sicherzustellen, dass eine gewisse Menge verfügbar ist (Beschleunigung der Rückgewinnung von schmutzigen Flash-Blöcken und Warten, bis eine geeignete Anzahl zurückgefordert wurde) .
Hallo Olin, ich mag deine Antwort so sehr. Seine Informationen gehen weit über meine Fragen hinaus. Es beinhaltet viele praktische Erfahrungen.
Könnte ich Ihre Antwort auf meine Frage wie folgt abschließen?: (1) Multitasking kann in vielen Mikrocontroller-Projekten eine nützliche Abstraktion sein, obwohl ein echter präemptiver Scheduler in den meisten Fällen zu schwerfällig und unnötig wäre. (2) Aufgaben sind nützlich, wenn viel Zustandskontext vorhanden ist. Tasks sind im Grunde Zustandsmaschinen, wobei der PC die Zustandsvariable ist. (3) Auf einer MCU, die ihren eigenen Aufrufstapel lesen oder schreiben kann, wie z. B. ein PIC 18 und höher, verwenden Sie ein echtes kooperatives Tasking-System.
Wenn meine Schlussfolgerung richtig ist, könnten Sie bitte erklären, "warum Multitasking in vielen Mikrocontroller-Projekten eine nützliche Abstraktion sein kann" ? (Mein Verständnis der Logik Ihrer Antwort auf meine Frage lautet: Multitasking ist gut -> nicht präemptive Planung ist leicht und ausreichend, während präventive Planung in den meisten Fällen zu schwer und unnötig ist -> daher ist es besser, eine nicht präemptive zu wählen Betriebssystem statt hausgemachtem Code mit Hintergrundschleife und Timer-Interrupt-Architektur) Ich habe einen guten Artikel darüber gefunden, aber ich würde auch gerne wissen, wie Sie das verstehen . Danke.
Eine andere Frage ist: Auf einer MCU, die ihren eigenen Aufrufstapel lesen oder schreiben kann, wie z. B. einem PIC 18 und höher, gibt es in der industriellen Praxis Fälle, in denen Sie sich dafür entscheiden, kein kooperatives Tasking-System zu verwenden, sondern einen einfacheren Loop + Timer Implementierung? Wenn ja, warum wählen Sie in diesen Fällen Loop+Timer?
@hailang: Selbst bei Prozessoren, die Stacks wechseln können, muss man auf Compiler-Bibliotheken oder andere Routinen achten, die möglicherweise auf statische Variablen angewiesen sind. Dies ist besonders wichtig in C++; Wenn man nicht aufpasst, kann eine Ausnahme, die in einem Thread ausgelöst wird, in einem anderen "gefangen" werden.
@hailang: Tut mir leid, ich war weg. Eigentlich bin ich noch weg, habe aber zufällig Internetzugang. Ja, Ihre Schlussfolgerungen 1-3 sind das, was ich sagen wollte. Ich dachte, dass viele Antworten darauf bestanden, warum Multitasking nützlich sein kann. Wenn ein Job stark zustandsgesteuert ist, kann es im Grunde umständlich sein, mit vielen Ereignisbedingungen umzugehen, da es so viele geben würde, die schwer zu warten sind. Eine Aufgabe erledigt dies gut, indem sie je nach Status zu Codeabschnitten springt. Das meinte ich damit, dass der PC die Zustandsvariable ist. Allerdings mache ich oft auch einfache Event-Loop-Systeme. Was passt.
@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...Ich bin mir ziemlich sicher, dass Sie nicht implizieren wollen, dass ein System (präventiv oder nicht) Mutexe (im Gegensatz zu kritischen Abschnitten) verwenden sollte, um Daten zu schützen, die zwischen einem Thread und einem Interrupt geteilt werden. Auf einen Mutex in einem (asynchronen) Interrupt warten - keine so gute Idee. Obwohl ich sicher bin, dass Sie das wissen, bin ich mir bei der OP nicht so sicher.
@Radian: Entschuldigung, wenn meine Terminologie schlampig war; Die Art von Konstrukt, die ich im Allgemeinen verwenden würde, würde beinhalten, dass der Hauptcode ein Flag setzt, das dem Interrupt mitteilt, etwas in Ruhe zu lassen, und / oder der Interrupt ein Flag setzt, das dem Hauptleitungscode mitteilt, dass er "warten" soll (durch Aufrufen, yield()bis das Flag gelöscht ist ). Nicht ganz das normale Muster für einen Mutex oder einen kritischen Abschnitt; Ich bin mir nicht sicher, wie es heißen soll.

Bearbeiten: (Ich werde meinen früheren Beitrag unten lassen; vielleicht hilft es jemandem eines Tages.)

Multitasking-Betriebssysteme jeglicher Art und Interrupt-Service-Routinen sind keine konkurrierenden Systemarchitekturen – oder sollten es nicht sein. Sie sind für verschiedene Jobs auf verschiedenen Ebenen des Systems gedacht. Interrupts sind wirklich für kurze Codesequenzen gedacht, um unmittelbare Aufgaben wie das Neustarten eines Geräts, möglicherweise das Abfragen von nicht unterbrechenden Geräten, Zeitmessung in Software usw. zu erledigen. Es wird normalerweise angenommen, dass der Hintergrund jede weitere Verarbeitung übernimmt, die danach nicht mehr zeitkritisch ist Sofortige Bedürfnisse wurden erfüllt. Wenn Sie lediglich einen Timer neu starten und eine LED umschalten oder ein anderes Gerät pulsieren müssen, kann der ISR dies normalerweise sicher im Vordergrund erledigen. Andernfalls muss es den Hintergrund informieren (indem es ein Flag setzt oder eine Nachricht in die Warteschlange stellt), dass etwas getan werden muss, und den Prozessor freigeben.

Ich habe sehr einfache Programmstrukturen gesehen, deren Hintergrundschleife nur eine Leerlaufschleife ist: for(;;){ ; }. Die gesamte Arbeit wurde in der Timer-ISR erledigt. Dies kann funktionieren, wenn das Programm einen konstanten Vorgang wiederholen muss, der garantiert in weniger als einer Timer-Periode abgeschlossen ist. bestimmte begrenzte Arten der Signalverarbeitung kommen mir in den Sinn.

Persönlich schreibe ich ISRs, die aufräumen und herauskommen, und lasse den Hintergrund alles andere übernehmen, was getan werden muss, selbst wenn das so einfach ist wie eine Multiplikation und Addition, die in einem Bruchteil einer Timer-Periode erledigt werden könnte. Warum? Eines Tages komme ich auf die brillante Idee, meinem Programm eine weitere „einfache“ Funktion hinzuzufügen, und „verdammt, es braucht nur eine kurze ISR, um es zu tun“, und plötzlich wächst meine zuvor einfache Architektur mit einigen Interaktionen, die ich nicht geplant hatte an und passieren inkonsistent. Diese zu debuggen macht nicht viel Spaß.


(Zuvor geposteter Vergleich zweier Arten von Multitasking)

Aufgabenumschaltung: Preemptive MT übernimmt die Aufgabenumschaltung für Sie, einschließlich der Sicherstellung, dass kein Thread CPU-hungrig wird und dass Threads mit hoher Priorität ausgeführt werden, sobald sie bereit sind. Cooperative MT erfordert, dass der Programmierer sicherstellt, dass kein Thread den Prozessor zu lange am Stück beschäftigt. Sie müssen auch entscheiden, wie lang zu lang ist. Das bedeutet auch, dass Sie bei jeder Änderung des Codes darauf achten müssen, ob ein Codesegment jetzt dieses Zeitquantum überschreitet.

Schutz nicht-atomarer Operationen: Bei einer PMT müssen Sie sicherstellen, dass Thread-Swaps nicht mitten in Operationen stattfinden, die nicht geteilt werden dürfen. Lesen/Schreiben bestimmter Geräteregisterpaare, die beispielsweise in einer bestimmten Reihenfolge oder innerhalb einer maximalen Zeit behandelt werden müssen. Mit CMT ist es ziemlich einfach - geben Sie den Prozessor nur nicht mitten in einer solchen Operation auf.

Debugging: Im Allgemeinen einfacher mit CMT, da Sie planen, wann/wo Threadwechsel stattfinden. Race-Conditions zwischen Threads und Bugs im Zusammenhang mit nicht Thread-sicheren Operationen mit einem PMT sind besonders schwer zu debuggen, da Thread-Änderungen probabilistisch und somit nicht wiederholbar sind.

Den Code verstehen: Threads, die für ein PMT geschrieben wurden, sind ziemlich genau so geschrieben, als ob sie alleine stehen könnten. Threads, die für ein CMT geschrieben wurden, werden als Segmente geschrieben und können je nach gewählter Programmstruktur für einen Leser schwieriger zu verstehen sein.

Verwenden von nicht Thread-sicherem Bibliothekscode: Sie müssen überprüfen, ob jede Bibliotheksfunktion, die Sie unter einem PMT-Thread-sicher aufrufen, sicher ist. printf() und scanf() und ihre Varianten sind fast immer nicht Thread-sicher. Bei einem CMT wissen Sie, dass keine Thread-Änderung auftritt, es sei denn, Sie geben den Prozessor ausdrücklich frei.

Ein von einem endlichen Zustandsautomaten gesteuertes System zur Steuerung eines mechanischen Geräts und/oder zur Verfolgung externer Ereignisse sind oft gute Kandidaten für CMT, da bei jedem Ereignis nicht viel zu tun ist – einen Motor starten oder stoppen, ein Flag setzen, den nächsten Zustand wählen usw. Somit sind Zustandsänderungsfunktionen von Natur aus kurz.

Ein hybrider Ansatz kann in solchen Systemen sehr gut funktionieren: CMT, um die Zustandsmaschine (und damit den größten Teil der Hardware) zu verwalten, die als ein Thread ausgeführt wird, und ein oder zwei weitere Threads, um länger laufende Berechnungen durchzuführen, die von einem Zustand ausgelöst werden Rückgeld.

Danke für deine Antwort, JRobert. Aber es ist nicht auf meine Frage zugeschnitten. Es vergleicht präemptives OS mit nicht-präemptivem OS, aber nicht präemptives OS mit Nicht-OS.
Richtig - Entschuldigung. Meine Bearbeitung sollte Ihre Frage besser beantworten.