BU 1: Beispiel einer Trace-Visualisierung mit Percepio Tracealyzer. Links ist – nach Prioritäten geordnet – die genaue Verarbeitung der Threads zu sehen, während sich in den rechts dargestellten Übersichten etwaige Abweichungen leichter erkennen lassen.
Eingebettete Systeme laufen heutzutage großenteils unter Multi-Threaded-Software, d. h. unter Linux oder einem Echtzeit-Betriebssystem (Real-Time Operating System, RTOS). Das Multi-Threading bietet in der Tat viele Vorteile gegenüber Single-Threaded-Designs, jedoch kann die Software komplexer sein, wodurch die Verifikation zu einer größeren Herausforderung wird.
Was macht Multi-Threaded-Software so anders?
Um Multi-Threaded-Software für eingebettete Systeme umfassend zu verifizieren, reichen traditionelle Verifikationsmethoden wie Codeprüfung, statische Analyse und Funktionstests nicht aus. Dies liegt daran, dass die Threads auf eine Art und Weise miteinander interagieren können, die aus dem Quellcode nicht ersichtlich ist. Zum Beispiel können Operationen an IPC-Objekten (Inter-Process Communication) wie etwa Semaphoren oder Mutexen dazu führen, dass die Verarbeitung infolge von Aktionen anderer Threads plötzlich blockiert wird. Ebenso können Threads gegenseitig um die Prozessorzeit konkurrieren und mit zeitabhängigen Bugs wie etwa Race Conditions behaftet sein.
Multi-Threaded-Systeme bilden daher ein diffiziles Geflecht gegenseitiger, durch explizite und implizite Interaktionen verursachter Abhängigkeiten zwischen den Threads. Hinzu kommt, dass solche Abhängigkeiten durch unterschiedliche Verarbeitungszeiten beeinflusst werden, die nicht explizit aus dem Code hervorgehen, sondern sich erst zur Laufzeit ergeben. Besonders problematisch ist dies in Multi-Core-Systemen, in denen Buskonflikte zwischen den Prozessorkernen unvorhersehbare Konsequenzen für die Verarbeitungszeiten haben können. Auswirkungen des Timings auf die Wechselwirkungen zwischen den Threads erweisen sich häufig als unkontrollierter Faktor beim Testen von Multi-Threaded-Systemen.
Gelegentlich können selbst sehr umfangreiche Tests nur die Oberfläche eines wahren Ozeans an potenziellen Verarbeitungsszenarien erfassen. Verschiedenste latente Defekte bleiben dann möglicherweise unentdeckt und treten erst im praktischen Einsatz zutage. Auch der Versuch, sie im Labor nachzustellen, ist mit großer Wahrscheinlichkeit zum Scheitern verurteilt.
Ein gutes Beispiel hierfür ist Pathfinder, der erste Mars-Rover der NASA. Dieses System dürfte wesentlich gründlicher getestet worden sein als die meiste andere Embedded-Software, aber dennoch traten auf dem Weg zum Mars mehrere Störungen auf. Ursache war ein Prioritätsinversions-Problem, das zum Zurücksetzen eines Watchdogs führte. Details hierzu enthält dieser Beitrag, der interessant zu lesen ist und nicht nur das Problem selbst, sondern auch seine Aufdeckung mithilfe von Software-Tracing beschreibt.
BU 2: Beispiel für die Darstellung von Prioritätsinversion in Percepio Tracealyzer. Die Grafik rechts gibt die variierenden Antwortzeiten des ausgewählten Threads wieder.
Wie lässt sich die Prüfbarkeit von Embedded-Software verbessern?
Wenn die Qualität von Embedded-Software verbessert werden soll, hilft es wenig, nur den Umfang der Tests auszuweiten. Zuallererst ist nämlich sicherzustellen, dass die Software wirklich prüfbar ist, dass also die gleiche Eingabe auf Systemebene stets das gleiche Ergebnis liefert – unabhängig vom Timing der Software. Erreichen lässt sich dies am besten, indem beim Design von Multi-Threaded-Software die anerkannten Regeln der Technik beachtet werden. Dies ist freilich ein weites Feld, aus dem ich deshalb nur die folgenden Beispiele herausgreifen möchte:
- Bei periodischen Threads ist eine solide Methode zur Zuweisung der Scheduling-Prioritäten des RTOS anzuwenden. Beim Rate-Monotonic Scheduling etwa wird der am häufigsten ausgeführten Task die höchste Priorität gegeben.
- Interrupt Handler sollten so kurz und deterministisch wie möglich gehalten werden. Anstatt den gesamten Ereignisbehandlungs-Code im Interrupt Handler unterzubringen, sollten bestimmte Tätigkeiten in Threads niedrigerer Priorität ausgelagert werden.
- Code mit langen oder stark variierenden Verarbeitungszeiten sollten mit möglichst niedriger (vorzugsweise sogar mit der niedrigsten) Scheduling-Priorität verarbeitet werden.
- Der Zugriff auf gemeinsam genutzte Ressourcen hat auf sichere Weise, d. h. ohne das Risiko von Race Conditions, Prioritätsinversionen oder Deadlocks zu erfolgen. In der Regel wird empfohlen, kritische Abschnitte mit Mutex-Objekten zu schützen, die eine Prioritätsvererbung unterstützen.
Steht das Softwaredesign im Hinblick auf Multi-Threading auf soliden Füssen, lässt sich ein stabileres, deterministisches Systemverhalten herbeiführen, was die Testabdeckung auf Systemebene verbessert, ohne den Testaufwand zu erhöhen. Das Risiko, dass schwer fassbare Bugs bis in den Produktions-Code gelangen, wird dadurch minimiert.
Verifikation der Echtzeitanforderungen
Eingebettete Systeme müssen oftmals echtzeitfähig sein und mehr oder weniger explizite Anforderungen an das Software-Timing erfüllen. Zum Beispiel kann an ein Steuerungssystem die Forderung gestellt werden, alle 5 ms Steuersignale an eine Motorsteuerung auszugeben. Jede zusätzliche Verzögerung würde hier als Fehler eingestuft.
Die Einhaltung solcher Anforderungen wird nicht nur durch die Verarbeitungszeit des jeweiligen Threads beeinflusst, sondern auch durch Abhängigkeiten von anderen Threads. Zum Beispiel kann es vorkommen, dass ein Thread höherer Priorität oder ein Interrupt die Verarbeitung über das erwartete Maß hinaus verzögert. Nutzt ein Thread geteilte Ressourcen wie etwa einen Mutex, kann das Ergebnis des Zugriffs vom Timing relativ zu anderen Threads abhängen, die ihrerseits von anderen Faktoren beeinflusst werden.
Beim Verifizieren von Echtzeitanforderungen geht es also nicht allein um das Messen von Timing-Parametern, sondern auch um das Aufdecken potenzieller Risiken durch Thread-Interaktionen, die Einfluss auf die Timing-Anforderungen haben können.
Analyse von Multi-Threaded-Software
Wie aber lässt sich ein Softwaresystem auf die gerade geschilderte Weise verifizieren? Wie lässt sich feststellen, ob ein Design in Sachen Multi-Threading korrekt ist? Wie kann man das Software-Timing messen und die Echtzeitanforderungen verifizieren?
Die De-facto-Lösung besteht darin, die Software per Software-Tracing zur Laufzeit zu beobachten. Für diesen Zweck bietet das Tool Percepio® Tracealyzer® zahlreiche visuelle Analysefeatures. Es wird häufig zum Debugging von RTOS- und Linux-Systemen auf Systemebene genutzt, verfügt aber ebenfalls über umfangreiche Funktionen für die Analyse von Softwaredesigns und zur Verifikation von Echtzeitanforderungen.
Das System-Tracing mit Tracealyzer bietet folgende Möglichkeiten:
- Erfassung detaillierter Laufzeitdaten über die Thread-Verarbeitung, Interaktionen zwischen Threads und das Timing. Dies ist über lange Testläufe hinweg möglich, ohne dass spezielle Hardware benötigt wird.
- Aufdeckung von Anomalien im Echtzeitverhalten mithilfe von Übersichtsdarstellungen wie etwa CPU-Auslastungsdiagrammen oder Statistikreports. Durch einfaches Anklicken eines anormalen Datenpunkts lassen sich Einzelheiten in der Trace-Ansicht abrufen.
- Analyse von Variationen im Software-Timing. Zum Beispiel zeigt der Actor Instance Graph verschiedene Timing-Werte der einzelnen Threads über die Zeit an.
- Übersicht über Thread-Abhängigkeiten. Der Communication Flow Graph etwa bietet einen Überblick über Thread-Interaktionen durch IPC-Objekte.
Tracealyzer benötigt keinen spezifischen Hardware-Support, sondern stützt sich auf eine effiziente Instrumentierung der Zielsoftware. Da hierbei auf bestehende Tracepunkte im Kernel zurückgegriffen wird, ist eine gesonderte Instrumentierung des Applikationscodes nicht nötig. Selbstverständlich ist es aber dennoch möglich, den Applikationscode mit expliziten Tracing-Aufrufen zu versehen, um das Tracing zu erweitern.
Die erfassten Tracedaten können anschließend auf unterschiedliche Weise an den Hostcomputer übertragen werden – unter anderem per Echtzeit-Streaming über eine Ethernetverbindung oder einen unterstützten Debug-Probe.
DIeser Artikel ist in der Zeitschrift POLYSCOPE erschienen.