Kapitel 16. Der physikalische Aufbau der Kernel-Quellen

Inhalt
Booten des Kernels
Vor dem Booten
Der init-Prozeß
Das Verzeichnis kernel
Das Verzeichnis fs
Das Verzeichnis mm
Das Verzeichnis net
ipc und lib
include und arch
Treiber

Bisher haben wir über den Linux-Kernel aus der Perspektive des Treiber-Autors geschrieben. Wenn Sie aber einmal anfangen, mit dem Kernel herumzuspielen, dann wollen Sie möglicherweise alles wissen. Es kann sogar so weit kommen, daß Sie ganze Tage damit zubringen, durch den Quellcode zu navigieren und den Quellbaum mit grep zu durchsuchen, um die Verbindungen zwischen den einzelnen Bestandteilen des Kernels zu entdecken.

Dieses massive Durchsuchen mit grep ist eine der Aufgaben, die die Autoren dieses Buches regelmäßig ausführen. Es handelt sich um eine effiziente Möglichkeit, Informationen aus dem Quellcode zu bekommen. Heutzutage können Sie auch Internet-Ressourcen ausnutzen, um den Kernel-Quellbaum zu verstehen; einige davon stehen in >. Aber trotz dieser Internet-Ressourcen ist der sinnvolle Einsatz von grep[1], less und möglicherweise ctags oder etags immer noch das beste Verfahren, um Informationen aus den Kernel-Quellen zu bekommen.

Unserer Meinung nach kann es hilfreich sein, sich ein wenig Wissen zu verschaffen, bevor man sich vor seine bevorzugte Shell-Eingabeaufforderung setzt. Daher enthält dieses Kapitel einen kurzen Überblick über die Dateien in den Linux-Kernel-Quellen der Version 2.4.2. Wenn Sie mit einer anderen Version arbeiten, dann stimmen möglicherweise nicht alle Beschreibungen Wort für Wort. Ganze Abschnitte können fehlen (wie das Verzeichnis drivers/media, das durch das Verschieben diverser bereits existierender Treiber in 2.4.0-test6 neu entstand). Wir hoffen, daß die Informationen in diesem Kapitel hilfreich, wenn auch nicht immer maßgeblich für das Durchsuchen anderer Kernel-Versionen sind.

Alle Pfadnamen sind relativ zur Wurzel des Quellbaums (normalerweise /usr/src/linux) zu lesen, während Dateinamen ohne Verzeichnisanteil im “aktuellen” Verzeichnis zu suchen sind, also in dem, das gerade besprochen wird. Header-Dateien, die in < und > eingeschlossen sind, sind relativ zum include-Verzeichnis des Quellbaums zu suchen. Wir werden das Verzeichnis Documentation hier nicht zerlegen, weil seine Rolle selbstverständlich sein sollte.

Booten des Kernels

Normalerweise schaut man sich ein Programm als erstes dort an, wo die Ausführung beginnt. Bei Linux ist das aber schwer zu sagen — es kommt darauf an, wie Sie “beginnen” definieren.

Der architekturunabhängige Einsprungpunkt ist start_kernel in init/main.c. Diese Funktion wird vom architekturspezifischen Code aufgerufen und kehrt nie zu diesem zurück. Sie bringt das Rad zum Rollen und kann daher als “Mutter aller Funktionen” betrachtet werden, gewissermaßen als der erste Atemzug im Leben des Computers. Vor start_kernel war das Chaos.

Wenn start_kernel aufgerufen wird, ist der Prozessor bereits initialisiert, befindet sich im geschützten Modus[2]und arbeitet auf der Ebene mit den größten Zugriffsrechten (manchmal supervisor mode genannt). Interrupts sind abgeschaltet. Die Funktion start_kernel ist dafür zuständig, alle Kernel-Datenstrukturen zu initialisieren. Dies geschieht durch Aufrufen externer Funktionen für Unteraufgaben, weil jede Initialisierungsfunktion im jeweiligen Subsystem des Kernels definiert ist.

Die erste von start_kernel aufgerufene Funktion nach dem Holen der Kernel-Sperre und dem Ausgeben der Start-Meldung ist setup_arch. Mit ihr kann plattformabhängiger C-Code ausgeführt werden. setup_arch bekommt einen Zeiger auf den lokalen command_line-Zeiger in start_kernel, kann diesen also auf die echte (plattformabhängige) Position der Kommandozeile zeigen lassen. Als nächsten Schritt übergibt start_kernel die gesamte Kommandozeile an parse_options (ebenfalls in init/main.c definiert), damit die Boot-Optionen berücksichtigt werden können.

Das Parsen der Kommandozeile erfolgt durch das Aufrufen von Handler-Funktionen, die zu jedem Kernel-Argument gehören (beispielsweise gehört video_setup zu video=). Alle diese Funktionen setzen im wesentlichen Variablen, die später, bei der Initialisierung der jeweiligen Funktionalität, verwendet werden. Die interne Organisation des Parsens der Kommandozeile ähnelt dem Mechanismus zum Aufruf der Initialisierungsfunktionen, den wir später besprechen.

Nach dem Parsen aktiviert start_kernel die diversen grundlegenden Funktionalitäten des Systems. Dazu gehören das Einrichten der Interrupt-Tabellen, das Aktivieren des Timer-Interrupts und das Initialisieren der Konsolen- und Speicherverwaltung. All dies geschieht durch Funktionen, die andernorts im plattformabhängigen Code deklariert sind. start_kernel fährt dann damit fort, weniger grundlegende Kernel-Subsysteme zu initialisieren, darunter die Puffer-Verwaltung, das Signal-System und die Verwaltung von Dateien und Inodes.

Schließlich startet start_kernel den init-Kernel-Thread (der die Prozeßnummer 1 bekommt) und führt die idle-Funktion aus (auch diese ist wiederum im architekturspezifischen Code definiert).

Die initiale Boot-Sequenz kann also folgendermaßen zusammengefaßt werden:

  1. Die Firmware des Systems oder ein Boot-Lader sorgen dafür, daß der Kernel an eine passende Stelle im Speicher geladen wird. Dieser Code befindet sich normalerweise nicht im Linux-Quellcode.

  2. Architekturspezifischer Assembler-Code erledigt Aufgaben auf allerniedrigster Ebene, wie etwa das Initialisieren des Speichers und das Einrichten der CPU-Register, so daß C-Code problemlos ausgeführt werden kann. Dazu gehören auch die Auswahl eines Stack-Bereichs und das korrekte Setzen des Stack-Zeigers. Wieviel Code dafür notwendig ist, unterscheidet sich von Plattform zu Plattform: Das kann von einigen Dutzend Zeilen bis zu mehreren tausend Zeilen gehen.

  3. start_kernel wird aufgerufen. Diese Funktion holt sich die Kernel-Sperre, gibt die Start-Meldung aus und ruft setup_arch auf.

  4. Architekturspezifischer C-Code vervollständigt die Initialisierung auf niedriger Ebene und holt eine Kommandozeile, die start_kernel verwenden kann.

  5. start_kernel parst die Kommandozeile und ruft die Handler auf, die zum jeweiligen Schlüsselwort gehören.

  6. start_kernel initialisiert die grundlegenden Funktionalitäten und startet den init-Thread.

Es ist die Aufgabe von init, alle weiteren Initialisierungen vorzunehmen. Dieser Thread steht ebenfalls in der Datei init/main.c. Der Großteil der Aufrufe von Initialisierungsfunktionen wird von do_basic_setup aus ausgeführt. Die Funktion initialisiert alle Bus-Subsysteme, die sie findet (PCI, SBus usw.). Dann ruft sie do_initcalls auf; hier erfolgt auch die Initialisierung von Gerätetreibern.

Die Idee der init-Aufrufe kam in Version 2.3.13 hinzu und steht in älteren Kerneln nicht zur Verfügung. Sie ist dafür gedacht, komplizierte #ifdef-Anweisungen im gesamten Initialisierungscode zu vermeiden. Alle optionalen Kernel-Funktionen (Gerätetreiber und andere) dürfen nur dann initialisiert werden, wenn sie im System konfiguriert sind, weswegen die Initialisierungsfunktionen von #ifdef CONFIG_FEATURE und #endif umgeben waren. Mit den init-Aufrufen deklariert jedes optionale Feature eine eigene Initialisierungsfunktion. Während der Kompilation wird dann eine Referenz auf die Funktion in einen speziellen ELF-Abschnitt gestellt. Beim Booten durchsucht do_initcalls dann den ELF-Abschnitt, um alle relevanten Initialisierungsfunktionen aufzurufen.

Die gleiche Technik wird auch für Kommandozeilenargumente verwendet. Jeder Treiber, der beim Booten Kommandozeilenargumente entgegennehmen kann, definiert eine Datenstruktur, die das Argument mit einer Funktion verknüpft. Ein Zeiger auf die Datenstruktur wird in einen separaten ELF-Bereich geschrieben, so daß parse_option diesen Abschnitt nach Kommandozeilenoptionen durchsuchen und die zugehörigen Treiberfunktionen aufrufen kann. Die verbleibenden Argumente landen entweder in der Umgebung oder auf der Kommandozeile des init-Prozesses. All diese Zauberei mit init-Aufrufen und ELF-Abschnitten finden Sie in <linux/init.h>.

Unglücklicherweise funktioniert diese Idee mit den init-Aufrufen nur dann, wenn unter den diversen Initialisierungsfunktionen keine Reihenfolge eingehalten werden muß, so daß sich doch noch einige wenige #ifdef-Konstrukte in init/main.c befinden.

Es interessant, wie sehr die Idee der init-Aufrufe und deren Anwendung auf die Kommandozeilenargumente die Anzahl der bedingten Kompilierungsanweisungen im Code reduziert hat:


morgana% grep -c ifdef linux-2.[024]/init/main.c
linux-2.0/init/main.c:120
linux-2.2/init/main.c:246
linux-2.4/init/main.c:35

Trotz der Vielzahl neuer Features ist die Anzahl bedingter Kompilierungsanweisungen in 2.4 dank der init-Aufrufe signifikant gefallen. Ein weiterer Vorteil besteht darin, daß Gerätetreiber-Autoren nicht mehr für jedes neue Kommandozeilenargument main.c ändern müssen. Durch diese Technik ist das Hinzufügen neuer Features zum Kernel deutlich vereinfacht worden, und der Boot-Code enthält keine kniffligen Cross-Referenzen mehr. Als Nebeneffekt kann 2.4 aber nicht mehr in ältere Dateiformate kompiliert werden, die weniger flexibel als ELF sind. Aus diesem Grund wechselten die uClinuxEntwickler[3] von COFF auf ELF, als sie ihr System von 2.0 auf 2.4 portierten.

Ein weiterer Nebeneffekt der massiven Verwendung von ELF-Abschnitten ist, daß der letzte Durchlauf beim Kompilieren des Kernels kein normaler Linker-Vorgang mehr ist. Alle Plattformen definieren mit Hilfe einer ldscript-Datei nun genau, wie das Kernel-Image (die vmlinux-Datei) gelinkt werden muß. Im Quellbaum der einzelnen Plattformen heißt diese Datei vmlinux.lds. Die Verwendung von ld-Skripten wird in der Standarddokumentation des binutils-Pakets beschrieben.

> > > Es hat noch einen weiteren Vorteil, den Initialisierungscode in einen speziellen Abschnitt zu stellen: Sobald die Initialisierung beendet ist, wird dieser Code nicht mehr benötigt. Weil er isoliert war, kann der Kernel ihn verwerfen und den davon belegten Speicher zurückgewinnen.

Fußnoten

[1]

Normalerweise braucht man find und xargs, um eine Kommandozeile für grep zusammenzubauen. Der durchdachte Einsatz von Unix-Werkzeugen ist nicht trivial, würde aber den Rahmen dieses Buches sprengen.

[2]

Dieses Konzept gibt es nur auf der x86-Architektur. Bessere Architekturen landen nicht erst in einem eingeschränkten abwärtskompatiblen Modus, wenn sie eingeschaltet werden.

[3]

uClinux ist eine Version des Linux-Kernels, die auf Prozessoren ohne MMU funktioniert. Dies ist typisch für eingebettete Systeme; mehrere M68k- und ARM-Prozessoren haben keine Speicherverwaltung in der Hardware. uCLinux steht für Microcontroller-Linux, weil es eher für Microcontroller als für vollständige Computer gedacht ist.