Wissenswertes über Betriebssysteme
Anbei einiges Wissenswertes über Betriebssysteme. Betriebssysteme stellen die Grundlage eines produktiven Systems dar, ohne diese wäre es nicht möglich die Komplexität eines Computersystems zu beherrschen. Einige Fragen nachfolgend, die zu großen Teilen aus einer Aufgabensammlung von Studenten der Hochschule Karlsruhe entnommen wurden.
Was sind die Hauptaufgaben eines Betriebssystems?
- Zusammengefasst: Abstraktion (von Hardware), Resourcenzuteilung/Koordination (von u. zwischen Rechenaufträgen)
- Ausführlich:
- Verwaltung der Betriebsmittel (Hard- und Software) eines Computersystems (Ein-/Ausgabe, Speicherverwaltung, Resourcenzuteilung zu Prozessen, ...)
- Abarbeitung und Durchführung von Rechenaufträgen (Organisation und Koordination des Betriebsablaufs, Steuerung der Ausführung von Programmen)
- Langfristige Datenhaltung auf geeigneten Datenträgern
- Abbildung der Benutzer auf die Maschinenwelt ("Abstrakter Rechner")
Was sind die Nebenaufgaben eines Betriebssystems?
- Zusammengefasst: Statistiken, Abrechnung
- Ausführlich:
- Führen von Statistiken aller Art (Speicherplatzverbrauch, Quota, Rechenzeiten auf Prozessor, ...)
- Abrechnung von Rechenkosten bei gemeinsamer Nutzung (Mehrbenutzersystem) einer Rechenanlage
Was sind die Randbedingungen unter denen die Aufgaben eines Betriebssystems erfüllt werden müssen?
- Optimale Ausnutzung der vorhanden Betriebsmittel (Hardware)
- Minimierung des Verwaltungsaufwands
- Robustheit, Fehlertoleranz
- Schutz von Daten und Programmen
- Aufälle von Datenträgern vermeiden
Welche typischen Hardwareklassen gibt es?
- Peripherie (externe I/O-Geräte): z.B. Lochkarten, Drucker, Tastatur, Maus, Display
- I/O-Steuerung: z.B. zentraler oder separater Prozessor
- Hilfsspeicher: z.B. Band (selten, veraltet), Disketten, Platten
- Hauptspeicher: z.B. kleine direkt adressierbare Speicher, große physikalische und virtuelle Speicher (meist RAM)
- Prozessor: z.B. Uniprozessir (1-Kern), Multiprozessor (n-Kern)
- System: z.B. Zentralrechner mit gemeinsam benutztem Speicher, Virtuelles System
Welche Ursachen können einen Interrupt auslösen?
- Stromausfall
- Paritätsfehler
- Tastendruck (z.B. Reset)
- Page-Fault
- Timer (z.B. Watchdog)
- Zeitscheibeninterrupt
- E/A-Signale (von Peripheriegeräten)
- Division durch die Zahl Null
Was ist eine ISR?
siehe auch http://de.wikipedia.org/wiki/Interrupt_Service_Routine
Eine Interrupt Service Routine (auch Unterbrechungsroutine genannt) ist ein kleines Programm, dass von der CPU ausgeführt wird, wenn sie durch einen Interrupt-Request (Unterbrechungsanforderung) über einen Interrupt-Signaleingang ein Signal über einen eingetroffenen Interrupt erhält. Mehrere Interrupt-Quellen werden extern der CPU über einen Interrupt-Controller-Baustein (Programmable Interrupt Controller, heute meist in der CPU integriert) zusammengeführt. Dieses Signal zwingt (triggert) die CPU dazu, den aktuellen Programmablauf zu unterbrechen und einen Interrupt auszuführen. Interrupts sind externe und damit asynchrone Unterbrechnungen.
Wie ist der grundsätzliche Ablauf einer ISR?
CPU besitzt eine Anzahl von n verschiedenen (je nach CPU-Architektur) Interruptvektoren (einer pro Interruptquelle <=> Interrupt-Request [IRQ]). Diese Interruptvektoren geben an, an welche Adresse gesprungen werden muss, wenn der zugehörige IRQ eintritt. An dieser Adresse stehen dann weitere Befehle, wie (mit welchen Befehlen) der Interrupt zu behandeln ist. Diese werden von der CPU abgeabeitet.
Ein Interrupt wird nur dann behandelt, wenn der zum IRQ passende Vektor nicht maskiert ist und gerade kein anderer Interrupt mit höherer Priorität abgearbeitet wird. Ist der Interrupt also nicht maskiert, keiner mit höher Priorität steht an oder wird bearbeitet, so werden vor dem Sprung in die programmierte Routine alle Arbeitsregister (Register der CPU) und das PSW (Processor Status Word) im Stack gespeichert. Das wird deshalb gemacht, damit das unterbrochene Programm dort weiterarbeiten kann, wo es unterbrochen wurde. [Zusätzlich wird evtl. eine Interrupt-Sperre gesetzt (atomarität)]. Anschließend wird zur im Interruptvektor hinterlegten Adresse gesprungen und der programmierte Code ausgeführt. Danach werden das PSW und die Arbeitsregister wiederhergestellt [und die evtl. Interruptsperre wird gelöscht].
Was sind die Charakteristiken einer ISR?
- keine Zyklen
- kurze Abläufe
- keine Dateizugriffe
Interrupts unterbrechen den Programmablauf. Sind Interrupts dann selbst auch unterbrechbar?
Interrupts sind durch Interrupts höherer Priorität unterbrechbar. Der unterbrochene Interrupt wird (wie ein gewöhnlicher Prozess) auf den Stack gelegt, bis er evtl. weiterverarbeitet werden kann.
Was ist der Unterschied zwischen Traps und Interrupts?
- Ein Interrupt ist asynchron, nicht reproduzierbar und nicht vorhersagbar.
- Ein Trap ist synchron, reproduzierbar und vorhersagbar (siehe http://de.wikipedia.org/wiki/Ausnahmebehandlung)
Welche Betriebsarten eines Betriebssystems gibt es?
- Einzelauftragsverarbeitung
- Stapelverarbeitung (Batch)
- Dialogverarbeitung (Mensch-Maschine)
- Echtzeitverarbeitung ("Rechtzeitigkeit", max. Verzögerung ist definiert und wird nicht überschritten, also "schnell genug")
Was sind Mermale der Dialogverarbeitung?
Ein Rechenauftrag besteht hier aus einer Sitzung, in deren Verlauf der Benutzer die auszuführenden Programme in einer bestimmten Kommandosprache explizit aufruft. Die Programme werden dabei immer sofort gestartet. Das Betriebssystem muss dafür sorgen, dass die Antwortzeit dafür möglichst niedrig ist, da es sich um eine "interaktive" Verarbeitung handelt und meist ein Benutzer mit dem System arbeiet (Wartezeit).
Was zeichnet die Einzelauftragsbearbeitung negativ aus?
Geringe Auslastung des Prozessors mit der eigentlichen Problemlösung. Rechner ist während der gesamten Verarbeitungszeit exklusiv durch einen Benutzer belegt.
Was bedeutet Spooling?
Simultaneous Peripherals Operation Online
Ankommende, zu verarbeitende Aufträge werden in einem Puffer im Speicher [oder auf einem externen Datenträger (z.B. Festplatte, Band, ...)] zwischengespeichert bis das entsprechende Ausgabegerät bereit ist. Durch diese Trennung der Produktion und Weiterverarbeitung (bzw. Ausgabe) von Daten, ist es möglich, die Auslastung der Teilsysteme zu verbessern. So können einerseits die produzierenden Prozesse ohne Verzögerungen weiterarbeiten (solange Speicher im Puffer vorhanden ist). Wenn die Verarbeitung langsamer erfolgt. Andererseits kann das verarbeitende System zurückliegende Aufträge abarbeiten, während das produzierende System zeitaufwändige Berechnungen durchführt.
Beispiel: Eine Druckerwarteschlange, in der Druckaufträge gesammelt und nacheinander abgearbeitet werden. Auch Mailserver sammeln zu versendende Mails üblicherweise in einem Spool-Verzeichnis, von dem aus sie dann verschickt werden.
Was bedeutet Batch-Verarbeitung?
Batchverarbeitung beschreibt die sequentielle, nicht interaktive Bearbeitung von Aufgaben. Die Batchverarbeitung ist nicht dialoggeführt, was soviel bedeutet, wie dass dem Betriebssystem schon vor der Ausführung alle Daten und Parameter zu Verfügung stehen müssen. Dies wird eingesetzt um ständig wiederholende Aufgaben sowie große Datenmengen zu bewältigen. Vorteil: Da Aufträge und Daten bereits vor der Ausführung zusammengefasst wurden, wird somit die CPU-Wartezeit reduziert, da es keine unnötigen Dialoge oder Datenträgerwechsel gibt.
Beispiel: Das Skalieren von mehreren Bildern auf eine bestimmte Pixelgröße.
Was ist der Vorteil von Multiprogramming?
Multiprogramming bedeutet, dass mehrere Programme ablaufbereit im Speicher stehen. Der Vorteil dabei ist der, dass Rechenzeiten, in denen ein Programm auf langsame I/O-Geräte wartet, durch andere Programme ausgenutzt werden können und somit eine höhere Prozessorauslastung erreichen.
Wie wird die Rechenzeit bei der Dialogverarbeitung aufgeteilt?
Die Verteilung der Rechenzeit erfolgt durch das Zeitscheibenverfahren (Time-Sharing). Jeder einzelne Nutzer, bzw. jedes Programm erhält eine gewisse Rechenzeit zugewiesen. Die Ein-/Ausgabe erfolgt über Dialogstationen. Somit ist es möglich, mehrere Benutzer an einem Computer quasi gleichzeitig arbeiten zu lassen, in dem sie sich die Rechenzeit des einzigen vorhandenen Prozessors teilen.
Was unterscheidet die Realzeitverarbeitung von der Dialogverarbeitung?
Die Programmausführung ist bei der Realzeitverarbeitung ereignisgesteuert. Die Programme besitzen Prioritäten. Durch diese sind sie miteinander gekoppelt. Im Gegensatz zur Unabhängigkeit der Programme bei Dialogverarbeitung. Weiterhin ermöglicht die Realzeitverarbeitung das Einhalten von vorgeschriebenen Reaktionszeiten, trotz inhomogener Geräte.
Internationale Normungsgremien (Auswahl)
Eine Auswahl an internationalen Gremien, die für die Normung wichtiger technischer Prozesse und Abläufe verantwortlich sind:
- ISO - Abkürzung für International Standardization Organization. Ein internationaler Zusammenschluss aller Normungsausschüsse aus über 50 Ländern. Die ISO ist im Bereich der Daten- und Telekommunikation das derzeit wichtigste Gremium, das Standards definiert.
- IEC - Abkürzung für International Electrotechnical Commission. Eine internationale Kommission mit Sitz in Genf, die sich mit Normung von Schnittstellen befasst. Die bekannteste Schnittstelle ist der IEC-Bus.
- CCITT - Abkürzung für Comité Consultatif International Télégraphique et Téléphonique. Ein in Genf sitzender Ausschuss, der Fernmeldeverwaltungen, der sich mit Vorschlägen zur Normung und Standardisierung der Daten- und Fernsprechdienste befasst. Vom CCITT genormte Schnittstellen gebinnen mit den Buchstaben V oder X (gefolgt von einem . (Punkt)). Die bekanntesten Schnittstellen sind V.24 oder X.21.
- IEEE - Abkürzung für Institute of Electrical and Electronics Engineers. Der Verband der amerikanischen Elektroingenieure mit Sitz in New York, der internationale Standards erstellt.
- DIN - Abkürzung für Deutsches Institut für Normung. Ein technisch-wissenschaftlicher Verein mit Sitz in Berlin.
Die Zahl 2.2250738585072011e-308 macht PHP Probleme [Update]
Rick Regan hat am 3. Januar auf seiner Seite einen vermuteten Bug im Kern von PHP bekanntgegeben. Mitlerweile hat sich bestätigt, dass seine Annahme richtig war. Er schreibt, dass sich bereits alleine das Speichern der Fließkommazahl 2.2250738585072011e-308 in einer Variable einen 32 Bit PHP-Server in eine Endlosschleife schickt. Das gleiche soll auch passieren, wenn man die Zahl in nicht wissenschaftlicher Schreibweise vollständig - also mit den kompletten 324 dezimalen Stellen - in eine Datei schreibt und den PHP-Parser darüberlaufen lässt. Auch jede Variation des Exponenten (also .22250738585072011e-307, 2.2250738585072011e-308, 22.250738585072011e-309, ..., .22250738585072011e-324) zwingt den Server in die Knie. Der Fehler tritt immer dann auf, sobald die Folge als Fließkommzahl verwendet wird. Eine Speicherung als String funktioniert dagegen solange bis man mit dem String anfängt zu rechnen, das liegt in der Natur der Scriptsprachen. Diese verwenden Duck-Typing und scheitern damit bei jedem Versuch die o.g. 64 Bit Darstellung als Zahl zu "verwenden". Das Problem an diesem Bug ist nun, dass Angreifer sehr einfach den Server einer DoS-Attacke unterziehen können: Der Webserver wird damit zwar nicht gänzlich zerstört oder gekillt, er geht aber extrem in die Knie, da Apache (und andere HTTP-Server) keine Chance haben unendlich viele Threads aus dem Pool zu holen, denn der ist irgendwann schließlich leer.
Schuld an dem Ganzen ist die rechnerische Annäherung an die 64 Bit Gleitkommazahl der durch die FPU (Floating Point Unit) Intel x87 auf Intel Prozessoren durchgeführt wird und ist demnach ein Fehler im Design. Es ist bereits ausreichend, die Zahl als Parameterwert über die superglobalen GET oder POST-Variablen an ein PHP-Script zu übergeben. Betroffen sind nach Kommentaren auf php.net und diversen anderen Seiten aber nur die 32 Bit Versionen ab Version 5.2.8 und Prozessoren von Intel, die die x87 implemeniert haben. Zur Zeit gibt es noch keinen Bugfix, man kann sich aber einem Userkommentar zufolge mit dem Start von PHP mit dem Parameter "-mfpmath=sse" abhelfen. Dadurch wird statt x87 die SSE verwendet.
Update: Mittlerweile wurde ein Bugfix von der PHPGroup bereitgestellt. Kommende Versionen ab 5.3.5 beinhalten diesen Fix dann ebenfalls. Es wurde die Methode zend_strtod() der Zend_API geändert und dabei das Keyword volatile in Zeile 2038 eingefügt. Das verhindert, dass eine übermäßige Optimierung der Variablen stattfindet und das Programm immer auf den tatsächlichen Wert (in der Hardware) zugreift. Damit macht man deutlich, dass sich der Wert außerhalb des Programmkontextes ändern kann (z.B. auf einem Co-Prozessor, wie dem x87).
Iteratoren: Trennung von Algorithmus und Datenstruktur (C++)
Mit Iteratoren lassen sich Algorithmen schreiben, die unabhängig von der konkreten Datenstruktur (Implementierung) arbeiten. Jeder Programmierer braucht Schleifen, um z.B. auf Arrays arbeiten zu können. Das geht auch ganz komfortabel, wofür sollte man denn dann Iteratoren verwenden?
Das Problem beim Zugriff auf Arrays ist, dass man für jede Implementierung einen eigenen kleinen Algorithmus basteln muss, der durch eine Schleife auf jedes Element zugreifen muss. Das lässt sich komfortabler mit Iteratoren handhaben. Diese dienen als Bindeglied zwischen der Container-Klasse und den Algorithmen auf Containern und folgt dem Prinzip "Umkehr der Abhängigkeiten". Iteratoren sind abstrakte Datentypen, mit deren Hilfe auf eine Folge von Objekten zugegeriffen werden kann und jeder Iterator ist ein Zeiger auf ein Objekt innerhalb einer Folge von Objekten.
An einem kleinen Beispiel lässt sich die Verwendung von Iteratoren aufzeigen:
Nehmen wir die Datenstruktur Vector: Jedes Element wird intern in einem Array, also hintereinander auf dem Heap abgelegt. Es lässt sich nun durch inkrementieren der Speicheradresse jedes Element ansprechen. Das gleiche gilt aber auch für List-Objekte. Hier zeigt zwar jedes Element auf das Nächste und es gibt kein Array, das man inkrementieren könnte aber innerhalb einer Folge von zusammenhängenden Objekten muss immer irgendwo die Speicheradresse zum nächsten Objekt gespeichert sein. Und so lässt sich auch auf der Liste iterieren. Jede Datenstruktur besitzt ihren eigenen Iterator. Mit dem Iterator können alle Elemente in ihrer Sortierreihenfolge besucht werden.
Iteratoren haben (vereinfacht gesagt) folgende Funktionalität:
- Haben eine Referenz auf das Element, auf das der Iterator gerade verweist
- Verweisen auf das (für die Sortierreihenfolge) nächste Element (Verwendung des Operators ++)
- Vergleiche (==, !=, <, >, <=, >=, ...)
Beispiel:
- int* ist ein Iterator für den Datentyp int[]
- list<int>::iterator ist ein Iterator für die Klasse list<int>
- queue<int>::iterator ist ein Iterator für die Klasse queue<int>
- vector<Pair<int> >::iterator ist ein Iterator für die Klasse vector<Pair<int> >
- ...
Jede Datenstruktur hat zwei Methoden:
- begin(): Iterator zeigt auf erstes Element der Datenstruktur
- end(): Iterator zeigt hinter das letzte Element der Datenstruktur
Sourcecode-Beispiel:
#include <vector>
#include <iterator>
#include <iostream>
using namespace std
// einfache Ausgabe auf der Konsole
template <class iterType>
void print(iterType first, iterType last) {
while(first != last) {
cout << *first++ << endl; // zuerst dereferenzieren, dann ausgeben und anschließend iterieren
}
}
// main.cpp:
int main() {
vector<int> vec;
vec.push_back(23);
vec.push_back(42);
print(vec.begin(), vec.end());
return 0;
}
Wie lassen sich Iteratoren (als Variable) greifen?
template<class type> void func(type container) {
typename type::iterator iter = container.begin();
// typename ist hier erforderlich, da der qualifizierende
// Bezeichner (type) ein Templateparameter ist.
}
template<class type> void func(vector<type> container) {
typename vector<type>::iterator iter = container.begin();
// typename ist hier erforderlich, da der qualifizierende Bezeichner
// (vector<...>) von einem Templateparameter (type) abhängt.
}
Funktionsweise am Beispiel einer einfachen Implementierung, die mit Hilfe von Iteratoren ein Array in ein anderes Array kopiert:
#include <iterator>
#include <iostream>
using namespace std;
template<class InIterator, class OutIterator>
void copy(InIterator first, InIterator last, OutIterator result) {
while (first != last) {
*result++ = *first++;
}
}
// main.cpp:
int main() {
int source[] = {1,2,3,4,5,6};
const int length = sizeof(source) / sizeof(int);
int dest[length];
copy(source, source+length, dest);
return 0;
}
Da es sich hierbei um ein Integer-Array handelt, nutzt der Compiler int* als Zeiger auf die Elemente in der Datenstruktur. Er übersetzt das Programm in folgenden Code:
void copy(int* first, int* last, int* result) {
while (first != last) {
*result++ = *first++;
}
}
Die Implementierung hätte man auch mit unterschiedlichen Datentypen, z.B. vector und list durchführen können. Dazu muss man in obigem Beispiel statt dem zweiten Array z.B. ein Vector anlegen und die copy()-Methode wie gewohnt aufrufen, und voilá es funktioniert auch mit unterschiedlichen Containern!
Zusammenfassend lässt sich sagen, dass Container (das sind Listen, Queues, Vektoren, ...) Iteratoren anbieten, die von Algorithmen genutzt werden können. Dadurch ergibt sich eine saubere und klar strukturiere Trennung zwischen Algorithmus und Datenstruktur.
Auswahl an Algorithmen
// *** Kopieren der Elemente eines Containers in einen anderen (mit Überschreiben). result muss Platz für alle Elemente bieten. template <class InIter, class OutIter> OutIter copy(InIter first, InIter last, OutIter result); // Bsp.: vector<int> vec; list<int> list; vec.push_back(23); copy(vec.begin(), vec.end(), list.begin()); // *** Überschreiben der Elemente eines Zielcontainers mit einem festen Wert template <class ForwardIter, class T> void fill(ForwardIter first, ForwardIter last, const T& value); // Bsp.: vector<int> vec; vec.push_back(23); vec.push_back(42); fill(vec.begin(), vec.end(), 1337); // *** Sortieren von Containerdaten mit Angabe der Vergleichsfunktion template <class RandomAccessIter> void sort(RandomAccessIter first, RandomAccessIter last); // Bsp.: vector<int> vec; vec.push_back(23); vec.push_back(42); sort(vec.begin(), vec.end()); // *** Durchsucht die Containerdaten und liefert einen Iterator auf das Objekt des ersten Treffers zurück template <class InputIter, class EqualutyCmp> InputIter find(InputIter first, InputIter last, const EqualityCmp& value); // Bsp.: vector<int> vec; vec.push_back(23); vec.push_back(42); typename vector<int>::iterator result = find(vec.begin(), vec.end(), 23);
Hinweis: Die Beispielimplementierungen oben stammen zum Teil aus der Vorlesung Informatik 2 an der Hochschule Karlsruhe.
Elementare Datenstruktur: Stack (C++)
Stack (Stapelspeicher)
- Die Daten werden mit push auf den Stack gelegt und mit pop wieder davon gelöscht
- Daten können ähnlich einem Teller-Stapel nur oben aufgelegt und wieder entnommen werden
- Es lassen sich viele Containter-Klassen der STL verwenden. Beispiel: list, aber vector nicht
- Optimiert für das Einfügen und Löschen von Elementen
- Kein Indexzugriff auf die Daten
- Keine Suche in den Daten
- Der Stack eignet sich besonders zur Zwischenspeicherung von Objekten oder Daten bei Rekursionen, die zu einer Iteration aufgelöst wird oder z.B. Text-Parsern für XML-Dokumente
2. Die wichtigsten Methoden eines Stack:
- void push(const type& value): Element oben auf den Stapel legen
- void pop(): Oberstes Element vom Stapel löschen (bei leerem Stack folgt kein Fehler)
- type& top() bzw. const type& top(): Oberstes Element vom Stapel lesen
- bool empty() const: Test, ob der Stapel leer ist
- size_type size() const: Anzahl der Elemente
- Die Laufzeiten hängen vom verwendeten Container ab