- Objektorientierte Designmuster ermöglichen auch in in C geschriebenen Kerneln durch Polymorphismus und Modularität ein flexibles Systemdesign
- Mit vtables (virtuellen Funktionstabellen) lassen sich Schnittstellen von Geräten und Diensten standardisieren; dynamische Änderungen zur Laufzeit unterstützen unterschiedliche Verhaltensweisen
- Kernel-Dienste und Scheduler bieten über vtables konsistente Schnittstellen für Start, Stopp und Neustart und kapseln dabei Implementierungsdetails
- In Verbindung mit Kernel-Modulen wird dynamisches Laden von Treibern unterstützt, wodurch sich das System ohne Neukompilierung erweitern lässt
- Dieser Ansatz bietet Flexibilität und experimentelle Freiheit, hat aber den Nachteil einer umständlicheren Syntax und der expliziten Übergabe von Objekten
Freiheit und objektorientierte Muster in der OS-Entwicklung
- Die Entwicklung eines eigenen OS erlaubt freies Experimentieren ohne die Zwänge von Zusammenarbeit oder realen Anwendungsfällen
- Frei von Sicherheitslücken, Wartungsaufwand und Release-Druck
- Genau das macht OS-Entwicklung attraktiv: unkonventionelle Programmiermuster lassen sich erkunden
- Der LWN-Artikel „Object-oriented design patterns in the kernel“ zeigt Beispiele dafür, wie der Linux-Kernel objektorientierte Prinzipien in C umsetzt
- Polymorphismus wird mit Strukturen umgesetzt, die Funktionszeiger enthalten
- Durch Kapselung, Modularität und Erweiterbarkeit lassen sich die Vorteile objektorientierter Ansätze auch in Low-Level-Kerneln nutzen
Grundkonzept der vtable
- Eine vtable ist eine Struktur mit Funktionszeigern, die die Schnittstelle eines Objekts definiert
- Beispiel: eine Struktur für Geräteoperationen
struct device_ops { void (*start)(void); void (*stop)(void); }; struct device { const char *name; const struct device_ops *ops; };
- Beispiel: eine Struktur für Geräteoperationen
- Unterschiedliche Geräte, etwa
netdevunddisk, verwenden dieselbe API, haben aber verschiedene Implementierungennetdev.ops->start()ruft das Verhalten des Netzwerkgeräts auf,disk.ops->start()das des Festplattengeräts
- Änderungen zur Laufzeit: Durch den dynamischen Austausch der vtable kann Verhalten geändert werden, ohne den aufrufenden Code anzupassen
- Mit geeigneter Synchronisierung ermöglicht das eine saubere dynamische Weiterentwicklung des Verhaltens
Anwendungsbeispiele im OS
Dienstverwaltung
- Kernel-Dienste wie Netzwerkmanager, Worker-Pools oder Fenster-Server lassen sich über eine konsistente Schnittstelle verwalten
- Dienststruktur:
struct service_ops { void (*start)(void); void (*stop)(void); void (*restart)(void); }; struct service { pid_t pid; const struct service_ops *ops; };
- Dienststruktur:
- Jeder Dienst implementiert sein eigenes Verhalten, lässt sich aber im Terminal auf standardisierte Weise starten/stoppen/neustarten
- Die Kopplung zwischen Code und Diensten sinkt, die Verwaltung wird vereinfacht
Scheduler
- Ein Scheduler kann unterschiedliche Strategien unterstützen, etwa Round Robin, Shortest Job First, FIFO oder Prioritätsplanung
- Die Schnittstelle wird auf
yield,block,addundnextvereinfacht - Über eine vtable definiert, kann die Scheduling-Policy zur Laufzeit ausgetauscht werden
- Die gesamte Policy lässt sich ändern, ohne den restlichen Kernel anzupassen
- Die Schnittstelle wird auf
Dateiabstraktion
- Die file_operations-Struktur von Linux setzt die Philosophie „Alles ist eine Datei“ um
- Beispiel: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
struct file_operations { struct module *owner; loff_t (*llseek)(struct file *, loff_t, int); ssize_t (*read)(struct file *, char __user *, size_t, loff_t *); ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *); ... };
- Beispiel: https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
- Sockets, Geräte und Textdateien bieten alle dieselbe read/write-Schnittstelle
- Code im Userspace kann konsistent arbeiten, ohne Implementierungsdetails kennen zu müssen
Verbindung mit Kernel-Modulen
- Kernel-Module unterstützen durch den Austausch der vtable das dynamische Laden von Treibern oder Hooks
- Wie bei Linux-Modulen lässt sich der Kernel ohne Neukompilierung oder Neustart erweitern
- Beim Hinzufügen neuer Funktionen muss nur die vtable der bestehenden Struktur aktualisiert werden
Nachteile
- Komplexe Syntax:
- Objekte müssen explizit übergeben werden, etwa wie in
object->ops->start(object) - Im Vergleich zur impliziten Übergabe in C++ ist das umständlicher
- Auch Funktionssignaturen werden ausführlicher:
static void object_start(struct object* this) { this->id = ... }
- Objekte müssen explizit übergeben werden, etwa wie in
- Vorteil: Durch die explizite Übergabe werden Abhängigkeiten von Funktionen klarer und die Kopplung zwischen Objekt und Verhalten transparenter
- Im Kernel-Code ist das ein sinnvoller tradeoff zwischen Komplexität und Klarheit
Erkenntnisse
- vtables bieten eine einfache Möglichkeit, Komplexität zu verringern und zugleich Flexibilität zu erhalten
- Verhalten zur Laufzeit austauschbar, konsistente Schnittstellen, einfaches Hinzufügen neuer Funktionen
- Sie eröffnen neue Wege, objektorientiertes Design in C umzusetzen, und unterstreichen den experimentellen Reiz der OS-Entwicklung
- Weiteres Material: Das xine-Projekt (https://xine.sourceforge.net/hackersguide#id324430) zeigt, wie sich mit vtables private Variablen verwalten lassen
- OS-Entwicklung ist ein Feld für kreative Experimente, und objektorientierte Muster erweisen sich auch in Low-Level-Systemen als mächtiges Werkzeug
1 Kommentare
Hacker-News-Kommentare
NULListvoid-Zeiger benutzt. Außerdem war der im Beitrag der Kernel-Entwickler genannte Hauptvorteil, dass man Speicher spart, weil nicht jede Strukturinstanz mehrere Funktionszeiger enthalten muss, sondern nur einen einzigen vtable-Zeiger. Speicherersparnis ist also der zentrale Punkt, während der OP diese vtable als Indirektionsschicht für Methodenaustausch zur Laufzeit und für Polymorphie verwendet. Dieses Muster unterscheidet sich von dem, was die Kernel-Entwickler beschrieben habenvoid-Zeiger, sondernvoidals Funktion ohne Argumente und ohne Rückgabewert. Eine vtable wird verwendet, um Polymorphie umzusetzen. Ohne Polymorphie würde man gar keine vtable einsetzen und damit noch mehr Speicher sparenthisgerade nicht. Man übergibt diethis-Instanz ohnehin ständig weiter, und ein explizitesthismacht klar, ob eine Variable zur Instanz gehört oder global beziehungsweise aus einer anderen Quelle stammtthisnicht verpflichtend istobject->ops->start(object)das Objekt zweimal explizit genannt werden muss: einmal für das Auflösen der vtable und einmal, um das Objekt an die C-Funktionsimplementierung zu übergebenmFoo,m_Foo,foo_usw. verwendet.foo_wird bevorzugt, weil es kürzer ist alsthis->foo. Natürlich kann man in C++ auchthisexplizit schreibenthismacht den Code knapper, und mit echten Methoden muss man denstruct-Präfix nicht in jeder Funktion wiederholen. Stattmystruct_dosmth(s);wirkt etwas->dosmth();natürlicherstructüber ihr erstes Mitglied castetthis-Zeiger annehmen. Das Beispielstruct file_operationsenthält Funktionszeiger, die keinenthis-Zeiger annehmen, und ist daher schwerlich eine echte vtablething->vtable->foo(thing, ...)einfachfoo(thing, ...)verwenden kann