Multicore-Systeme Ereignisanalyse in Echtzeit

Basiert ein Echtzeitsystem auf einer Multicore-Architektur, nehmen Geschwindigkeit und Anzahl der Interaktionen stark zu, was einerseits die Leistungsfähigkeit des Systems verbessern, andererseits aber die Echtzeit-Sequenzierung von Applikationsereignissen komplizierter machen kann.

Für Multicore-Entwickler bedeutet die erhöhte Komplexität, die das Management der großen Zahl von Ereignissen und ihre Gleichzeitigkeit mit sich bringen, dass die vom Systemdesign gestellten Anforderungen exponentiell zunehmen. Weit schwieriger als bei einem Ein-Prozessor-System gestaltet es sich auch, die Ursache von Systemfehlern oder Effizienzmängeln zu diagnostizieren. Da das Angebot an Multicore-tauglichen Tools überschaubar ist, waren die Entwickler bisher meist auf primitive, auf dem Einfügen von Print-Befehlen basierende Techniken angewiesen, um während des Betriebs sogenannte »Breadcrumbs« (Brotkrumen) zu hinterlassen, die Daten zu bestimmten relevanten Ereignissen enthalten. Der Entwickler kann diese Breadcrumbs anschließend »einsammeln«, um aus ihnen Rückschlüsse auf den Zustand des Systems zu ziehen. Diese Vorgehensweise erfordert nicht selten eine anschließende Re-Instrumentierung, um eine feiner
abgestufte Beobachtung zu ermöglichen und den Prozess danach noch einmal zu starten.

Das Einfügen derartiger Aktionen verschlingt jedoch eine Menge Zeit, speziell wenn man bedenkt, dass der Instrumentierungscode häufig nicht auf Anhieb wie vorgesehen arbeitet, sodass ein Debugging erforderlich ist. Bis zu 70% der Applikationsentwicklung entfallen auf das Debugging. Ist der betreffende Abschnitt der Applikation verifiziert, muss der Instrumentierungscode wieder entfernt werden, und selbstverständlich ist nach diesem Entfernen ein erneutes Debugging erforderlich. Ein großer Teil des Instrumentierungsprozesses läuft also
manuell ab, was nicht nur einen großen Zeitaufwand verursacht, sondern auch das Risiko mit sich bringt, dass sich neue Fehler einschleichen.

Vom Instrumentieren des Codes abgesehen, muss der Entwickler auch einen Weg finden, die generierten Daten zu interpretieren. Allein wegen des schieren Umfangs der vom Instrumentierungscode generierten Daten ist es eine echte Herausforderung, zu ermitteln, welche Systemereignisse in welcher Reihenfolge vorgekommen sind.

Um die komplexe Abfolge von Operationen in einem Multicore-System effizienter entschlüsseln zu können, benötigen Entwickler eine »Instant Replay«-Funktion, mit der sich die unmittelbar vor einem interessanten Ereignis im System stattfindenden Abläufe »zurückspulen« und noch einmal betrachten lassen.

Vorteile des neuen Konzepts

»TraceX«, ein Tool von Express Logic, kann System- und Applikationsereignisse erfassen, sie automatisch analysieren und grafisch darstellen. Ereignisse wie zum Beispiel Thread-Kontextwechsel, Präemptionen, zeitweilige Programmunterbrechungen, Abbrüche und System-Interrupts hinterlassen jeweils ein Breadcrumb, das von TraceX erkannt und dargestellt wird. Neben vielen weiteren nützlichen Informationen enthalten die Breadcrumbs vor allem Angaben darüber, welches Ereignis gerade vorkam, welcher Thread involviert war, auf welchem Core der Thread lief und wann das Ereignis auftrat.

Mit TraceX können Anwender alle gewünschten Applikationsereignisse mithilfe eines API (Application Programming Interface) aufzeichnen lassen. Die ereignisbezogenen Informationen werden in einem zyklischen Puffer, dessen Größe von der Applikation bestimmt wird, im Zielsystem abgelegt. Die zyklische Struktur des Puffers hat zur Folge, dass im Puffer zu jedem Zeitpunkt die letzten n Ereignisse zur Inspektion verfügbar sind, wenn es im System zu einer Fehlfunktion oder zu einem anderen wichtigen Ereignis kommt.

Gute Werkzeuge für das Multicore-Debugging sollten die Möglichkeit bieten, dieses »Event Logging« vom Applikationsprogramm dynamisch starten und stoppen zu lassen (zum Beispiel wenn die Verarbeitung einen bestimmten interessierenden Bereich des Programms erreicht). Hierdurch vermeidet man, dass während des korrekten Programmablaufs unnötige Daten gesammelt werden und den Speicher des Zielsystems belegen. Das Event-Log kann zu Analysezwecken jederzeit an den Host hochgeladen werden - beispielsweise bei einem Breakpoint, bei einem Systemabsturz oder wenn die Verarbeitung der Applikation beendet ist.

Nach dem Upload des Event-Logs an den Host zeigt TraceX die Ereignisse an in grafischer Form und entlang einer horizontal verlaufenden Zeitachse (Bild 1). Die zu den Ereignissen gehörenden Applikations-Threads und Systemroutinen sind entlang der Hochachse aufgereiht, und die Ereignisse selbst erscheinen in der jeweiligen Zeile. In Multicore-Systemen sind die Ereignisse außerdem dem jeweiligen Prozessorkern zugeordnet und so gruppiert, dass die Entwickler problemlos erkennen können, welche Ereignisse zu einem bestimmten Core gehören.

Alle Ereignisse erscheinen zusätzlich in der ganz oben angeordneten »Summary Row«. Hier können sich die Entwickler schnell einen Überblick über alle Systemereignisse verschaffen, ohne durch alle Threads und Cores scrollen zu müssen. Die einzelnen Ereignisse sind dabei als farblich codierte Icons dargestellt, die einerseits zum Zeitpunkt ihres Auftretens auf der horizontalen Zeitachse erscheinen und anderseits rechts vom zugehörigen Thread oder der betreffenden Systemroutine dargestellt sind. Der Darstellungsmaßstab der Achsen kann entweder vergrößert werden, um mehr Ereignisdetails zu zeigen, oder aber verkleinert werden, um mehr Ereignisse sehen zu können.

Außerdem lässt sich die Zeitachse nach links (früher) und rechts (später) abfahren, um alle Punkte im Trace-Puffer zu zeigen. Wird wie in Bild 2 ein bestimmtes Ereignis ausgewählt, erscheinen Detailangaben zu diesem Ereignis, wie zum Beispiel Core, Kontext, Ereignis, Thread-Pointer, neuer Zustand, Stack-Pointer und nächster Thread-Punkt.

Prioritätsumkehrungen erkennen

Prioritätsumkehrungen gehören zu den anspruchsvollsten Problemen, die in Echtzeitsystemen vorkommen können. Zu einem solchen Ereignis kommt es, weil Echtzeitbetriebssysteme einen prioritätsbasierten präemptiven Scheduler verwenden. Dieser sorgt normalerweise dafür, dass von den verarbeitungsbereiten Threads jeweils derjenige mit der höchsten Priorität ausgeführt wird. Hierzu kann der Scheduler auch die gerade laufende Verarbeitung eines Threads niedrigerer Priorität unterbrechen.

Problematisch kann es jedoch werden, wenn Threads mit niedriger und hoher Priorität bestimmte Ressourcen (z.B. Pufferspeicher) gemeinsam nutzen. Belegt ein Thread mit niedriger Priorität die gemeinsam genutzte Ressource, wenn ein Thread mit höherer Priorität verarbeitungsbereit wird, so muss der Thread mit der höheren Priorität warten, bis der weniger wichtige Thread die Ressource freigibt. Hat der Thread mit der höheren Priorität eine kritische Deadline einzuhalten, so muss der Entwickler für die Berechnung seiner Worst-Case-Performance die maximale Zeit zugrunde legen, die der Thread möglicherweise auf alle seine gemeinsam genutzten Ressourcen warten muss. Eine Prioritätsumkehrung liegt immer dann vor, wenn ein Thread mit hoher Priorität warten muss, während die CPU einen Thread niedrigerer Priorität bedient.

Prioritätsumkehrungen lassen sich nur schwierig erkennen und beheben. Sie äußern sich in der Regel durch eine unzureichende Performance, die jedoch auch andere Ursachen haben kann. Zusätzlich erschwert wird die Ursachenermittlung durch die Tatsache, dass sich Prioritätsumkehrungen möglicherweise nicht durch Tests herausfinden lassen, also nicht-deterministischer Natur sind.

Mit einem Systemereignis-Tool wie TraceX lassen sich Prioritätsumkehrungen jedoch automatisch aufdecken. Der Trace-Puffer des Werkzeugs gibt klar Auskunft darüber, welche Threads zu einem bestimmten Zeitpunkt laufen. Auch alle Änderungen der Verarbeitungsbereitschaft eines Threads zeichnet es auf. Somit kann ein Entwickler rückblickend feststellen, ob ein Thread mit höherer Priorität für die Verarbeitung bereit ist, aber durch einen Thread niedrigerer Priorität blockiert wird, weil dieser eine vom wichtigeren Thread benötigte Ressource belegt.

Die in Bild 3 dargestellte Prioritätsumkehrung ist nicht deterministisch, lässt sich aber in der folgenden Darstellung von TraceX entdecken. Man sieht hier, wie Low_thread einen Mutex belegt und von High_thread unterbrochen wird. High_thread versucht dann, eben diesen Multicore zu nutzen, muss aber warten, bis dieser von Low_thread freigegeben wird. Allerdings kommt jetzt Medium_thread dazwischen und kann beliebig lange laufen, wodurch er sowohl Low_thread als auch High_thread ausbremst. Nur wenn Low_thread von Medium_ 
thread genügend Zeit eingeräumt bekommt, um seine Verarbeitung abzuschließen und den Mutex freizugeben, kann High_thread mit seiner Verarbeitung fortfahren.

Applikationsperformance verbessern

Die meisten Entwickler beginnen mit dem Einsatz von Tools wie TraceX, um Probleme zu verstehen und zu beheben. Vielleicht noch vorteilhafter einsetzen lässt sich ein solches Werkzeug jedoch zum Erstellen eines Verarbeitungsprofils, um die Performance der Applikation auf Systemebene zu analysieren und zu verbessern.

Das Verarbeitungsprofil offenbart den Entwicklern, wie viel CPU-Zeit die einzelnen Threads und Systemdienste beanspruchen (Bild 4). Zu Diagnosezwecken können sie anschließend einzelne Ereignisse genau unter die Lupe nehmen.

Für Multicore-Systeme noch relevanter ist das gleichmäßige Aufteilen der Verarbeitungsaufgaben auf alle verfügbaren Cores. Der Systemdurchsatz lässt sich damit überaus wirksam steigern. Wenn ein Systemprofil wie in Bild 4 sichtbar macht, welche Cores sich mehr im Leerlauf befinden, erhalten Entwickler wertvolle Informationen, wie sich Verarbeitungsaufgaben auf einen ansonsten untätigen Core verlagern lassen.

Über den Autor:

John A. Carbone ist Vice President of Marketing von Express Logic.