(An extract of the book titled "Sichere und fehlertolerante Steuerungen")
Sergio Montenegro (http://www.first.fhg.de/~sergio)
GMD-FIRST (http://www.first.fhg.de)
Rudowerchaussee 5
D-12489 Berlin
Safe and fault-tolerant systems are needed where a functionality failure could lead to large losses and/or fatal casualties including dead (safety critical applications). Such systems must continue their normal operation despite any potential anomalies and internal errors and failures. These systems must protect themselves against incorrect inputs and unexpected circumstances in their environment. They need to be robust, in order to be able to stay operable.
Robustness and fault-tolerance are very important for a safe operation, but lets imagine you build a perfectly robust and fault-tolerant autonomous system -- e.g., a 100% automatic locomotive -- no external circumstance and no internal failure can perturb its operation. It will always stay operable and nothing can stop it.
Unfortunately, you made a development error and your system does not do what you wanted it to do, but rather it performs the functions you implemented (something else). Nevertheless it does it quite surely, and no one can stop it any more. Your safety-approach produced danger instead of safety.
Therefore the first step to build safety critical systems should be a correct development, so that the system does, what it should do and nothing else.
Now a days, it is possible to protect halfway your systems against run time errors using redundancy and other fault tolerance techniques. However, handling unexpected external circumstances and development errors remain today as open problems.
Development errors can creep in at diverse points in time and phases in the development process. The standard development phases and their problems are represented in figure 1. Figure 2 shows how development errors are introduced in each phase and figure 3 shows how to check your development against errors. This paper does not introduce a new development model, it only shows what you can do to prevent errors in each development phase.
In order to manage the complexity you have to divide your development in Phases. Each Phase has 3 kind of general errors: interpretation errors (from the documents of the previous phase), think errors and encode errors. Errors in the final product come from high complexity, low testability, unexplored environment, bad preparation of the developers and unsystematic tests.
Therefore you should stay simple, use prototypes, study the environment of the system, protocol all your errors, use review, separate safety critical from the not critical parts, do not underestimate the effort to test the fault tolerance and exception handling and if possible use mathematical formal methods for small safety critical parts.
(Auszug aus dem Buch Sichere und fehlertolerante Steuerungen, Hanser Verlag 1999)
Sichere und fehlertolerante Systeme müssen trotz interner Fehler und Ausfälle weiterlaufen. Sie müssen sich vor fehlerhaften Inputs und unerwarteten Umständen in ihrer Umwelt schützen (robust sein), um weiterlaufen zu können. Stellen Sie sich aber vor, Sie haben ein vollkommen fehlertolerantes und robustes System entwickelt - eine Lokomotive. Kein interner Fehler, keine äußeren Umstände können sie stoppen. Leider hat ihr System einen Entwicklungsfehler und es macht nicht das, was es soll, sondern etwas anderes, dafür aber ganz sicher, und man kann es nicht mehr anhalten. Ein sicheres System mit Entwicklungsfehler wird mit Sicherheit gefährlich sein. Deswegen ist bei der Konstruktion von sicherheitsrelevanten Systemen eine korrekte Entwicklung so wichtig, damit das System tut, was es tun soll und nichts anderes.
Einerseits muß man versuchen, Fehler bei der Entwicklung zu vermeiden. Aber viele Entwicklungsfehler werden unentdeckt bleiben und stellen eine latente Gefahr dar. Mit diesen Fehlern muß man leben können - deswegen muß die Anlage darauf vorbereitet sein, beim Auftreten möglicher Gefahren, wie beispielsweise beim Ausfall einer Hardwarekomponente, aber auch bei einem Steuerungsfehler (meistens Software) selbst, richtig zu reagieren, so daß das System in einem sicheren Zustand verbleibt oder noch besser, sicher weiterarbeitet.
Während man Laufzeitfehler durch Redundanz und Fehlertoleranz-Techniken halbwegs in den Griff bekommen kann, bleiben die Entwicklungsfehler ein offenes Problem. Das Problem wird von zwei Seiten angegangen. 1. Es wird versucht, die gebräuchlichen Entwicklungsmethoden zu verbessern und systematisch zu entwickeln. 2. Es werden neue Wege durch die Verwendung von mathematischen Methoden (formale Methoden) gegangen. Das Zweite könnte sicherer sein, ist aber noch im experimentellen Stadium und sehr aufwendig zu benutzen. Ein zweiter Grund, der hindert, das Vorgehen bei der Entwicklung zu verändern, ist der erwartete Einbruch der Produktivität. Die Abbildung 1 A zeigt die ideale bzw. gewünschte Produktivitätssteigerung bei der Einführung von neuen Technologien. Die Produktivität sollte sich durch neue, bessere Methoden, Techniken und Werkzeuge eigentlich steigern. Leider gibt es einen Einbruch in der Produktivität (Abbildung 1 B), bis die Entwickler sich an die neue Umgebung gewöhnt haben.
Entwicklungsfehler können zu den verschiedensten Zeitpunkten und Phasen im Verlauf des Entwicklungsprozesses entstehen. Die (Standard) Phasen und ihre Probleme werden in Abbildung 2 dargestellt. Hier wird nicht versucht, ein Vorgehensmodell einzuführen, es werden einfach Einzelheiten erwähnt, an die Sie in den verschiedenen Entwicklungsphasen denken sollten.
Bei jedem Schritt von einer Phase in die nächste entstehen Fehler aus drei Kategorien (Abbildung 3): Interpretations-, Denk- und Gedankenniederlegungsfehler. Je mehr Phasen die Entwicklung hat, um so mehr Fehler werden sich akkumulieren.
Bei jedem Entwicklungsschritt können sich Fehler einschleichen, deswegen muß jede Komponente gegen ihre Anforderungen systematisch (und vollständig) getestet werden (Abbildung 4). Bei der Integration werden einige Programmfehler gefunden. Dies sind Sachen, an die der Programmierer nicht gedacht hat. Bei Systemtest und Validierung werden einige Entwurfs- und Analysefehler gefunden. Das sind die, die der Analytiker nicht gesehen hat. Und im Betrieb werden die meisten Fehler gefunden. Dies sind solche Sachen, die keiner wußte und an die keiner dachte, nicht einmal der Kunde oder Endbenutzer selbst. Beispiel: Ein Kraftfahrzeugfabrikant gibt einen erweiterten Tempomat in Auftrag, der die Geschwindigkeit des vorausfahrenden Fahrzeugs messen und damit die eigene Geschwindigkeit begrenzen soll. In der Testphase funktioniert er gut, aber im normalen Betrieb boykottiert der Tempomat das Überholen, indem er abbremst, wenn plötzlich ein anderes Fahrzeug entgegenkommt. So verhindert er, daß der Fahrer nach dem Überholen wieder einscheren kann und bringt ihn in Gefahr - und nicht nur ihn.
Viele Fehler kommen aus folgenden Quellen:
Die beste Fehlerprävention ist, diesen Fehlerquellen entgegenzuwirken. Eine hohe Entwicklungsqualität und damit eine niedrige Fehlerrate kann zusätzlich durch folgende allgemeine Maßnahmen erreicht werden:
1. Menschliche Fehler sind repetitiv, ihre Muster wiederholen sich immer wieder. Deswegen ist es eine Hilfe, Ihre eigenen Fehler zu protokollieren und zu klassifizieren. Dann können Sie bei neuen Entwicklungen nach Ihrem eigenen Fehlermuster suchen und dadurch eine Fehlerwiederholung vermeiden und mehr als 50% der Entwicklungsfehler frühzeitig korrigieren.
2. Der Entwicklungsprozeß erfordert viele Entscheidungen der Entwickler. Getroffene Entscheidungen müssen nachvollziehbar sein. Man muß festhalten, warum eine Entscheidung einer anderen vorzuziehen ist. Auch falsche Entscheidungen, Sackgassen, Probleme und Mißerfolge müssen protokolliert werden (Projekthistorie).
3. Der technische Anfang der Entwicklung sind die Anforderungen an das System. Diese Anforderungen müssen über die gesamte Entwicklung nachvollziehbar sein (Tracebility). Man muß protokollieren, welche Anforderungen in welchen Modulen implementiert werden und wo sie getestet werden. Dies ist eine Hilfe, um Anforderungen nicht zu übersehen und um die Nebenwirkungen von Änderungen zu ermitteln. Wenn möglich, sollte man eine durchgängige Strukturierung von den Systemanforderungen hin bis zum Code anstreben.
4. Fremde Inspektionen (Audits, Reviews) verbessern die Entwicklungsqualität. Allein durch die Vorstellung, daß Sie Ihr Design, Code oder Hardware jemandem zeigen und erklären müssen, werden Sie viel ordentlicher und aufmerksamer entwickeln. Bei der Erklärung Ihrer Arbeit werden Sie selbst Details (und eventuell Fehler) erkennen, die Ihnen beim Entwickeln nicht gegenwärtig waren. Hier ist eine Liste von positiven Erfahrungen durch Inspektionen:
IBM 1975: Inspektionen von Spezifikation, Design und Implementierung reduzierte die Testzeit um bis zu 95%.
Imperial Chemical Industries 1982: Die Wartungszeit bei inspiziertem Code war 0,6 Minuten/Zeile/Jahr, dagegen 7 Minuten/Zeile/Jahr bei nicht inspiziertem Code.
ICL 1986: Der Aufwand, um Fehler durch Tests zu finden, konnte durch Inspektion von 8,5 auf 1,6 Personen-Stunden pro Fehler gesenkt werden.
Stratus Conimuum Languages 1995: Die Inspektionsaufwand betrug 10% des Projektaufwands, der Testaufwand 50%, aber 64% der Fehler wurden durch Inspektion gefunden.
Analyse bedeutet "auflösen". Aber was man braucht, ist eine Sicht des gesamten Systems und nicht nur der einzelnen Teile. Die daraus entstehende (System-) Spezifikation sollte folgendes berücksichtigen:
1. Die Sprache des Anwenders soll benutzt und gelernt werden, ohne zu versuchen, dem Anwender unseren Informatik- bzw. Elektrotechnik-Slang aufzuzwingen. Die Beschreibung muß sich so nah wie möglich an die informelle Beschreibung des Anwenders halten. Eine fachgebietsorientierte Spezifikation minimiert die Übersetzungsfehler. Bezeichner sollten so deskriptiv wie möglich gewählt werden und auf Abkürzungen sollte weitestgehend verzichtet werden. Vor allem soll die Spezifikation so verständlich und kurz (handlich) wie möglich sein, sonst hilft alles andere nichts. Gerade dieser extrem wichtige Punkt wird zu oft mißachtet (siehe z.B. die OSI Standards).
2. Die Spezifikation beschreibt nicht nur "Was" sonder auch "Wofür" das System ist. Dies erleichtert das Verständnis des Systems und des Zusammenwirkens seiner Komponenten. Der Entwickler muß wissen, wofür das System gebaut wird und nicht nur, was gebaut werden soll. Dadurch kann auch er Spezifikationslücken beim Entwickeln entdecken.
3. Für eingebettete Systeme muß immer eine Grenze zwischen System und Umgebung gezogen werden. Das System können wir gestalten und es soll definierte Schnittstellen zu seiner Umgebung haben. Weiterhin stellt auch die Umgebung bestimmte Anforderungen an unser System.
4. Es ist hilfreich, alle Annahmen über Gesamtsystem und Umgebung festzuhalten. Leider kann der Entwickler meistens zwischen Annahmen und Tatsachen nicht unterscheiden. Oft ist ihm nicht einmal bewußt, daß er nur auf Annahmen baut. Sind die Annahmen falsch, so fällt damit alles zusammen. Deswegen müssen die Annahmen so weit wie möglich als solche identifiziert werden, damit ihre Richtigkeit während des Entwicklungsprozesses geprüft werden kann. Sonst wird ihre Richtigkeit nicht in Frage gestellt und Fehler, die auf falschen "Tatsachen" beruhen, werden in der gesamten Entwicklung und auch beim Testen nicht entdeckt (man testet, was man angenommen hat), sondern fallen irgendwann in der Anwendungsphase unangenehm auf. Dies ist das schlimmste.
Beispiel: In Tiefflügen bewährte amerikanische Flugzeuge wurden von Israel gekauft und auch über dem Toten Meer eingesetzt, mit dem Ergebnis, daß sie fast abgestürzt wären. Warum? Das Tote Meer liegt fast 400 m unterhalb des Meeresspiegels und das System zur Bestimmung der Flughöhe wurde niemals unter diesen Bedingungen getestet. Die ,Tatsache", daß ein Flugzeug immer oberhalb Normalnull fliegt, erwies sich als falsche Annahme. Hätten Sie daran gedacht?
5. Durch das Aufteilen von Spezifikation und Anforderungen in 1. Umgebung, 2. Funktionsanforderungen und 3. Sicherheitsanforderungen wird eine bessere Verständlichkeit und damit eine höhere Sicherheit erreicht. Manchmal ist es sogar möglich, die Anforderungen weiter zu unterteilen:
Die Umgebungsbeschreibung enthält die physikalischen Eigenschaften, Schnittstellen zwischen Umgebung und System, Annahmen und Tatsachen zur Umgebung, Unterstellungen über den Betrieb, Terminologien und Begriffsdefinitionen.
Die Funktionsanforderungen beschreiben, was das System machen soll, wofür das System gebraucht wird oder wofür es gebaut wird. Die Sicherheitsanforderungen beschreiben alle Gefahren und alles was niemals geschehen darf. Zuerst werden die Funktionsanforderungen im normalen Betrieb und die dabei entstehenden ersten Sicherheitsanforderungen betrachtet und getrennt festgehalten. Danach werden die Anforderungen im realitätsnäheren Fall, unter Berücksichtigung von Störungen, Ausfällen, Fehlern, Fehlbedingungen usw. untersucht und wiederum getrennt Funktion/Sicherheit erfaßt. Anschließend beschreiben Sie was zu tun ist, wenn nichts mehr geht! Dieser Fall wird mit Sicherheit auch eintreten.
Die gesamten Funktionsanforderungen eines Systems sind wesentlich komplexer als seine Sicherheitsanforderungen und deswegen sind mehr Fehler bei der Funktion als bei der Sicherheit zu erwarten. Wenn wir die Sicherheitsaspekte abtrennen, können wir uns gezielter auf die Sicherheit konzentrieren, ohne viele irrelevante Aspekte mit berücksichtigen zu müssen. Bei der Spezifikation des Systems sollte eine explizite Trennung funktionaler und sicherheitsrelevanter Bestandteile erfolgen. Nach Möglichkeit sollten beide Spezifikationen sogar von verschiedenen Entwicklern gemacht werden. Die sicherheitsrelevanten Teile sollten so klein und einfach wie möglich und frei von für die Sicherheit irrelevanten Teilen gehalten werden.
Die Spezifikation kann auf zwei verschiedene Weisen geschrieben werden, die nicht gemischt werden sollten: operational und axiomatisch. In einer operationalen Spezifikation passiert nur das, was explizit spezifiziert wurde, wie in einer imperativen Programmiersprache. In einer axiomatischen Spezifikation kann alles eintreten, was nicht explizit ausgeschlossen wurde. Eine operationale Spezifikation beschreibt die Funktion des Systems, während eine axiomatische Spezifikation die Eigenschaften beschreibt. Ein Teil einer operationalen Spezifikation könnte z.B. sagen: "Wenn die Temperatur zu hoch ist, schalte die Flamme aus!". In einer axiomatischen Spezifikation würde es so lauten: "Immer wenn die Temperatur zu hoch ist, soll die Flamme aus sein.".
Einige Techniken sind eindeutig axiomatisch und andere operational. Z.B. Timing Diagramme sind axiomatisch. Sie beschreiben, welche Zeitanforderungen erfüllt werden müssen, aber sie sagen nicht, was zu tun ist. Programmiersprachen, Statecharts u.a. sind operational. Sie sagen was getan werden muß, aber nicht welche Randbedingungen einzuhalten sind. Problematisch wird es, wenn eine Technik für beide (axiomatisch und operational) benutzt werden kann (natürliche Sprache, Z, VDM, CSP). Eine Vermischung produziert nur Konfusionen. Bei jeder Aussage stellt sich die Frage: Handelt es sich hierbei um Axiome, um das zulässige Verhalten des Systems einzuschränken? Oder handelt es sich um Operationen, die durchgeführt werden sollen? Gefährlich wird es, wenn beide Formen unkontrolliert gemischt werden.
Eine operationale Spezifikation ist vollständig, wenn alle Funktionen und Operationen spezifiziert sind. Eine axiomatische Spezifikation ist vollständig, wenn jedes unerwünschte Verhalten ausgeschlossen ist. Beide Teile sind konsistent zueinander, wenn die axiomatische Spezifikation die Aktionen der operationalen Spezifikation zuläßt. Sonst kann eine Spezifikation nicht implementiert werden, weil es unmöglich ist, ihre Anforderungen zu erfüllen.
Wenn "alle" Anforderungen formuliert sind, wird das System in Module aufgeteilt (Entwurf). Dabei werden die Anforderungen auf die Module verteilt, die Schnittstellen und das Zusammenspiel der Module werden definiert. Die Funktionsanforderungen können relativ einfach auf die Module verteilt werden, die Sicherheitsanforderungen aber nicht. Sie gelten nur für das gesamte System. In diesem Prozeß müssen neue Sicherheitsanforderungen für jedes Modul formuliert werden.
Die Modularisierung ist nötig, um die Komplexität beherrschen zu können. Die Entwicklungsfehlerrate, Entwicklungszeit und Kosten wachsen schneller als die Komplexität, und die Komplexität wächst viel schneller als die Anzahl der Programm LOC (Lines of code), der Gatter und Hardwarekomponenten. Deswegen ist die Modularisierung die beste Maßnahme gegen das exponentielle Wachstum der Probleme. Weiterhin ist der erste Feind der Sicherheit die Komplexität. Wenn die Komplexität eines Systems steigt, sinken seine Sicherheit und seine Verläßlichkeit, und die Entwicklung wird unübersichtlicher. Hinzu kommt, daß man bei einer modularen Anwendung einen Teil der Anlage herunterfahren und ersetzen/reparieren kann, ohne die gesamte Anlage abschalten zu müssen.
Aber Vorsicht: Die größte Schwachstelle jeder Entwicklung kommt aus dem Entwurf der Schnittstellen. Dort werden die meisten Fehler gemacht. Der Grund dafür sind Fehlinterpretationen und schlechte Dokumentation. Schnittstellen sollen genau und mit Beispielen dokumentiert werden. Bei Software sollen die Beispiele mit Cut-and-Paste übernommen werden. Bei Hardware sollen die Schnittstellen besonders aufmerksam simuliert werden. Beispiel: Beim Entwickeln zweier Hardware-Module definierten, simulierten und verifizierten wir besonders aufmerksam die Schnittstelle zwischen den beiden. Die Module wurden von verschiedenen Personen getrennt entwickelt. Beide implementierten die Schnittstelle haargenau gleich, auch den Stecker - zweimal Buchsen (Mutter) -, so daß wir die Module nicht zusammenstecken konnten.
Außer der üblichen Modularisierung für eine saubere Systementwicklung sollte auch eine sicherheitsorientierte Modularisierung stattfinden. Die sicherheitsrelevante Funktionalität wird von der restlichen Funktionalität getrennt und in wenigen sicheren Modulen eingekapselt. Wo wenige Module vorhanden sind ist es möglich, einen höheren Aufwand darin zu investieren, sie sicherer zu gestalten. Wenn die Sicherheitsaspekte im ganzen System zerstreut sind, sind sie nicht mehr durchschaubar und können nicht besonders behandelt werden.
Alle Module, die sicherheitsrelevante Daten bearbeiten (und weitergeben) werden als sicherheitskritische Module betrachtet. Wenn man nicht darauf achtet und die sicherheitskritischen Daten eine Tour durch das ganze System machen läßt, bekommt man ein für die Sicherheit undurchschaubares System, in dem alle Komponenten sicherheitskritisch geworden sind (siehe Abbildung 5).
Viel besser und sicherer ist es, die sicherheitskritischen Daten in sichere Schnittstellen-Module einzukapseln. Diese Module können mit höherem Aufwand entwickelt werden, so daß sie das kritische Gerät nur im sicheren Bereich betreiben und steuern (Abbildung 6). Diese Module können dann gefährliche Befehle aus anderen (unsicheren) Modulen filtrieren und nur sichere Ansteuerung weitergeben.
Die Sicherheitsaspekte des Systems sollten nach Möglichkeit getrennt von der restlichen Funktionalität implementiert werden, denn sie stehen in direkter Konkurrenz mit anderen Entwicklungszielen wie Kosten, Zeit usw. Deswegen ist es wichtig, daß der Verantwortliche für die Sicherheit keine weiteren Verantwortungen wie z.B. für die Minimierung der Entwicklungskosten oder das Einhalten des Zeitplans übernimmt. Sonst wird die Sicherheit (unbewußt) vernachlässigt, wenn es bei den anderen sichtbaren Projektzielen eng wird. Dies geschieht, weil die Sicherheit nicht sichtbar ist und auch ihre Mängel nicht gesehen werden können, bis es zu spät ist.
Die Sicherheit sollte mit den einfachsten Mitteln implementiert werden, die es gibt (es ist sicherer, s. Kap. 3). Wenn sie von einem Rechner gewährleistet werden soll, sollte sie so einfach wie möglich programmiert werden und deutlich vom Rest der Funktion getrennt und übergeordnet sein.
Der Sicherheitsteil kann als einfache Bedingungstests implementiert werden, wie bereits in Kapitel 5 am Beispiel einer fahrenden Anlage erklärt wurde. Das sicherste wäre, alle Sicherheitstests in einem getrennten Prozessor zu implementieren, so daß die restliche Funktion diese kritischen, aber einfachen Teile nicht stören kann. Meistens ist dies aus Kostengründen nicht praktikabel. In diesen Fällen sollte die Funktion in einer sicheren Schale eingekapselt werden. Diese Schale soll klein und übersichtlich sein, z.B.:
if(abstand < 3.0) Emergency_Action();
Jede Komponente hat eine geplante Interaktion mit ihrer Umgebung, aber oft genug wird vergessen, daß es auch eine ungeplante und unerwünschte Interaktion gibt, wie es in Kapitel 4 erläutert wurde. Deswegen muß das System immer für das Unerwartete und Unvorhersehbare vorbereitet sein. Beispiel: An einer deutschen Autobahn steht ein Sendemast. Einige Fahrzeuge bremsten ohne Zutun des Fahrers kräftig an dieser Stelle. Warum? Der Rundfunksender produzierte Interferenzen mit deren ABS.
Bei der Implementierung von Hardwaremodulen kann man folgende Maßnahmen einsetzen, um Entwicklungsfehler zu vermeiden:
Und solche, um Laufzeitfehler zu vermeiden:
1. Die Integration in Chips erhöht die Sicherheit, denn Chips sind sicherer (und billiger) als Kabel, Verbindungen und Platinen.
2. Die Hardware wird in physikalische Module aufgeteilt, um die erwarteten Interferenzen (Funk-, Wärme- und andere Formen von Strahlung) zu reduzieren, sowie zur Stabilisierung ihrer Versorgungsspannungen. Die Module sollten abgeschirmt werden, inklusive Leitungen und Verbindungen. Es ist empfehlenswert, bei Platinen die oberste und unterste Layer als Ground-Layer auszuführen, um die internen Verbindungen abzuschirmen.
3. Viele Hardwareausfälle treten entweder in den ersten Betriebsstunden oder nach sehr langer Betriebszeit auf (siehe Abbildung 7). Bei sicherheitskritischen Systemen werden Ausfälle in der Altersschwäche-Phase durch regelmäßige Wartung und Überholungen, wie z.B. bei den Flugzeugen, minimiert. Die Ausfälle in der "Kindersterblichkeitsphase" werden durch ein "Burn-in"-Verfahren minimiert. Dabei werden die elektronischen Komponenten bei ca. 100 Grad über 24 Stunden betrieben. Hierbei fallen viele der schwachen Komponenten aus, und es bleiben fast nur die, die ihr Arbeitsleben anfangen können.
Bei der Implementierung von Softwaremodulen kann man folgende Maßnahmen einsetzen, um Entwicklungsfehler zu vermeiden:
1. Die Software soll ständig das innere Modell gegen die Realität abgleichen. Beispiel: Wenn die Steuerung der Meinung ist, das Entleerungsventil sei geöffnet, soll sie kontrollieren, ob der Wasserstand sinkt. Wenn nicht, dann stimmt etwas mit ihrem internen Modell der Anlage nicht. Oder wenn die Steuerung der Meinung ist, die Tür sei offen, und es kommt ein Signal, daß die Tür soeben geöffnet wurde, dann stimmt wiederum etwas nicht.
2. Bei der Entwicklung sicherheitsrelevanter eingebetteter Systeme ist ADA mit Abstand die vorgezogene (geeignetste) Programmiersprache. Dennoch beharren viele Entwickler auf C und C++, obwohl diese Sprachen allgemein als unsicherer eingestuft werden. Die Sprache Java vereinigt Sicherheit mit der geliebten C-(und C++) Syntax. Aber noch sind einige Steine aus dem Weg zu räumen, bevor Java für eingebettete Realzeitsysteme geeignet ist. Wenn Sie trotzdem C oder C++ benutzen, sollten Sie weitgehend auf Pointerarithmetik verzichten. Sie sollten Werkzeuge einsetzen, die Ihre Quelltexte genauer prüfen als der C/C++Compiler: Pointerplausibilität, Initialisierung der Variablen, Type-Check u.a. (z.B. das Purify Werkzeug).
3. Bei den sicherheitskritischen Programmen gibt es Restriktionen bei der Speicherverwaltung:
3.1. Es ist empfehlenswert, alle Ressourcen und Speicherbereiche von Anfang an bei der Systeminitialisierung zu alloziieren und reservieren. Dies erspart das Risiko, daß die Ressourcen nicht vorhanden sind, wenn man sie braucht. Leider ist das nicht immer möglich; bei großen Systemen oder bei dynamischen Strukturen kann man nicht im voraus alles reservieren.
3.2. Bei Echtzeitsystemen sollte man auf Garbage-collection verzichten, denn ihre Funktion und Timing sind indeterministisch. Man kann nicht wissen, wann der Garbage-collector aktiviert wird und für wie lange.
3.3. Bei Systemen, die sehr lange Zeit (Jahre) ununterbrochen laufen sollen, bringt eine dynamische Speicher-Alloziierung das Fragmentationsproblem. Es kann vorkommen, daß genügend freier Speicher vorhanden ist, aber nur in kleinen Stückchen, die man nicht nutzen kann. Eine Lösung dafür ist die Benutzung von Blöcken nur mit festen Größen. Diese Methode wird von vielen Echtzeit-Betriebssystemen zur Verfügung gestellt.
4. Bei komplexen, nicht deterministischen oder nicht so einfach wiederholbaren Operationen ist es sinnvoll, wichtige Informationen zu protokollieren, z.B. in einer ".log"-Datei. Dies erleichtert eine eventuelle spätere Fehlerdiagnose, besonders dann, wenn man den Fall nicht rekonstruieren kann.
5. Rechnerunterstützte Regelung und Steuerung wird in der Regel mit parallel laufenden, echtzeit-reaktiven Tasks implementiert. Am Ende dieses Kapitels wird dieses Thema genau behandelt. Dort finden Sie wertvolle Tips für die Implementierung von Echtzeit-Systemen.
Oft ist die Systemintegration die Stunde der Wahrheit, denn man weiß häufig nicht, wie sich das System verhalten wird, bis man das System integriert hat (z.B. der legendäre Film "Das Monster von Dr. Frankenstein"). Dies ist meistens bei völlig neuen Systemen der Fall, z.B. eine neue Rakete. Bei der Ariane 5 wußte keiner, was sie wirklich machen würde (sie explodierte). Beim ersten Flugzeug wußten die Konstrukteure nicht, wie es fliegen würde (die Flügel sind gebrochen), viele der ersten Dampfmaschinen sind unerwartet explodiert, bei der ersten Kanone war der Rückschlag eine gefährliche Überraschung. Vollautomatische Fahrzeuge (z.B. U-Bahn) werden jahrelang erprobt, weil keiner weiß, was sie machen werden. Das ist keine Übertreibung, denn die meisten Fehler werden nach der Integration entdeckt, und dann sind sie am schwierigsten zu beheben. Standard-Applikationen, z.B. noch ein Buchhaltungsprogramm, werden nicht so viele Überraschungen bringen wie neue Maschinen. Außerdem ist nach der Integration das System funktionsfähig, inklusive seiner Gefahren, und einige der verborgenen, bis dahin unbekannten Gefahren offenbaren sich. Dies kann auch ein gefährlicher Moment werden, denn ab hier sind die Gefahren des Systems aktiv - aber noch unbekannt. Bei der Inbetriebnahme einer vollautomatisierten Fertigungsstraße mit starken Robotern beispielsweise sollte keiner in der Nähe sein, es könnte tödlich enden. Noch schlimmer ist es, wenn man nach der Aktivierung das System nicht mehr anhalten kann, z.B. Raketen oder wiederum das Monster von Frankenstein. Solche Überraschungen könnte man sich durch Simulation des gesamten Systems ersparen. Leider basieren die Simulationen auf Annahmen, und viele davon werden sich bei der Integration sich als falsche Vermutungen entpuppen. Einige Überraschungen werden uns auch dadurch nicht erspart bleiben können. Außerdem ist eine Gesamtsystem-Simulation äußerst schwierig, und die Werkzeuge dafür sind selten vorhanden. Besonderes schwierig ist die Simulation von komplexen Systemen in Echtzeit. Dafür braucht man eine enorme Rechenleistung.
Viele der Entwicklungsfehler liegen bei der Systemintegration. Wenn viele Module zusammengeschaltet werden, können neue Probleme entstehen. Jedes Modul kann z.B. alleine seine Aufgabe erfüllen, aber alle zusammen nicht mehr. Diese Probleme kommen meistens aus Mißverständnissen an den Schnittstellen. Solche Intermodulkommunikationsfehler können das System in undefinierte Zustände bringen, die sehr schwer zu finden sind. Gegen solche Fehler helfen die Erstellung von Prototypen und/oder Simulation, da sie eine bessere Kommunikation zwischen den Entwicklern fördern.
Oft entstehen bei der Integration einfache kleine Fehler mit großen Auswirkungen. Beispiel: eine Vertauschung von Gas und Bremse bei der Verkabelung. Diese Fehler werden erst beim Testen gefunden und sind kaum zu vermeiden. Sehr oft erwachsen Katastrophen aus läppischen Fehlern, weil manche Fälle so einfach sind, daß keiner einen zweiten Gedanken daran verschwenden will. Beispiel: Wenn bei der Beschriftung der Bedienungskonsole Min und Max vertauscht werden, kann dadurch ein "Bedienungsfehler" mit katastrophalen Folgen entstehen. Solche Fehler können weder mit formalen Methoden noch mit diversitärer Entwicklung noch mit anderen raffinierten Techniken vermieden werden. Da hilft nur, doch einen zweiten Gedanken in die einfachen Sachen zu investieren.
Trotz all dieser Versuche, Fehler zu vermeiden, werden immer welche übrig bleiben. Während die Zuverlässigkeit von einfachen Maschinen sich durch Prüfungen und Tests ermitteln läßt, ist die Beurteilung der Sicherheit von softwaregesteuerten Maschinen mit komplexem Verhalten äußerst schwierig. Hier können Tests nur das Vorhandensein von Fehlern beweisen, nicht aber deren Abwesenheit. Es ist nicht realisierbar, alle möglichen Systemzustände (mit der Zeit als Teilzustand), ihre Kombinationen und Reihenfolgen durchzuspielen. Auch können Tests nicht immer unter allen möglichen oder erwarteten realistischen Bedingungen durchgeführt werden. Es kann nicht vollständig verifiziert werden, ob der Entwurf eine richtige Umsetzung der Anforderungen ist und ob Programmcode und Hardware eine richtige Umsetzung des Entwurfes sind, und die Anzahl der unentdeckten Entwicklungsfehler wächst exponentiell mit der Komplexität des Systems. Alle übrigen unentdeckten Fehler stellen eine latente Gefahr dar. Außerdem werden Anforderungsfehler und Fehler, die auf falschen Annahmen beruhen, nicht bei Testen entdeckt, sondern fallen frühestens in Praxistests oder schlimmstenfalls in der Anwendungsphase auf.
Der Anteil der Test-Kosten bei den sicherheitsrelevanten Systemen kann bis zu 80% der Gesamtentwicklungskosten betragen. Deswegen versucht man, bei den Tests zu sparen - an der falschen Stelle. Das System wird nicht nach jeder Änderung wieder vollständig getestet, so wie es sein soll. Daher bringt jede Änderung ungeahnte, neue latente Gefahren mit sich. Besonders dort, wo man sie nicht erwartet und deswegen nicht getestet hat.
Meistens wird nur die Funktionalität des Systems (teilweise) getestet und der gesamte Fehlertoleranz-Teil dabei vergessen. Z.B. ist die Aufwandsverteilung bei der Telekommunikation 50% LOC (Lines of code) bei der Kernfunktionalität und 50% LOC bei der Fehlerbehandlung, dagegen wird beim Testen der Fehlerbehandlung weniger als 10% des gesamten Testaufwands gewidmet. Fehlererkennung und Recovery-Mechanismen sind so wichtig wie die restliche Funktionalität, aber sie werden häufig unterschätzt und deswegen vernachlässigt: Sie werden als letztes entwickelt und als letztes und am wenigsten getestet, dabei ist dieser Test der schwierigste. Das System muß dafür in extreme Streß-Situationen gebracht werden, und die Fehler-Injektion, ohne das bestehende System zu modifizieren, ist sehr schwierig.
Beispiel: Wie würden Sie eine Gebäude-Feueralarmanlage testen? Richtig wäre, Feuer in versteckten Ecken zu legen - nicht unbedingt unter dem Rauchdetektor - und abwarten, ob es erkannt wird. Dies wäre der realistische Test, kann aber gefährlich werden. Der Schaden beim Testen könnte größer als der Nutzen sein. Aber wie wird die Anlage getestet? Jemand drückt einen Knopf und dann ertönt die Sirene. Dies ist ein Test nur für die Sirene, nicht für den Feueralarm. Ein Kompromiß wäre, mit einer Rauchquelle im Gebäude umherzugehen. Oder wie würden sie einen Notabschalter eines Kernreaktors testen, der bei einer extrem hohen Temperatur im Kern aktiviert werden soll? Können Sie nicht! Sie können nur Teile getrennt testen und hoffen, daß alles zusammen auch arbeiten wird. Tests der gesamten Anlage werden nach der Integration durchgeführt, sollen aber auch in regelmäßigen Abständen Bestandteil der Wartung sein. Es gibt auch Sachen, die nur einmal benutzt werden können - sie können daher nicht als Ganzes im voraus getestet werden, z.B. die Airbags in Autos, Raketen, Bomben (dies ist zwar das Gegenteil von sicherheitsrelevanten Systemen, aber trotzdem ein Beispiel). Hier kann man wiederum nur die Einzelteile testen und dann hoffen, daß alles zusammen funktionieren wird.
Man kann mit systematischen Tests versuchen, relevante Zustandsreihenfolgen zu identifizieren, zu minimieren und durchzuspielen. Es gibt zwei komplementäre Kategorien von Tests (Abbildung 8): Blackbox und Whitebox und zwei Zusätze: Test-Suite und Random-Tests.
Der Blackboxtest wird nur anhand der Spezifikation generiert. Er berücksichtigt nicht die Implementierungsdetails. Dabei wird die Funktion des gesamten Systems getestet, ohne implementierungskritische Aspekte genauer zu untersuchen. Ein Teil des Blackboxtests ist die Sensibilitätsanalyse der Output-Signale. Dabei wird getestet, wie empfindlich ein Output auf die verschiedenen Input-Werte und internen Zustände des Programms reagiert.
Der Whiteboxtest berücksichtigt nur die Implementierung, ohne die gesamte Funktion zu untersuchen. Besonders wichtig sind die Grenzwerte, z.B. bei einem FIFO mit 20 Plätzen wird das Verhalten des Systems bei 0, 1, 2, 19, 20 und 21 belegten Plätzen im FIFO getestet.
Bei jedem Entwicklungsschritt, Programmteil und jeder Hardwarekomponente denkt man automatisch an Sachen, die getestet werden sollen. Man kann gleich das passende Testprogramm schreiben und aufbewahren. Alle diese Programme zusammen bilden eine Test- oder Validierungs-Suite. Diese Suite wird ständig erweitert. Bei jedem Durchlauf sollten die Ergebnisse aufbewahrt werden, um sie automatisch mit den Ergebnissen von nachfolgenden Tests vergleichen zu können. Eine manuelle Bewertung der Testergebnisse ist dann nur für die Ergebnisse der neuen Testprogramme erforderlich. Es ist wichtig, auch alte Dinge, die bereits getestet wurden, wieder zu testen, weil Änderungen und Erweiterungen Nebenwirkungen bei den bereits laufenden Teilen haben können.
Und schließlich, wenn man nicht mehr weiß, was man testen soll, kann man Tests mit Zufallsgrößen durchführen. So ein Test ist nicht so realitätsfern, wie man auf den ersten Blick denken könnte. Dabei werden immer wieder Fälle und Fehler entdeckt, an die keiner gedacht hat und die trotzdem existieren.
Die Testergebnisse müssen natürlich ausgewertet werden. Bei der Verwendung von formalen Methoden kann man die Reaktion des Systems semiautomatisch gegen die Spezifikation vergleichen. Weiterhin sollte man eine Test-Coverage-Analyse durchführen. Diese Analyse kann vollautomatisch mit Werkzeugen geschehen. Beim Programmcode wird untersucht, ob alle Programmfäden durchgelaufen sind (sequentielle Blocks, Schleifen und if-then-else). Bei der Hardwaresimulation wird geprüft, ob alle Flip-Flops, Gatter, Buffer usw. einmal Eins und einmal Null waren. Dies ist hilfreich, um die Qualität unserer Tests zwar nur grob, aber besser als gar nicht zu bewerten.
Nach der Lieferung des Systems fängt die "echte Testphase" unter realen Bedingungen an. Alles davor sind nur simulierte Bedienungen, die nur auf Annahmen basieren und deshalb falsch sein können. Viele Entwicklungsfehler kommen aus unbekannten oder ungenannten Situationen der realen Welt. Die meisten entstehen durch falsche Annahmen, und dadurch entspricht die Spezifikation nicht der Realität. Das Problem wird weiter verschärft, weil die Umstände in der Umgebung nicht alle vorhersehbar sind. Die Korrektur der in dieser Phase gefundenen Fehler erfordert einen enormen Aufwand. Nach jeder Korrektur (Änderung) sollte erneut der gesamte Test durchgeführt werden. Dieser Aufwand ist so enorm, daß es besser ist, Fehler zu sammeln, alle zusammen zu korrigieren und erst dann alle Änderungen auf einmal erneut zu testen.
Große Systeme haben bekannte Fehler, deren Korrektur man sich nicht leisten kann: zu viel Risiko, zu hohe Kosten usw. Es ist dann besser, mit diesen Fehlern zu leben und sie als Teil der Fehlertoleranz-Mechanismen aufzufangen und zu behandeln. Dies ist der Fall bei vielen der größten Telefonnetze der Welt.
Die Korrekturen können auch ein gefährliches Spiel werden. Bei komplexen Systemen, wo eine Änderung sehr umfangreich werden kann, kann man nicht ahnen, was für weitere Folgen die Korrektur bringt (z.B. ein Fehler wird korrigiert und zwei neue schleichen sich ein). In diesem Fall gilt: Zwei wohlbekannte Fehler sind besser als ein unbekannter. Von einem bekannten Fehler weiß man, wann er eintritt und wie man ihn umgehen kann. Man kann die Operatoren warnen und vorbereiten. Bei einem unbekannten Fehler wird sein Eintritt eine böse Überraschung sein, die Gefahr lauert im Verborgenen wie in einem Hinterhalt. Deswegen hat man bei sehr komplexen Systemen das Dilemma, ob ein Fehler korrigiert werden soll, oder ob man lieber damit lebt. Beispiel: Einmal lehnte ein Flugzeug den Befehl des Piloten ab, die Bremsklappen der Triebwerke auszufahren. Es war der Meinung, es sei noch nicht auf dem Boden, obwohl es schon gelandet war. Die Landebahn war so glatt, daß die Räder sich nicht mit der erwarteten Geschwindigkeit drehten. Außerdem war es so windig, daß das Flugzeug nicht sauber aufsetzen konnte. Dadurch rutschte es aus der Landebahn. Der Flugzeughersteller scheute die Konsequenzen solch einer umfangreichen Änderung und zog es vor, lediglich die Parameter der entsprechenden Sicherheitsfunktion extrem niedrig zu setzen.
Nicht nur bei jeder Änderung soll das System unter den aktuellen realen Bedingungen erneut getestet werden, sondern auch jedesmal, wenn man neue oder geänderte Bedingungen hat. Man darf nicht denken, daß ein getestetes System überall gleich arbeiten wird, oder daß die Wiederverwendung von bereits bewährter Software oder anderen Komponenten eine sichere Sache sei. Zwei Beispiele:
1. Ein 100-fach erprobtes und bewährtes amerikanisches Luftraumkontrollsystem wurde in England installiert und verhielt sich extrem merkwürdig. Auf dem Monitor verschwanden Flugzeuge und tauchten woanders wieder auf. Das Problem war der Nullmeridian, der über England geht. In den USA brauchte man diesen Sonderfall nicht zu berücksichtigen.
2. Die bewährte Steuerungssoftware aus der Ariane 4 wurde auf die Ariane 5 übertragen. Vierzig Sekunden nach den Start produzierte der Fligth Control Computer (FCC) einen fatalen Fehler und die Rakete wurde vernichtet. Das Problem bestand darin, daß für die Steuerung kritische Annahmen über das Verhalten der Ariane-4-Raketen nicht dokumentiert waren und sie galten auch nicht bei der Ariane 5. Nebenbei, das Grundproblem im ersten Fall waren ebenfalls undokumentierte Annahmen, und noch mehr als das, die Annahmen waren nicht einmal bewußt.
Das Einsetzen von formalen (mathematischen) Methoden ist eine Hoffnung, die Entwicklungsqualität deutlich zu verbessern, bringt aber einen enormen Aufwand mit sich. Die formalen Methoden bestehen aus einer Menge von Techniken und Werkzeugen, die auf mathematischer Modellierung und formaler Logik aufbauen. Sie werden benutzt, um Software zu spezifizieren und zu prüfen. Mit formalen Methoden kann man von einem (selbst entworfenen) mathematischen Modell ausgehend durch logische Berechnungen die logischen Eigenschaften eines Systems voraussagen. Dieses ist nur insoweit wahr, als das Modell der Realität entspricht. Der Abgleich des Modells mit der Realität geht nur durch Erfahrungen und durch Probieren (eine Schwachstelle!).
Mit formalen Methoden kann berechnet werden, ob eine Systembeschreibung (Spezifikation) in sich konsistent ist, ob bestimmte Eigenschaften garantiert werden können und ob die Anforderungen richtig entworfen und implementiert worden sind (z.Z. nur theoretisch). Weiterhin kann die Spezifikation für Simulation/Animation, zur Codegenerierung, zur Testgenerierung und Testauswertung benutzt werden.
Diese Methoden entsprechen der Rolle der Mathematik in der Elektrotechnik, in Maschinenbau und Aerodynamik. Die Mathematik stellt Methoden zur Modellierung der Objekte bereit, um ihr Verhalten durch Berechnung voraussagen zu können. Z.B. kann vor dem Bau einer Flugzeugtragfläche deren Luftwiderstand ermittelt werden. Bei der Elektrotechnik können mittels mathematischer Modelle magnetische Felder, Reflexionen u.a. vorausberechnet werden.
Die Anwendung von formalen Methoden in der Softwareentwicklung ist aber noch nicht so reif wie in der Elektrotechnik und im Maschinenbau. Zur Zeit ist sie immer noch sehr experimentell. Es gibt viele Erfolgsberichte, aber es gibt auch Berichte über das Scheitern ihrer Anwendung. Sie sind keine magischen Formeln zum Erfolg! Ihre Anwendung ist noch sehr aufwendig und benötigt besonders geschultes Personal. Ihr Aufwand wächst (erfahrungsgemäß) sogar exponentiell zur Komplexität. Deshalb ist es besonders sinnvoll, den sicherheitsrelevanten Teil so klein und einfach wie möglich und frei von für die Sicherheit irrelevanten Teilen zu halten.
Der zusätzliche Aufwand, etwas formal zu beschreiben, muß eine Rechtfertigung haben. Nur zu formalisieren, um eine formale Spezifikation zu erhalten, ist nicht sinnvoll. Eine formale Spezifikation ist auch nicht um jeden Preis und für alle Teile eines Systems sinnvoll. Nicht sicherheitskritische Teile müssen nicht unbedingt formal beschrieben werden. Zu jedem formalen Bestandteil muß klar sein, in welchen Analysen und Konstruktionen die Formalisierung benutzt wird, oder wenn es als Dokumentation gedacht ist, soll klar sein, wer es lesen soll (kann er es lesen?). Ist das nicht klar definiert, lohnt sich der Aufwand für eine formale Spezifikation nicht.
Eine Entwicklung mit formalen Methoden unterscheidet sich von einer traditionellen Entwicklung in mehreren Punkten. Um diese Punkte zu erläutern, muß man zuerst zwei Sorten von formaler Spezifikation unterscheiden: operational und axiomatisch (siehe Unterkapitel Analyse und Spezifikation). Meistens können axiomatische Spezifikationen nicht animiert (simuliert), sondern nur verifiziert werden. Auf der anderen Seite können operationale Spezifikationen meistens animiert, aber nicht formal verifiziert werden.
Einige Problemklassen lassen sich sehr gut und einfach mit formalen Methoden ausdrücken. Andere dagegen, die unregelmäßig und nicht algorithmisch sind, lassen sich nur sehr umständlich ausdrücken. Um die formalen Methoden zu verdeutlichen, nehme ich natürlich die am besten geeignete Problemklasse als Beispiel: eine mathematische Berechnung. Wörtlich heißt die Aufgabe "Die Quadratwurzel einer Zahl berechnen" (einfacher geht es nicht). Die Aufgabe wird in "Z" axiomatisch spezifiziert. Diese Spezifikation beschreibt nur die Eigenschaften der Ergebnisse, ohne zu sagen, wie die Aufgabe zu lösen ist. Diese Eigenschaften werden später zum Beweisen von anderen Eigenschaften benutzt:
--- sqrt ---------- ; Name der Operation
| x? : Realzahl ; "?" kennzeichnet eine Input Variable
| a! : Realzahl ; "!" kennzeichnet eine Output Variable
| x? >= 0 ; Vorbedingung: Die Input Variable x?
| a! * a! = x? ; Die Output Variable a! multipliziert
| ; mit sich selbst ergibt die Input Variable x?
Dagegen besteht die C Funktion sqrt(x) aus ca. 100 Zeilen und ist kaum verständlich.
Als Beispiel für eine Verifikationsaufgabe nehmen wir "Die Wurzel jedes Wertes größer 1 soll kleiner als der Wert sein". Aus dem C Programm können wir das nicht verifizieren. Wir können nur das Programm viele Male laufen lassen und zeigen, daß wir kein Gegenbeispiel gefunden haben. Alle reellen Werte werden wir nicht testen können und ob es immer klappt, können wir auch nicht sagen. Mathematisch können wir die Verifikationsaufgabe folgendermaßen ausdrücken:
x >1 ^ a*a=x -> a<x
In Worten: (x>1 UND a*a = x ) impliziert a<x.
Ein mathematischer Beweis für diese Formel besteht aus ca. einer halben Seite Mathematik. Aber dann weiß man, daß es immer stimmt, ohne ein Programm schreiben und testen zu müssen - obwohl das Programm sowieso geschrieben und getestet werden muß.
http://www.first.fhg.de/~sergio/public.html
Verschiedene Online Publikationen im Bereich Sicherheit, Fehlertoleranz und Steuerung.
Mission management system for an autonomous underwater vehicle. Madsen, H.O. Proceedings. 4th IFAC Conference on Manoeuvring and Control odf Marine Craft. MCMC '97
Praktisches Beispiel für die Konstruktion von fehlertoleranten Systemen.
System stress tests ensure the availability of electronic interlockings [rail traffic control]. Birtel, P. Signal und Draht (June 1997) vol.89, no.6,
Praktisches Beispiel für Softwaretest.
System wide joint position sensor fault tolerance in robot systems using Cartesian accelerometers. Aldridge, H.A.; Juang, J.-N. Proceedings of the SPIE - The International Society for Optical Engineering (1996)
Praktische Methode für die Konstruktion von fehlertoleranten Sensoren.
Correct and robust decision systems for high complexity critical control systems. Browne, J.C.; Emerson, E.A.; Gouda, M.; Miranker, D.; Mok, A.; Chodrow, S.; Wang, R.-H.; Tsou, D.; Obermeyer, L.
Praktische Methode für die Konstruktion von fehlertoleranten Systeme.
A practical method for creating plant diagnostics applications. Karsal, C.; Padalkar, S.; Franke, H.; Sztipanovits, J.; Decaria, F. Integrated Computer-Aided Engineering (1996)
Praktische Methode für die Konstruktion von Nonstop-Systemen.
Hardware and software fault tolerance using fail-silent virtual duplex systems. Echtle, K.; Lovric, T. Fault-Tolerant Parallel and Distributed Systems. Los Alamitos, CA, USA: IEEE Comput. Soc. Press, 1995.
Praktische Methode für die Konstruktion von diversitären Systemen.
Implementation of a digital reactor control and protection system. Heyck, H. Advanced Control and Instrumentation Systems in Nuclear Power Plants. Design, Verification and Validation. IAEA/IWG/ATWR & NPPCI Technical Committee Meeting (VTT-SYMP-147) Espoo, Finland: Tech. Res. Centre of Finland, 1995.
Praktisches Beispiel für Design ohne Trennung von Funktion und Sicherheit.
Dependable flight control system using data diversity with error recovery. Christmansson, J.; Kalbarczyk, Z.; Torin, J. Computer Systems Science and Engineering (April 1994)
Praktische Methode für die Konstruktion mit Diversität.
Control reconfiguration in the presence of software failures. Bodson, M. ; Lehoczky, J.; Rajkumar, R.; Sha, L.; Soh, D.; Smith, M.; Stephan, J. Proceedings of the 32nd IEEE Conference on Decision and Control. New York, NY, USA: IEEE, 1993.
Theoretische Methode für Konstruktion mit dem Ziel Einfachheit.