Entwicklungskosten für Steuerungssysteme senken

Ein modernes Steuerungssystem kann Dutzende oder sogar hunderte von Software-Aufgaben umfassen, die alle um einen begrenzten Umfang an Memory und CPU-Zeit konkurrieren. Um die Entwicklung solcher komplexen Systeme zu beschleunigen, teilen viele Unternehmen die Arbeit unter mehreren Entwicklungsteams auf und weisen jedem Team die Erstellung eines separaten Software-Subsystems zu. Bedingt durch die parallele Entwicklung treten jedoch in der Integrationsphase häufig Performance-Schwierigkeiten auf. Hervorgerufen werden diese aufgrund der Tatsache, dass die verschiedenen Subsysteme nun erstmalig untereinander um die Systemressourcen wetteifern. So können Subsysteme, die isoliert gut arbeiteten, jetzt eine lange Reaktionszeit zeigen oder einfach nicht mehr funktionieren. Partitionierung als Lösung Die Diagnose und Behebung solcher Probleme ist grundsätzlich schwierig. Die Designer müssen mit den Prioritäten der einzelnen Aufgaben jonglieren, unter Umständen das Thread-Verhalten im System ändern und ihre Modifikationen dann erneut testen und verfeinern. Der gesamte Prozess kann durchaus mehrere Wochen, manchmal sogar Monate in Anspruch nehmen, so dass die Kosten steigen und die Produktentwicklung verzögert wird. Die Partitionierung von Ressourcen bietet eine Möglichkeit, diese komplexen Probleme bei der Integration zu bewältigen. Systementwickler isolieren dabei Software-Subsysteme in separate Bereiche oder Partitionen und weisen diesen dann jeweils ein garantiertes Budget an Memory oder CPU-Zeit zu. Der Designer kann beispielsweise eine Reihe an Threads, die einen gemeinsamen Zweck haben, wie etwa die Bewegungssteuerung, in einer Partition platzieren und ihr 50% der gesamten CPU-Kapazität zuweisen. Der Partitions-Scheduler stellt dann sicher, dass diese Partition stets die zugewiesene CPU-Leistung erhält. Somit verfügt jede Partition über eine stabile, bekannte Laufzeitumgebung, die das Entwicklerteam individuell aufbauen und prüfen kann. Wenn also die Software-Prozesse innerhalb der Partition während der Tests der Einheit gut funktionieren, dann werden sie diese Performance mit großer Wahrscheinlichkeit auch bei der Integration zeigen. Unvorhergesehene Konkurrenzsituationen um Ressourcen zwischen Subsystemen können so beseitigt werden. Stillstände bei der Thread- Bearbeitung vermeiden Um die Vorzüge der Zeitpartitionierung zu verstehen, muss man sich vor Augen führen, wie Threads in einem typischen Steuerungssystem zeitlich festgelegt sind. In den meisten Fällen nutzt das System einen prioritätsbasierten Scheduler, der jeweils dem Thread mit der höchsten Priorität das Vorrecht auf CPU-Zeit einräumt. Diese Art von Priorisierung wird weitläufig verwendet und ist gut verstanden. Sie sorgt dafür, dass jeweils die zeitkritischen Threads ihre Deadlines erfüllen. Dadurch wird jedoch ein Problem aufgeworfen: Wenn ein Thread lediglich eine Prioritätsstufe höher ist als ein anderer, kann er den weniger zeitkritischen Thread um die nötige CPU-Zeit bringen. Nehmen wir ein Beispielszenario mit zwei Threads, A und B, bei dem A eine geringfügig höhere Priorität aufweist als B. Sobald A mit Aufgaben überflutet wird, wird er den Thread B, als auch alle weiteren Threads mit niedrigeren Prioritäten, vom Zugang zu CPU-Zeit abschneiden. In einem industriellen Steuerungssystem könnte A der Regelkreis des Roboterarms sein und B die Mensch-Maschine-Schnittstelle (HMI). Wenn nun der Regelkreis zu viel CPU-Zyklen belegt, wird die HMI daran gehindert Updates anzuzeigen, oder sie wird nicht mehr auf Bedienereingaben reagieren. Da Steuerungssysteme zunehmend komplexer werden und Designteams wachsen, wird die Zuweisung und Pflege von Prioritäten für eine große Anzahl von Threads immer schwieriger. Systementwickler haben erkannt, dass die ungeregelte Zuweisung von Prioritäten zu chaotischen Zuständen oder sogar zu einem funktionsunfähigen System führt. Daher sind sie bemüht, die Anzahl der verwendeten Prioritäten auf eine überschaubare Zahl zu beschränken. Diese Lösung hat jedoch einen ungewollten Nebeneffekt: erhöhte Latenzzeit. Da viele Threads dieselbe Priorität haben, kann die Warteliste der bereiten Threads mit einer bestimmten Priorität sehr lang werden. Ein bereiter Thread muss demzufolge warten, bis er an die vorderste Position der Warteliste vorgerückt ist, bis er ausgeführt werden kann. Parallele Entwicklung und Integrationstests vereinfachen Mit der Zeitpartitionierung können Designer ein OS-gestütztes CPU-Budget für jedes Software-Subsystem definieren. Zudem kann jedes Team sein Subsystem testen, um sicherzustellen, dass dasselbe innerhalb des jeweiligen definierten Budgets funktioniert. Zum Zeitpunkt der Integration wird das Echtzeitbetriebssystem (RealTimeOperatingSystem) die Ressourcen-Budgets umsetzen und so verhindern, dass eines der Subsysteme Ressourcen belegt, die von anderen Subsystemen benötigt werden. Jedes System wird erwartungsgemäß – und wie durch die Tests belegt – arbeiten. Im Ergebnis erleichtert die Partitionierung den Entwicklerteams die parallele Arbeit. Der Entwickler muss sich dabei nicht länger Gedanken über die Prioritäten der Threads außerhalb seines Subsystems machen: Diese Threads werden die Performance seines Subsystems nicht mehr beeinflussen. Auch dann nicht, wenn diese eine höhere Priorität besitzen. Innerhalb der Partition werden die Threads nach den bisherigen Regeln eines Steuerungsprogramms prioritätsbasiert abgearbeitet. In der Folge wird jede Partition quasi zu einem virtuellen Prozessor, der es jedem Designteam erlaubt, ein geeignetes Prioritätsschema auf Subsystemebene zu definieren. Damit wird die Umsetzung eines globalen Prioritätsschemas obsolet. Partitionierungsbeispiel Um die Vorteile zu veranschaulichen, lässt sich ein einfaches System heranziehen, das ohne die Verwendung von Zeitpartitionierung aufgebaut wurde. Das System, dargestellt in Bild 1, umfasst die folgenden Prozesse: – Einen Prozess mittlerer Priorität, der die lokale Mensch-Maschine- Schnittstelle (HMI) handhabt; – Einen Prozess mit niedrigerer Priorität für einen remote Überwachungsagenten, der regelmäßig Informationen an ein zentrales, webbasiertes Überwachungssystem sendet; – Einen Prozess mittlerer Priorität, der periodisch den Sensor ab scannt; – Einen Prozess hoher Priorität, der den Motor steuert. Während der Integrationsphase arbeitet das Web-Überwachungs­system zufrieden stellend. Allerdings nur bis zu dem Zeitpunkt, an dem der Benutzer das lokale HMI benutzt. Dann stoppt das Überwachungssystem häufig und zeigt keine weiteren Informationsupdates mehr an. Bei der Fehlersuche stellt sich heraus, dass der remote Überwachungsagent keine CPU-Zeit mehr zugewiesen bekommt, sobald über die HMI Befehle abgesetzt werden, die zu einer erhöhten Beanspruchung der Motorsteuerung führen. Eine Durchsicht der Prioritäten zeigt, warum dies der Fall ist: Da der remote Überwachungsagent die niedrigste Priorität hat, wird ihm der CPU-Zugang abgeschnitten, sobald das System auf voller CPU-Last arbeitet. Bei dem Versuch, dieses Problem zu beheben, weist der Systemdesigner nun dem lokalen HMI eine niedrigere Priorität zu als dem remote Überwachungsagenten. Allerdings führt dieser Ansatz zu einem inakzeptablen Performance-Level für die HMI. Auch der Versuch, den remote Überwachungsagenten sowie den Sensor-Scanner und die HMI mit einer mittleren Priorität auszustatten, erweist sich als nicht gangbare Lösung, da somit die Performance aller drei Prozesse leidet. Da eine Veränderung der Prioritäten das Problem nicht behebt, muss das Entwicklungsteam zur nächsten Maßnahme übergehen und versuchen, das Thread-Verhalten zu ändern – im Stadium der Integration eine kostenintensive Lösung. Partitionierung liefert einen Weg, diese Problempunkte bei der Integration zu umgehen. Beispielsweise kann der Systemdesigner für jedes Subsystem eine Partition einrichten und ein CPU-Budget für jede der vier Partitionen festsetzen: 10% des Budgets für die HMI-Partition, 10% für die Partition der remote Überwachung, 30% für die Partition des Sensor-Scanners und 50% des Budgets für die Motorsteuerungs-Partition, wie in Bild 2 gezeigt. Bei diesem Ansatz kann jede Partition gemäß ihres CPU-Budgets getestet werden. Wenn die Systeme schließlich bei der Integration zusammen gebracht werden, erhalten alle Prozesse anteilig ihr vorher bestimmtes Budget an CPU-Zeit. Konkret bedeutet das, dass der remote Überwachungsagent nicht länger von CPU-Zeit abgeschnitten wird, sobald das System vollständig ausgelastet ist. Zudem können die Entwickler die Budgets der einzelnen Partitionen ohne weiteres ändern, sollte die Reaktionszeit des lokalen HMI gegen eine bessere remote Update-Zeit eingetauscht werden, etwa um das System auf das gewünschte Performance-Niveau abzustimmen. Richtig implementiert erlaubt ein Partitionierungs-Scheduler den Entwicklern eine Anpassung während der Laufzeit vorzunehmen, und zwar ohne dabei die Applikationen oder den Systemaufbau neu gestalten zu müssen. Bild 3 zeigt ein QNX Tool für die dynamische Anpassung von Partitions-Budgets. Eine Neuzuweisung der Prioritäten als Problemlösung? Es ließe sich argumentieren, dass ein adäquates Systemdesign und eine sorgfältige Zuweisung von Prioritäten das Problem in diesem relativ simplen System beheben kann. Die vielen Interaktionen in einem wesentlich komplexeren System münden jedoch in Problemen mit der Abarbeitung von Aufgaben, die wesentlich schwieriger zu korrigieren und zu beheben sind. Folglich sind es diese Systeme, die am meisten von der Partitionierung profitieren. Einsparungen bei der Entwicklung quantifizieren Häufig führte eine Verzögerung der Abarbeitung zu einem unregelmäßigen, unerklärlichen Verhalten und nur selten zu einem kompletten Versagen des Systems. Entsprechend schwierig ist es, die nötigen Daten für eine nachfolgende Fehlersuche zu sammeln. Meist sind zur Fehlersuche breit gefächerte und tiefgehende Systemkenntnisse erforderlich, so dass für die Lokalisierung und Behebung des Problems ein ganzes Team benötigt wird. Die Fehlersuche kann folgende Schritte umfassen: 1.Ein Tester erzeugt einen Bericht des Problems, der das unerwartete Verhalten während der Prüfung be- schreibt. Beispielsweise versäumt die HMI-Anzeige Updates oder reagiert nicht mehr auf Befehle. Da sich das Problem nicht ohne weite- res reproduzieren lässt, kann der Tester nicht auf Anhieb die richti- gen Informationen sammeln und die Problemlösung unterstützen. 2.Der Entwickler führt mehrere Versuche nach dem Trial-and-Error-Prinzip durch, um das Problem zu reproduzieren. Schließlich ermittelt er, dass die Ursache nicht das HMI selbst ist, sondern ein anderer Prozess, der sehr viel CPU-Zeit belegt und somit für die HMI keine mehr übrig bleibt. 3.Die Maßnahmen zur Fehlerbehe- bung werden nun ausgeweitet und nehmen mehrere Personen in An- spruch. Die Lösung kann darin be- stehen, die Prioritäten der Threads neu zu bestimmen oder den Ablauf eines Prozesses zu ändern. 4.Jeder betroffene Mitarbeiter führt die notwendigen Veränderungen und Tests durch und integriert dann seine Änderungen in die Systemsoftware. 5.Der Tester führt einen erneuten Test im Hinblick auf das Problem durch und schließt den Problem- bericht ab. Dies setzt voraus, dass keine weiteren Probleme im Zuge der Veränderungen aufgetreten sind. Basierend auf diesen Annahmen, führt Tabelle 1 den Personalaufwand zur Problembehebung auf. Anhand dieses Beispiels zeigt sich deutlich, wie diese Problemstellungen rasch zu Entwicklungsverzögerungen führen können; in diesem Fall in Höhe von zwei oder drei Kalenderwochen. Überdies beinhaltet dieses Beispiel lediglich vier Threads viele industrielle Steuerungssysteme umfassen Dutzende von Threads, die auf mehr als hundert Arten interagieren, während sie um CPU-Zeit konkurrieren. Entsprechend ist es normal, dass Thread-Ausfälle auch in mäßig großen Systemen auftreten. Indem verhindert wird, dass ein Subsystem einem anderen Subsystem den Zugang zu CPU-Zeit abschneidet, schafft die Partitionierung einen effizienten Weg, diese Problemstellungen zu beheben. Maximale CPU-Auslastung Es gibt unterschiedliche Arten von Partitionierungs-Schedulern: Einige setzen strikt und zu jeder Zeit die CPU-Budgets um, so dass jede Partition ihr volles Budget verbraucht, auch wenn keine Aufgaben vorliegen. Andere Implementierungen sind flexibel und können ungenutzte CPU-Zyklen dynamisch anderen Partitionen zuweisen. Dadurch wird die gesamte CPU-Auslastung maximiert und das System in die Lage versetzt, auch Bedarfsspitzen abzuwickeln.