- Partage d’une expérience d’implémentation d’un noyau prototype de système d’exploitation à temps partagé sur l’architecture RISC-V
- Explication, centrée sur la pratique, du concept et du fonctionnement d’un noyau time-sharing, implémenté en Zig plutôt qu’en C afin d’améliorer la reproductibilité
- Adoption d’une approche unikernel qui regroupe le noyau et le code utilisateur dans un seul binaire, avec une architecture en couches s’appuyant sur OpenSBI pour la sortie console et le contrôle du timer
- Les threads s’exécutent en mode utilisateur (U-mode), tandis que le noyau effectue les changements de contexte en mode superviseur (S-mode) via des interruptions de timer, et franchit la frontière au moyen des appels système
- Le point clé est une technique qui remplace la stack frame empilée par le prologue/épilogue d’interruption afin de restaurer l’ensemble des registres et les CSR d’un autre thread, et ainsi basculer le flot d’exécution
- En s’appuyant sur la machine virtuelle QEMU et une version récente de OpenSBI, l’article propose un environnement d’apprentissage reproductible par tous et relie conceptuellement le spectre de la virtualisation — threads, processus, conteneurs —, ce qui en fait une base utile à des fins pédagogiques et pratiques
Vue d’ensemble
- Présentation du processus d’implémentation directe d’un noyau de système d’exploitation à temps partagé sur l’architecture RISC-V
- Le lectorat visé est composé de débutants en logiciels système et en architecture des ordinateurs, d’étudiants, ainsi que d’ingénieurs intéressés par la compréhension des mécanismes bas niveau
- Cette expérimentation utilise le langage Zig à la place du C afin d’améliorer la reproductibilité des manipulations, avec une installation simple
- Le code final est publié dans le dépôt popovicu/zig-time-sharing-kernel, avec un léger décalage possible par rapport au texte de l’article
- Il est recommandé de considérer la version du dépôt comme source de vérité unique plutôt que les extraits de code de l’article
- Pour les manipulations, il est plus pratique d’aligner l’environnement sur les scripts de l’éditeur de liens et les options de build du dépôt
Recommended reading
- L’article suppose des bases en architecture des ordinateurs comme les registres, l’adressage mémoire ou les interruptions
- Comme ressources préalables, il recommande Bare metal on RISC-V, le processus de boot SBI et des exemples d’interruptions de timer
- L’article sur une micro-distribution Linux peut aussi être utile, de façon optionnelle, pour comprendre la philosophie de séparation entre noyau et espace utilisateur
Unikernel
- Adoption d’une configuration unikernel qui lie l’application et le noyau de l’OS dans un unique exécutable
- Cela permet d’éviter la complexité du chargeur et de l’éditeur de liens à l’exécution, tout en simplifiant le chargement du code utilisateur en mémoire aux côtés du noyau
- Pour un objectif pédagogique et de reproductibilité, cela offre l’avantage d’une distribution simplifiée et d’une cohérence d’environnement
Couche SBI
- RISC-V utilise un modèle de privilèges en modes M/S/U, et cette expérimentation place OpenSBI en mode M tandis que le noyau fonctionne en mode S
- La sortie console et le contrôle du périphérique timer sont délégués au SBI afin d’assurer la portabilité
- En l’absence de SBI, un UART MMIO est utilisé en repli, mais l’usage d’une version récente d’OpenSBI est recommandé pour les manipulations
Goal for the kernel
- Pour simplifier, seul le support de threads statiques est prévu, et les threads sont constitués de fonctions qui ne se terminent pas
- Les threads s’exécutent en mode U et envoient des appels système au noyau en mode S
- Une planification en temps partagé sur cœur unique est implémentée afin de permettre un basculement vers un autre thread à chaque tick du timer
Virtualization and what exactly is a thread
- Le threading en temps partagé est une forme de virtualisation qui permet d’exécuter plusieurs tâches en parallèle sur un seul cœur sans modifier le modèle de programmation
- Contrairement à l’ordonnancement coopératif, le basculement se produit via une interruption de timer sans yield explicite
- Chaque thread possède son propre ensemble de registres intouchable et sa propre pile, tandis que le reste de la mémoire peut être partagé
The stack and memory virtualization
- Un thread doit disposer de sa propre pile, indispensable au maintien du contexte d’exécution selon la convention d’appel, notamment pour les variables locales et la préservation de
ra- Le spectre de la virtualisation s’étend de thread < processus < conteneur < VM, avec des niveaux d’isolation et de vue différents
- Sous Linux, les conteneurs sont mis en œuvre comme une combinaison de mécanismes du noyau tels que chroot et cgroups
Virtualizing a thread
- L’objectif minimal de virtualisation dans cette expérimentation est de conserver un modèle de programmation inchangé, de protéger les registres et certains CSR, et d’allouer une pile distincte à chaque thread
- L’article souligne que sans protection de la vue sur les registres, aucun calcul significatif n’est possible
- Des valeurs initiales comme a0 sont préchargées dans la pile afin de transmettre simplement les arguments au démarrage du thread
Interrupt context
- Une interruption peut être comprise comme un modèle proche d’un appel de fonction, avec un prologue/épilogue qui sauvegarde et restaure les registres sur la pile
- Le respect de la convention de sauvegarde est indispensable pour que des interruptions de timer asynchrones ne corrompent pas les registres
- L’assembleur d’exemple sauvegarde et restaure non seulement x0–x31, mais aussi des CSR comme sstatus, sepc, scause, stval
Implémentation (vue d’ensemble)
Leveraging the interrupt stack convention
- Le corps de la routine d’interruption se situe entre le prologue et l’épilogue, et si l’on remplace
sppar une autre zone mémoire, on restaure alors l’ensemble des registres d’un autre contexte- Cela correspond précisément à un changement de contexte, idée centrale de cette implémentation du temps partagé
- L’interruption de timer intervient périodiquement pour faire alterner le flot principal et le flot d’interruption
Kernel/user space separation
- La frontière entre noyau en mode S et utilisateur en mode U est maintenue, et le traitement des interruptions comme des appels système est assuré par le trap handler en mode S
- Le démarrage suit la séquence OpenSBI en mode M → initialisation du noyau en mode S → lancement des threads en mode U
- Des interruptions de timer périodiques rendent possibles l’ordonnancement et le changement de contexte
Implémentation (code)
Assembly startup
- Dans
startup.S, une séquence minimale initialise la BSS et le pointeur de pile initial avant de sauter vers lemainde Zig- Le point d’entrée du noyau utilise la convention
exportafin d’assurer l’interopérabilité avec l’ABI C
- Le point d’entrée du noyau utilise la convention
Main kernel file and I/O drivers
- Dans
kernel.zig, lemaincommence par vérifier les fonctions console de OpenSBI, puis bascule sur UART MMIO en cas d’échecsbi.debug_printeffectue l’appel en configurant les registres a0/a1/a6/a7 conformément au protocole ECALL- Après configuration du timer, le code enregistre le gestionnaire d’interruptions en mode S et active les ticks
S-mode handler and the context switch
- Le gestionnaire est écrit avec la convention
nakedde Zig afin de construire manuellement un prologue/épilogue complet incluant la sauvegarde des CSR- Dans son corps, il appelle
handle_kernel(sp), puis remplacesppar la valeur retournée pour décider s’il faut effectuer un basculement scausepermet de distinguer un ECALL en mode U d’une interruption de timer et d’aiguiller le traitement
- Dans son corps, il appelle
The user space threads
- Le code utilisateur est inclus avec le noyau dans un binaire unique, et les threads d’exemple répètent affichage d’une chaîne → boucle de temporisation
syscall.debug_printplace le numéro d’appel système 64 dansa7, ainsi que le buffer et sa longueur dansa0/a1, puis exécute ECALL- Lors de l’initialisation des threads, la pile est préchargée avec l’adresse de retour et les valeurs initiales des registres, de sorte que les arguments soient utilisables dès le premier retour
Running the kernel
- La compilation se fait avec
zig build, et l’exécution s’effectue dans QEMU en spécifiant la machinevirt+nographic+OpenSBI fw_dynamic- Au démarrage, après la bannière OpenSBI, les sorties périodiques de chaque ID de thread apparaissent en alternance
- Une build avec
-Ddebug-logs=trueaffiche en détail la source des interruptions, la pile courante et les logs d’enfilement/défilement
Conclusion
- Cette expérimentation modernise un noyau pédagogique avec la combinaison RISC-V + OpenSBI + Zig, en améliorant sa reproductibilité et sa lisibilité
- Bien qu’elle conserve des simplifications comme une gestion d’erreurs minimale ou des piles surallouées, elle met l’accent sur l’essence du changement de contexte et la séparation des privilèges
- La portabilité vers une machine réelle reste possible à condition d’ajuster les constantes de l’éditeur de liens et des pilotes et de disposer d’un SBI
Note supplémentaire : récapitulatif du spectre de la virtualisation
- Threads : surtout virtualisation des registres et de la pile, avec forte possibilité de mémoire partagée
- Process : isolation mémoire via la virtualisation de l’espace d’adressage, avec possibilité d’inclure plusieurs threads en interne
- Container : unité d’isolation construite par combinaison d’une vue sur l’environnement comme les espaces de noms du système de fichiers et du réseau
- VM : vise une virtualisation complète de l’ensemble du matériel
Résumé des points clés d’implémentation
- Changement de contexte réalisé par remplacement de la pile d’interruption
- Sauvegarde/restauration de l’état complet, CSR compris, dans le trap handler en mode S
- Double chemin de sortie avec priorité au SBI, repli sur UART MMIO
- Ordonnancement simple centré sur des threads statiques, un cœur unique et des tranches de temps
- Frontière U/S clarifiée par des appels système basés sur ECALL
1 commentaires
Avis Hacker News
On peut découvrir un travail similaire sous forme de « paquet » avec "Operating System in 1000 Lines of Code" ; je l’avais suivi il y a quelque temps en Zig (en convertissant les extraits de code C en Zig au fur et à mesure) et c’était très amusant ; mon code et la VOD sont ici : https://github.com/kristoff-it/kristos/
Il s’agit d’un envoi séparé par l’auteur de l’article https://news.ycombinator.com/item?id=45236479 ; l’auteur a lui-même recréé le Tiny OS Kernel, voulait plus précisément expérimenter sur RISC-V et dans l’environnement OpenSBI, et a utilisé Zig au lieu du C traditionnel ; je pense qu’on pourrait aussi facilement suivre le même parcours en C ou en Rust ; l’ensemble est un peu brut, mais c’est une expérience et une introduction pour faire ses premiers pas dans le développement de kernel OS et l’architecture des ordinateurs ; ça me semble être un projet amusant à tester le temps d’un week-end ; le walkthrough complet et le lien GitHub sont disponibles ci-dessus
Ce genre de projet est vraiment impressionnant ; Linux, au fond, n’est lui aussi qu’un kernel, mais ce travail a ouvert la voie à l’installation d’un Unix open source sur des milliards d’appareils ; je trouve ça vraiment génial
Ce qui est encore plus amusant, c’est qu’au tout début de la diffusion de Linux, Torvalds avait écrit dans un e-mail : « c’est juste un hobby, ce ne sera pas gros ni professionnel comme GNU » https://groups.google.com/g/comp.os.minix/c/dlNtH7RRrGA/m/SwRavCzVE7gJ
Je ne dirais pas que ce genre de projet est <i>énormément</i> impressionnant ; la manière de créer un kernel multitâche minimal est connue depuis des décennies ; faire un kernel qui boote et exécute quelques tâches simples demande surtout un certain niveau d’intelligence et de persévérance ; sur RISC-V c’est un peu plus compliqué que sur x86, mais les informations d’initialisation matérielle sont faciles à trouver (voir https://wiki.osdev.org/RISC-V_Meaty_Skeleton_with_QEMU_virt_board) ; dans ce cas aussi, l’auteur a lui-même expliqué qu’il s’agissait de « refaire l’exercice réalisé en cours de systèmes d’exploitation » ; à mon avis, toute personne titulaire d’un diplôme en génie logiciel pourrait y arriver ; il y aura bien sûr des bugs ou des parties incomplètes, mais le multiprocessus ou l’isolation des processus via MMU n’est plus quelque chose de difficile aujourd’hui
Autant que Linux, le fait que Stallman ait commencé en 1984 à écrire un compilateur C et des utilitaires Unix a aussi ouvert la voie à l’installation d’un Unix open source sur des milliards de machines
Zig est vraiment excellent pour le développement d’OS, et RISC-V aussi ; j’avais commencé le même exercice sur x86 mais je me suis vite épuisé à cause de toute la boilerplate legacy ; côté RISC-V, il n’y a presque rien de tout ça, donc c’est bien plus simple pour démarrer https://github.com/Fingel/aeros-v
Je pense que si on commence sur x86, il n’y a pas tant de boilerplate que ça à condition d’avoir un bon bootloader ; un chargeur multiboot laisse généralement le mode réel de côté, et comme la plupart des gens veulent le mode protégé, il suffit de configurer quelques tables puis de faire un saut ; si on veut désactiver l’ancien contrôleur d’interruptions, il y a un peu plus à faire, mais on a l’avantage de pouvoir booter sur un PC de bureau (avec quelques précautions pour l’interface console) ; mon OS hobby utilisait un boot BIOS et quelques fonctions VGA, et j’ai souffert de problèmes de compatibilité ; une console série est bien plus simple, mais les machines récentes n’ont souvent plus de port série
En pratique, c’est une remise au goût du jour de la sûreté d’Object Pascal ou de Modula-2, reconditionnée avec une syntaxe C ; C n’avait rien de particulièrement spécial en dehors du fait qu’il s’est largement diffusé grâce à la licence d’UNIX
J’aimerais bien essayer moi aussi, mais je me demande dans quel environnement vous exécutez le kernel RISC-V ; est-ce uniquement avec Qemu, ou bien avez-vous du matériel réel à recommander ?
Je trouve l’ISA RISC-V vraiment très accessible ; la documentation est excellente, il y a énormément d’exemples et beaucoup d’émulateurs ; même le machine code non compressé est facile à lire ; je suis en train d’écrire moi-même un livre pour ma fille tout en construisant un petit OS avec Forth et du time-sharing https://punkx.org/projekt0/book/part1/os.html ; je pense que je n’aurais même pas essayé si c’était du x86 ; j’apprends Forth et l’assembleur RISC-V en même temps au fil du processus, et c’est vraiment un plaisir ; si vous voulez créer un OS jouet à partir de zéro, c’est le moment idéal (aussi enthousiasmant que dans les années 1980) ; prenez une carte RISC-V bon marché (rp2350, etc.) et importez les sections pertinentes du manuel dans une IA comme Claude : ça aide énormément quand on bloque
Ce genre d’essai est toujours amusant et intéressant ; j’ai aussi envie d’encourager les gens à tenter leur propre chiffrement ou d’autres choses difficiles ; le conseil « n’implémentez pas votre propre crypto » veut dire qu’il ne faut pas utiliser en production quelque chose qui n’a pas été validé en conditions réelles ; pour l’expérimentation ou la recherche, il n’y a pas de danger, donc allez-y sans retenue ; nous avons besoin de plus de systèmes d’exploitation et de plus de choix
(citation d’une décision de justice espagnole) Bon, bloquer http, passe encore, mais là c’est abusé...
J’ai entendu dire qu’en Espagne, Cloudflare (et probablement d’autres aussi) se fait bloquer par erreur à cause de problèmes liés à la diffusion de matchs de football ; je me demande comment les gens contournent ça, peut-être avec un VPN ? Si des IP importantes sont bloquées, ça doit aussi avoir un impact sur le travail
En voyant dans la décision qu’il est question de la Ligue espagnole de football professionnel et de Telefónica Audiovisual Digital, je me dis que ces gens sont des criminels
...quoi ? Une organisation du football espagnol aurait le pouvoir de restreindre l’accès à Internet pour tout un pays ?
Je me demande comment se procurer du matériel RISC bon marché
AliExpress vend une carte Milk-V Duo S à 10 dollars, elle apparaît souvent dans mes recommandations récentes ; principales caractéristiques : SG2000 Master amélioré, 512 Mo de RAM, E/S plus étendues, Wi-Fi 6/BT5 sur certains modèles (sauf 512M-Basic/eMMC), port hôte USB 2.0, Ethernet 100 Mbps avec prise en charge PoE, double MIPI CSI, interrupteur de bascule de boot entre RISC-V et ARM, etc. https://aliexpress.com/w/wholesale-Milk%2525252dV-Duo-S.html
J’ai déjà plusieurs cartes, mais celle que j’ai trouvée intéressante au point de la financer est la VisionFive 2 Lite https://www.kickstarter.com/projects/starfive/visionfive-2-lite-unlock-risc-v-sbc-at-199/description ; je n’ai pas la VisionFive2 de première génération, mais elle a bonne réputation et l’écosystème semble se développer ; il reste encore des choses inachevées, mais j’espère qu’elle sera bientôt expédiée ; celle que j’utilise personnellement, c’est la PolarFire SoC Discovery Kit, une carte avec un RISC-V quadricœur et un FPGA ; c’est un peu cher (130 dollars) et ce n’est pas pour tout le monde, mais le plus amusant, c’est que la carte coûte moins cher que la puce elle-même https://www.microchip.com/en-us/development-tool/MPFS-DISCO-KIT ; la documentation et la toolchain de Microchip sont vieillottes et pas extraordinaires, mais une fois qu’on s’y habitue, exécuter du code bare-metal RISC-V devient vraiment facile ; les exemples Linux/bare-metal sont bien faits
Je proposerais déjà de tester sur émulateur sur une machine x86 ou Apple, sans matériel réel ; le développement y est plus rapide que sur une vraie carte, et avec quelque chose comme QEMU on peut s’y mettre tout de suite https://www.qemu.org/docs/master/system/target-riscv.html
Le Raspberry Pi Pico 2 prend aussi en charge RISC-V, donc c’est une bonne option https://www.raspberrypi.com/products/raspberry-pi-pico-2/
Merci à tous pour vos réponses ; en revanche, je ne vois aucune raison de remercier ceux qui ont downvoté la question