- Les patterns de conception orientés objet permettent d’implémenter le polymorphisme et la modularité même dans un kernel écrit en C, rendant possible une conception système plus souple
- L’utilisation d’une vtable (table de fonctions virtuelles) standardise l’interface des périphériques et des services, et permet de prendre en charge des comportements variés via des changements dynamiques à l’exécution
- Les services du kernel et le scheduler fournissent, via des vtables, une interface cohérente pour des actions comme démarrer, arrêter ou redémarrer, tout en encapsulant les détails d’implémentation
- Couplée aux modules kernel, cette approche permet le chargement dynamique de pilotes et l’extension du système sans recompilation
- Cette approche offre de la souplesse et une liberté d’expérimentation, mais a pour inconvénient une certaine verbosité liée à une syntaxe complexe et au passage explicite des objets
Liberté et patterns orientés objet dans le développement d’OS
- Développer son propre OS permet d’expérimenter librement, sans les contraintes de la collaboration ou des applications réelles
- On est libéré des vulnérabilités de sécurité, de la maintenance du code et de la pression des releases
- C’est l’un des attraits du développement d’OS, qui permet d’explorer des patterns de programmation non standard
- L’article LWN “Object-oriented design patterns in the kernel” présente des cas où le kernel Linux met en œuvre des principes orientés objet en C
- Le polymorphisme y est implémenté avec des structures contenant des pointeurs de fonction
- Encapsulation, modularité et extensibilité permettent ainsi de profiter des avantages de l’orienté objet même dans un kernel bas niveau
Concept de base de la vtable
- Une vtable est une structure contenant des pointeurs de fonction qui définit l’interface d’un objet
- Exemple : une structure pour les opérations d’un périphérique
struct device_ops { void (*start)(void); void (*stop)(void); }; struct device { const char *name; const struct device_ops *ops; };
- Exemple : une structure pour les opérations d’un périphérique
- Des périphériques différents (par ex.
netdev,disk) utilisent la même API, mais avec des implémentations distinctesnetdev.ops->start()invoque le comportement d’un périphérique réseau, tandis quedisk.ops->start()appelle celui d’un périphérique disque
- Changement à l’exécution : remplacer dynamiquement une vtable permet de modifier le comportement sans changer le code appelant
- Avec une synchronisation appropriée, cela permet une évolution dynamique propre du comportement
Exemples d’application dans un OS
Gestion des services
- Les services du kernel (gestionnaire réseau, pool de workers, serveur de fenêtres, etc.) sont gérés via une interface cohérente
- Structure de service :
struct service_ops { void (*start)(void); void (*stop)(void); void (*restart)(void); }; struct service { pid_t pid; const struct service_ops *ops; };
- Structure de service :
- Chaque service implémente son propre comportement, mais les opérations démarrer/arrêter/redémarrer s’exécutent de manière standardisée depuis le terminal
- Cela réduit le couplage entre le code et les services, et simplifie l’administration
Scheduler
- Le scheduler peut prendre en charge différentes stratégies comme le round-robin, le plus court d’abord, FIFO ou l’ordonnancement par priorité
- L’interface est simplifiée autour de
yield,block,add,next - Définie via une vtable, elle permet de changer de politique d’ordonnancement à l’exécution
- On peut ainsi modifier l’ensemble de la politique sans toucher au reste du kernel
- L’interface est simplifiée autour de
Abstraction des fichiers
- La structure file_operations de Linux met en œuvre la philosophie « tout est fichier »
- Exemple : 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 *); ... };
- Exemple : https://elixir.bootlin.com/linux/v6.15/source/include/linux/fs.h
- Sockets, périphériques et fichiers texte exposent tous la même interface read/write
- Le code en espace utilisateur peut donc fonctionner de manière cohérente sans connaître les détails d’implémentation
Couplage avec les modules kernel
- Les modules kernel permettent de charger dynamiquement des pilotes ou des hooks en remplaçant une vtable
- Comme avec les modules Linux, on peut étendre le kernel sans recompilation ni redémarrage
- Lorsqu’une nouvelle fonctionnalité est ajoutée, il suffit de mettre à jour la vtable de la structure existante
Inconvénients
- Complexité syntaxique :
- Il faut passer explicitement l’objet, comme dans
object->ops->start(object) - C’est plus verbeux que le passage implicite en C++
- Les signatures de fonction sont elles aussi longues :
static void object_start(struct object* this) { this->id = ... }
- Il faut passer explicitement l’objet, comme dans
- Avantage : ce passage explicite rend plus claires les dépendances des fonctions et le couplage entre objet et comportement
- Dans le code kernel, c’est un tradeoff pertinent entre complexité et clarté
Ce qu’il faut en retenir
- Les vtables offrent un moyen simple de réduire la complexité tout en préservant la souplesse
- Remplacement du comportement à l’exécution, maintien d’interfaces cohérentes, ajout facile de nouvelles fonctionnalités
- Elles proposent une nouvelle manière d’implémenter une conception orientée objet en C, tout en soulignant le plaisir expérimental du développement d’OS
- Ressource complémentaire : le projet xine (https://xine.sourceforge.net/hackersguide#id324430) montre comment gérer des variables privées avec des vtables
- Le développement d’OS est un terrain d’expérimentation créative, et les patterns orientés objet y prouvent qu’ils restent des outils puissants même dans les systèmes bas niveau
1 commentaires
Avis sur Hacker News
void. De plus, l’avantage principal mentionné par les développeurs du noyau est d’économiser de la mémoire en plaçant un seul pointeur de vtable dans chaque instance de structure, plutôt que plusieurs pointeurs de fonction. Autrement dit, l’économie mémoire est le point clé, alors que l’OP utilise cette vtable comme niveau d’indirection pour remplacer des méthodes à l’exécution et implémenter le polymorphisme. Ce pattern est donc différent de ce dont parlent les développeurs du noyauvoid, mais devoidau sens de fonction sans paramètre et sans valeur de retour. Une vtable sert justement à implémenter le polymorphisme. Sans polymorphisme, on n’utiliserait pas de vtable du tout, ce qui économiserait encore plus de mémoirethisimplicite. On passe bien en pratique cette instancethisen permanence, et le fait qu’elle soit explicite évite de confondre l’origine d’une variable : instance, globale ou autrethislors de la référence à un membre d’instance est considéré comme l’une des grandes erreurs de la syntaxe OOPobject->ops->start(object): une fois pour résoudre la vtable, une fois pour passer l’objet à l’implémentation C de la fonctionmFoo,m_Foo,foo_, etc.foo_est préféré àthis->foocar plus concis. Bien sûr, en C++ on peut aussi écrirethisexplicitementthisimplicite rend l’écriture plus concise et, avec de vraies méthodes, évite de répéter le préfixe de structure dans chaque fonction. Par exemple,mystruct_dosmth(s);devient plus naturellements->dosmth();structvia son premier membre, l’héritage de champs devient plus naturel qu’on ne le pensethis. L’exemple destruct file_operationscontient des pointeurs de fonction qui ne reçoivent pas de pointeurthis, donc il est difficile d’y voir une véritable vtablefoo(thing, ...)au lieu dething->vtable->foo(thing, ...)