Axel Rogat
Betriebssysteme und betriebssystemnahes Programmieren
 
2.6: Windows Kapitel 2 3: Grundkonzepte 
 
  2.7 UNIX  
 

UNIX ist jedes Betriebssystem, das einem der Standards entspricht, die im ersten Kapitel erwähnt wurden (hauptsächlich SVR4, BSD, POSIX). Die Definitionen sind völlig unabhängig von der Rechnerarchitektur. Vorgeschrieben wird ein Satz von Systemaufrufen und deren Funktionalität. Ein direkter Zugriff auf die Hardware ist normalerweise nicht möglich.

Es gibt wohl keinen Fall, in dem ein UNIX schrittweise für wachsende Hardware angepaßt werden mußte, wie das bei MS-DOS und Windows der Fall war. Daher ergeben sich auch nicht so komplizierte Strukturen von Zusammenarbeit mehrerer Teile unterschiedlicher Herkunft und Geschichte.

Abstraktere Funktionalität wie Kommandozeilen-Interpreter und Editor sind aus dem Kern ausgegliedert. Der Kern selbst ist allerdings sehr groß und beinhaltet viel Funktionalität (Prozesse, Speicher, Filesystem). Bei vielen UNIXen ist er in sich fast völlig ungegliedert und entsprechend unübersichtlich.

Die beiden angegebenen Schnittstellen müssen deutlich voneinander unterschieden werden! Bibliotheks-Aufrufe erfolgen durch normale Unterprogramm-Aufrufe, bei denen der User-Mode der Programme nicht verlassen wird. Aufrufe des Kernels erfolgen nur innerhalb dieser Bibliotheken - meist durch Belegen von Registern mit Werten und Auslösen eines Software-Interrupts.

Die meisten normalen Benutzer identifizieren UNIX eher mit der Benutzerschnittstelle, d.h. mit der Shell und den Standard-Utilities, wie sie von der Shell aus aufgerufen werden. Diese hat zwar Parallelen in der Bibliotheks-Schnittstelle, liegt aber auf einer ganz anderen Ebene. Die Systemaufruf-Schnittstelle kann völlig anders geartet sein.

Die verschiedenen Standardisierungs-Versuche legen dabei immer nur das Bibliotheks-Interface fest. Das Kernel-Interface ist hardwareabhängig und kann von UNIX zu UNIX völlig unterschiedlich sein. Oft gibt es aber zwischen Bibliotheksfunktionen und Systemaufrufen starke Entsprechungen in der Funktionalität.

Als Beispiel geben wir fünf Bibliotheks-Routinen an, wie sie im System V definiert sind:

create(name,amode)
  Legt eine (leere) neue Datei mit dem Namen name (ggf. inklusive Pfad) an mit den Zugriffsrechten, die amode beschreibt.
link(f1,f2)
Legt einen Hard Link an, d.h. einen "neuen Namen" f2 für die schon existierende Datei mit dem Namen f1.
mount(filesys,dir,rflag)
Das neue Dateisystem filesys wird an der Stelle dir in den Dateibaum eingehängt. Wenn rflag nicht 0 ist, wird das System Read-Only.
kill(pid,sig)
Schickt dem Prozeß Nummer pid das Signal sig (z.B. das Signal zum Abbruch).
_exit(status)
Beendet den aktuellen Prozeß und gibt den Wert status an den erzeugenden Prozeß zurück, der ihn mit wait oder waitpid abfragen kann. (Nicht verwechseln: Die C-Funktion exit räumt vor dem Aufruf dieser Routine noch Dateien und Speicher auf.)

2.7.1 Linux

Linux auf 80x86-Systemen existiert meistens nicht allein auf dem Rechner, sondern lebt mit DOS oder/und Windows zusammen. Wenn es gewünscht wird, gibt es aber auch Konfigurationsmöglichkeiten, ohne andere Systeme auszukommen.

Das Booten des Rechners erledigt (aus offensichtlichen technischen Gründen) auch bei Linux das BIOS. Danach können mehrere Wege beschritten werden:

Nach seinem Start ist Linux jedenfalls nicht auf irgendwelche Dienste von DOS angewiesen. Zum Zugriff auf Ressourcen auf dem Motherboard (z.B. Platten/IDE-Controller) wird entweder das BIOS bemüht, oder die Ressourcen werden über das BIOS erkannt und später direkt angesprochen.

2.7.2 Linux-Systemaufrufe

Unter Linux auf PCs wird für Systemaufrufe ebenfalls ein Interrupt bemüht, nämlich der mit der Nummer 0x80=128. In Prozessorregistern werden die Nummer der gewünschten Funktion (EAX) und die eigentlichen Argumente übergeben. Auf diese Weise gelangt man am bequemsten in den privilegierten Modus.

Die Nummern aller Systemaufrufe findet man beispielsweise als Konstanten definiert in der Datei include/asm/unistd.h. Ein Auszug:

#define __NR_setup 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_kill 37 #define __NR_rename 38 #define __NR_mkdir 39 #define __NR_rmdir 40

Von C aus kann man einen der syscall-Makros (aus derselben Datei) verwenden, die automatisch Inline-Assemblercode erzeugen.

Es wird eine Kernel-Routine ab der Adresse _system_call aufgerufen. Üblicherweise ist sie in Assembler geschrieben (z.B. in der Datei arch/i386/kernel/entry.S). Sie rettet zunächst alle Prozessor-Register auf den Stack:

pushl eax ; Funktionsnummer auf dem Stack merken push gs ; ab hier: Register retten push fs ...
Danach überprüft sie, ob die angegebene Nummer einen gültigen Aufruf darstellt (sonst wird abgebrochen), und ermittelt dann die tatsächliche Adresse der aufzurufenden Routine, wobei sie auf die Tabelle sys_call_table zugreift. Schließlich ruft sie diese Routine auf:
cmpl $(NR_syscalls),eax ; eax=Nummer, Vergleich mit größter Nummer jae ret_from_sys_call ; zu groß --> Abbruch movl sys_call_table(eax,4),eax ; Adresse aus Tabelle lesen ... call *eax ; eigentlicher Aufruf
Die Tabelle ist auch in entry.S definiert:
.data ENTRY(sys_call_table) .long SYMBOL_NAME(sys_setup) .long SYMBOL_NAME(sys_exit) .long SYMBOL_NAME(sys_fork) .long SYMBOL_NAME(sys_read) ...
Es sind einige zusätzliche Arbeiten notwendig, wenn sich der aufrufende Prozeß gerade im Einzelschritt-Modus befindet. Außerdem werden ggf. an dieser Stelle "aufgeschobene" Aktionen aus zurückliegenden Interrupts ("bottom halves") ausgeführt -- die Details würden hier zu weit führen. Falls der Prozeß durch den Aufruf nicht mehr weiterlaufen kann (wait, sleep) oder zufällig seine Zeit um ist, wird hier außerdem der Scheduler aufgerufen.

Ansonsten werden zum Schluß die Register des Prozesses restauriert, und es wird aus der Interrupt-Routine zurückgekehrt:

... pop fs pop gs ; bis hier: Register retten addl $4,esp ; Funktionsnummer vom Stack nehmen iret ; Interrupt beenden
Durch Ergänzung der Aufruftabelle sys_call_table, die Anpassung von NR_syscalls und Neuübersetzung des Kernels kann man natürlich leicht zusätzliche Aufrufe implementieren. Das entstehende Linux ist aber nicht mehr mit anderen Versionen kompatibel!

Beispiel: Ein sehr einfacher Systemaufruf ist getpid, der die Nummer des gerade aktuellen Prozesses liefert. Sein Code steht in kernel/sched.c. struct task_struct *current ist eine statische Variable im Kernel und zeigt auf die Verwaltungsstruktur des aktuellen Prozesses. Auch ohne deren genaue Kenntnis können wir den Code von getpid schon verstehen:

asmlinkage int sys_getpid(void) { return current->pid; }
Die Aufrufe für die ID des Benutzers, der Gruppe etc. sehen ähnlich aus. Die meisten anderen Systemfunktionen sind natürlich wesentlich komplexer ;-)

Beispiel 2: Interessant zu lesen ist auch der Code für den Systemaufruf reboot (Nummer 88), definiert in kernel/sys.c:

asmlinkage int sys_reboot(int magic, int magic_too, int flag) { if (!suser()) return -EPERM; if (magic != 0xfee1dead || magic_too != 672274793) return -EINVAL; if (flag == 0x01234567) hard_reset_now(); else if (flag == 0x89ABCDEF) C_A_D = 1; else if (!flag) C_A_D = 0; else if (flag == 0xCDEF0123) { printk(KERN_EMERG "System halted\n"); sys_kill(-1, SIGKILL); do_exit(0); } else return -EINVAL; return 0; }
Die Fehlerkonstanten EPERM (permission denied) und EINVAL (invalid) sind in errno.h definiert.

Man sieht, daß nur ein Prozeß des Super-Users den Aufruf tätigen darf. Zur Vorsicht muß auch er einige kryptische magische Zahlen als Parameter übergeben. Abhängig vom Wert des letzten Parameters flag passiert folgendes:

0x01234567: Es wird sofort ein Prozessor-Reset ausgelöst (gefährlich, da nicht aufgeräumt wird).
0x89abcdef: Control-Alt-Delete wird zugelassen. Es wird nicht gebootet.
0: Control-Alt-Delete wird abgeschaltet. Es wird nicht gebootet.
0xcdef0123: Das System wird regulär heruntergefahren, indem der Startprozeß das KILL-Signal erhält.

 
2.6: Windows Startseite 3: Grundkonzepte