Was sind die Vorteile eines nicht vorbeugenden Betriebssystems? und der Preis für diese Leistungen?

14

Was sind die Vorteile eines nicht präventiven Betriebssystems im Vergleich zu hausgemachtem Code mit Hintergrundschleife und Timer-Interrupt-Architektur? Welche dieser Vorteile sind attraktiv genug für ein Projekt, um ein nicht präventives Betriebssystem einzuführen, anstatt selbst erstellten Code mit Hintergrundschleifenarchitektur zu verwenden?
.

Erklärung zur Frage:

Ich schätze es wirklich, dass alle meine Fragen beantwortet haben. Ich habe das Gefühl, die Antwort ist fast da. Ich füge diese Erklärung meiner Frage hier hinzu, die meine eigene Überlegung zeigt und dazu beitragen kann, die Frage einzugrenzen oder genauer zu machen.

Ich versuche 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 Grundkonzepte und der attraktivsten Vorteile der verschiedenen RTOS-Arten und des entsprechenden Preises, da es nicht für alle Anwendungen das beste RTOS gibt.
Ich habe vor ein paar Jahren Bücher über OS gelesen, aber ich habe sie nicht mehr dabei. Ich habe im Internet gesucht, bevor ich meine Frage hier gepostet und festgestellt habe, dass diese Informationen am hilfreichsten sind: http://www.ustudy.in/node/5456 .
Es gibt viele andere hilfreiche Informationen wie die Einführungen in die Website verschiedener RTOS, Artikel zum Vergleich von vorbeugender Planung und nicht vorbeugender Planung usw.
Ich habe jedoch kein Thema gefunden, in dem erwähnt wurde, wann ein nicht präemptives RTOS ausgewählt werden soll, und wann es besser ist, einfach mit Timer-Interrupt und Hintergrundschleife eigenen Code zu schreiben.
Ich habe zwar meine eigenen Antworten, bin aber damit nicht zufrieden genug.
Ich würde wirklich gerne die Antwort oder Meinung von erfahreneren Leuten erfahren, insbesondere in der Industriepraxis.

Mein bisheriges Verständnis ist:
Unabhängig von der Verwendung oder Nichtverwendung eines Betriebssystems sind bestimmte 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 verwenden können, auch wenn sie sich zuvor nicht in demselben Projekt befanden. Mit der gleichen Sicht auf die Konzeptaufgabe können Ingenieure dann verschiedene Aufgaben bearbeiten und testen und sie so weit wie möglich unabhängig voneinander profilieren.
Aber wie viel können wir wirklich davon profitieren? Wenn Ingenieure im selben Projekt arbeiten, können sie die gleiche Ansicht auch ohne ein nicht präemptives Betriebssystem verwenden.
Wenn ein Ingenieur aus einem anderen Projekt oder Unternehmen stammt, hat er den Vorteil, dass er das Betriebssystem bereits kannte. Aber wenn er es nicht getan hat, scheint es keinen großen Unterschied für ihn zu machen, ein neues Betriebssystem oder einen neuen Code zu erlernen.

Vorteil 2:
Wenn der Betriebssystemcode gut getestet wurde, spart dies Zeit beim Debuggen. Das ist wirklich ein guter Vorteil.
Aber wenn die Anwendung nur ungefähr 5 Aufgaben hat, ist es meiner Meinung nach nicht sehr umständlich, eigenen Code mit Timer-Interrupt und Hintergrundschleife zu schreiben.

Ein nicht präemptives Betriebssystem wird hier als kommerzielles / freies / Legacy-Betriebssystem mit einem nicht präemptiven Scheduler bezeichnet.
Bei der Beantwortung dieser Frage denke ich hauptsächlich an bestimmte Betriebssysteme wie:
(1) KISS Kernel (Ein kleines RTOS ohne Prävention - behauptet von seiner Website)
http://www.frontiernet.net/~rhode/kisskern.html
(2) uSmartX (Leichtgewichtiges RTOS - wird von seiner Website beansprucht)
(3) FreeRTOS (Es ist ein präemptives RTOS, aber
meines Wissens 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 von der Firma selbst erstellt und verwaltet)
(Weitere Links können nicht hinzugefügt werden, da das neue StackOverflow-Konto eingeschränkt ist.)

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 Aufgaben, Mutex, Synchronisation und Zeitsteuerung.
(3) Speicherverwaltung.
(4) Andere hilfreiche Funktionen / Bibliotheken wie Dateisystem, Netzwerkstapel, GUI usw. (FreeRTOS und uC / OS stellen diese zur Verfügung, aber ich bin mir nicht sicher, ob sie noch funktionieren, wenn der Scheduler als nicht präemptiv konfiguriert ist.)
Einige von Sie sind nicht immer da. Aber der Scheduler ist ein Muss.

hailang
quelle
Das ist so ziemlich alles auf den Punkt gebracht. Wenn Sie eine Workload haben, für die Multithreading erforderlich ist, und sich den Overhead leisten können, verwenden Sie ein Threading-Betriebssystem. Andernfalls reicht in den meisten Fällen ein einfacher zeit- oder aufgabenbasierter "Scheduler" aus. Und um herauszufinden, ob präventives oder kooperatives Multitasking am besten ist ... Ich denke, es hängt vom Aufwand ab und davon, wie viel Kontrolle Sie über das Multitasking haben möchten, das Sie tun müssen.
Akohlsmith

Antworten:

13

Das riecht etwas unangenehm, aber ich werde versuchen, es wieder auf Kurs zu bringen.

Präventives Multitasking bedeutet, dass das Betriebssystem oder der Kernel den aktuell ausgeführten Thread anhalten und basierend auf den vorhandenen Planungsheuristiken zu einem anderen wechseln kann. In den meisten Fällen haben die ausgeführten Threads keine Vorstellung davon, dass andere Dinge auf dem System vor sich gehen. Dies bedeutet für Ihren Code, dass Sie darauf achten müssen, dass Sie einen Thread so gestalten, dass er vom Kernel in der Mitte eines Threads angehalten wird 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 eine Weile laufen lassen, damit sich diese beiden Threads nicht gegenseitig stören.

Ein beliebiges Beispiel: Nehmen wir an, Sie sind neu in Multithread-Embedded-Systemen und verfügen über ein kleines System mit einem I2C-ADC, einem SPI-LCD und einem I2C-EEPROM. Sie haben entschieden, dass es eine gute Idee ist, zwei Threads zu haben: einen, der aus dem 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 ungefähr so ​​aussehen (stark 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. Code nicht so!

Denken Sie jetzt daran, dass das vorbeugende Multitasking-Betriebssystem einen dieser Threads an einer beliebigen Codezeile anhalten 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 adc_thread()zwischen dem Einstellen der EE-Adresse zum Schreiben und dem Schreiben der eigentlichen Daten eine Pause einlegen würde. lcd_thread()würde laufen, mit dem I2C-Peripheriegerät herumspielen, um die benötigten Daten zu lesen, und wenn es adc_thread()wieder an der Reihe wäre, würde sich das EEPROM nicht in dem Zustand befinden, in dem es belassen wurde. Die Dinge würden überhaupt nicht gut funktionieren. Schlimmer noch, es könnte sogar die meiste Zeit funktionieren, aber nicht die ganze Zeit, und Sie würden verrückt werden, wenn Sie herausfinden würden, warum Ihr Code nicht funktioniert, wenn er so aussieht, wie er sollte!

Das ist ein Best-Case-Beispiel. das O vorgreifen kann entscheiden , ob i2c_write()von adc_thread()‚s Kontext und starten Sie es erneut aus laufenden lcd_thread()‘ s Kontext! Die Dinge können sehr schnell sehr unordentlich werden.

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

Kooperatives Multitasking bedeutet andererseits, 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 haben. Ein anderes 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 funktioniert nicht so, wie Sie denken, oder selbst wenn er zu funktionieren scheint, funktioniert er nicht, wenn die Datenrate des Echo-Threads zunimmt. Nehmen wir uns noch einmal eine Minute Zeit, um es uns anzuschauen.

echo_thread()Wartet, bis ein Byte an einem UART angezeigt wird, und holt es dann ab. Wartet, bis Platz zum Schreiben vorhanden ist, und schreibt es dann. Danach können andere Threads ausgeführt werden. seconds_counter()erhöht eine Zählung, wartet 1000 ms und gibt dann den anderen Threads die Chance, ausgeführt zu werden. Wenn während dieser Zeit zwei Bytes in den UART gelangen sleep(), können Sie diese möglicherweise übersehen, da unser hypothetischer UART kein FIFO zum Speichern von Zeichen hat, während die CPU andere Aufgaben ausführt.

Der richtige Weg, um dieses sehr schlechte Beispiel zu implementieren, wäre, immer dort zu platzieren, yield_cpu()wo Sie eine Besetztschleife haben. Dies wird helfen, die Dinge voranzutreiben, könnte aber auch andere Probleme verursachen. Wenn z. B. das Timing kritisch ist und Sie die CPU an einen anderen Thread übergeben, der länger als erwartet dauert, kann das Timing möglicherweise deaktiviert werden. Ein vorbeugendes Multitasking-Betriebssystem hat dieses Problem nicht, da es Threads zwangsweise anhält, um sicherzustellen, dass alle Threads ordnungsgemäß 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 kommt dem kooperativen Threading-Beispiel ziemlich nahe. Sie haben einen Timer, der Ereignisse einrichtet, und eine Hauptschleife, die danach sucht und auf atomare Weise auf sie einwirkt. Sie müssen sich keine Sorgen machen, dass die ADC- und LCD- "Threads" sich 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()30ms dauert? Sie werden drei Gelegenheiten verpassen, nach einem Charakter zu suchen und ihn zu wiederholen.

Die Implementierung von Loop + Timer ist häufig viel einfacher als die Implementierung eines kooperativen Multitasking-Mikrokerns, da Ihr Code spezifischer auf die jeweilige Aufgabe zugeschnitten werden kann. Sie arbeiten nicht wirklich mit Multitasking, sondern entwerfen ein festes System, bei dem Sie jedem Subsystem Zeit geben, seine Aufgaben auf eine sehr spezifische und vorhersehbare Weise zu erledigen. Selbst ein kooperatives Multitasking-System muss für jeden Thread eine generische Taskstruktur haben, und der nächste auszuführende Thread wird durch eine Planungsfunktion bestimmt, die sehr komplex werden kann.

Die Verriegelungsmechanismen sind für alle drei Systeme gleich, der jeweils erforderliche Aufwand ist jedoch sehr unterschiedlich.

Persönlich codiere 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 es erfordert auch mehr Overhead (ein präemptiver Multitasking-Mikrokernel wird immer größer sein als ein dumm einfacher Timer und ein Hauptschleifenereignisfolger).

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

:-)

akohlsmith
quelle
Vielen Dank für Ihre Antwort mit ausführlichen Beispielen, akohlsmith. Ich kann jedoch nicht aus Ihrer Antwort schließen, warum Sie sich für eine einfache Timer- und Hintergrundschleifenarchitektur anstatt für das kooperative Multitasking entschieden haben . Versteh mich nicht falsch. Ich freue mich sehr über Ihre Antwort, die viele nützliche Informationen zu verschiedenen Zeitplänen enthält. Ich habe es einfach nicht verstanden.
Hagang
Könnten Sie bitte ein bisschen mehr daran arbeiten?
Hagang
Danke, Akohlsmith. Ich mag den Satz, den Sie am Ende setzen. Es hat eine Weile gedauert, bis ich es erkannt habe :) Zurück zum Punkt Ihrer Antwort, Sie codieren fast immer zur Implementierung der Schleife + des Timers. In den Fällen, in denen Sie diese Implementierung aufgegeben und sich dem nicht vorbeugenden Betriebssystem zugewandt haben, was hat Sie dazu veranlasst?
Hagang
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. Sogar in einigen dieser Situationen habe ich die einfache und effektive Timer + Event-Schleife verwendet (fällt mir poll()sofort ein).
Akohlsmith
Ich bin kein Fan von Threading oder Multitasking in Embedded, aber ich weiß, dass es für komplexe Systeme die einzig vernünftige Option ist. Mit eingespielten Mikrobetriebssystemen können Sie schnell einsatzbereit sein und häufig auch Gerätetreiber bereitstellen.
Akohlsmith
6

Multitasking kann in vielen Mikrocontroller-Projekten eine nützliche Abstraktion sein, obwohl ein echter präventiver Scheduler in den meisten Fällen zu schwer und unnötig wäre. Ich habe weit über 100 Mikrocontroller-Projekte durchgeführt. Ich habe mehrmals kooperatives Tasking verwendet, aber ein vorbeugender Task-Wechsel mit dem dazugehörigen Gepäck war bisher nicht angebracht.

Die Probleme mit vorbeugendem Tasking im Zusammenhang mit kooperativem Tasking sind:

  1. Viel schwerer. Vorbeugende Aufgabenplaner sind komplizierter, beanspruchen mehr Code und mehr Zyklen. Sie benötigen außerdem mindestens einen Interrupt. Dies ist häufig eine inakzeptable Belastung für die Anwendung.

  2. Mutexe sind für Strukturen erforderlich, auf die gleichzeitig zugegriffen werden kann. In einem kooperativen System rufen Sie TASK_YIELD nicht in der Mitte einer atomaren Operation auf. Dies wirkt sich auf Warteschlangen, den gemeinsam genutzten globalen Status und auf viele Bereiche 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 historienabhängigen Vorgängen kompliziert genug ist, um ihn in einige separate Einzelereignisse aufzuteilen, was mühsam wäre. Dies ist im Allgemeinen der Fall, wenn ein Kommunikationseingabestrom verarbeitet wird. Solche Dinge sind normalerweise stark zustandsgetrieben, abhängig von einigen vorherigen Eingaben. Beispielsweise kann es Opcode-Bytes geben, gefolgt von Daten-Bytes, die für jeden Opcode eindeutig sind. Dann gibt es das Problem, dass diese Bytes auf Sie zukommen, wenn etwas anderes Lust hat, sie zu senden. Mit einer separaten Task, die den Eingabestream behandelt, können Sie ihn im Taskcode so anzeigen lassen, als würden Sie das nächste Byte abrufen.

Insgesamt sind Aufgaben nützlich, wenn viel staatlicher Kontext vorhanden ist. Aufgaben sind im Grunde genommen Zustandsautomaten, wobei der PC die Zustandsvariable ist.

Viele Dinge, die ein Mikro zu tun hat, können als Reaktion auf eine Reihe von Ereignissen ausgedrückt werden. Daher habe ich normalerweise eine Hauptereignisschleife. Dies überprüft nacheinander jedes mögliche Ereignis, springt dann nach oben und erledigt alles erneut. Wenn das Behandeln eines Ereignisses mehr als nur ein paar Zyklen dauert, springe ich nach dem Behandeln des Ereignisses normalerweise zum Anfang der Ereignisschleife zurück. Dies bedeutet, dass Ereignisse eine implizite Priorität haben, die davon abhängt, wo sie in der Liste markiert sind. Bei vielen einfachen Systemen ist dies ausreichend.

Manchmal bekommt man etwas kompliziertere Aufgaben. Diese lassen sich oft in eine Abfolge von wenigen, zu erledigenden Aufgaben aufteilen. In diesen Fällen können Sie interne Flags als Ereignisse verwenden. Ich habe so etwas schon oft auf Low-End-PICs gemacht.

Wenn Sie die grundlegende Ereignisstruktur wie oben haben, aber beispielsweise auch auf einen Befehlsstrom über den UART antworten müssen, ist es nützlich, den empfangenen UART-Strom von einer separaten Task zu behandeln. Einige Mikrocontroller verfügen über begrenzte Hardwareressourcen für Multitasking, z. B. ein PIC 16, der seinen eigenen Aufrufstapel nicht lesen oder schreiben kann. In solchen Fällen verwende ich eine sogenannte Pseudotask für den UART-Befehlsprozessor. Die Hauptereignisschleife behandelt 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 Pseudotask ausführt. Das UART-Befehlsmodul enthält den Aufgabencode, und die Ausführungsadresse und einige Registerwerte der Aufgabe werden in diesem Modul im RAM gespeichert. Der Code, zu dem die Ereignisschleife springt, speichert die aktuellen Register, lädt die gespeicherten Taskregister, und springt zur Task-Neustartadresse. Der Taskcode ruft ein YIELD-Makro auf, das den umgekehrten Vorgang ausführt und schließlich zum Anfang der Hauptereignisschleife zurückspringt. In einigen Fällen führt die Hauptereignisschleife die Pseudotask einmal pro Durchgang aus, normalerweise am unteren Rand, 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 Statuselemente und der Datenstapelzeiger für jede Task in einem Speicherpuffer gespeichert. Damit alle anderen Tasks einmal ausgeführt werden, ruft eine Task TASK_YIELD auf. Dadurch wird der aktuelle Taskstatus gespeichert, die Liste nach dem nächsten verfügbaren Task durchsucht, sein Status geladen und anschließend ausgeführt.

In dieser Architektur ist die Hauptereignisschleife nur eine weitere Aufgabe, mit einem Aufruf von TASK_YIELD oben in der Schleife.

Mein gesamter Multitasking-Code für PICs ist kostenlos verfügbar. Installieren Sie dazu die PIC Development Tools- Version unter http://www.embedinc.com/pic/dload.htm . Suchen Sie nach Dateien mit dem Namen "task" im Verzeichnis SOURCE> PIC für die 8-Bit-PICs und im Verzeichnis SOURCE> DSPIC für die 16-Bit-PICs.

Olin Lathrop
quelle
In kooperativen Multitasking-Systemen können immer noch Mutexe 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 Auswahl eines geeigneten Datencontainers für die kritischen Daten vermieden werden.
Akohlsmith
@akoh: Ja, ich habe gelegentlich Mutexe verwendet, um eine gemeinsam genutzte Ressource wie den Zugriff auf den SPI-Bus zu verwalten. Mein Punkt war, dass Mutexe nicht von Natur aus erforderlich sind, sofern sie sich in einem präventiven System befinden. Ich wollte nicht sagen, dass sie in einem kooperativen System niemals gebraucht oder benutzt werden. Ein Mutex in einem kooperativen System kann auch so einfach sein, als würde er in einer TASK_YIELD-Schleife ein einzelnes Bit prüfen. In einem präventiven System müssen sie im Allgemeinen in den Kernel eingebaut werden.
Olin Lathrop
@OlinLathrop: Ich denke, der größte Vorteil von nicht-präemptiven Systemen bei Mutexen besteht darin, dass sie nur dann benötigt werden, wenn sie direkt mit Interrupts interagieren (die von Natur aus präemptiv sind) oder wenn entweder die Zeit benötigt wird, um eine geschützte Ressource zu halten überschreitet die Zeit, die man zwischen "Ertrag" -Anrufen verbringen möchte, oder man möchte eine geschützte Ressource um einen Anruf halten, der "Ertrag" könnte (z. B. "Daten in eine Datei schreiben"). In einigen Fällen, in denen es ein Problem gewesen wäre, eine Rendite innerhalb eines "Write Data"
-Aufrufs zu erzielen
... eine Methode, um zu überprüfen, wie viele Daten sofort geschrieben werden können, und eine Methode (die wahrscheinlich einen Ertrag bringt), um sicherzustellen, dass eine bestimmte Menge verfügbar ist (Beschleunigen der Rückgewinnung von schmutzigen Flash-Blöcken und Warten, bis eine geeignete Anzahl zurückgefordert wurde). .
Supercat
Hallo Olin, ich mag deine Antwort so sehr. Ihre Informationen stehen weit über meinen Fragen. Es beinhaltet viele praktische Erfahrungen.
Hagang
1

Bearbeiten: (Ich lasse meinen früheren Beitrag unten; vielleicht hilft es irgendwann jemandem.)

Multitasking-Betriebssysteme jeglicher Art und Interrupt-Serviceroutinen sind keine konkurrierenden Systemarchitekturen und sollten es auch nicht sein. Sie sind für verschiedene Jobs auf verschiedenen Ebenen des Systems gedacht. Interrupts sind eigentlich für kurze Codesequenzen gedacht, um unmittelbare Aufgaben wie das Neustarten eines Geräts, möglicherweise das Abrufen nicht unterbrechender Geräte, die Zeitmessung in der Software usw. zu erledigen. In der Regel wird davon ausgegangen, dass der Hintergrund eine weitere Verarbeitung ausführt, die nach dem nicht mehr zeitkritisch ist unmittelbare Bedürfnisse wurden erfüllt. Wenn Sie lediglich einen Timer neu starten und eine LED umschalten oder ein anderes Gerät anstoßen müssen, kann der ISR dies normalerweise sicher im Vordergrund tun. Andernfalls muss es den Hintergrund (durch Setzen eines Flags oder Einreihen einer Nachricht) darüber informieren, dass etwas getan werden muss, und den Prozessor freigeben.

Ich habe sehr einfache Programmstrukturen , deren Hintergrund zu sehen Schleife ist nur eine Leerlaufschleife: for(;;){ ; }. Die gesamte Arbeit wurde im Timer-ISR erledigt. Dies kann funktionieren, wenn das Programm einen konstanten Vorgang wiederholen muss, der garantiert in weniger als einer Timer-Periode beendet ist. man denke an bestimmte begrenzte Arten der Signalverarbeitung.

Persönlich schreibe ich ISRs, die ein Rauskommen bereinigen, und lasse den Hintergrund alles andere übernehmen, was getan werden muss, auch wenn dies so einfach ist wie eine Multiplikation und Addition, die in einem Bruchteil einer Timer-Periode durchgeführt werden könnte. Warum? Eines Tages werde ich auf die gute Idee kommen, meinem Programm eine weitere "einfache" Funktion hinzuzufügen, und "zum Teufel, es wird nur eine kurze ISR-Zeit dauern, um dies zu tun", und plötzlich wächst meine zuvor einfache Architektur um einige Interaktionen, die ich nicht geplant hatte auf und passieren inkonsistent. Es macht nicht viel Spaß, diese zu debuggen.


(Zuvor veröffentlichter Vergleich zweier Arten von Multitasking)

Taskwechsel: Das präventive MT kümmert sich für Sie um den Taskwechsel, einschließlich der Sicherstellung, dass kein Thread die CPU verliert und dass Threads mit hoher Priorität ausgeführt werden, sobald sie bereit sind. Beim kooperativen MT muss der Programmierer sicherstellen, dass kein Thread den Prozessor zu lange hält. Sie müssen auch entscheiden, wie lange zu lang ist. Das bedeutet auch, dass Sie bei jeder Änderung des Codes wissen müssen, ob ein Codesegment jetzt dieses Zeitmaß überschreitet.

Schutz nichtatomarer Operationen: Bei einer PMT müssen Sie sicherstellen, dass Thread-Auslagerungen nicht mitten in Operationen auftreten, die nicht geteilt werden dürfen. Lesen / Schreiben bestimmter Geräte-Register-Paare, die beispielsweise in einer bestimmten Reihenfolge oder innerhalb einer maximalen Zeitspanne verarbeitet werden müssen. Mit CMT ist es ziemlich einfach - geben Sie den Prozessor während eines solchen Vorgangs einfach nicht frei.

Debugging: Generell einfacher mit CMT, da Sie planen, wann / wo Threadwechsel stattfinden. Die Race-Bedingungen zwischen Threads und Bugs im Zusammenhang mit nicht thread-sicheren Operationen mit einem PMT sind besonders schwierig zu debuggen, da Thread-Änderungen wahrscheinlich und daher nicht wiederholbar sind.

Den Code verstehen: Threads, die für ein PMT geschrieben wurden, sind so gut wie so geschrieben, als könnten sie für sich allein stehen. Für ein CMT geschriebene Threads werden als Segmente geschrieben. Je nach gewählter Programmstruktur ist es für einen Leser möglicherweise schwieriger, diesen zu folgen.

Verwenden von nicht threadsicherem Bibliothekscode: Sie müssen überprüfen, ob jede Bibliotheksfunktion, die Sie aufrufen, unter einem threadsicheren PMT-Code ausgeführt wird. printf () und scanf () und ihre Varianten sind fast immer nicht threadsicher. Mit einem CMT wissen Sie, dass keine Thread-Änderung auftritt, es sei denn, Sie geben den Prozessor speziell aus.

Ein maschinengesteuertes System mit endlichen Zuständen zur Steuerung eines mechanischen Geräts und / oder Verfolgung externer Ereignisse ist häufig ein guter Kandidat für CMT, da bei jedem Ereignis nicht viel zu tun ist: Starten oder Stoppen eines Motors, Setzen einer Flagge und Auswählen des nächsten Zustands usw. Somit sind Zustandsänderungsfunktionen von Natur aus kurz.

Ein hybrider Ansatz kann in solchen Systemen sehr gut funktionieren: CMT zum Verwalten der Zustandsmaschine (und daher des größten Teils der Hardware), die als ein Thread ausgeführt wird, und ein oder zwei weitere Threads zum Ausführen von Berechnungen, die von einem Zustand ausgelöst wurden Veränderung.

JRobert
quelle
Danke für deine Antwort, JRobert. Aber es ist nicht auf meine Frage zugeschnitten. Es vergleicht preemptive Betriebssysteme mit nicht preemptiven Betriebssystemen, vergleicht jedoch nicht nicht preemptive Betriebssysteme mit nicht preemptiven Betriebssystemen.
Hagang
Richtig - Entschuldigung. Meine Bearbeitung sollte Ihre Frage besser beantworten.
JRobert