Fehlertolerante Datenstrukturen Henning Kühn Technische Universität Dortmund [email protected] Zusammenfassung In dieser Ausarbeitung werden Verfahren zur Fehlererkennung und Möglichkeiten zu deren Behebung mit Hilfe fehlertoleranter Datenstrukturen beschrieben. Dabei wird genauer auf modified(2) double-linked lists als spezielle Datenstruktur und die Spezifikation von Consistency Constraints zum Umgang mit Fehlern eingegangen. Zum Schluss werden mit Resilient Data Structures Datenstrukturen vorgestellt, die auch in Gegenwart von nicht korrigierten Fehlern noch sinnvoll zu benutzen sind. 1 Motivation Warum ist es sinnvoll, Fehlertoleranz zu Datenstrukturen hinzuzufügen? Im Idealfall sollten Fehler natürlich gar nicht erst auftreten, doch lässt sich dies nicht immer vermeiden. Es gibt im realen Betrieb eine Vielzahl möglicher Fehlerquellen. Hardware kann durch immer weiter voranschreitende Miniaturisierung unzuverlässiger werden, Umgebungsbedingungen wie ionisierende Strahlung können Fehler hervorrufen. Eine Erkennung auf Hardwareebene ist zwar möglich aber aufgrund hoher Komplexität und der damit verbundenen Kosten nicht immer zweckmäßig. Auch wenn die Hardware zuverlässig arbeiten sollte, ist immer noch die darrauf laufende Software selber als Fehlerquelle nicht auszuschließen. So kann ein Algorithmus ganz einfach fehlerhaft sein und in manchen Fällen ein inkorrektes Ergebnis produzieren. Es können irgendwo ungültige Annahmen gemacht worden sein, es wird falsches Locking bei mehreren Threads, die auf die gleiche Datenstruktur zugreifen, verwendet, oder ein Speicherbereich kann aus Versehen überschrieben werden. In diesen Fällen ist Fehlertoleranz günstig und flexibel mit Software zu realisieren. Ein Minimalziel hierbei ist zumindest das Erkennen von Fehlern, um darauf reagieren zu können. Wenn möglich wird eine Wiederherstellung des Originalzustands versucht, andernfalls kann möglicherweise ein beliebiger gültiger Zustand hergestellt werden wenn die Ausführungsbedingungen dies zulassen. Notfalls muss die Existenz von Fehlern hingenommen werden, dabei sollten zumindest die noch gültigen Daten von den Fehlerhaften unbeeinflusst bleiben. 2 Begriffsklärung Versagen Beobachtbares Ereignis das auftritt, wenn ein System nicht seinen Spezifikationen entspricht fehlerhafter Zustand Zustand, der zu einem Versagen des Systems führen kann Fehler Teil eines Zustands, der zum Versagen führen kann Störung Ursache eines Fehlers Fehlertolerantes System System, welches verhindert, dass fehlerhafte Zustände zum Versagen führen 3 3.1 Möglicher Umgang mit Fehlern Fehlererkennung und -behebung durch Redundanz Um Fehler erkennen zu können, müssen genügend Informationen in der Datenstruktur vorhanden sein, um durch sich ergebende Widersprüche aus mehrfach vorhandenen Informationen auf Inkonsistenz schließen zu können. Ein einfaches Beispiel ist die Angabe der erwarteten Anzahl der Elemente im Header der Datenstruktur im Vergleich zur Anzahl der tatsächlich vorhandenen oder erreichbaren Elemente. Statt auf Informationen über den Zustand der Datenstruktur kann Redundanz auch auf die Datenelemente selber angewendet werden. Trivialerweise müssen hierzu bei maximal δ zu erwartenden Fehlern 2δ+1 Kopien eines Datums gespeichert werden. So können alle Kopien auf Gleichheit überprüft werden, bei sich widersprechenden Daten wird die größte Anzahl an gleichen Daten als richtig angenommen. Dies bringt jedoch einen von δ abhängigen, unter Umständen nicht zu vertretend großen zusätzlichen Aufwand bei jedem Elementzugriff mit sich [3]. 3.2 Consistency Constraints Eine weitere Möglichkeit mit Fehlern umzugehen besteht darin, für die betrachteten Daten Consistency Constraints anzugeben, deren Einhaltung hin und wieder überprüft wird. Wird eine Inkonsistenz festgestellt, kann versucht werden diese zu beheben, um mit der Programmausführung fortsetzen zu können. Die wiederhergestellten Daten müssen dabei nicht den ursprünglichen, fehlerfreien Daten entsprechen. Wichtig ist nur die Konsistenz der Datenstruktur. 3.3 Resilient Data Structures Wenn man Fehler nicht beheben will oder kann, weil dies vielleicht zu kostspielig wäre, kann man Resilient Data Structures benutzen. Diese garantieren auch in Gegenwart von Fehlern ein bestimmtes Verhalten bezüglich der noch gültigen Daten. 4 4.1 Redundanz Arten von Redundanz Neben der eingangs bereits erwähnten Möglichkeit der trivialen mehrfachen Speicherung der zu schützenden Daten selber, kann auch strukturelle Redundanz innerhalb einer Datenstruktur zur Anwendung kommen. Häufig angewandte Techniken, um Redundanz zu Datenstrukturen hinzuzufügen: – Speichern der Anzahl der enthaltenen Elemente Ein Feld mit der Anzahl der Elemente kann zum Header der Datenstruktur hinzugefügt werden. – Eindeutiges Identifikationsfeld Jeder Datenstruktur wird eine eindeutige Identifikationsnummer zugewiesen welche in jedem Element gespeichert wird um dessen Zugehörigkeit zur Struktur anzugeben. Es können auch Informationen zum erwarteten Typ des Elements gespeichert werden, bei einem Baum z.B. “Blatt” oder “innerer Knoten”. – Zusätzliche Zeiger Es können voneinander unabhängige Zeiger gespeichert werden wie z.B. die next- und back-Zeiger in doppelt verketteten Listen mit denen sich jeweils die Datenstruktur rekonstruieren lässt. 4.2 Erkennbarkeit, Korrigierbarkeit Es ist möglich, Datenstrukturen durch bestimmte Änderungen von einem gültigen Zustand in einen anderen gültigen, aber möglicherweise fehlerhaften Zustand zu überführen. Wird der next-Zeiger eines Elements einer einfach verketteten Liste ohne Speicherung der Elementanzahl auf NULL gesetzt, ergibt sich wieder eine gültige Liste, es wurden jedoch durch nur eine einzige Änderung Daten verloren ohne dass dies erkannt oder behoben werden kann. Wird die zu erwartende Elementanzahl der Liste in einem Header gespeichert, kann zumindest dieser Datenverlust erkannt werden, auch wenn nichts dagegen unternommen werden kann. Fehler können erkannt werden, wenn eine Datenstruktur durch eine oder mehrere Änderungen in einen ungültigen Zustand gelangt. Definition 1. Sind N + 1 Änderungen notwendig, um von einem gültigen Zustand in einen anderen gültigen Zustand zu gelangen, ist die Struktur N -erkennbar. Definition 2. Ist es möglich, aus einer vormals gültigen Datenstruktur, welche höchstens N Änderungen erfahren hat, wieder die ursprüngliche Datenstruktur herzustellen, so ist diese N -korrigierbar. Demnach ist eine einfach verkettete Liste ohne Speicherung der Elementanzahl 0-erkennbar und 0-korrigierbar, mit Speicherung der Elementanzahl immerhin 1-erkennbar aber immer noch 0-korrigierbar. Durch Hinzufügen weiterer Redundanz in Form von back-Zeigern erhält man eine doppelt verkettete Liste. Dadurch kann 1-Korrigierbarkeit erreicht werden und es erhöht sich die Erkennbarkeit auf 2 [6]. 4.3 Verkettete Listen Durch eine einfache Änderung kann die Erkennbarkeit einer doppelt verketteten Liste noch weiter erhöht werden. Bei einer modified(2) double-linked list wird der back-Zeiger eines Elements auf das Element zwei Elemente vor diesem gespeichert. Dabei gibt die Zahl in Klammern die Distanz zum gespeicherten Element an und kann auch noch weiter erhöht werden. Diese Änderung erzielt bei modified(2) double-linked lists 3-Erkennbarkeit, bei modified(3) double-linked lists 4-Erkennbarkeit. Eine weitere Erhöhung der überbrückten Distanz führt zu keiner weiteren Verbesserung, jedoch wird das Modifizieren der Liste immer teurer da immer mehr back-Zeiger angepasst werden müssen [6]. Abbildung 1. Löschen eines Elements aus einer doppelt verketteten Liste Header count=3 1 2 3 Es soll nun das zweite Element einer Liste gelöscht werden. Bei einer doppelt verketteten Liste reicht die Veränderung von zwei Zeigern und die Anpassung der im Header gespeicherten Elementanzahl um wieder eine gültige Liste herzustellen. Bei einer modified(2) double-linked list ist noch eine Zeigerveränderung mehr erforderlich. Die nach der Löschoperation geänderten Zeiger sind rot dargestellt. 4.4 Binärbäume Eine ähnliche Situation ergibt sich bei Binärbäumen. Ohne Speicherung der Anzahl der enthaltenen Elemente sind diese wie einfach verkettete Listen 0erkennbar. Doch auch wenn in jedem Knoten die Größe der Bäume seiner Kinder gespeichert wird, lassen sich manche Veränderungen nicht erkennen, nämlich wenn ein Zeiger durch einen anderen Zeiger auf einen Baum gleicher Größe ersetzt wird. Mit einem eindeutigen Identifikationsfeld kann zumindest sichergestellt werden, dass auf einen Knoten im richtigen Baum gezeigt wird. Um Abbildung 2. Löschen eines Elements aus einer modified(2) double-linked list Header count=3 1 2 3 nun erkennen zu können, ob in einem Baum ein Knoten mehrfach referenziert wird, kann beim Durchlaufen ein Flag im Knoten gesetzt werden, welches anzeigt, dass dieser schon besucht wurde. Falls eine Veränderung des Baums nicht möglich oder gewollt ist, muss entweder eine Liste mit schon besuchten Knoten verwaltet werden, oder es muss der gesamte Teil des Baum bis zum betrachteten Knoten jeweils erneut durchlaufen werden. Wie bei einer doppelt verketteten Liste können auch zu einem Baum redundante Zeiger hinzugefügt werden. Bei einem right threaded tree werden in Knoten, die kein rechtes Kind besitzen, Zeiger auf deren in-order Nachfolger gespeichert. Damit es möglich ist, bei einer beliebigen Veränderung eines Zeigers den ursprünglichen Baum wiederherzustellen, muss jeder Knoten allerdings mindestens zwei eingehende Zeiger besitzen, was bei threaded trees nicht der Fall ist. Werden jetzt noch alle Knoten, die kein linkes Kind besitzen, in in-order miteinander verbunden, erhält man einen chained and threaded binary tree (CT-Tree). Dieser ist 2-erkennbar und 1-korrigierbar [6]. 5 Consistency Constraints Durch die Angabe von Consistency Constraints einer Datenstruktur ist es möglich, automatisch Inkonsistenzen zu erkennen und zu beheben. Demsky und Rinard stellen ein System vor, das die Beschreibung solcher Constraints und die darauf aufbauende automatische Reparatur von Datenstrukturen erlaubt [2]. 5.1 Ziel Es wird nicht davon ausgegangen, dass genügend Daten zur Rekonstruktion des Ausgangszustands zur Verfügung stehen. Statt dessen wird die Herstellung eines beliebigen gültigen Zustands, um die Programmausführung fortsetzen zu können, verfolgt. Dies kann in bestimmten Fällen einem Programmabbruch vorzuziehen sein, wenn dies z.B. katastrophale Folgen hätte. Die angenommenen Daten können mit der Zeit durch neue ersetzt und irrelevant werden und sich das System so, wenn es weiterhin lange genug erfolgreich ausgeführt wird, von selbst wieder in einen fehlerfreien Zustand versetzen. Es kann mehrere Möglichkeiten geben die Reparatur durchzuführen. Um die auszuführenden Reparaturoperationen auszuwählen, können diesen Operationen Kosten zugewiesen werden. So kann man andere Operationen Löschoperationen vorziehen, um Datenverlust zu vermeiden. Außerdem sollte die Anzahl der notwendigen Operationen möglichst klein gehalten werden. 5.2 Spezifikation von Consistency Constraints Zur Angabe von Consistency Constraints können Datenstrukturen eines Programms in einer C-ähnlichen Sprache beschrieben werden. Hier zum Beispiel ein Auszug aus der Definition der Karte eines Spielfeldes eines Strategiespiels, auf das später noch genauer eingegangen wird [1]. structure city { int x; int y; //[...] } structure tile { int terrain; city *city; //[...] } structure tilegrid { tile grid[map.xsize*map.ysize]; } Städte haben eine Position, Felder einen Terraintyp sowie möglicherweise einen Zeiger auf eine Stadt, die Karte besteht aus einem Array von Feldern. Diese Strukturdefinitionen können nun verwendet werden, um Mengen von Objekten und deren Verhältnis zueinander anzugeben. set GRID(tilegrid): set TILE(tile): STILE set STILE(tile): set TERRAINTYPES(int): water set water(int): set CITY(city): CITYMAP: STILE -> CITY (1->1) – Die Menge GRID enthält Objekte vom Typ tilegrid. – Die Mengen TILE und STILE enthalten Objekte vom Typ tile, also Felder der Karte. STILE ist eine Untermenge von TILE. – Die Mengen TERRAINTYPES und water enthalten Integer. water ist eine Untermenge von TERRAINTYPES. – CITY enthält Städte, also Objekte vom Typ city. CITYMAP ist eine Relation, die Feldern aus STILE Städten zuordnet. [forall t in TILE], !t.city = NULL => t.city in CITY Es werden alle Elemente der Menge TILE und damit die Felder der Karte betrachtet. Wenn der city-Zeiger eines Feldes nicht NULL ist, so muss die Stadt, auf die gezeigt wird, auch tatsächlich existieren und somit Element der Menge CITY sein. [],sizeof(GRID)=1 [forall t in GRID, for x=literal(0) to map.xsize-literal(1), for y=literal(0) to map.ysize-literal(1)], true => t.grid[x+(y*map.xsize)] in TILE An allen Positionen des einzigen Elements aus GRID müssen sich tatsächlich existierende Felder aus TILE befinden. [for i=literal(0) to literal(6)], true => i in TERRAINTYPES [for i=literal(8) to literal(13)], true => i in TERRAINTYPES [for i=literal(7) to literal(7)], true => i in water Die Werte 0 bis 6 und 8 bis 13 sind gültige, nicht näher angegebene Terraintypen. 7 ist der Wert für Wasser. Da water eine Untermenge von TERRAINTYPES ist, zählt auch Wasser als Terraintyp. Einige Strukturdefinitionen für ein einfaches Dateisystem [1]: structure Inode { int referencecount; //[...] } structure DirectoryEntry { int inodenumber; //[...] } Inodes haben einen Referenzzähler, Verzeichniseinträge verweisen auf einen Inode. Darauf aufbauende Mengen- und Relationsangaben: set Inode(int): partition UsedInode | FreeInode set FreeInode(int): set UsedInode(int): partition FileInode | DirectoryInode set FileInode(int): set DirectoryInode(int): RootDirectoryInode set RootDirectoryInode(int): set DirectoryEntry(DirectoryEntry): inodeof: DirectoryEntry -> UsedInode (many->1) inodestatus: Inode -> token (many->1) referencecount: Inode -> int (many->1) – Die Menge aller Inodes wird in disjunkte Teilmengen benutzter und noch freier Inodes aufgeteilt. Die benutzten Inodes wiederum teilen sich in Datei- und Verzeichnis-Inodes auf. Der Inode des Wurzelverzeichnisses ist ein VerzeichnisInode. – Die Relation inodeof ordnet einem Verzeichniseintrag einen benutzten Inode zu. – inodestatus gibt den Status eines Inodes an, referencecount die Anzahl der Referenzen auf einen Inode. [for j=literal(0) to d.s.NumberofInodes-literal(1)], !(j in UsedInode) => j in FreeInode Wenn ein Inode nicht benutzt ist, so ist er frei. [forall <de, u> in inodeof], true => de.inodenumber=u Jeder Verzeichniseintrag, der mit einem benutzten Inode in Relation steht, enthält dessen Inodenummer. [forall u in UsedInode], u.inodestatus=literal(Used) [forall f in FreeInode], f.inodestatus=literal(Free) Benutzte und freie Inodes haben einen entsprechenden Status. [forall i in UsedInode], i.referencecount=sizeof(i.~inodeof) Der Referenzzähler eines Inodes ist gleich der Anzahl der Verzeichniseinträge, mit denen dieser in Relation steht. [],sizeof(RootDirectoryInode)=1 Es gibt nur ein einziges Wurzelverzeichnis. 5.3 Anwendung von Consistency Constraints Die Fehlererkennung und gegebenenfalls Reparatur kann zu unterschiedlichen Zeiten der Programmausführung vorgenommen werden. Bei automatischer Ausführung muss darauf geachtet werden, dass keine zu überprüfende Datenstruktur gerade von dem Programm verändert wird, da es dadurch zu temporären Inkonsistenzen kommen kann, die bei korrekter Beendigung der Operation behoben werden. So können beim Einfügen in eine Liste schon die Zeiger angepasst worden sein, aber die Aktualisierung der Elementanzahl im Header steht noch aus. Dies darf natürlich nicht von einer Überprüfungsroutine, die gerade den normalen Programmablauf unterbrochen hat, fälschlicherweise erkannt und vermeintlich repariert werden. Einerseits kann durch eine geeignete Auswahl von Stellen, an denen Konsistenz angenommen wird, eine automatische regelmäßige Kontrolle durchgeführt werden. Indem die Überprüfungen an manchen dieser Stellen je nach Bedarf deaktiviert werden, kann zwischen höherer Zuverlässigkeit und mehr Performanz abgewogen werden. Weiterhin ist es möglich, erst bei Erkennen eines Problems den Überprüfungsund Reparaturvorgang zu starten und dadurch das System in einen lauffähigen Zustand zurückzuversetzen, um danach die problematische Operation von einem vorher gespeicherten Konsistenzpunkt zu wiederholen. Natürlich können beide Techniken auch kombiniert werden. Auch können so z.B. auf Festplatte befindliche Daten von einem eigenständigen Programm überprüft werden, ohne dass diese Daten Teil eines gerade in Ausführung befindlichen Programms sind. Ein Problem, das bei der Reparatur auftreten kann, sind zyklische Abhängigkeiten. Veränderungen zur Konsistenzwiederherstellung an einer Stelle können Beschränkungen an einer anderen Stelle verletzen, deren Behebung wiederum selbst Beschränkungen verletzt. Diese Abhängigkeiten sind zyklisch, wenn durch eine spätere Veränderung die Verletzung einer schon früher behobenen Beschränkung wieder hervorgerufen wird. Eine Spezifikation muss also vor ihrer Anwendung auf diese Abhängigkeiten überprüft werden, um sicherzustellen, dass die Reparatur auch wirklich terminiert. 5.4 Beispiele der erfolgreichen Anwendung Um die Praxistauglichkeit dieses Ansatzes zu untersuchen, wurden Spezifikationen für mehrere sich täglich in Benutzung befindender Datenstrukturen sowie geeignete Fehlerinjektionsstrategien, um diese auf ihre Funktionalität zu testen, entwickelt. CTAS Als erstes Beispiel dient das Center-TRACON Automation System, kurz CTAS, ein Flugverkehrskontrollsystem, das Fluglotsen eine grafische Darstellung der Position und des vorhergesagten Weges der in der Luft befindlichen Flugzeuge bietet. Dazu werden Informationen der Flugpläne mit Start- und Zielflughäfen der einzelnen Flugzeuge an CTAS übermittelt, wobei es aufgrund der hohen strukturellen Komplexität der übertragenen Daten zu Verarbeitungsfehlern kommen kann. Die möglichen Flughäfen werden dabei in einem Array gespeichert. Wird versucht, aufgrund eines fehlerhaft gespeicherten und damit ungültigen Flughafens auf ein Element außerhalb des Arrays zuzugreifen, stürzt das System ab. Dies kann abgefangen und der ungültige Wert durch einen beliebigen Gültigen ersetzt werden. Danach kann die Ausführung an einem vorher festgelegten Konsistenzpunkt fortgesetzt werden. Nun wird zwar ein Start- oder Zielflughafen eines einzelnen Flugzeugs falsch angezeigt, dennoch ist das System weiterhin benutzbar. Außerdem werden die angenommenen Daten, sobald das Flugzeug landet oder den Überwachungsbereich der Kontrollstation verlässt, irrelevant. Dies ist einem Totalausfall klar vorzuziehen, da auch nach einem Neustart des Systems mit den alten, fehlerhaften Daten gearbeitet werden und so der Fehler wieder auftreten würde. vereinfachtes ext2-Dateisystem Weiterhin wurde eine vereinfachte Version des ext2-Dateisystems betrachtet. Dazu wurde bei Schreiboperationen ein Systemabsturz simuliert und das Verhalten bei anschließender weiterer Benutzung des Dateisystems mit und ohne Ausführung der Reparaturfunktionen verglichen. Dabei zeigte sich, dass ohne Reparatur Inkonsistenzen bestehen bleiben wie falsche Referenzzähler in Inodes oder ungültige Zustände für Verzeichniseinträge. Dies kann zu mehrfach verwendeten Blöcken und falschen Dateiinhalten führen. Das verwendete Reparaturverfahren war in der Lage, diese Probleme zu verhindern und eine weitere fehlerfreie Benutzung des Dateisystems zu ermöglichen. Freeciv Als Nächstes wurde das Kartenformat des Spiels Freeciv analysiert. Bei diesem Spiel kommen mehrere Arten von Terrain zum Einsatz und es können sich Städte auf der Karte befinden. Es soll sichergestellt werden, dass jedes Feld auf der Karte einen gültigen Eintrag für den Terraintyp besitzt, dass jede Stadt eine eindeutige Position besitzt, dass eine Stadt sich nicht auf einem Ozeanfeld befindet und dass die in der Stadt gespeicherte Position mit der in der Karte Gespeicherten übereinstimmt. Zum Testen wurden in vom Spiel erstellten Karten die Werte für das Terrain einiger Felder auf zufällige Werte gesetzt. Dadurch kann es zu zwei Arten von Fehlern kommen: der Terraintyp eines Feldes kann ungültig sein oder eine Stadt kann sich auf einem Ozeanfeld befinden. Zur Reparatur wurde Feldern mit ungültigen Terrainwerten gültige Werte zugewiesen und Städte wenn nötig auf Landfelder verschoben. Beim Verschieben der Städte muss sowohl die in der Karte gespeicherte Position der Stadt als auch die in der Stadt selber gespeicherte Position angapasst werden damit es nicht zu neuen Inkonsistenzen kommt. Durch diese Änderungen war es möglich, ansonsten abstürzende Spiele erfolgreich auszuführen. Word-Dokumente Zuletzt wurde die Reparatur beschädigter Word-Dokumente in Angriff genommen. Dieses Dateiformat benutzt Blockzuweisungstabellen wie beim FAT-Dateisystem, das eine einfach verkettete Liste verwendet, um eine Kette der einer Datei zugewiesenen Blöcke zu speichern. In der Liste können statt der Adresse des nächsten Blocks spezielle Werte stehen um z.B. das Ende einer Kette zu kennzeichnen. Hierbei kann es zu Problemen wie mehrfach zugewiesenen Blöcken oder Zeiger auf nicht existierende Blöcke kommen. Um Fehler einzufügen, wurden Endmarkierungen von Ketten durch Zeiger auf vorhandene Blöcke anderer Ketten ersetzt, Zeiger auf nicht existierende Blöcke eingefügt und benutzte Blöcke als nicht zugewiesen markiert. Während es mit Word noch möglich war, Dateien zu laden in denen Blöcke fälschlicherweise als nicht benutzt markiert waren, schlug dies bei Dateien mit anderen Defekten fehl. Nach erfolgter Reparatur war es wieder möglich diese Dateien zu laden, auch wenn dadurch das ursprüngliche Dokument verändert wurde, da es nicht zur Wiederherstellung von Daten sondern nur zur Anpassung der Struktur der Blockzuweisungstabelle kam. 5.5 Probleme der automatischen Reparatur Durch die Annahme von beliebigen gültigen Daten ist das Verfahren nicht unbedingt für alle Zwecke geeignet. Bei manchen kritischen Anwendungen ist eine Beendigung des Programms unumgänglich. Auch können manche Beschränkungen durch das Verbot von zyklischen Abhängigkeiten gar nicht erst ausgedrückt werden. Es ist nicht auszuschließen, dass Datenstrukturen, die vom Reparaturalgorithmus selber verwendet werden, beschädigt werden und dieser dann bestenfalls nicht mehr richtig funktioniert oder sogar selber Fehler verursacht. Auch wenn der Algorithmus nach Spezifikation arbeitet, ist es schwer vorherzusagen, welche Veränderungen tatsächlich vorgenommen werden. Durch manche Reparaturoperationen können zusätzliche Daten verloren gehen welche bei besserer Auswahl der auszuführenden Operationen hätten erhalten bleiben können. Kaskadierende Reparaturen können große Teile der Datenstruktur und somit vom ursprünglichen Fehler eigentlich unbeeinflusste Daten verändern. 6 Resilient Data Structures Da Fehlererkennung und -korrektur mit teilweise nicht zu tolerierend hohen Kosten an zusätzlich verwendetem Speicher und längerer Ausführungszeit verbunden sind, kann es sich in bestimmten Fällen anbieten, auf diese zu verzichten, wenn mit den noch fehlerfreien Daten weiterhin korrekt gearbeitet werden kann. Für einen solchen Umgang mit Fehlern existieren Resilient Data Structures, die unter fehleranfälligen Ausführungsbedingungen bestimmte Garantien für ihre unterstützten Operationen bieten. Dabei stellen diese Datenstrukturen einige Anforderungen, so wird eine Obergrenze δ an möglichen Fehlern angenommen und es muss eine bestimmte Menge an garantiert fehlerfreiem Speicher zur Verfügung stehen, um interne Daten verwalten zu können. Ein nützliches Modell zur Analyse dieser Datenstrukturen ist die faulty-memory random access machine. In diesem Modell können beliebige Speicherzellen jederzeit falsche Werte annehmen, es existiert jedoch eine konstante Menge des geforderten fehlerfreien Speichers [3]. 6.1 Priority Queue Unterstützte Operationen: – insert Fügt ein Element hinzu. – deletemin Gibt entweder den kleinsten gültigen oder einen fehlerhaften Wert zurück und löscht diesen. Es werden mehrere Puffer verwendet, deren Größe und Anzahl von der Nummer der Elemente in der Queue abhängt. In diesen Puffern werden die Elemente der Queue sortiert gespeichert. Diese Puffer werden in einer doppelt verketteten Liste verwaltet, deren Zeiger redundant mit 2δ + 1 Kopien gespeichert werden. Zeiger auf den Anfang dieser Liste sowie auf einen Einfügepuffer zusammen mit der Anzahl an Elementen in diesem Puffer werden in fehlerfreiem Speicher gehalten. Bei Über- oder Unterschreiten einer festgelegten Anzahl an Elementen in den Puffern werden diese mit fehlertoleranten Algorithmen aufgeteilt oder zusammengefasst. Dabei wird die Sortierung der nicht fehlerhaften Elemente beibehalten. Die Puffergröße erhöht sich zum Ende der Queue hin, damit die teuren Operationen zur Anpassung der Puffergröße seltener stattfinden müssen [5]. 6.2 Dictionary Unterstützte Operationen: – insert Fügt ein Element hinzu. – delete Löscht ein Element. – search Sucht nach einem Schlüssel κ. Gibt bei Vorhandensein eines gültigen Wertes mit diesem Schlüssel entweder diesen Wert oder einen fehlerhaften mit dem gleichen Schlüssel zurück. Gibt bei Abwesenheit eines Wertes mit Schlüssel κ entweder einen leeren Wert oder einen fehlerhaften Wert mit diesem Schlüssel zurück. In einem Suchbaum wird eine Partition der Menge der möglichen Schlüssel gespeichert. Ein Knoten darf dabei zwischen δ/2 und 2δ Schlüssel enthalten. Bei Einfüge- oder Löschoperationen müssen Knoten gegebenenfalls aufgeteilt oder zusammengefasst werden. Jeder Knoten speichert redundant mit 2δ + 1 Kopien die Endpunkte des abgedeckten Intervalls, die Anzahl der enthaltenen Schlüssel, das von seinem Unterbaum abgedeckte Intervall, Zeiger auf die Kinder- und den Elternknoten sowie möglicherweise benötigte Informationen um den Baum balanciert zu halten. Die Menge der enthaltenen Schlüssel wird nicht redundant gespeichert. Der Baum selber wird dabei in einem Array gespeichert, um das Erkennen ungültiger Zeiger zu vereinfachen. Wird die maximale Anzahl an Elementen im Array überschritten oder wird weniger als ein Viertel der möglichen Elemente verwendet, wird ein neues Array mit der doppelten beziehungsweise halben Größe angelegt und der alte Inhalt hineinkopiert. Zeiger auf das Array und den Wurzelknoten sowie die momentane Anzahl an Elementen im Array werden in fehlerfreiem Speicher gehalten [4]. 6.3 Verbleibende Probleme Die vorgestellten Resilient Data Structures machen einige Annahmen, die nicht unbedingt realistisch sind. Auch wenn nur eine konstante Menge an fehlerfreiem Speicher benötigt wird, kann es schwer sein, die Fehlerfreiheit tatsächlich zu garantieren. Außerdem ist die Anzahl der auftretenden Fehler nicht unbedingt nach oben beschränkt. Mit der faulty-memory random access machine wird ein sehr simples Modell eines fehleranfälligen Systems betrachtet. Da Resilient Data Structures vor allem bei sehr großen Datenmengen und damit hohem Bedarf an viel billigem Speicher sinnvoll sind, wäre eine Unterstützung komplexerer Speicherhierarchien wünschenswert. Die Performanz der hier betrachteten Queue ist stark von der angenommenen Obergrenze an Fehlern abhängig und wird mit steigendem δ schnell schlechter [3]. Literatur 1. http://www.cag.lcs.mit.edu/∼bdemsky/repair. 2. Brian Demsky and Martin Rinard. Automatic detection and repair of errors in data structures. Proceedings of the 18th annual ACM SIGPLAN conference on Objectoriented programing, systems, languages, and applications, 2003. 3. Umberto Ferraro-Petrillo, Irene Finocchi, and Giuseppe F. Italiano. Experimental study of resilient algorithms and data structures. In Proceedings of the 9th international conference on Experimental Algorithms, SEA’10, pages 1–12, Berlin, Heidelberg, 2010. Springer-Verlag. 4. Irene Finocchi, Fabrizio Grandoni, and Giuseppe F. Italiano. Resilient dictionaries. ACM Trans. Algorithms, 6(1):1:1–1:19, December 2009. 5. Allan Grønlund Jørgensen, Gabriel Moruz, and Thomas Mølhave. Priority queues resilient to memory faults. In Proceedings of the 10th international conference on Algorithms and Data Structures, WADS’07, pages 127–138, Berlin, Heidelberg, 2007. Springer-Verlag. 6. David J. Taylor, D. E. Morgan, and J. P. Black. Redundancy in data structures: Improving software fault tolerance. IEEE Transactions on Software Engineering, 1980.