Crear un kernel de sistema operativo desde cero
(popovicu.com)- Comparte la experiencia de implementar un kernel prototipo de sistema operativo con time-sharing en la arquitectura RISC-V
- Explica de forma práctica el concepto y el funcionamiento de un kernel de time-sharing, e incrementa la reproducibilidad al implementarlo en Zig en lugar de C
- Adopta un enfoque de unikernel que agrupa el kernel y el código de usuario en un solo binario, y utiliza una capa basada en OpenSBI para la salida de consola y el control del temporizador
- Los hilos se ejecutan en modo usuario (U-mode), mientras que el kernel realiza el context switch en modo supervisor (S-mode) mediante interrupciones de temporizador, cruzando el límite mediante llamadas al sistema
- La idea central es una técnica que cambia el stack frame acumulado por el prólogo/epílogo de la interrupción para restaurar el conjunto de registros y los CSR de otro hilo, cambiando así el flujo de ejecución
- Ofrece un entorno de aprendizaje reproducible para cualquiera, basado en la máquina virtual QEMU y la versión más reciente de OpenSBI, y tiene valor como material base para educación y práctica al conectar conceptualmente el espectro de virtualización de hilos, procesos y contenedores
Descripción general
- Presenta el proceso de implementar directamente un kernel de sistema operativo con time-sharing sobre la arquitectura RISC-V
- Sus principales lectores son principiantes en software de sistemas y arquitectura de computadoras, estudiantes e ingenieros interesados en comprender principios de funcionamiento de bajo nivel
- Este experimento usa el lenguaje Zig en lugar del lenguaje C tradicional para aumentar la reproducibilidad de la práctica, además de que su instalación es sencilla
- El código final está publicado en el repositorio popovicu/zig-time-sharing-kernel, aunque puede haber una ligera diferencia de sincronización con el contenido del artículo
- Se recomienda considerar la versión del repositorio como la única fuente de verdad por encima de los fragmentos de código del texto
- Para la práctica, conviene basar el entorno en el linker script y las opciones de compilación del repositorio
Lectura recomendada
- El artículo asume conocimientos básicos de arquitectura de computadoras, como registros, direccionamiento de memoria e interrupciones
- Como material previo, recomienda Bare metal on RISC-V, proceso de arranque de SBI y ejemplos de interrupciones de temporizador
- El texto sobre microdistribución de Linux también puede ser útil de forma opcional para entender la filosofía de separación entre kernel y espacio de usuario
Unikernel
- Adopta una configuración de unikernel que enlaza la aplicación y el kernel del SO en un único ejecutable
- Evita la complejidad del loader y del linker del runtime, y simplifica la carga del código de usuario en memoria junto con el kernel
- Para fines educativos y de reproducibilidad, ofrece ventajas en simplicidad de distribución y consistencia del entorno
Capa SBI
- RISC-V usa un modelo de privilegios con modos M/S/U, y en este experimento OpenSBI se ubica en modo M mientras el kernel opera en modo S
- Delega en SBI la salida de consola y el control del dispositivo temporizador para asegurar la portabilidad
- Si SBI no está disponible, usa UART MMIO como fallback, aunque para la práctica se recomienda la versión más reciente de OpenSBI
Objetivo del kernel
- Para simplificar, solo soporta hilos estáticos, y los hilos están compuestos por funciones que no terminan
- Los hilos se ejecutan en modo U y envían llamadas al sistema al kernel en modo S
- Implementa scheduling por time-sharing sobre un solo núcleo para que en cada tick del temporizador pueda cambiar a otro hilo
Virtualización y qué es exactamente un hilo
- El threading con time-sharing es una forma de virtualización que permite ejecutar varias tareas en paralelo sobre un solo núcleo sin cambiar el modelo de programación
- A diferencia del scheduling cooperativo, el cambio ocurre mediante interrupciones de temporizador sin necesidad de un yield explícito
- Cada hilo tiene su propio conjunto de registros y stack inaccesibles para los demás, mientras que el resto de la memoria puede compartirse
El stack y la virtualización de memoria
- Los hilos deben tener un stack separado, un elemento esencial para mantener el contexto de ejecución según la convención de llamadas, incluidas las variables locales y la preservación de
ra- El espectro de virtualización sigue hilo < proceso < contenedor < VM, y el nivel de aislamiento y la vista cambian en cada caso
- En Linux, los contenedores se implementan combinando mecanismos del kernel como chroot y cgroups
Virtualizar un hilo
- El objetivo mínimo de virtualización en este experimento es mantener intacto el modelo de programación, proteger registros y algunos CSR, y asignar un stack individual
- Se enfatiza que sin una vista protegida de los registros, el cómputo deja de ser significativo
- Al sembrar valores iniciales como a0 en el stack, el paso de argumentos al inicio del hilo se resuelve de forma sencilla
Contexto de interrupción
- Las interrupciones pueden entenderse como un modelo similar a una llamada a función en el que el prólogo/epílogo guarda y restaura registros en el stack
- Para que una interrupción asíncrona de temporizador no corrompa los registros, es indispensable respetar la convención de preservación
- El ensamblador de ejemplo guarda y restaura no solo x0–x31, sino también CSR como sstatus, sepc, scause y stval
Implementación (alto nivel)
Aprovechar la convención del stack de interrupción
- El cuerpo de la rutina de interrupción se ubica entre el prólogo y el epílogo; si se reemplaza sp por otra región de memoria, se restaurará el conjunto de registros de otro contexto
- Eso equivale a un context switch y es la idea central para implementar el time-sharing en este experimento
- La interrupción de temporizador interviene periódicamente para ejecutar de forma alternada el flujo principal y el flujo de interrupción
Separación entre kernel y espacio de usuario
- Se mantiene el límite entre kernel en modo S y usuario en modo U, y el manejo de interrupciones y llamadas al sistema se realiza en el trap handler de modo S
- El arranque sigue la secuencia OpenSBI en modo M → inicialización del kernel en modo S → inicio de hilos en modo U
- Las interrupciones periódicas del temporizador hacen posible el scheduling y el cambio de contexto
Implementación (código)
Inicio en ensamblador
- En
startup.Sse compone una secuencia mínima que inicializa BSS, configura el stack pointer inicial y luego salta al main de Zig- El punto de entrada del kernel usa la convención
exportpara enlazarse con el ABI de C
- El punto de entrada del kernel usa la convención
Archivo principal del kernel y drivers de I/O
- El
maindekernel.zigprimero comprueba las funciones de consola de OpenSBI, y si fallan, usa UART MMIO como fallbacksbi.debug_printse invoca configurando los registros a0/a1/a6/a7 de acuerdo con el protocolo ECALL- Después de configurar el temporizador, registra el handler de interrupciones de modo S y activa los ticks
Handler de modo S y el context switch
- El handler está escrito con la convención
nakedde Zig para construir manualmente un prólogo/epílogo completo que incluye la preservación de CSR- En el cuerpo llama a
handle_kernel(sp)y decide si debe hacer el cambio sustituyendo por el sp devuelto - Usa
scausepara distinguir entre un ECALL de modo U y una interrupción de temporizador, y bifurca el procesamiento según el caso
- En el cuerpo llama a
Los hilos en espacio de usuario
- El código de usuario se incluye en el mismo binario junto con el kernel, y el hilo de ejemplo repite impresión de cadenas → bucle de espera
syscall.debug_printcoloca el número de llamada al sistema 64 en a7 y el buffer/longitud en a0/a1, y luego ejecuta ECALL- Durante la inicialización del hilo, se siembran en el stack la dirección de retorno y los valores iniciales de registros para que los argumentos puedan usarse inmediatamente en el primer retorno
Ejecutar el kernel
- La compilación se hace con
zig build, y la ejecución en QEMU especificando máquina virt + nographic + OpenSBI fw_dynamic- Al arrancar, después del banner de OpenSBI, aparecen de forma alternada salidas periódicas según el ID de cada hilo
- Si se compila con
-Ddebug-logs=true, se muestran en detalle el origen de la interrupción, el stack actual y los logs de encolado y desencolado
Conclusión
- Este experimento moderniza un kernel educativo con la combinación RISC-V + OpenSBI + Zig, mejorando la reproducibilidad y la legibilidad
- Aunque hay simplificaciones, como manejo mínimo de errores y stacks sobredimensionados, el enfoque está puesto en aprender la esencia del context switch y la separación de privilegios
- La portabilidad a hardware real es posible siempre que se ajusten el linker, las constantes de drivers y se garantice la disponibilidad de SBI
Nota adicional: resumen del espectro de virtualización
- Threads: centrados en la virtualización de registros y stack, con alta posibilidad de memoria compartida
- Process: virtualización del espacio de direcciones para aislamiento de memoria, con posibilidad de contener varios hilos internamente
- Container: unidad de aislamiento construida combinando visibilidad del entorno como filesystem namespaces y network namespaces
- VM: orientada a la virtualización completa del hardware
Resumen de puntos clave de implementación
- El context switch se logra mediante reemplazo del stack de interrupción
- En el trap handler de modo S se preserva y restaura el estado completo, incluidos los CSR
- Ruta de salida redundante con SBI primero y UART MMIO como fallback
- Scheduling simple centrado en hilos estáticos, un solo núcleo y time slice
- Las llamadas al sistema basadas en ECALL clarifican el límite entre U y S
1 comentarios
Comentarios de Hacker News
Se puede experimentar algo parecido en formato de “paquete” con "Operating System in 1000 Lines of Code"; yo lo seguí hace tiempo en Zig (convirtiendo los fragmentos de código en C a Zig mientras avanzaba) y fue muy divertido. Mi código y el VOD están aquí: https://github.com/kristoff-it/kristos/
Es una publicación enviada por el propio autor por separado https://news.ycombinator.com/item?id=45236479. El autor rehízo personalmente el Tiny OS Kernel, quería experimentar específicamente con RISC-V y el entorno OpenSBI, y usó Zig en lugar del C tradicional. En realidad, creo que también se podría seguir fácilmente con C o Rust. Todo el proceso es algo tosco, pero sirve como experimento e introducción para dar los primeros pasos en el desarrollo de kernels de SO y en arquitectura de computadoras. Me parece un proyecto divertido para probar un fin de semana, y arriba se puede ver el walkthrough completo y el enlace a Github.
Este tipo de proyectos realmente impresiona. Linux, al final, también es solo un kernel, pero ese trabajo abrió el camino para que un Unix de código abierto pudiera instalarse en miles de millones de dispositivos. Me parece algo genial.
Lo más divertido es que, cuando Linux se publicó por primera vez, Torvalds escribió en un correo que “solo era un hobby y no sería algo grande ni profesional como GNU” https://groups.google.com/g/comp.os.minix/c/dlNtH7RRrGA/m/SwRavCzVE7gJ
Yo no diría que este tipo de proyectos sea <i>tremendamente</i> impresionante. La forma de construir un kernel minimalista con multitarea ya es un camino conocido desde hace décadas. Hacer un kernel que arranque y ejecute tareas simples es algo que cualquiera con cierta inteligencia y constancia puede lograr. En RISC-V es un poco más complejo que en x86, pero la información de inicialización de hardware se consigue fácilmente (ver https://wiki.osdev.org/RISC-V_Meaty_Skeleton_with_QEMU_virt_board). En ese sentido, el propio autor también dijo que “rehizo un ejercicio que había hecho en una clase de sistemas operativos”. Me parece del nivel que podría completar cualquiera con un título en ingeniería de software. Claro, puede haber bugs o partes incompletas, pero hacer multiproceso o aislamiento de procesos con MMU ya no es algo especialmente difícil.
Tanto como Linux, también el hecho de que Stallman empezara en 1984 a hacer un compilador de C y utilidades de Unix abrió el camino para instalar Unix de código abierto en miles de millones de máquinas.
Zig es realmente muy bueno para desarrollo de sistemas operativos, y RISC-V también. Yo empecé la misma tarea en x86 y me cansé enseguida por todo el boilerplate heredado; del lado de RISC-V casi no hay nada de eso. Es mucho más fácil empezar a desarrollar https://github.com/Fingel/aeros-v
Creo que si empiezas en x86 no hay tanto boilerplate siempre que uses bien el bootloader. Un cargador multiboot normalmente te deja en real mode, y como la mayoría quiere protected mode, basta con configurar algunas tablas y hacer un salto. Si quieres desactivar el controlador de interrupciones legado hay algo más que tocar, pero tiene la ventaja de que puedes arrancarlo en una PC de escritorio real (aunque hay que tener cuidado con la interfaz de consola). Mi SO de hobby usaba arranque BIOS y algunas funciones VGA, y sufrí por la mala compatibilidad; una consola serial es mucho más fácil, pero hoy en día muchas computadoras ya no traen puerto serial.
En la práctica, no es más que volver a sacar la seguridad de Object Pascal o Modula-2 y reempaquetarla con sintaxis de C. C nunca tuvo nada especialmente único más allá de haberse difundido gracias a la licencia de UNIX.
Yo también quiero intentarlo, pero me da curiosidad en qué entorno están ejecutando el kernel RISC-V: si solo usan Qemu o si hay algún hardware real recomendable.
Creo que la ISA de RISC-V es realmente muy accesible. La documentación es excelente, hay muchísimos ejemplos y también bastantes emuladores. Incluso el machine code sin comprimir es fácil de leer. Yo estoy escribiendo un libro para mi hija mientras hago un pequeño SO con Forth y time-sharing https://punkx.org/projekt0/book/part1/os.html; si hubiera sido x86, creo que ni lo habría intentado. En el proceso estoy aprendiendo Forth y ensamblador RISC-V al mismo tiempo, y es muy entretenido. Si quieres hacer un SO de juguete desde cero, diría que ahora es el momento ideal (tan emocionante como en los años 80). Consigue una placa RISC-V barata (rp2350, por ejemplo) y súbele las secciones relevantes del manual a una IA como Claude; ayuda muchísimo cuando te atoras.
Este tipo de intentos siempre son divertidos e interesantes. También animaría a la gente a probar con su propia criptografía o con cosas difíciles. El consejo de “no implementes tu propia criptografía” significa que no debes usar en producción algo que no haya sido validado en la práctica; para experimentación o investigación no hay peligro, así que siéntanse libres de intentarlo. Necesitamos más sistemas operativos y más opciones.
(Cita de una sentencia española) Bueno, bloquear http puede pasar, pero esto ya es demasiado...
Escuché que en España Cloudflare (y probablemente otros también) queda bloqueado por error por temas relacionados con transmisiones de fútbol. Me pregunto cómo están esquivando ese problema; ¿usan VPN? Si bloquean IP importantes, eso también debe afectar el trabajo.
Al ver en la sentencia que esto fue iniciado por la Liga de Fútbol Profesional de España y Telefónica Audiovisual Digital, pienso que esta gente son criminales.
...¿qué? ¿Un organismo del fútbol español tiene autoridad para restringir el acceso a internet de todo un país?
Me pregunto cómo se puede conseguir hardware RISC barato.
En AliExpress venden una placa llamada Milk-V Duo S por 10 dólares, y últimamente me aparece seguido en las recomendaciones. Las especificaciones principales son un SG2000 Master mejorado y 512 MB de RAM, IO más amplio, algunos modelos con WI-FI6/BT5 (excepto los modelos 512M-Basic/eMMC), puerto host USB 2.0, Ethernet de 100 Mbps con soporte PoE, doble MIPI CSI y switch para cambiar entre arranque RISC-V y ARM, entre otras cosas https://aliexpress.com/w/wholesale-Milk%2525252dV-Duo-S.html
Ya hay varias placas, pero una que me interesó tanto que la apoyé con financiamiento es la VisionFive 2 Lite https://www.kickstarter.com/projects/starfive/visionfive-2-lite-unlock-risc-v-sbc-at-199/description. No tengo la VisionFive2 de primera generación, pero dicen que tiene buena reputación y que el ecosistema está creciendo poco a poco. Aún hay partes incompletas, pero espero que empiece a enviarse pronto. La que yo uso personalmente es la PolarFire SoC Discovery Kit, una placa con RISC-V de cuatro núcleos y FPGA. Es algo cara (130 dólares) y no es para todo el mundo, pero lo curioso es que la placa cuesta menos que el propio chip https://www.microchip.com/en-us/development-tool/MPFS-DISCO-KIT. La documentación y el toolchain de Microchip son viejos y no muy buenos, pero una vez que te acostumbras, correr código bare-metal de RISC-V es realmente fácil. Los ejemplos de Linux/bare-metal están bien explicados.
Yo sugeriría probar primero en un emulador, incluso desde una máquina x86 o Apple, sin hardware real por ahora. El desarrollo va más rápido que en una placa física, y con algo como QEMU puedes empezar enseguida https://www.qemu.org/docs/master/system/target-riscv.html
La Raspberry Pi Pico 2 también soporta RISC-V, así que está bastante bien https://www.raspberrypi.com/products/raspberry-pi-pico-2/
Gracias a todos por las respuestas; a quienes votaron negativamente la pregunta, no creo que haya motivo para agradecerles.