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.
quelle
Antworten:
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):
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 esadc_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()
vonadc_thread()
‚s Kontext und starten Sie es erneut aus laufendenlcd_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:
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 gelangensleep()
, 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:
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:
:-)
quelle
poll()
sofort ein).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:
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.
quelle
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.
quelle