feat: integra renaser (kernel SASOS bare-metal) al monorepo

renaser —kernel asíncrono de espacio de direcciones único, no-POSIX,
`no_std` x86_64— entra al monorepo como su PROPIO workspace de Cargo,
no fusionado: usa toolchain nightly, target `x86_64-unknown-none` y
`panic = "abort"`, incompatibles con los perfiles globales de brahman.

- `renaser/` — copia del proyecto (sin su `.git`; el repo original
  conserva su historia standalone). Workspace propio con su
  `rust-toolchain.toml` y `.cargo/`.
- `exclude = ["renaser"]` en el workspace de brahman: Cargo lo trata
  como ajeno.
- El kernel de renaser path-depende `mirada-layout` cruzando la
  frontera de workspace — primer núcleo compartido. Semilla de la
  Fase 8 (compositor): geometría de teselado compartida, framebuffer
  nativo de renaser; smithay se queda en el lado Linux.

Verificado: `cargo build -p boot` compila kernel + imagen UEFI con
mirada-layout enlazado para bare-metal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-22 14:37:14 +00:00
parent 1c6aafbc24
commit e2272c0ed3
55 changed files with 6668 additions and 0 deletions
+7
View File
@@ -302,6 +302,13 @@ members = [
"crates/apps/charka", "crates/apps/charka",
] ]
# renaser — el SO bare-metal SASOS. Vive en el mismo repo pero es su
# PROPIO workspace de Cargo: usa toolchain nightly, target
# `x86_64-unknown-none` y `panic = "abort"`, incompatibles con los
# perfiles globales de este workspace. Cargo lo trata como ajeno; los
# crates compartidos se referencian por `path` cruzando la frontera.
exclude = ["renaser"]
[workspace.package] [workspace.package]
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
+19
View File
@@ -0,0 +1,19 @@
# =============================================================================
# renaser :: configuracion de cargo
# -----------------------------------------------------------------------------
# El kernel se compila contra el target NATIVO precompilado `x86_64-unknown-none`
# (bare-metal, soft-float, sin SSE). No hay JSON propio ni `build-std`: cero
# burocracia del compilador, aislamiento de arquitectura sin friccion.
# =============================================================================
[unstable]
# Dependencias de artefacto (RFC 3028): permiten que el miembro `boot` ordene a
# cargo compilar el kernel para `x86_64-unknown-none` de forma automatica y
# reciba la ruta de su ELF. Sigue siendo una funcion inestable (nightly).
bindeps = true
[alias]
# Compila unicamente el kernel, en aislamiento de arquitectura.
kernel = "build -p kernel --target x86_64-unknown-none"
# Construye la imagen UEFI y abre QEMU (equivale a `cargo run -p boot`).
qemu = "run -p boot"
+24
View File
@@ -0,0 +1,24 @@
# =============================================================================
# renaser :: archivos que NO se versionan
# =============================================================================
# Artefactos de compilacion de Rust — kernel, boot y apps WASM.
/target
/kernel/target
/apps/*/target
# Bloqueo de dependencias del kernel: se genera al compilarlo en aislamiento.
# La verdad de versiones la fija el Cargo.lock raiz — el kernel es dependencia
# de artefacto de `boot` y se resuelve con el.
/kernel/Cargo.lock
# Ajustes locales del entorno de desarrollo (personales, no compartidos).
/.claude/settings.local.json
# Capturas temporales de QEMU.
*.ppm
*.png.tmp
# Borrador de trabajo local — puede contener notas y credenciales; nunca se
# versiona. (Fue subido por error y purgado del historial; ver el README.)
/renaser.txt
+173
View File
@@ -0,0 +1,173 @@
# renaser — Arquitectura
Este documento describe la arquitectura del kernel `renaser` subsistema a
subsistema. Para el estado por fases, ver `ROADMAP.md`.
## 1. Filosofía
renaser es un **SASOS***Single Address Space Operating System*. No hay un
espacio de direcciones por proceso ni cambios de contexto de hardware: todo el
sistema —kernel y aplicaciones— comparte una única RAM plana.
El aislamiento NO lo da la MMU ni los anillos de privilegio de la CPU. Lo da el
**software**: las aplicaciones se distribuyen como bytecode WebAssembly y se
ejecutan dentro de un intérprete que acota matemáticamente cada acceso a
memoria. Una aplicación solo puede hacer aquello para lo que el kernel le haya
inyectado una *capacidad* (una función de host). Lo que no está importado no
tiene camino físico que recorrer.
La interfaz es **visual desde el primer microsegundo**. No hay TTY; el texto se
rasteriza como gráfico vectorial. "El texto es un caso particular del dibujo."
## 2. La cadena de arranque
```
Firmware UEFI → crate `bootloader` 0.11 → kernel::_start → kernel_main
```
El miembro `boot/` (que corre en el **anfitrión**) toma el ELF del kernel,
lo fusiona con el cargador `bootloader` 0.11 en una imagen de disco UEFI GPT y
la lanza en QEMU con el firmware OVMF. El cargador deja la CPU en modo largo de
64 bits, mapea el kernel y le entrega una `BootInfo` con el framebuffer GOP.
## 3. Estructura del espacio de trabajo
El workspace tiene **un solo miembro**, `boot`. El `kernel` y las `apps` están
**excluidos** a propósito:
- El kernel es `#![no_std] #![no_main]` y define su propio `_start`. Si fuese
miembro del workspace, `cargo build` intentaría compilarlo para el anfitrión
y fallaría por un símbolo `_start` duplicado con el `crt0` del sistema.
- Por eso el kernel se construye **solo** como *dependencia de artefacto*
(RFC 3028) de `boot`, que le fija el target `x86_64-unknown-none`.
- Las apps WASM tienen su propio target (`wasm32-unknown-unknown`) y se
compilan aparte.
El kernel usa el target **nativo precompilado** `x86_64-unknown-none`: sin
target JSON propio, sin `build-std`. Soft-float y sin SSE, para que las
interrupciones no corrompan registros de punto flotante.
## 4. Subsistemas del kernel
### `grafico` — el sustrato visual
`Color` (RGB de 24 bits, independiente del hardware), `Pantalla` (el
framebuffer GOP físico, envuelto en seguridad) y `Lienzo` (el búfer intermedio
en RAM). Toda composición ocurre en el `Lienzo`; `Pantalla::presentar` lo
vuelca de un solo gesto con escrituras volátiles — **doble búfer**, sin
desgarros. `codificar` traduce un `Color` al formato nativo del framebuffer.
### `consola` — la superficie de texto e imagen
Une `Lienzo` + `Pantalla` + una pluma de escritura. Rasteriza glifos con
`fontdue` y los funde sobre el lienzo mezclando por cobertura (anti-aliasing).
También vuelca fotogramas crudos del userspace WASM. Es global, tras un `Mutex`.
### `baliza` — la red de seguridad visual
Publica de forma atómica y sin cerrojos los datos mínimos del framebuffer. Los
manejadores `#[panic_handler]` y `#[alloc_error_handler]` los usan para pintar
una franja de advertencia —**roja** si el sistema colapsa, **naranja** si el
heap se agota— escribiendo directo sobre el silicio, sin confiar en el heap ni
en estructura dinámica alguna.
### `gdt` — cimientos del manejo de fallos
GDT propia con segmentos de código y datos del kernel, y un TSS cuyo único
cometido es alojar un **stack de emergencia** (IST) para el doble fallo: ni un
desbordamiento de la pila del kernel impide su diagnóstico.
### `interrupts` — la tabla de reflejos
IDT con manejadores de excepción de CPU (breakpoint recuperable; el resto,
fatales → `panic!`) e interrupciones de hardware. El doble fallo se atiende
sobre el stack de emergencia del TSS.
### `pic` — el latido del hardware
Remapea el par 8259 (PIC) fuera del rango de las excepciones de CPU y programa
el temporizador de intervalos (PIT). El teclado (IRQ1) deposita scancodes en
una cola lock-free. `desenmascarar` abre una línea de IRQ concreta —la del
disco, descubierta en tiempo de ejecución (Fase 6.2)—.
### `drivers` — el hardware que el kernel conquista (Fases 6.1, 6.2)
A diferencia del framebuffer o el temporizador —que el firmware sirve en
bandeja—, el disco hay que DESCUBRIRLO y reclamarlo.
- `pci``CamPuertos`: acceso al espacio de configuración del bus PCI por el
mecanismo #1 (puertos `0xCF8`/`0xCFC`); `linea_irq` lee la IRQ del dispositivo.
- `disco` — el disco virtio-blk. Un asignador de marcos por **mapa de bits**
—con liberación real— reparte páginas físicas para el DMA; `KernelHal`
implementa el `trait Hal` de `virtio-drivers` (traducción de direcciones,
memoria rebote para el DMA); el `VirtIOBlk` se monta una vez y persiste tras
un `Mutex`. La E/S de bloques es **asíncrona** (Fase 6.2): `EsperaDisco` es un
`Future` que envía la petición por la API no bloqueante de `virtio-drivers`,
cede la CPU y se reanuda con la IRQ del disco (`atender_irq`). `bloquear_en`
conduce ese `Future` desde los contextos síncronos durmiendo la CPU con
`hlt`; sobre él, `leer_sectores`/`escribir_sectores` sirven al grafo de
objetos. La IRQ del disco se enruta por el PIC 8259, no por el IOAPIC.
### `almacen` — el grafo de objetos direccionado por contenido (Fase 6.1c)
renaser no tiene un sistema de archivos plano POSIX: tiene un DAG de objetos.
Un `Objeto` es una carga útil de bytes y una lista de aristas (hashes de
hijos). La identidad de un objeto es el hash BLAKE3 de su forma serializada
(`postcard`) — de ahí integridad (un objeto se verifica al leerlo) y
deduplicación (contenido idéntico, un solo registro). El disco es un **log**:
el sector 0 es el superbloque (magia, versión, cursor, raíz); tras él se anexan
los registros de objetos. Un índice hash→sector se reconstruye al arrancar
recorriendo el log. Las capacidades `sys_object_*` exponen el grafo al
userspace.
### `memory` — el heap dinámico
`linked_list_allocator` como `#[global_allocator]` sobre una región estática de
64 MiB en `.bss`. Desbloquea `alloc::*``Box`, `Vec`, `BTreeMap`, `Arc`.
### `async_system` — el reactor cooperativo
- `task``Task`: un `Future` anclado (`Pin<Box<dyn Future>>`) con `TaskId`.
- `executor` — el `Executor`: censo de tareas, cola de listas, y un `hlt`
controlado cuando no hay trabajo.
- `waker` — un `Waker` que reinyecta el `TaskId` en la cola al despertar.
- `teclado` — los canales de scancodes. Cada app abre el suyo; la IRQ1 **difunde**
cada scancode a todos, de modo que varias apps reciben la entrada en paralelo.
- `reloj` — convierte la IRQ0 (PIT, 100 Hz) en el `Future` `EsperaFrame`, que se
resuelve en el siguiente pulso. Es el **compás de los fotogramas**: una tarea
WASM hace su trabajo de un fotograma y `.await`-ea el siguiente.
Las interrupciones de hardware no conmutan el contexto de la CPU: **despiertan
tareas**. El kernel avanza cooperativamente.
### `texto` — tipografía vectorial
Empotra un `.ttf` en el binario (`include_bytes!`) y lo rasteriza con `fontdue`
glifo a glifo, bajo demanda.
### `wasm` — el escudo de aislamiento
- `mod` — el runtime y la `AplicacionWasm`: una instancia **persistente** entre
fotogramas (`Store` + `TypedFunc<(), ()>` + región). El ABI del userspace es
`init()` (una vez) y `tick()` (un fotograma, y retorna). Dos guardarraíles
acotan a cada app: el **temporal** —un presupuesto de **combustible** (`fuel`)
por `tick`; agotarlo la desaloja (baliza púrpura)— y el **espacial** —un techo
de memoria lineal vía `Store::limiter`; rebasarlo la desaloja (baliza
amarilla)—. En ambos casos `wasmi` lanza una trampa, el kernel recupera el
mando y el sistema no sufre. El `Drop` de `AplicacionWasm` reconcilia el ciclo
de vida: da de baja su canal de teclado.
- `env` — la **matriz de capacidades**: las funciones de host que el módulo
WASM puede invocar. Hoy son siete:
- `sys_render_frame(ptr, len)` — compone un fotograma dentro de la **región**
de pantalla asignada a la app. El kernel valida **matemáticamente** que
`[ptr, ptr+len)` cae dentro de la memoria lineal del módulo, y que el
tamaño es el de la región, antes de leer un byte; si no, aborta la app.
- `sys_get_scancode()` — entrega, sin bloquear, el siguiente scancode del
canal de teclado **propio** de la app.
- `sys_object_put` / `sys_object_datos` / `sys_object_hijo` /
`sys_object_raiz` / `sys_object_fijar_raiz` — el acceso al grafo de objetos
persistente (ver `almacen`): grabar un objeto, leer su carga útil, recorrer
sus aristas, y leer o fijar la raíz del DAG. Cada puntero que la app
entrega se valida contra su memoria lineal, igual que en `sys_render_frame`.
Todo puntero inválido aborta la app —es su culpa: el `Error` se traduce en una
trampa de WASM, la app se desaloja y el resto del sistema sigue—. Un fallo del
almacenamiento, en cambio, no es culpa de la app: se le devuelve un código de
error que ella decide cómo afrontar.
## 5. Restricciones de ingeniería
- **Escrituras volátiles** obligatorias para framebuffer y MMIO.
- **`unsafe`** confinado en células mínimas, cada una con un contrato
`SEGURIDAD:` y envuelta en una abstracción segura. `unsafe_op_in_unsafe_fn`
está en `deny`.
- **Alineación**: 16 bytes para estructuras genéricas; 4096 (página) para
buffers de asignador y el lienzo de respaldo.
- **`no_std` estricto**: `core` + `alloc`; nada de `std`.
+457
View File
@@ -0,0 +1,457 @@
# Registro de cambios — renaser
Registro técnico detallado, fase a fase, en orden cronológico. Formato
inspirado en *Keep a Changelog*. Para la crónica en lenguaje llano, ver
`DIARIO.md`; para el estado y los planes, `ROADMAP.md`.
---
## Fase 1 — El primer microsegundo — 2026-05-21
### Infraestructura
- Espacio de trabajo Cargo, `kernel/Cargo.toml`, `.cargo/config.toml`,
`rust-toolchain.toml`.
- Target inicial: especificación JSON propia `x86_64-renaser.json` — PIC,
soft-float, sin SSE, enlazador `lld`.
### Añadido
- `kernel/src/main.rs`: punto de entrada con el macro `entry_point!` de
`bootloader_api` 0.11.
- Adopción y verificación con seguridad de tipos del framebuffer GOP (ancho,
alto, formato de píxel, *stride*).
- Lienzo intermedio estático de 8 MiB en `.bss`, alineado a página: la técnica
de doble búfer para evitar parpadeos.
- `#[panic_handler]` que dibuja una franja roja directamente sobre el
framebuffer mediante escrituras volátiles.
- Dependencias: `bootloader_api` 0.11, `x86_64` 0.15, `embedded-graphics` 0.8.
### Notas
- Corrección clave: `bootloader` (constructor de imagen, lado anfitrión) no es
`bootloader_api` (la API `no_std` que consume el kernel).
---
## Fase 1.5 — Empaquetado y arranque — 2026-05-21
### Cambiado
- Migración del target del kernel: de la JSON propia `x86_64-renaser.json` al
target nativo precompilado `x86_64-unknown-none`. Elimina `build-std` y la
burocracia de compilador asociada.
### Añadido
- Miembro `boot/`: orquestador de anfitrión. Dependencia de artefacto
(RFC 3028) sobre el kernel; construcción de la imagen de disco UEFI con
`bootloader::UefiBoot`; lanzador de QEMU.
### Toolchain
- Instalación de la toolchain nightly (la máquina solo tenía la estable).
### Corregido
- Los nightly recientes exigen `-Zjson-target-spec` para usar specs JSON
(resuelto al migrar al target nativo).
- Sintaxis del acelerador de QEMU: `-machine q35,accel=kvm:tcg`.
### Verificado
- Arranque en QEMU: superficie índigo limpia a 1280×800.
- Baliza de pánico (franja roja) confirmada inyectando un pánico de prueba.
---
## Fase 2.0 — Cimientos del manejo de fallos — 2026-05-21
### Añadido
- `kernel/src/gdt.rs`: GDT propia, TSS y un stack de emergencia (IST)
reservado para el manejador de doble fallo.
- `kernel/src/interrupts.rs`: IDT con manejadores de excepción de CPU. El
breakpoint (#BP) es recuperable; #UD, #DE, #GP, #PF y #DF son fatales y
encienden la baliza.
- `#![feature(abi_x86_interrupt)]`.
### Corregido
- La GDT debe recargar los registros SS/DS/ES con un segmento de datos del
kernel: el cargador deja `SS = 0x10`, valor que en la GDT nueva pasa a ser el
descriptor del TSS; el primer `iretq` de una rutina de excepción provocaba un
#GP. Diagnosticado con la traza `-d int` de QEMU.
### Verificado
- Excepción `int3` atrapada y superada; línea-latido teal dibujada como prueba.
---
## Fase 2.1 — Interrupciones de hardware — 2026-05-21
### Añadido
- `kernel/src/pic.rs`: remapeo del par 8259 (PIC) fuera del rango de las
excepciones (vectores 0x20+); programación del temporizador PIT a 100 Hz;
IRQ1 de teclado.
- Bucle de render en `kernel_main`, despertado por el temporizador, en
sustitución del `hlt` estático.
### Verificado
- La línea-latido se anima (temporizador en marcha); las pulsaciones de teclado
inyectadas cambian el cuadro-eco (teclado en marcha).
---
## Fase 3 — Memoria dinámica y reactor asíncrono — 2026-05-21
### Añadido
- `kernel/src/memory/`: `linked_list_allocator` como `#[global_allocator]`
sobre una región estática en `.bss`.
- `#[alloc_error_handler]`: franja naranja de agotamiento de memoria.
`#![feature(alloc_error_handler)]`.
- `kernel/src/async_system/`: reactor cooperativo — `Executor`, `Task`/`TaskId`,
`Waker` (vía `alloc::task::Wake`) y `ScancodeStream`.
- `kernel/src/texto.rs`: rasterización de tipografía vectorial con `fontdue`;
TTF Adwaita Mono empotrada con `include_bytes!`.
- Dependencias: `linked_list_allocator` 0.10, `spin` 0.9, `crossbeam-queue`
0.3, `futures-util` 0.3, `fontdue` 0.9.
### Cambiado
- Heap ampliado de 16 MiB a 64 MiB: `fontdue`, al analizar una tipografía real,
agotaba los 16 MiB iniciales. El `#[alloc_error_handler]` lo delató pintando
la franja naranja — el guardarraíl funcionó en silicio real.
### Corregido
- El modo `no_std` de `fontdue` está condicionado a su feature `hashbrown`; sin
ella recae en `std`. Se fija `default-features = false, features = ["hashbrown"]`.
### Verificado
- Rótulo de bienvenida rasterizado al arranque; el texto tecleado aparece en vivo.
---
## Fase 4 — El escudo de aislamiento WASM — 2026-05-21
### Añadido
- `kernel/src/wasm/`: intérprete `wasmi` 1.0.9 en modo `no_std`. `env.rs`
define la matriz de capacidades — exactamente dos funciones de host:
`sys_render_frame(ptr, len)`, con validación infranqueable de los límites de
la memoria lineal, y `sys_get_scancode()`.
- `apps/hello_wasm/`: primera aplicación del userspace — módulo
`wasm32-unknown-unknown` (cdylib), un cuadrado móvil dirigido por teclado. Se
empotra en el kernel con `include_bytes!`.
- Dependencia: `wasmi` 1.0.
### Cambiado
- El kernel se EXCLUYE del espacio de trabajo (`exclude = ["kernel", "apps"]`):
al añadir `wasmi`, `cargo build` intentaba compilar el kernel para el
anfitrión y fallaba por un símbolo `_start` duplicado. El kernel pasa a
construirse únicamente como dependencia de artefacto de `boot`.
### Corregido
- `wasmi` `no_std` necesita `default-features = false, features = ["hash-collections"]`.
- `wasmi` 1.0.9 usa `Linker::instantiate_and_start` (no existe `instantiate`).
### Verificado
- La aplicación WASM pinta su propia superficie y responde a las pulsaciones de
teclado inyectadas, sin más vía hacia el kernel que las dos capacidades.
---
## Mantenimiento — Estructura, documentación e integración — 2026-05-21
### Cambiado
- Refactorización: `main.rs` dividido de 692 a ~155 líneas. Se extraen los
módulos `sync.rs`, `grafico.rs`, `consola.rs` y `baliza.rs`. Sin cambios de
comportamiento (verificado en QEMU).
### Añadido
- Documentación: `CLAUDE.md`, `README.md`, `ARCHITECTURE.md`, `ROADMAP.md`,
`CHANGELOG.md` y `DIARIO.md`.
- Integración con git: repositorio inicializado, remoto `origin` en Gitea,
`.gitignore`.
### Seguridad
- `renaser.txt` —un borrador de trabajo que contenía una credencial— se subió
por error en el commit inicial. Se purgó de todo el historial con
`git filter-branch` y se reescribió el remoto con `push --force`. La
credencial expuesta debe rotarse.
---
## Fase 5 — Multitarea cooperativa, guardarrail de fuel y reloj — 2026-05-22
Unificación de la Fase 3 (reactor) y la Fase 4 (WASM): el userspace deja de ser
una sola app que monopoliza la CPU y pasa a ser un conjunto de aplicaciones
cooperativas, aisladas también en el TIEMPO.
### Añadido
- `kernel/src/async_system/reloj.rs`: convierte la IRQ0 (PIT, 100 Hz) en una
primitiva asíncrona. `CONTADOR_PULSOS` (`AtomicU64`) y `EsperaFrame`, un
`Future` que se resuelve en el siguiente pulso — la unidad de cesión
cooperativa del userspace. Censo de wakers tras `Mutex`, drenado por la IRQ0.
- `kernel/src/wasm`: ABI de fotograma. `AplicacionWasm` —instancia PERSISTENTE
entre fotogramas (`Store` + `TypedFunc<(), ()>` + región)— sustituye al
`wasm::ejecutar` de fuego-y-olvido. El módulo del userspace exporta ahora
`init()` (una vez) y `tick()` (un fotograma, y retorna).
- Escudo de combustible: `Engine` con `Config::consume_fuel(true)` y
`CompilationMode::Eager` (el `fuel` mide solo ejecución). Presupuesto recargado
antes de cada `tick``FUEL_ARRANQUE` (20 M) y `FUEL_FOTOGRAMA` (2 M).
- `kernel/src/wasm/env.rs`: capacidad `sys_render_frame` con **regiones de
dibujo** — cada fotograma se compone desplazado por el `(offset_x, offset_y)`
de la app; un tamaño ajeno a la región se rechaza. Canal de teclado **por
aplicación**: la IRQ1 difunde cada scancode a TODOS los canales, de modo que
varias apps reciben la entrada en paralelo sin robársela.
- `kernel/src/grafico.rs`: `RegionPantalla` y `Color::DESALOJO` (púrpura).
- `apps/discola/`: aplicación WASM construida para portarse mal — su `tick` es
un bucle cerrado. Demuestra el guardarrail de fuel en vivo.
### Cambiado
- `apps/hello_wasm`: migrada al ABI `init`/`tick`; su lienzo se dimensiona a la
región (480×560). El estado del cuadrado persiste en su memoria lineal.
- `interrupts.rs`: la IRQ0 avanza el `reloj`; la IRQ1 difunde a los canales.
- `consola.rs`: `volcar_marco` compone en una sub-región; `pintar_desalojo`
tatúa la baliza púrpura.
- Contención de fallos: el subsistema WASM ya no usa `.expect()`. Toda falla
—carga, instanciación, desbordamiento, agotamiento de fuel— se devuelve como
`Result`; la tarea desaloja la app (baliza púrpura) y el kernel sigue vivo.
### Verificado
- QEMU: tres apps concurrentes. Dos instancias de `hello_wasm` (mismo bytecode,
regiones izquierda y derecha) renderizan y responden a W/A/S/D **en paralelo**,
con movimiento idéntico. La app díscola es desalojada en su primer fotograma
por agotamiento de combustible: su región queda púrpura y el sistema —kernel y
apps vecinas— no sufre un solo sobresalto.
---
## Fase 6.0 — Cuotas de memoria y ciclo de vida del userspace — 2026-05-22
El aislamiento de las aplicaciones, que la Fase 5 hizo temporal (combustible),
se completa ahora en la dimensión espacial (memoria) y se cierra la fuga de
recursos del ciclo de vida.
### Añadido
- Techo de memoria lineal por aplicación: `4 MiB`. `wasm/mod.rs` construye un
`StoreLimits` (`StoreLimitsBuilder::memory_size` + `trap_on_grow_failure`) y lo
liga al `Store` con `Store::limiter`. Un `memory.grow` que rebase la cuota se
convierte en trampa; el kernel la captura y desaloja la app.
- `FallaApp::SinMemoria` y `Color::DESALOJO_MEMORIA` (amarillo pálido). El
desalojo distingue la causa por color: púrpura (tiempo/aborto), amarillo
(memoria). La clasificación es robusta — `Error::as_trap_code()` da un código
público y unívoco: `TrapCode::GrowthOperationLimited`.
- `Drop` para `AplicacionWasm`: al morir una app desalojada, su canal de teclado
se da de baja del censo de difusión de la IRQ1 — cierra la fuga señalada en la
Fase 5. `async_system::teclado` se reorganiza en `crear_canal` /
`registrar_canal` / `cerrar_canal`; el canal se inscribe al FINAL de la carga,
de modo que una carga fallida no deja canales huérfanos.
- `apps/glotona/`: aplicación WASM construida para devorar memoria — su `tick`
invoca `memory.grow` sin freno. Demuestra el guardarrail espacial en vivo.
### Verificado
- QEMU: cuatro apps concurrentes. Las dos `hello_wasm` renderizan y responden al
teclado en paralelo; la app díscola es desalojada por combustible (franja
púrpura) y la app glotona por cuota de memoria (franja amarilla), ambas en su
primer fotograma, sin que el kernel ni las apps honradas se inmuten.
---
## Fase 6.1a — Sonda PCI y disco de pruebas — 2026-05-22
Primer paso de la estrategia incremental hacia el almacenamiento: derribar el
muro del descubrimiento de hardware antes de diseñar nada encima.
### Añadido
- `kernel/src/drivers/`: nuevo subsistema de drivers. `pci.rs` enumera el bus
PCI por fuerza bruta mediante el mecanismo de configuración #1 —puertos de E/S
`0xCF8` (dirección) y `0xCFC` (datos)—; recorre buses y dispositivos leyendo
el Vendor/Device ID, y localiza el disco virtio-blk (vendor `0x1AF4`, device
`0x1001`/`0x1042`), devolviendo su ubicación y sus seis BARs.
- `kernel_main`: tras fundar la consola, sondea el bus PCI y deja constancia
visual del hallazgo.
### Cambiado
- `boot/src/main.rs`: forja un disco de pruebas `target/disk.img` (fichero
disperso de 32 MiB; se respeta si ya existe) y lo adjunta a QEMU como
`virtio-blk-pci`. **Corrección de plataforma:** la máquina `q35` es x86_64 y
exige la transmisión PCI; `virtio-blk-device` (su gemelo MMIO) es de ARM.
### Verificado
- QEMU: el kernel enumera el bus y reporta en pantalla
`virtio-blk en bus 0 dev 3 :: BAR0 E/S 0x6000` — un dispositivo virtio-blk
transicional (device `0x1001`), con su BAR0 en espacio de E/S. El muro del
descubrimiento PCI queda derribado; las cuatro apps del userspace siguen
operando sin alteración.
---
## Fase 6.1b — HAL, DMA y lectura del sector 0 — 2026-05-22
Segundo paso del sustrato de almacenamiento: el diálogo real con el disco. El
kernel monta el dispositivo virtio-blk y lee su primer sector por DMA.
### Añadido
- Dependencia `virtio-drivers` 1.13 (`no_std`, `default-features = false`,
feature `alloc`).
- `kernel/src/drivers/disco.rs`:
- **Asignador de marcos** «bump» — reparte páginas físicas de 4 KiB para el
DMA, tomadas de la mayor región de RAM libre que el cargador reporta. No
libera: suficiente para una sonda (la gestión fina llegará con el grafo).
- **`KernelHal`** — implementa el `trait Hal` de `virtio-drivers`. `dma_alloc`
entrega marcos físicos a cero; `mmio_phys_to_virt` traduce los BARs (el
cargador mapea ≥ 4 GiB de memoria física, que cubre todo MMIO de PCI);
`share`/`unshare` usan un buffer rebote para que cualquier región del kernel
pueda viajar al dispositivo.
- **`montar_y_leer_sector0`** — enumera el bus, habilita E/S + memoria +
bus-master, monta el `PciTransport` y el `VirtIOBlk`, y lee el sector 0 por
**sondeo** del *used ring* (sin depender aún de interrupciones).
- `kernel/src/drivers/pci.rs`: reescrito como `CamPuertos`, la implementación de
`ConfigurationAccess` de `virtio-drivers` sobre los puertos `0xCF8`/`0xCFC`.
- `kernel_main` captura `physical_memory_offset` y la región de RAM de
`BootInfo`, funda el subsistema de disco y reporta el resultado de la sonda.
### Cambiado
- `boot/src/main.rs`: al forjar el disco de pruebas graba una firma
(`renaser-6.1b`) en su sector 0 — el testigo del viaje de ida y vuelta.
### Verificado
- QEMU: el kernel reporta `virtio-blk :: bus 0 dev 3 :: 65536 sectores ::
s0=renaser-6.1b`. La firma grabada por el anfitrión se lee de vuelta intacta:
descubrimiento PCI, transporte, DMA y transferencia funcionan de punta a
punta. Las cuatro apps del userspace siguen operando sin alteración.
---
## Fase 6.1c — El grafo de objetos direccionado por contenido — 2026-05-22
Tercer y último paso del sustrato de almacenamiento. El disco deja de ser un
dispositivo que se sondea y pasa a ser una MEMORIA QUE PERDURA: un grafo
dirigido acíclico de objetos direccionados por contenido — no un sistema de
archivos plano POSIX.
### Añadido
- `kernel/src/almacen.rs` — el grafo de objetos:
- **Objeto** — una carga útil de bytes (`datos`) y una lista de aristas
(`hijos`: hashes de otros objetos). Las aristas hacen del almacén un DAG.
- **Direccionamiento por contenido** — la identidad de un objeto es el hash
BLAKE3 de su forma serializada. De ello se siguen dos propiedades que un
sistema de archivos jamás regala: INTEGRIDAD (el contenido leído se
rehashea y se verifica contra el hash pedido) y DEDUPLICACIÓN (contenido
idéntico produce el mismo hash; se almacena una sola vez).
- **Disco como log** — el sector 0 es el superbloque (magia `RENASGRF`,
versión, cursor del log y hash de la raíz); tras él se anexan los registros
de objetos, `[longitud u32 LE][payload postcard][relleno a cero]`. Un
índice en memoria (hash -> sector) se reconstruye al arrancar recorriendo
el log de cabo a rabo.
- API: `init` —monta el disco, lee o forja el superbloque, reconstruye el
índice—, `almacenar`, `recuperar`, `raiz` y `fijar_raiz`.
- Cinco capacidades nuevas del host en `wasm/env.rs` — `sys_object_put`,
`sys_object_datos`, `sys_object_hijo`, `sys_object_raiz` y
`sys_object_fijar_raiz` —, con la misma validación infranqueable de límites
de la memoria lineal que `sys_render_frame`. Distinguen dos clases de fallo:
un puntero inválido ABORTA la app —es su culpa, se traduce en trampa—; un
fallo del almacenamiento le devuelve un código de error negativo —no lo es—.
- `apps/cronista/` — la primera aplicación del userspace que escribe en el
almacenamiento PERSISTENTE. En cada arranque consulta la raíz del grafo,
graba un objeto nuevo —`datos`: el número de arranque; `hijos`: la raíz
anterior, el eslabón del DAG— y lo corona como raíz. Pinta una celda por
arranque registrado y un testigo de integridad que recorre la cadena entera.
- Dependencias: `serde` 1 y `postcard` 1 (serialización binaria compacta, el
formato que viaja al disco) y `blake3` 1 (la función hash). Las tres `no_std`.
### Cambiado
- `kernel/src/drivers/disco.rs` reescrito para un sustrato PERMANENTE:
- El asignador de marcos «bump» de la Fase 6.1b cede el paso a uno de MAPA DE
BITS con liberación real. `dma_dealloc` y `unshare` devuelven los marcos a
la arena: un almacén vivo, con su trasiego incesante de DMA, ya no la agota.
- El `VirtIOBlk` deja de montarse y destruirse en cada llamada: se monta UNA
vez y queda tras un `Mutex` global. `leer_sectores` / `escribir_sectores`
exponen la E/S de bloques — el disco deja de ser de solo lectura. Se retira
`montar_y_leer_sector0`, la sonda de un solo uso de la Fase 6.1b.
- `boot/src/main.rs`: el disco de pruebas pasa a ser el disco de objetos. Ya
no se le graba una firma — se forja virgen, a cero, y el kernel lo formatea
la primera vez que no halle el superbloque.
- `main.rs`: `informar_disco` (la sonda de la Fase 6.1b) se sustituye por
`informar_almacen`, que funda el grafo. El userspace pasa de cuatro a cinco
apps; las regiones de discola y glotona se reajustan para alojar a cronista.
### Notas
- **blake3 forzado a escalar.** El target del kernel corre sin SSE; un camino
SIMD de blake3 activado por detección en tiempo de ejecución ejecutaría
instrucciones que la CPU, sin `CR4.OSFXSR`, rechazaría con un #UD. Se fija
blake3 con `pure` + los cuatro `no_*` (`sse2`, `sse41`, `avx2`, `avx512`):
implementación puramente escalar, sin SIMD ni ensamblador.
### Verificado
- QEMU, TRES arranques consecutivos sobre el mismo disco persistente: la app
cronista pinta 1, luego 2, luego 3 celdas — la cuenta de arranques sobrevive
a los reinicios porque vive en el grafo, en el disco, no en la RAM. El
testigo de integridad queda VERDE en los tres: el DAG entero se recorre, de
la raíz al primer eslabón, y su profundidad cuadra con la cuenta. El
superbloque en disco confirma la magia `RENASGRF`, versión 1, cursor 4 y la
raíz del tercer arranque; el registro del primer arranque guarda `datos = 1`
y cero hijos. Las otras cuatro apps siguen su curso sin alteración — las dos
`hello_wasm` renderizando, discola desalojada en púrpura, glotona en amarillo.
---
## Fase 6.2 — E/S de disco asíncrona por interrupción — 2026-05-22
La Fase 6.1 hizo hablar al disco, pero por SONDEO: el procesador se quedaba en
espera activa vigilando el *used ring* de virtio, incapaz de atender nada más.
La 6.2 libera el planificador cooperativo — la E/S de bloques pasa a ser
REACTIVA, guiada por la interrupción física del dispositivo.
### Añadido
- `EsperaDisco` (`drivers/disco.rs`): una transferencia de bloques expresada
como `Future` nativo. Posee sus buferes DMA —`BlkReq`, `BlkResp` y los datos,
en el heap para una dirección estable—; su `poll` envía la petición por la
API NO BLOQUEANTE de `virtio-drivers` (`read_blocks_nb`/`write_blocks_nb`),
consulta el *used ring* (`peek_used`) y, si la transferencia sigue en vuelo,
inscribe el waker y cede. `leer_bloques`/`escribir_bloques` lo construyen.
- La IRQ del disco: `montar` descubre la línea de IRQ legada del dispositivo
(registro «Interrupt Line», offset 0x3C del espacio de configuración PCI),
registra un manejador en la IDT y abre la línea en el PIC.
`interrupts::irq_disco` → `disco::atender_irq` reconoce la interrupción en el
dispositivo —leer su registro ISR baja la línea INTx— y despierta, vía un
waker de ranura única, a la tarea que aguardaba el bloque.
- `bloquear_en` — el puente para los contextos SÍNCRONOS (el arranque, las
capacidades WASM, que no pueden `.await`): lleva un `Future` de disco hasta
su final durmiendo la CPU con `hlt` —la despiertan la IRQ del disco o el
temporizador, como red de seguridad—; jamás en espera activa con el sistema
ya en marcha.
- `pic::desenmascarar` / `pic::vector_irq`: abrir una línea concreta del par
8259 (con su cascada, si vive en el esclavo) y mapear línea → vector de IDT.
- `pci::linea_irq`: leer el registro «Interrupt Line» de un dispositivo.
- `tarea_sonda_disco` (`main.rs`): una tarea del reactor que lee el sector 0 de
forma asíncrona — la prueba viva de que la IRQ conduce la E/S sin detener a
las aplicaciones.
### Cambiado
- `leer_sectores`/`escribir_sectores` se reescriben sobre la maquinaria
asíncrona (`bloquear_en` + `EsperaDisco`). El `almacen` y las capacidades
`sys_object_*` NO cambian una línea: heredan la E/S por interrupción de
forma transparente — la espera de disco deja de quemar ciclos en sondeo.
- El `VirtIOBlk` pide al dispositivo que emita interrupciones al completar cada
petición (`enable_interrupts`).
### Decisiones de ingeniería
- **PIC, no IOAPIC.** El kernel corre íntegramente sobre el par 8259 (`pic.rs`)
y la crate `x86_64` no ofrece abstracción de APIC. Las IRQ legadas de PCI de
la máquina `q35` se enrutan por el 8259: basta leer la línea que el firmware
UEFI ya asignó y abrirla. Migrar al IOAPIC habría exigido levantar LAPIC +
IOAPIC + parseo de tablas ACPI — un subsistema entero, desproporcionado para
el objetivo. El resultado funcional es idéntico: E/S conducida por la
interrupción física.
- **Las capacidades WASM no `.await`-ean.** El intérprete `wasmi` ejecuta las
funciones de host de forma SÍNCRONA: no hay manera de suspender un módulo a
mitad de una llamada de host. Por eso `sys_object_*` no se vuelven
asíncronas; usan `bloquear_en`, que duerme la CPU con `hlt` en lugar de
sondear. El verdadero solapamiento E/S ↔ render lo aprovechan las TAREAS del
reactor (`tarea_sonda_disco` hoy; la carga dinámica de módulos, mañana).
- Una IRQ legada de PCI es de NIVEL: el manejador reconoce primero al
dispositivo —lo que baja su línea— y sólo después cierra el EOI del PIC; el
orden inverso reavivaría la interrupción en bucle.
- Todo acceso al disco toma su `Mutex` con las interrupciones desactivadas
(`without_interrupts`): la IRQ del disco jamás encuentra el cerrojo ocupado,
lo que hace imposible el interbloqueo por interrupción.
### Verificado
- QEMU: el disco virtio-blk se enruta a la IRQ 11. La `tarea_sonda_disco`
reporta «sonda asíncrona OK -- 2 IRQ de disco atendidas»: la interrupción del
disco dispara de verdad. La app cronista, sobre el disco persistente heredado
de la Fase 6.1c, continúa la cuenta de arranques (3 → 4 → 5 celdas) con el
testigo de integridad del DAG en verde — la nueva E/S asíncrona lee y escribe
el grafo sin un solo fallo. Las cuatro apps WASM siguen su curso, sin un
sobresalto ni un micro-congelamiento.
+88
View File
@@ -0,0 +1,88 @@
# renaser — guía operativa
**renaser** es un kernel asíncrono de *Espacio de Direccionamiento Único*
(SASOS) en Rust `#![no_std]`, bare-metal x86_64. Rompe con POSIX: interfaz
visual nativa desde el arranque, aislamiento **por software** (no por MMU),
el texto como caso particular del dibujo.
Documentos hermanos: `ARCHITECTURE.md` (arquitectura), `ROADMAP.md` (fases y
plan), `CHANGELOG.md` (registro técnico de cambios), `DIARIO.md` (crónica en
lenguaje llano), `README.md` (presentación).
## Construir y ejecutar
Desde la raíz del repositorio:
| Comando | Efecto |
|---|---|
| `cargo run` | compila el kernel, forja la imagen UEFI y abre QEMU |
| `cargo build` | compila el kernel + la imagen, sin lanzar QEMU |
| `cargo kernel` | alias: compila solo el kernel para `x86_64-unknown-none` |
Verificación headless (sin ventana): añadir
`-- -display none -monitor unix:/tmp/q.sock,server,nowait` y capturar con
`screendump` por el socket del monitor.
Reconstruir la app WASM del userspace tras tocar `apps/hello_wasm`:
```sh
cd apps/hello_wasm
cargo build --target wasm32-unknown-unknown --release
cp target/wasm32-unknown-unknown/release/hello_wasm.wasm ../../kernel/assets/app.wasm
```
## Estructura del espacio de trabajo
- `boot/` — orquestador de **anfitrión**: construye la imagen de disco UEFI con
la crate `bootloader` 0.11 y lanza QEMU. Es el **único miembro** del workspace.
- `kernel/` — el kernel **bare-metal**. Está **excluido** del workspace y se
compila solo como dependencia de artefacto de `boot` (target
`x86_64-unknown-none`). NUNCA debe ser miembro del workspace ni compilarse
para el anfitrión: lo haría fallar por un `_start` duplicado.
- `apps/` — aplicaciones del userspace, módulos `.wasm`
(target `wasm32-unknown-unknown`). Workspaces propios, excluidos.
Módulos del kernel (`kernel/src/`): `main`, `grafico`, `consola`, `baliza`,
`sync`, `gdt`, `interrupts`, `pic`, `drivers/`, `almacen`, `memory/`,
`async_system/`, `texto`, `wasm/`. El detalle de cada uno está en
`ARCHITECTURE.md`.
## Toolchain
Nightly, fijado en `rust-toolchain.toml`. Targets `x86_64-unknown-none` y
`wasm32-unknown-unknown`. Componentes `rust-src` y `llvm-tools` (los exige el
build script de la crate `bootloader`). Entorno verificado: Artix Linux,
QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
## Convenciones
- **Idioma:** comentarios E identificadores en español.
- **`unsafe`:** confinado en células mínimas, envuelto de inmediato en una
abstracción segura. `#![deny(unsafe_op_in_unsafe_fn)]`. Cada bloque `unsafe`
lleva un comentario `SEGURIDAD:` que justifica su corrección.
- **Framebuffer / MMIO:** siempre `core::ptr::write_volatile`; jamás asignación
indexada directa (el optimizador la elidiría).
- **`no_std` estricto:** solo `core::*` y `alloc::*` (heap vivo desde la Fase 3).
Prohibido cualquier ruta de `std::*`.
- **Alineación:** 16 bytes para estructuras genéricas; 4096 bytes (página) para
buffers de memoria y regiones de asignador.
- Código densamente comentado; se cuida la coherencia de estilo entre módulos.
## Estado
Fases 1 a 5, la 6.0, la 6.1 completa (sustrato de almacenamiento: sonda PCI,
HAL/DMA y el grafo de objetos) y la 6.2 (E/S de disco asíncrona por
interrupción) completadas y verificadas en QEMU. Ver `ROADMAP.md`.
## Flujo de trabajo
En **cada iteración** de trabajo, sin excepción:
1. Actualizar `CHANGELOG.md` — la entrada técnica detallada del cambio.
2. Actualizar `DIARIO.md` — la misma jornada contada en lenguaje llano y
elegante, sin carga técnica (es una crónica humana, no un registro técnico).
3. Verificar en QEMU si el cambio es observable (con captura de pantalla).
4. `git commit` (mensaje en español, descriptivo) y `git push origin main`.
El remoto `origin` es `gitea.gioser.net/sergio/renaser`. Mensajes de commit en
español. Verifica una fase en QEMU antes de darla por cerrada.
+1178
View File
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
# =============================================================================
# renaser :: manifiesto raíz del espacio de trabajo
# -----------------------------------------------------------------------------
# Un único espacio de direccionamiento, una única verdad de versiones.
# Las dependencias se declaran aqui una sola vez y persisten, coherentes,
# a traves de todos los miembros del sistema.
# =============================================================================
[workspace]
resolver = "2"
# Solo `boot` es miembro del espacio de trabajo: corre en el anfitrion. El
# kernel queda EXCLUIDO a proposito —es bare-metal puro y jamas debe compilarse
# para el anfitrion— y se construye unica y exclusivamente como dependencia de
# artefacto de `boot`, que le fija el target `x86_64-unknown-none`.
members = ["boot"]
# El kernel (bare-metal) y las apps WASM (target wasm32) se compilan aparte,
# cada cual con su propio target; quedan fuera del espacio de trabajo.
exclude = ["kernel", "apps"]
# -----------------------------------------------------------------------------
# Metadatos compartidos: cada miembro hereda esta identidad con `*.workspace`.
# -----------------------------------------------------------------------------
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
authors = ["JL Soltech <gerencia@jlsoltech.com>"]
description = "renaser :: kernel asincrono SASOS basado en aislamiento por software"
# -----------------------------------------------------------------------------
# Dependencias del ecosistema. Reutilizamos infraestructura madura en lugar
# de reinventar boilerplate. `default-features = false` se aplica con bisturi,
# unicamente alli donde las features por defecto arrastrarian `std`.
# -----------------------------------------------------------------------------
[workspace.dependencies]
# Constructor de la imagen de disco UEFI/BIOS. Es una herramienta de ANFITRION
# (usa std), la consume el miembro `boot` y NO debe enlazarse jamas en el kernel.
bootloader = "0.11"
# API `#![no_std]` que el kernel SI consume: expone `entry_point!`, `BootInfo`
# y la descripcion del framebuffer GOP. Su version debe acompasar a `bootloader`.
bootloader_api = "0.11"
# Abstraccion de la CPU x86_64: registros de control, GDT, IDT e instrucciones
# privilegiadas sin ensamblador inline. Es `#![no_std]` de nacimiento; su
# feature `instructions` (activa por defecto) nos da `hlt` y el control de IRQ.
x86_64 = "0.15"
# Rasterizacion de primitivas vectoriales sobre el framebuffer. `#![no_std]`,
# sin heap: en la Fase 1 solo aporta el rasgo `DrawTarget` del lienzo.
embedded-graphics = "0.8"
# Rasterizador de tipografias. REQUIERE `alloc`, de modo que solo se teje en el
# kernel una vez exista el asignador (Fase 2). `default-features = false`
# silencia su feature `std`. Se declara ya para congelar la version.
# `hashbrown` es lo que ACTIVA el modo `no_std` de fontdue; sin el, la crate
# recae en `std::collections` y no compila para bare-metal. `simd` queda fuera:
# nuestro target no tiene SSE.
fontdue = { version = "0.9", default-features = false, features = ["hashbrown"] }
# --- Fase 3 :: heap dinamico y reactor asincrono ---
# Asignador del heap del kernel — algoritmo probado, configurado `no_std`.
linked_list_allocator = "0.10"
# Exclusion mutua sin sistema operativo: `Mutex` e inicializacion unica `Once`.
spin = "0.9"
# Cola lock-free, segura frente a interrupciones, para el canal de scancodes.
crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] }
# Combinadores `Future`/`Stream` y `AtomicWaker`. Sin ejecutor ni I/O: solo
# las piezas que renaser teje en su propio reactor.
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
# --- Fase 4 :: interprete WebAssembly para el userspace aislado ---
# `wasmi` en modo bare-metal: sin `std`, sin `wat`, sin `simd` (el target no
# tiene SSE). `hash-collections` le da mapas basados en hashbrown, `no_std`.
wasmi = { version = "1.0", default-features = false, features = ["hash-collections"] }
# -----------------------------------------------------------------------------
# Perfiles de compilacion. Cargo exige que los perfiles vivan en la RAIZ del
# espacio de trabajo. `panic = "abort"` es obligatorio: sin sistema operativo
# subyacente no hay desenrollado de pila (`unwinding`) posible.
# -----------------------------------------------------------------------------
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s" # codigo compacto: cada byte cuenta en bare-metal
lto = true # el optimizador cruza fronteras de crate
codegen-units = 1 # una sola unidad => maxima oportunidad de optimizacion
+226
View File
@@ -0,0 +1,226 @@
# Diario de renaser
*Una crónica, en lenguaje llano, de cómo va naciendo un sistema operativo.
Cada jornada de trabajo añade aquí su página.*
---
## El primer día — la luz
Todo empezó con un gesto pequeño y luminoso: lograr que la máquina, al
despertar, no mostrara la fría consola de texto de siempre, sino una superficie
limpia y serena, de un azul profundo, casi nocturno. Puede parecer poca cosa,
pero esa superficie es el cimiento de cuanto vendrá: el lienzo en blanco de un
sistema entero.
Junto a esa primera luz nació también una promesa de honestidad. Si alguna vez
algo saliera terriblemente mal, renaser no se quedaría callado: pintaría una
franja roja, ancha y visible, para confesarlo sin rodeos. Una alarma sincera.
## El empaquetado — un cuerpo para viajar
Un sistema no sirve de nada si no puede encenderse en una máquina de verdad.
Así que el segundo paso fue darle a renaser un cuerpo: envolverlo en un disco
capaz de arrancar por sí solo, y enseñar al ordenador a lanzarlo. Desde
entonces, una sola orden basta para verlo cobrar vida ante nuestros ojos.
## Los reflejos — aprender a reaccionar
Hasta aquí, renaser sabía mostrarse, pero no sabía protegerse. La tercera etapa
consistió en darle reflejos: la capacidad de reaccionar ante lo inesperado en
lugar de derrumbarse en silencio. Se le tejió una red de seguridad interna para
que, ante un tropiezo, supiera recogerse con dignidad y avisar de lo ocurrido.
## El latido — sentir el tiempo y escuchar
Un sistema vivo necesita pulso. Se le dio a renaser un latido regular, como un
metrónomo interno, y oídos para escuchar el teclado. Por primera vez el sistema
dejó de estar simplemente quieto: empezó a respirar, a redibujarse con cada
latido y a notar cuándo una mano pulsaba una tecla.
## La memoria y la fluidez — crecer y atender muchas cosas
Para crecer, renaser necesitaba memoria de la que disponer con libertad. Y para
no atascarse, necesitaba aprender a atender varias cosas a la vez sin que unas
estorbaran a otras. En esta etapa recibió ambas. Además aprendió a escribir de
verdad: con una tipografía elegante, dibujando cada letra con esmero. El texto
dejó de ser un molde rígido para volverse, también él, un dibujo.
## La habitación segura — invitar a otros programas
La etapa más ambiciosa hasta ahora: abrir la puerta a que otros programas
vivieran dentro de renaser. Pero no de cualquier modo. Cada programa invitado
entra en una habitación sellada de la que no puede salir; solo puede hacer
aquello para lo que renaser, expresamente, le tiende un puente. Todo lo demás,
sencillamente, no existe para él. La primera visita fue modesta y entrañable:
un cuadrado de color que se desliza por la pantalla obedeciendo al teclado.
## Una jornada de orden — ordenar la casa y guardar la historia
No todo es construir; a veces toca ordenar. Se revisó la estructura del
proyecto y se repartió mejor el trabajo entre sus piezas, para que cada una
tuviera un cometido claro y nítido. Se escribió la documentación que cuenta qué
es renaser y hacia dónde camina. Y se guardó todo el trabajo en un lugar
seguro, donde queda registro fiel de cada cambio, de modo que ninguna jornada
se pierda. Hubo, además, un pequeño susto: una contraseña se coló por error
donde no debía; se retiró de inmediato y se dejó constancia para corregirla.
## El reparto del tiempo — muchos huéspedes, ningún tirano
Hasta esta jornada, el programa invitado de renaser, una vez dentro, se quedaba
con la casa entera: hablaba sin pausa y no dejaba sitio a nadie más. Era un solo
huésped, y se comportaba como dueño.
Esta etapa enseñó a renaser a repartir el tiempo. Se acordó un compás —el latido
del sistema marca, una y otra vez, el turno de cada cual— y se acordó también un
gesto de cortesía: cada programa hace su pequeña tarea, pinta su fotograma y, al
terminar, cede el paso. Así, lo que antes era un único inquilino pasaron a ser
varios, conviviendo en paz, cada uno en su propia ventana de la pantalla.
Pero la cortesía, por sí sola, es frágil: basta un huésped maleducado —uno que
no quiera ceder nunca el turno— para paralizarlo todo. Por eso se añadió una
salvaguarda serena pero firme: a cada programa se le entrega, en cada turno, una
ración medida de tiempo. Si la agota sin terminar —porque se ha enredado en un
bucle sin fin—, renaser le retira el turno con delicadeza, tiñe su ventana de un
púrpura inconfundible y prosigue. El sistema no se cuelga; simplemente despide al
inquilino díscola y sigue atendiendo a los demás, que ni se enteran.
Para comprobarlo se invitó, a propósito, a un huésped díscola: un programa hecho
para enredarse en un bucle eterno. Al arrancar, las dos ventanas honradas
cobraron vida y obedecieron al teclado a la vez, en perfecta armonía; la tercera,
la del díscola, se apagó al instante en un púrpura tranquilo. La promesa quedó
demostrada: renaser es veloz porque confía, pero nunca ingenuo.
## La otra mitad del muro — el espacio, no solo el tiempo
La jornada anterior enseñó a renaser a repartir el tiempo y a contener al que no
quería cederlo. Pero un huésped puede portarse mal de dos maneras: acaparando el
tiempo, sí, o acaparando el espacio. Quedaba media casa por proteger.
En esta etapa se le puso a cada programa un techo a la memoria que puede
reclamar. Mientras se conforme con su habitación, vive tranquilo; si pretende
derribar las paredes para anexarse la casa entera, renaser se lo impide con
serenidad y, como con el díscola, lo despide —esta vez tiñendo su ventana de un
amarillo pálido, para que de un vistazo se distinga al que abusó del espacio del
que abusó del tiempo.
Se cerró además una pequeña negligencia de la jornada anterior: cuando un
huésped era despedido, renaser olvidaba recoger del todo sus cosas. Ahora, al
marcharse cualquier programa, el sistema reclama hasta el último de sus enseres;
no queda rastro. Y se invitó a un nuevo huésped de prueba, una aplicación
glotona hecha para devorar memoria: al arrancar, fue frenada y despedida al
instante, su ventana amarilla junto a la púrpura del díscola, mientras las dos
aplicaciones honradas seguían su baile, ajenas a todo.
Con esto, las dos dimensiones físicas —el tiempo y el espacio— quedan bajo el
gobierno firme y sereno de renaser. El siguiente horizonte es más hondo: dar al
sistema una memoria que perdure, un lugar donde guardar las cosas más allá del
apagado. Pero esa es ya otra página.
## El primer golpe a la roca — encontrar el disco
Dar a renaser una memoria que perdure exige, antes que nada, hablar con un
disco. Y un disco, a diferencia de la pantalla o del teclado, no se le anuncia
al sistema al nacer: hay que salir a buscarlo. Es una empresa grande, así que se
decidió acometerla con prudencia, a pasos cortos y firmes.
El primer paso fue, sencillamente, encontrar el disco. renaser aprendió a
recorrer el bus por el que se enganchan los periféricos de la máquina,
preguntando puerta por puerta «¿quién hay aquí?», hasta dar con el disco virtual
que se le había preparado. Y al hallarlo, lo dijo en voz alta sobre su propia
pantalla: lo había localizado, y supo decir en qué dirección vivía.
Puede parecer un gesto pequeño —una línea de texto—, pero es el primer golpe de
pico contra una roca dura. La conversación con el disco apenas empieza; pero la
puerta, al menos, ya está encontrada.
## La primera palabra del disco — leer lo que se escribió
Encontrada la puerta, faltaba lo más difícil: cruzarla. Hablar de verdad con un
disco no es pedirle las cosas y esperar: es tender con él una memoria
compartida, un terreno común donde el sistema deja una petición y el disco
deposita su respuesta. Montar ese terreno —y aprender el protocolo para usarlo—
fue el trabajo de esta jornada, el más cercano al metal de cuantos renaser ha
acometido.
Para no equivocar el paso se ideó una prueba sencilla y honesta. Al preparar el
disco, el sistema anfitrión grabó en su primer renglón una breve firma, una
palabra clave. Y entonces se le pidió a renaser que, ya por sus propios medios,
abriera el disco y leyera ese mismo renglón. Si la palabra volvía intacta, no
habría duda: el camino completo —encontrar el disco, tender la memoria
compartida, pedir, esperar y recibir— funcionaba de cabo a rabo.
Al arrancar, renaser lo dijo en su pantalla, sereno: había leído el sector
cero, y la palabra que mostró era, letra por letra, la que se había grabado. El
disco había hablado, y renaser lo había entendido. La roca, por fin, cedió.
Queda por delante lo más hermoso: sobre esta conversación recién abierta,
levantar una verdadera memoria duradera —no un archivo de los de antes, sino un
tejido de objetos—. Pero el cimiento ya está puesto, y es firme.
## Un tejido de objetos — la memoria que perdura
La conversación con el disco estaba abierta; faltaba decidir qué contarle. El
mundo que renaser hereda de la informática de siempre habría respondido sin
pensarlo: archivos, carpetas, rutas con barras. renaser eligió otra cosa.
En lugar de un archivero de cajones con etiquetas, se tejió un grafo de
objetos. Cada objeto es un puñado de datos y unos cuantos hilos que lo enlazan
con otros. Y —esta es la idea hermosa— ningún objeto lleva un nombre que
alguien le haya puesto: su nombre es su contenido. De cada uno se calcula una
huella única, irrepetible, y esa huella es su identidad. Dos cosas idénticas
tienen la misma huella, de modo que nada se guarda dos veces; y si una se
corrompe, su huella deja de cuadrar y la mentira se delata sola.
Para que esa memoria mereciera el nombre de duradera hubo que pulir el trato
con el disco: enseñarle a renaser no solo a leer, sino a escribir. Y poner
orden en la despensa de la que el sistema toma prestada memoria para hablar con
el hardware — que ahora, al terminar cada gestión, devuelve lo que tomó en vez
de acumularlo sin fin.
Y entonces llegó la prueba más bonita. Se escribió una pequeña aplicación, una
cronista, con un único oficio: llevar la cuenta de las veces que el sistema
despierta. En cada arranque consulta el grafo, encuentra el último apunte,
añade el suyo —enlazado al anterior, como el eslabón nuevo de una cadena— y
pinta una casilla por cada despertar registrado.
Se apagó la máquina y se volvió a encender. Y otra vez. La cronista dibujó
primero una casilla, luego dos, luego tres. La cuenta no se perdía con el
apagón: vivía en el disco, en el tejido de objetos, y cada reinicio la
encontraba intacta y la hacía crecer. Por primera vez, renaser recordaba algo
de una vida a la siguiente.
La memoria duradera ya no es una promesa. Es un tejido, y tiene sus primeros
hilos.
## El disco que avisa — la espera que ya no congela
Hablar con el disco, hasta esta jornada, tenía un precio oculto. Cada vez que
renaser le pedía un bloque, el sistema entero contenía el aliento: el procesador
se quedaba mirando fijamente al disco, preguntando una y otra vez «¿ya?, ¿ya?»,
sin hacer nada más, hasta que la respuesta llegaba. Para una lectura suelta
apenas se notaba. Pero el futuro de renaser —cargar aplicaciones enteras desde
el disco— habría convertido esos instantes en tirones visibles, en pequeños
congelamientos de la imagen.
Así que se le enseñó al disco a AVISAR. En lugar de que el sistema vigile, ahora
es el disco quien, al terminar, da un golpecito en el hombro del kernel —una
interrupción— para decir «listo». Y mientras tanto, renaser no aguarda de brazos
cruzados: si hay trabajo, lo hace; si no, se adormece un instante, y el propio
golpecito del disco lo despierta. Ni un ciclo malgastado.
Para no equivocar el paso se escribió una pequeña sonda: una tarea que pide al
disco su primer bloque y, en vez de quedarse esperando, cede el turno. Las
aplicaciones de la pantalla siguieron pintándose, fluidas, mientras el disco
trabajaba por su cuenta; y cuando el bloque estuvo listo, su aviso reanudó la
sonda. En la pantalla quedó escrito: la lectura se había atendido por aviso, no
por vigilancia.
Es un cambio que casi no se ve —la cronista sigue contando arranques, una
casilla más cada vez— pero que se sentirá en todo lo que venga después. El disco
y renaser ya no se miran fijamente: se hablan, y entre frase y frase, cada cual
atiende lo suyo.
---
*El diario continúa. La próxima página la escribirá la próxima jornada.*
+48
View File
@@ -0,0 +1,48 @@
# renaser
**renaser** es un kernel asíncrono de *Espacio de Direccionamiento Único*
(SASOS), escrito en Rust `#![no_std]` para x86_64 bare-metal.
Es un sistema operativo disruptivo que rompe por completo con el paradigma
POSIX de los años 70: no emula Linux, no usa archivos planos, no usa TTYs ni
capas GNU. El aislamiento entre aplicaciones no descansa en la MMU ni en los
anillos de privilegio de la CPU, sino en **límites matemáticos sobre el
bytecode** — aislamiento por software (SFI). La interfaz es visual desde el
primer microsegundo: el texto es, simplemente, un caso particular del dibujo.
## Qué hace, hoy
- Arranca por **UEFI** y adopta el framebuffer GOP con doble búfer sin parpadeo.
- Se **autoempaqueta** en una imagen de disco UEFI y se lanza en QEMU.
- Tiene **reflejos de fallo**: GDT/TSS, IDT y manejadores de excepción; si
colapsa, lo dibuja (franja roja de pánico, naranja de memoria agotada).
- **Late con el hardware**: PIC remapeado, temporizador (PIT) y teclado.
- Gestiona **memoria dinámica** (heap de 64 MiB, asignador global).
- Ejecuta un **reactor asíncrono cooperativo** sobre los `Future` nativos de
Rust: las interrupciones no conmutan contexto, despiertan tareas.
- Rasteriza **texto vectorial** al vuelo con `fontdue`.
- Ejecuta un **userspace WebAssembly** aislado por capacidades (`wasmi`): las
aplicaciones solo tocan el mundo a través de funciones de host concedidas.
## Construir y ejecutar
Requisitos: `rustup` con toolchain nightly, QEMU y firmware OVMF.
```sh
cargo run
```
Compila el kernel para `x86_64-unknown-none`, forja la imagen de disco UEFI y
abre QEMU. Ver `CLAUDE.md` para el resto de comandos y el flujo de la app WASM.
## Documentación
| Documento | Contenido |
|---|---|
| `ARCHITECTURE.md` | la arquitectura del sistema, subsistema a subsistema |
| `ROADMAP.md` | fases completadas y plan de las siguientes |
| `CLAUDE.md` | guía operativa: comandos, estructura y convenciones |
## Licencia
MPL-2.0
+108
View File
@@ -0,0 +1,108 @@
# renaser — Hoja de ruta
Estado de las fases del proyecto. Para la arquitectura, ver `ARCHITECTURE.md`.
## Fases completadas
Todas verificadas en QEMU (captura de pantalla incluida en su momento).
### Fase 1 — el primer microsegundo
Arranque UEFI, adopción del framebuffer GOP, lienzo de doble búfer y la baliza
de pánico (franja roja al colapsar). Punto de entrada con `bootloader_api`.
### Fase 1.5 — empaquetado y arranque
Miembro `boot/`: constructor de la imagen de disco UEFI (crate `bootloader`
0.11, vía dependencia de artefacto) y lanzador de QEMU. `cargo run` pasa a
compilar, forjar la imagen y arrancar, todo de un gesto.
### Fase 2.0 — cimientos del manejo de fallos
GDT propia + TSS con stack de emergencia (IST) para el doble fallo. IDT con
manejadores de excepción de CPU. El breakpoint es recuperable; el resto de
excepciones encienden la baliza.
### Fase 2.1 — interrupciones de hardware
Remapeo del PIC 8259 fuera del rango de las excepciones. Temporizador (PIT) a
100 Hz e IRQ1 de teclado. El kernel pasa de "pintar una vez" a un bucle de
render despertado por el hardware.
### Fase 3 — memoria dinámica y reactor asíncrono
Heap de 64 MiB con `linked_list_allocator` como asignador global; manejador
propio de OOM (franja naranja). Reactor cooperativo: `Executor`, `Task`,
`Waker`. Texto vectorial con `fontdue`: el texto deja de ser mapa de bits.
### Fase 4 — el escudo de aislamiento WASM
Runtime `wasmi` `no_std`. Matriz de capacidades con dos funciones de host
(`sys_render_frame`, `sys_get_scancode`) y validación infranqueable de límites
de la memoria lineal. Primera app del userspace: `apps/hello_wasm`, un módulo
`wasm32` aislado que pinta y responde al teclado.
### Fase 5 — multitarea cooperativa, fuel y reloj
Unificación del reactor (Fase 3) y el runtime WASM (Fase 4). El ABI del
userspace pasa a `init`/`tick`: cada `tick` es un punto de cesión cooperativa.
`async_system::reloj` convierte la IRQ0 en el `Future` `EsperaFrame`, que marca
el compás de los fotogramas. Cada app es una `AplicacionWasm` persistente y una
tarea del reactor. Escudo de combustible (`fuel`): cada `tick` corre con un
presupuesto estricto; agotarlo desaloja la app sin tocar al kernel. Capacidades
ampliadas — regiones de dibujo por app y canal de teclado por app. Verificado
en QEMU con tres apps concurrentes, una de ellas díscola y desalojada en vivo.
### Fase 6.0 — cuotas de memoria y ciclo de vida
Completa el aislamiento espacial del userspace. Cada `AplicacionWasm` instancia
su `Store` con un `StoreLimits` (techo de memoria lineal de 4 MiB, vía
`Store::limiter`); rebasarlo es una trampa que desaloja la app con baliza
amarilla — gemela de la púrpura del desalojo por combustible. `Drop` para
`AplicacionWasm` reconcilia el ciclo de vida: da de baja el canal de teclado de
la difusión de la IRQ1. Verificado en QEMU con cuatro apps — una díscola
(desalojo temporal) y una glotona (desalojo espacial), ambas en vivo.
## Fase 6.1 — sustrato de almacenamiento (completada)
Estrategia incremental para el almacenamiento, frente al riesgo del hardware:
- **6.1a — Sonda PCI** *(hecha)*`drivers/pci.rs` enumera el bus PCI por
`0xCF8`/`0xCFC` y localiza el disco virtio-blk; `boot` forja el disco de
pruebas y lo adjunta como `virtio-blk-pci`. El muro del descubrimiento de
hardware queda derribado.
- **6.1b — HAL y lectura de sector** *(hecha)*`drivers/disco.rs`: asignador
de marcos «bump», `KernelHal` (el `trait Hal` de `virtio-drivers`: DMA y
traducción de direcciones) y `montar_y_leer_sector0`, que lee el sector 0 por
sondeo. Verificado por una firma que viaja del anfitrión al disco y vuelve.
- **6.1c — Grafo de objetos** *(hecha)*`almacen.rs`: el almacenamiento como
DAG direccionado por contenido —la identidad de un objeto es el hash BLAKE3
de su forma serializada (`postcard`); el disco se organiza como un log con
superbloque e índice—, y las cinco capacidades `sys_object_*` que lo exponen
al userspace. `drivers/disco.rs` gana un asignador de marcos con liberación
real (mapa de bits), escritura de sectores y un `VirtIOBlk` persistente. La
app `cronista` lleva la cuenta de los arranques en el grafo: la cuenta
perdura entre reinicios.
## Fase 6.2 — E/S de disco asíncrona por interrupción (completada)
La Fase 6.1 hizo hablar al disco, pero por **sondeo**: el procesador se quedaba
en espera activa vigilando el *used ring* de virtio. La 6.2 libera el
planificador cooperativo — la E/S de bloques pasa a ser **reactiva**:
- `EsperaDisco` — una transferencia de bloques como `Future` nativo, sobre la
API no bloqueante de `virtio-drivers` (`read_blocks_nb` / `peek_used` /
`complete_*`). Cede la CPU mientras el disco trabaja.
- La **IRQ del disco**`montar` descubre la línea de IRQ legada del
dispositivo (registro «Interrupt Line» del espacio de configuración PCI), la
enruta por el 8259 y registra su manejador; `atender_irq` reconoce la
interrupción y despierta a la tarea que aguardaba el bloque.
- `bloquear_en` — el puente para los contextos síncronos (el arranque, las
capacidades WASM): duerme la CPU con `hlt` en vez de sondear.
Decisión de ingeniería: las IRQ se enrutan por el **PIC 8259** que el kernel ya
gobierna, no por el IOAPIC — basta leer la línea que el firmware ya asignó.
Verificado en QEMU: el disco se enruta a la IRQ 11; una tarea-sonda del reactor
lee un bloque de forma asíncrona mientras las apps siguen pintando.
Líneas abiertas posteriores: carga/descarga dinámica de apps desde el grafo de
objetos; más capacidades del host (temporización, audio).
## Principios que persisten entre fases
- Reutilizar infraestructura madura de la comunidad antes que reinventar.
- `unsafe` mínimo, confinado y justificado.
- Verificar cada fase en QEMU antes de cerrarla.
- `git commit` + `git push` tras cada iteración.
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "cronista"
version = "0.1.0"
+31
View File
@@ -0,0 +1,31 @@
# =============================================================================
# renaser :: apps/cronista — Fase 6.1c :: el primer escriba del userspace
# -----------------------------------------------------------------------------
# Un modulo WebAssembly que, a diferencia de sus vecinas, deja HUELLA. En cada
# arranque graba un objeto en el grafo persistente del kernel —enlazado al del
# arranque anterior, formando una cadena— y lo corona como raiz. Asi lleva la
# cronica de cuantas veces ha despertado el sistema: una cuenta que sobrevive
# a los reinicios porque vive en el disco, no en la RAM.
# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
# =============================================================================
[package]
name = "cronista"
version = "0.1.0"
edition = "2021"
description = "renaser :: app WASM cronista — la cronica persistente de los arranques"
[workspace]
# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
# wasmi instancia.
[lib]
crate-type = ["cdylib"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true
+234
View File
@@ -0,0 +1,234 @@
// =============================================================================
// renaser :: apps/cronista — Fase 6.1c :: el primer escriba del userspace
// -----------------------------------------------------------------------------
// Las apps de fases anteriores eran efimeras: su mundo se borraba al apagar la
// maquina. `cronista` es la primera que deja HUELLA. En cada arranque:
//
// 1. pregunta al kernel por la RAIZ del grafo de objetos —la cabeza de la
// cadena de arranques anteriores—;
// 2. lee de ella el numero del ultimo arranque;
// 3. graba un objeto nuevo —sus datos: el numero de arranque; su hijo: la
// raiz anterior— y lo corona como raiz;
// 4. recorre la cadena entera para verificar que el DAG persiste integro;
// 5. pinta una celda por cada arranque registrado.
//
// La cuenta NO vive en la RAM: vive en el disco, en el grafo direccionado por
// contenido. Sobrevive a los reinicios. Cada vez que renaser despierta, la
// cronista añade un eslabon a la cadena y una celda a su rejilla.
//
// Sus unicas vias hacia el mundo son las capacidades `sys_object_*` que el
// kernel le inyecta. No conoce el disco, ni el bus PCI, ni el formato en
// sectores: solo objetos, hashes y aristas.
// =============================================================================
#![no_std]
// --- Las capacidades que el kernel `renaser` inyecta a esta aplicacion. ---
#[link(wasm_import_module = "renaser")]
extern "C" {
/// Compone un bufer de pixeles (de ESTA memoria lineal) en la region que el
/// kernel asigno a esta aplicacion.
fn sys_render_frame(ptr: u32, len: u32);
/// Graba un objeto en el grafo: `datos` es su carga util; `hijos` apunta a
/// un arreglo de `hijos_cnt` hashes de 32 bytes —las aristas—. El hash
/// resultante se escribe en `salida`. Devuelve 0 si todo fue bien.
fn sys_object_put(datos: u32, datos_len: u32, hijos: u32, hijos_cnt: u32, salida: u32) -> i32;
/// Copia la carga util del objeto `hash` en `salida`. Devuelve el numero de
/// bytes copiados, o un valor negativo si fallo.
fn sys_object_datos(hash: u32, salida: u32, capacidad: u32) -> i32;
/// Devuelve el numero de hijos del objeto `hash` y, si `indice` es valido,
/// escribe el hash de ese hijo en `salida`. Negativo si el objeto no existe.
fn sys_object_hijo(hash: u32, indice: u32, salida: u32) -> i32;
/// Escribe en `salida` el hash de la raiz del grafo. Devuelve 1 si hay
/// raiz, 0 si el grafo aun esta vacio.
fn sys_object_raiz(salida: u32) -> i32;
/// Corona el objeto `hash` como raiz del grafo. Devuelve 0 si lo logro.
fn sys_object_fijar_raiz(hash: u32) -> i32;
}
/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
#[panic_handler]
fn al_fallar(_: &core::panic::PanicInfo) -> ! {
loop {}
}
// --- Geometria de la escena. El ancho y el alto DEBEN coincidir con la region
// que el kernel asigna a esta app. ---
const ANCHO: usize = 360;
const ALTO: usize = 80;
/// Lado de paso de la rejilla de celdas, en pixeles.
const PASO: usize = 20;
/// Lado de una celda, en pixeles.
const LADO: usize = 16;
/// Celdas que caben en una fila.
const POR_FILA: usize = ANCHO / PASO;
/// Celdas que caben en la rejilla entera — el techo de lo que se pinta.
const MAX_CELDAS: usize = POR_FILA * (ALTO / PASO);
/// Indigo casi negro: el fondo del lienzo de la cronista.
const FONDO: u32 = 0x0E_14_22;
/// Ambar calido: una celda por arranque registrado.
const CELDA: u32 = 0xF2_B2_33;
/// Verde: el DAG se recorrio integro de la raiz al primer eslabon.
const VERDE: u32 = 0x35_C4_6A;
/// Rojo: la cadena se rompio — un objeto no resolvio.
const ROJO: u32 = 0xD4_1E_2C;
/// El lienzo de la aplicacion, en SU propia memoria lineal.
static mut LIENZO: [u32; ANCHO * ALTO] = [0; ANCHO * ALTO];
// --- Buferes de intercambio con las capacidades `sys_object_*`. El kernel lee
// y escribe hashes y datos AQUI, siempre dentro de esta memoria lineal. ---
/// El hash de la raiz anterior — la cabeza de la cadena al arrancar.
static mut HASH_RAIZ: [u8; 32] = [0; 32];
/// El hash del objeto que esta cronista graba en este arranque.
static mut HASH_NUEVO: [u8; 32] = [0; 32];
/// Hash de trabajo para recorrer la cadena del DAG.
static mut HASH_AUX: [u8; 32] = [0; 32];
/// Los ocho bytes del numero de arranque, de ida y de vuelta del grafo.
static mut DATOS_IO: [u8; 8] = [0; 8];
/// Preparacion: el kernel la invoca UNA sola vez. Aqui ocurre toda la cronica —
/// leer el grafo, grabar el eslabon nuevo, verificar el DAG y pintar.
#[no_mangle]
pub extern "C" fn init() {
// 1. ¿Hay ya una raiz? Es la cabeza de la cadena de arranques anteriores.
let tiene_raiz = unsafe { sys_object_raiz(core::ptr::addr_of_mut!(HASH_RAIZ) as u32) } == 1;
// 2. El numero del ultimo arranque vive en los `datos` de esa raiz.
let mut previo: u64 = 0;
if tiene_raiz {
let leidos = unsafe {
sys_object_datos(
core::ptr::addr_of!(HASH_RAIZ) as u32,
core::ptr::addr_of_mut!(DATOS_IO) as u32,
8,
)
};
if leidos == 8 {
// SEGURIDAD: lectura de un escalar `Copy` de un estatico propio.
previo = u64::from_le_bytes(unsafe { *core::ptr::addr_of!(DATOS_IO) });
}
}
let cuenta = previo + 1;
// 3. Grabar el objeto de ESTE arranque. Sus `datos` son el numero de
// arranque; su unico hijo, la raiz anterior — el eslabon nuevo del DAG.
// SEGURIDAD: escritura de un escalar `Copy` a un estatico propio.
unsafe {
*core::ptr::addr_of_mut!(DATOS_IO) = cuenta.to_le_bytes();
}
let (hijos_ptr, hijos_cnt) = if tiene_raiz {
(core::ptr::addr_of!(HASH_RAIZ) as u32, 1u32)
} else {
(0u32, 0u32)
};
let grabado = unsafe {
sys_object_put(
core::ptr::addr_of!(DATOS_IO) as u32,
8,
hijos_ptr,
hijos_cnt,
core::ptr::addr_of_mut!(HASH_NUEVO) as u32,
)
};
// 4. Coronar el objeto nuevo como raiz y verificar la integridad del DAG.
let mut integro = false;
if grabado == 0
&& unsafe { sys_object_fijar_raiz(core::ptr::addr_of!(HASH_NUEVO) as u32) } == 0
{
integro = verificar_cadena(cuenta);
}
// 5. Pintar la cronica: una celda por arranque, un testigo de integridad.
pintar(cuenta, integro);
}
/// Un fotograma de trabajo. El numero de arranque no cambia durante una sesion:
/// la cronica que `init` pinto persiste en el lienzo del kernel. `tick` solo
/// cede el control, fiel al ABI cooperativo — no toda app necesita redibujar.
#[no_mangle]
pub extern "C" fn tick() {}
/// Recorre la cadena del DAG desde el objeto recien grabado, descendiendo por
/// el hijo 0, y comprueba que su profundidad coincide con el numero de
/// arranque. Si coincide, el grafo entero se leyo de vuelta del disco integro.
fn verificar_cadena(cuenta: u64) -> bool {
// Partir del objeto recien grabado.
// SEGURIDAD: copia de un arreglo `Copy` entre dos estaticos propios.
unsafe {
*core::ptr::addr_of_mut!(HASH_AUX) = *core::ptr::addr_of!(HASH_NUEVO);
}
let mut profundidad: u64 = 0;
loop {
profundidad += 1;
// `sys_object_hijo` lee el hash de HASH_AUX y, si hay hijo, escribe el
// del hijo 0 en el MISMO bufer: la cadena desciende un eslabon.
let hijos = unsafe {
sys_object_hijo(
core::ptr::addr_of!(HASH_AUX) as u32,
0,
core::ptr::addr_of_mut!(HASH_AUX) as u32,
)
};
// Sin hijos: fin de la cadena — el primer arranque de todos. Un valor
// negativo seria un objeto que no resolvio: la cadena estaria rota.
if hijos <= 0 || profundidad >= 4096 {
break;
}
}
profundidad == cuenta
}
/// Pinta la cronica: el fondo, una celda ambar por arranque y, en la esquina,
/// el testigo de integridad del grafo.
fn pintar(cuenta: u64, integro: bool) {
// SEGURIDAD: durante `init` esta es la unica via de acceso a LIENZO, y el
// kernel jamas reentra el modulo mientras `init` corre.
let lienzo: &mut [u32] = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
for pixel in lienzo.iter_mut() {
*pixel = FONDO;
}
// Una celda ambar por cada arranque registrado, dispuestas en rejilla.
let celdas = (cuenta as usize).min(MAX_CELDAS);
for i in 0..celdas {
let x = (i % POR_FILA) * PASO + 2;
let y = (i / POR_FILA) * PASO + 2;
rellenar(lienzo, x, y, LADO, LADO, CELDA);
}
// El testigo de integridad: verde si la cadena se recorrio entera de la
// raiz al primer eslabon, rojo si algo se rompio.
let testigo = if integro { VERDE } else { ROJO };
rellenar(lienzo, ANCHO - 14, 4, 10, 10, testigo);
// SEGURIDAD: `sys_render_frame` es una capacidad del host; el (ptr, len)
// describe nuestra propia memoria lineal y el host lo verifica sin piedad.
unsafe {
sys_render_frame(lienzo.as_ptr() as u32, (ANCHO * ALTO * 4) as u32);
}
}
/// Rellena un rectangulo, recortado con firmeza a los limites del lienzo.
fn rellenar(lienzo: &mut [u32], x: usize, y: usize, ancho: usize, alto: usize, color: u32) {
let x1 = (x + ancho).min(ANCHO);
let y1 = (y + alto).min(ALTO);
let mut fila = y;
while fila < y1 {
let base = fila * ANCHO;
let mut col = x;
while col < x1 {
lienzo[base + col] = color;
col += 1;
}
fila += 1;
}
}
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "discola"
version = "0.1.0"
+30
View File
@@ -0,0 +1,30 @@
# =============================================================================
# renaser :: apps/discola — el inquilino discolo del userspace
# -----------------------------------------------------------------------------
# Un modulo WebAssembly construido a proposito para portarse mal: su `tick`
# cae en un bucle cerrado y jamas retorna. Existe para una sola cosa —
# demostrar que el escudo de combustible (fuel) del kernel `renaser` lo
# desaloja sin colgar el sistema ni perturbar a sus apps vecinas.
# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
# =============================================================================
[package]
name = "discola"
version = "0.1.0"
edition = "2021"
description = "renaser :: app WASM discola — un bucle infinito que el fuel fulmina"
[workspace]
# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
# wasmi instancia.
[lib]
crate-type = ["cdylib"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true
+46
View File
@@ -0,0 +1,46 @@
// =============================================================================
// renaser :: apps/discola — Fase 5 :: el inquilino discolo del userspace
// -----------------------------------------------------------------------------
// Esta aplicacion esta MAL a proposito. Su `tick` no hace un fotograma de
// trabajo y retorna —como manda el ABI cooperativo—: cae en un bucle cerrado
// y jamas devuelve el control. En un sistema cooperativo ingenuo, eso colgaria
// la maquina entera.
//
// Pero renaser ejecuta cada `tick` con un presupuesto estricto de COMBUSTIBLE.
// Cuando este bucle lo agota, el runtime `wasmi` lanza una trampa, el kernel
// recupera el mando y desaloja a este modulo — tatuando su region de purpura.
// El resto del userspace ni se entera. Eso es lo que esta app demuestra.
// =============================================================================
#![no_std]
/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
#[panic_handler]
fn al_fallar(_: &core::panic::PanicInfo) -> ! {
loop {}
}
/// Sumidero de las escrituras del bucle: obliga al compilador a CONSERVAR cada
/// iteracion —y, con ella, su consumo de combustible— en lugar de elidirla.
static mut SUMIDERO: u64 = 0;
/// Preparacion. No hay nada honrado que preparar: el kernel la invoca, retorna
/// sin incidentes, y la app pasa por buena... hasta su primer `tick`.
#[no_mangle]
pub extern "C" fn init() {}
/// El fotograma que nunca termina. Un bucle cerrado, deliberado: jamas retorna.
/// El kernel `renaser` lo cortara por agotamiento de combustible y desalojara
/// esta aplicacion sin que el sistema sufra un solo sobresalto.
#[no_mangle]
pub extern "C" fn tick() {
let mut contador: u64 = 0;
loop {
contador = contador.wrapping_add(1);
// SEGURIDAD: escritura volatil a un escalar estatico; su unico fin es
// que el optimizador no pueda vaciar el bucle. No se crea referencia.
unsafe {
core::ptr::write_volatile(core::ptr::addr_of_mut!(SUMIDERO), contador);
}
}
}
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "glotona"
version = "0.1.0"
+30
View File
@@ -0,0 +1,30 @@
# =============================================================================
# renaser :: apps/glotona — el inquilino voraz del userspace
# -----------------------------------------------------------------------------
# Un modulo WebAssembly construido a proposito para devorar memoria: su `tick`
# reclama paginas de memoria lineal sin freno. Existe para demostrar que el
# techo ESPACIAL del kernel `renaser` lo desaloja —baliza amarilla— sin agotar
# el heap del sistema ni perturbar a sus apps vecinas.
# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
# =============================================================================
[package]
name = "glotona"
version = "0.1.0"
edition = "2021"
description = "renaser :: app WASM glotona — devora memoria; el techo espacial la frena"
[workspace]
# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
# wasmi instancia.
[lib]
crate-type = ["cdylib"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true
+39
View File
@@ -0,0 +1,39 @@
// =============================================================================
// renaser :: apps/glotona — Fase 6.0 :: el inquilino voraz del userspace
// -----------------------------------------------------------------------------
// Esta aplicacion esta MAL a proposito, como su hermana `discola` — pero en la
// otra dimension. Donde `discola` devora TIEMPO (un bucle sin fin), `glotona`
// devora ESPACIO: su `tick` invoca `memory.grow` reclamando memoria lineal sin
// freno alguno.
//
// renaser le impone a cada modulo un techo de memoria. Cuando esta peticion lo
// rebasa, el runtime `wasmi` —configurado para ello— lanza una trampa en vez
// de devolver un discreto -1; el kernel la captura, desaloja a este modulo y
// tiñe su region de amarillo palido. El heap del sistema jamas corre peligro.
// =============================================================================
#![no_std]
/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
#[panic_handler]
fn al_fallar(_: &core::panic::PanicInfo) -> ! {
loop {}
}
/// Preparacion. Nada honrado que preparar: la app pasa por buena hasta su
/// primer `tick`, igual que su hermana `discola`.
#[no_mangle]
pub extern "C" fn init() {}
/// El fotograma voraz. Reclama 4096 paginas de memoria lineal de un golpe —
/// 256 MiB, muy por encima de cualquier techo razonable. El kernel `renaser`
/// denegara la expansion con una trampa y desalojara esta aplicacion.
#[no_mangle]
pub extern "C" fn tick() {
// `memory_grow` sobre la memoria 0. Con el techo espacial activo y la
// denegacion configurada como trampa, esta instruccion NO retorna: aborta.
let _ = core::arch::wasm32::memory_grow(0, 4096);
// Red de seguridad: si la denegacion no fuese una trampa, este bucle
// cerrado garantiza el desalojo —por combustible— de todos modos.
loop {}
}
+7
View File
@@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "hello_wasm"
version = "0.1.0"
+28
View File
@@ -0,0 +1,28 @@
# =============================================================================
# renaser :: apps/hello_wasm — primera aplicacion del userspace aislado
# -----------------------------------------------------------------------------
# No es un ELF: es un modulo WebAssembly puro. Se compila para wasm32 y el
# kernel lo ejecuta dentro de wasmi, encerrado en su propia memoria lineal.
# Tiene su propio `[workspace]`: queda fuera del espacio de trabajo del kernel.
# =============================================================================
[package]
name = "hello_wasm"
version = "0.1.0"
edition = "2021"
description = "renaser :: app WASM de prueba — un cuadrado movil dirigido por teclado"
[workspace]
# `cdylib` produce un modulo `.wasm` que exporta funciones — el formato que
# wasmi instancia. La aplicacion solo habla con el kernel por funciones del host.
[lib]
crate-type = ["cdylib"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"
opt-level = "s"
lto = true
+140
View File
@@ -0,0 +1,140 @@
// =============================================================================
// renaser :: apps/hello_wasm — Fase 4/5 :: el primer ciudadano del userspace
// -----------------------------------------------------------------------------
// Esta aplicacion vive DENTRO de su propia memoria lineal de WebAssembly. No
// conoce la MMU, no conoce los anillos de privilegio de la CPU: su unica via
// hacia el mundo son las dos capacidades que el kernel le inyecta. Lo que no
// este importado, sencillamente, no tiene camino fisico que recorrer.
//
// FASE 5 :: el ABI deja de ser un `run()` que se queda dentro para siempre.
// Ahora la app exporta `init()` —preparacion, una sola vez— y `tick()` —un
// fotograma de trabajo, y RETORNA—. Ese retorno es el punto de cesion
// cooperativa: el kernel recupera el control y atiende a las demas apps.
// =============================================================================
#![no_std]
// --- Las dos UNICAS capacidades que el kernel `renaser` expone al modulo. ---
#[link(wasm_import_module = "renaser")]
extern "C" {
/// Compone un bufer de pixeles (de ESTA memoria lineal) en la region que el
/// kernel asigno a esta aplicacion.
fn sys_render_frame(ptr: u32, len: u32);
/// Devuelve el ultimo scancode crudo del teclado, o 0 si no hay ninguno.
fn sys_get_scancode() -> u32;
}
/// Sin sistema operativo bajo nosotros, un panico solo puede detenerse en seco.
#[panic_handler]
fn al_fallar(_: &core::panic::PanicInfo) -> ! {
loop {}
}
// --- Geometria de la escena. El ancho y el alto DEBEN coincidir con la region
// que el kernel asigna a esta app: el host rechaza cualquier fotograma de
// un tamaño que no sea, exactamente, el de su ventana. ---
const ANCHO: usize = 480;
const ALTO: usize = 560;
const LADO: usize = 96;
const PASO: i32 = 24;
/// Azul nocturno: el fondo del lienzo de la aplicacion.
const FONDO: u32 = 0x0A_18_30;
/// Ambar: el cuadrado que el usuario gobierna.
const CUADRO: u32 = 0xFF_B0_00;
/// El lienzo de la aplicacion, en SU propia memoria lineal. El kernel jamas lo
/// ve directamente: solo recibe el (ptr, len) que cada fotograma le entrega.
static mut LIENZO: [u32; ANCHO * ALTO] = [0; ANCHO * ALTO];
/// Posicion del cuadrado. Vive entre fotogramas en la memoria lineal del modulo
/// — el estado persiste porque la instancia, en la Fase 5, ya no es efimera.
static mut POS_X: i32 = 0;
static mut POS_Y: i32 = 0;
/// Preparacion: el kernel la invoca UNA sola vez, al cargar el modulo. Pinta el
/// fondo, centra el cuadrado y vuelca el primer fotograma.
#[no_mangle]
pub extern "C" fn init() {
// SEGURIDAD: durante `init` y `tick` esta es la unica via de acceso a
// LIENZO, y el kernel jamas reentra el modulo mientras una de ellas corre.
let lienzo: &mut [u32] = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
for pixel in lienzo.iter_mut() {
*pixel = FONDO;
}
let x = (ANCHO / 2 - LADO / 2) as i32;
let y = (ALTO / 2 - LADO / 2) as i32;
rellenar(lienzo, x, y, CUADRO);
// SEGURIDAD: escritura de escalares `Copy`; no se crea referencia alguna.
unsafe {
POS_X = x;
POS_Y = y;
}
volcar(lienzo);
}
/// Un fotograma de trabajo: escucha el teclado, mueve el cuadrado, vuelca la
/// imagen y RETORNA. El retorno cede la CPU al kernel y a las apps vecinas.
#[no_mangle]
pub extern "C" fn tick() {
let lienzo: &mut [u32] = unsafe { &mut *core::ptr::addr_of_mut!(LIENZO) };
let (mut x, mut y) = unsafe { (POS_X, POS_Y) };
// 1. Escuchar al teclado a traves de la capacidad del host.
let (dx, dy) = match unsafe { sys_get_scancode() } {
0x11 => (0, -PASO), // tecla W -> arriba
0x1F => (0, PASO), // tecla S -> abajo
0x1E => (-PASO, 0), // tecla A -> izquierda
0x20 => (PASO, 0), // tecla D -> derecha
_ => (0, 0),
};
// 2. Borrar el cuadrado anterior repintando su hueco con el fondo.
rellenar(lienzo, x, y, FONDO);
// 3. Moverlo, manteniendolo siempre dentro del lienzo.
x = (x + dx).clamp(0, (ANCHO - LADO) as i32);
y = (y + dy).clamp(0, (ALTO - LADO) as i32);
// 4. Dibujar el cuadrado en su nueva posicion y guardar el estado.
rellenar(lienzo, x, y, CUADRO);
unsafe {
POS_X = x;
POS_Y = y;
}
// 5. Volcar el fotograma: el host lo compondra dentro de nuestra region.
volcar(lienzo);
}
/// Entrega el lienzo completo al kernel. El (ptr, len) apunta SIEMPRE dentro de
/// nuestra memoria lineal, y su tamaño es, exactamente, el de la region.
fn volcar(lienzo: &[u32]) {
// SEGURIDAD: `sys_render_frame` es una capacidad del host; el (ptr, len)
// describe nuestra propia memoria lineal y el host lo verifica sin piedad.
unsafe {
sys_render_frame(lienzo.as_ptr() as u32, (ANCHO * ALTO * 4) as u32);
}
}
/// Rellena un cuadrado de lado `LADO`, con su esquina en (x, y), recortado con
/// firmeza a los limites del lienzo.
fn rellenar(lienzo: &mut [u32], x: i32, y: i32, color: u32) {
let x0 = x.max(0) as usize;
let y0 = y.max(0) as usize;
let x1 = (x0 + LADO).min(ANCHO);
let y1 = (y0 + LADO).min(ALTO);
let mut fila = y0;
while fila < y1 {
let base = fila * ANCHO;
let mut col = x0;
while col < x1 {
lienzo[base + col] = color;
col += 1;
}
fila += 1;
}
}
+27
View File
@@ -0,0 +1,27 @@
# =============================================================================
# renaser :: boot (Fase 1.5) — orquestador host-side de empaquetado y arranque
# -----------------------------------------------------------------------------
# Este paquete se ejecuta en el ANFITRION (Artix Linux), nunca en bare-metal.
# Toma el ELF nativo del kernel, lo fusiona con el cargador UEFI y lanza QEMU.
# =============================================================================
[package]
name = "boot"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
description = "renaser :: constructor de imagen de disco UEFI y lanzador de QEMU"
[dependencies]
# Constructor de la imagen de disco UEFI. Corre en el anfitrion, usa `std`.
bootloader.workspace = true
# Dependencia de ARTEFACTO (RFC 3028). Cargo compila el kernel para
# `x86_64-unknown-none` —en aislamiento total de arquitectura— y nos inyecta la
# ruta de su ELF en la variable de entorno `CARGO_BIN_FILE_KERNEL_kernel`,
# accesible desde `main.rs` mediante el macro `env!`.
[dependencies.kernel]
path = "../kernel"
artifact = "bin"
target = "x86_64-unknown-none"
+172
View File
@@ -0,0 +1,172 @@
// =============================================================================
// renaser :: boot/src/main.rs — Fase 1.5 :: el puente hacia el silicio
// -----------------------------------------------------------------------------
// Un kernel bare-metal no nace solo: alguien debe fusionarlo con un cargador,
// sellarlo en una imagen de disco arrancable y entregarlo al hardware. Esa es
// la unica mision de este orquestador de ANFITRION.
//
// El flujo es deliberadamente lineal y sin ambiguedad:
//
// 1. Localizar el ELF nativo del kernel (lo inyecta la dep. de artefacto).
// 2. Fusionarlo con el cargador UEFI en una imagen de disco GPT.
// 3. Lanzar QEMU con esa imagen y el firmware OVMF.
//
// Cada paso que pueda fallar lo hace en voz alta, con un mensaje accionable:
// preferimos un error claro a un arranque silencioso hacia la nada.
// =============================================================================
use std::path::{Path, PathBuf};
use std::process::Command;
/// Ruta del ELF del kernel, ya compilado para `x86_64-unknown-none`.
///
/// La dependencia de artefacto define esta variable de entorno en tiempo de
/// compilacion: cuando este binario de anfitrion existe, el kernel ya existe.
const KERNEL_ELF: &str = env!("CARGO_BIN_FILE_KERNEL_kernel");
/// Firmware UEFI OVMF tal como lo empaqueta Artix Linux (paquete `edk2-ovmf`).
/// Es la imagen combinada codigo+variables, apta para `-bios`.
const OVMF_POR_DEFECTO: &str = "/usr/share/edk2/x64/OVMF.4m.fd";
/// Nombre de la imagen de disco UEFI que renaser genera.
const NOMBRE_IMAGEN: &str = "renaser-uefi.img";
/// Ruta del disco de objetos del grafo persistente (Fase 6.1c). Relativa al
/// directorio de trabajo —la raiz del repo—, comun a `boot` y a QEMU.
const NOMBRE_DISCO: &str = "target/disk.img";
/// Tamaño del disco de objetos: 32 MiB. Se crea como fichero disperso.
const TAM_DISCO: u64 = 32 * 1024 * 1024;
fn main() {
if let Err(fallo) = orquestar() {
// Un error de orquestacion se anuncia en rojo y aborta con codigo 1:
// ninguna falla del anfitrion debe disfrazarse de exito.
eprintln!("\x1b[1;31m[renaser/boot] fallo:\x1b[0m {fallo}");
std::process::exit(1);
}
}
/// Ejecuta, en orden, las tres operaciones de la Fase 1.5.
fn orquestar() -> Result<(), String> {
// --- 1. Localizar el artefacto del kernel. ---
let kernel = Path::new(KERNEL_ELF);
if !kernel.is_file() {
return Err(format!(
"no se encontro el ELF del kernel en {}\n \
(¿se interrumpio la compilacion de la dependencia de artefacto?)",
kernel.display()
));
}
println!("[renaser/boot] kernel localizado :: {}", kernel.display());
// --- 2. Fusionar kernel + cargador UEFI en una imagen de disco. ---
let imagen = ruta_imagen(kernel);
println!("[renaser/boot] forjando imagen UEFI :: {}", imagen.display());
bootloader::UefiBoot::new(kernel)
.create_disk_image(&imagen)
.map_err(|e| format!("la crate `bootloader` no pudo crear la imagen UEFI: {e:?}"))?;
// --- 3. Garantizar el disco de objetos del grafo persistente. ---
preparar_disco_objetos()?;
// --- 4. Lanzar QEMU sobre esa imagen. ---
let ovmf = localizar_ovmf()?;
lanzar_qemu(&imagen, &ovmf)
}
/// Garantiza la existencia del disco de objetos del grafo persistente. Si no
/// existe, lo forja como un fichero disperso de 32 MiB, ENTERAMENTE A CERO: el
/// kernel, al no hallar la firma de su superbloque, lo formateara como un grafo
/// virgen. Si ya existe, lo respeta — el grafo perdura entre arranques.
fn preparar_disco_objetos() -> Result<(), String> {
let disco = Path::new(NOMBRE_DISCO);
if disco.is_file() {
println!("[renaser/boot] disco de objetos presente :: {}", disco.display());
return Ok(());
}
if let Some(directorio) = disco.parent() {
std::fs::create_dir_all(directorio)
.map_err(|e| format!("no se pudo crear el directorio del disco de objetos: {e}"))?;
}
// Forjar el disco: un fichero disperso, a cero, de 32 MiB. El kernel
// escribira su superbloque la primera vez que lo monte.
let fichero = std::fs::File::create(disco)
.map_err(|e| format!("no se pudo crear el disco de objetos «{}»: {e}", disco.display()))?;
fichero
.set_len(TAM_DISCO)
.map_err(|e| format!("no se pudo dimensionar el disco de objetos: {e}"))?;
println!(
"[renaser/boot] disco de objetos forjado :: {} ({} MiB, virgen)",
disco.display(),
TAM_DISCO / (1024 * 1024)
);
Ok(())
}
/// Calcula la ruta de la imagen: junto al propio ELF del kernel, es decir,
/// dentro de `target/`. Una ubicacion predecible y siempre escribible.
fn ruta_imagen(kernel: &Path) -> PathBuf {
kernel
.parent()
.unwrap_or_else(|| Path::new("."))
.join(NOMBRE_IMAGEN)
}
/// Resuelve la ruta del firmware OVMF. Permite sobreescribirla con la variable
/// de entorno `RENASER_OVMF` para entornos cuyo `edk2-ovmf` viva en otra ruta.
fn localizar_ovmf() -> Result<String, String> {
let ruta = std::env::var("RENASER_OVMF").unwrap_or_else(|_| OVMF_POR_DEFECTO.to_string());
if Path::new(&ruta).is_file() {
Ok(ruta)
} else {
Err(format!(
"firmware UEFI OVMF no encontrado en «{ruta}»\n \
instala el paquete `edk2-ovmf`, o exporta RENASER_OVMF=<ruta a OVMF.fd>"
))
}
}
/// Invoca QEMU como subproceso. Los argumentos se ciñen a las primitivas
/// minimas necesarias para que el Framebuffer GOP cobre vida:
///
/// * `-bios` firmware UEFI OVMF.
/// * `-drive raw` la imagen de disco UEFI, sin capa de traduccion.
/// * `-vga std` VGA estandar => framebuffer lineal que el GOP expone.
/// * `-serial stdio` telemetria serial del procesador hacia esta consola.
/// * `--no-reboot` un fallo triple detiene la maquina en vez de reiniciar
/// en bucle: asi la baliza de panico permanece visible.
/// * `virtio-blk-pci` el disco de objetos, sobre el bus PCI (q35 es x86_64;
/// `virtio-blk-device`, su gemelo MMIO, es cosa de ARM).
fn lanzar_qemu(imagen: &Path, ovmf: &str) -> Result<(), String> {
println!("[renaser/boot] arrancando QEMU :: la superficie indigo nace ahora\n");
let mut qemu = Command::new("qemu-system-x86_64");
// `accel=kvm:tcg` intenta KVM y, si no esta disponible, recae en TCG puro.
qemu.arg("-machine").arg("q35,accel=kvm:tcg")
.arg("-m").arg("256M")
.arg("-bios").arg(ovmf)
.arg("-drive").arg(format!("format=raw,file={}", imagen.display()))
.arg("-vga").arg("std")
.arg("-serial").arg("stdio")
.arg("--no-reboot")
// El disco de objetos, como dispositivo virtio-blk sobre el bus PCI.
.arg("-drive").arg(format!("format=raw,file={NOMBRE_DISCO},if=none,id=drv0"))
.arg("-device").arg("virtio-blk-pci,drive=drv0");
// Cualquier argumento extra tras `--` se reenvia a QEMU intacto.
// Ejemplo: `cargo run -p boot -- -display none -d int`.
qemu.args(std::env::args().skip(1));
match qemu.status() {
Ok(estado) if estado.success() => {
println!("\n[renaser/boot] QEMU finalizo limpiamente.");
Ok(())
}
Ok(estado) => Err(format!("QEMU termino con estado anomalo: {estado}")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err(
"`qemu-system-x86_64` no esta en el PATH; instala el paquete `qemu-full`".to_string(),
),
Err(e) => Err(format!("no se pudo ejecutar QEMU: {e}")),
}
}
+68
View File
@@ -0,0 +1,68 @@
# =============================================================================
# renaser :: kernel — el corazon bare-metal que late en el espacio unico
# -----------------------------------------------------------------------------
# Este paquete esta EXCLUIDO del espacio de trabajo (ver el Cargo.toml raiz):
# es codigo puramente bare-metal y solo se compila como dependencia de
# artefacto de `boot`, que le impone el target `x86_64-unknown-none`. Por eso
# fija sus versiones de forma explicita, sin herencia del workspace.
# =============================================================================
[package]
name = "kernel"
version = "0.1.0"
edition = "2021"
license = "MPL-2.0"
authors = ["JL Soltech <gerencia@jlsoltech.com>"]
description = "renaser :: kernel asincrono SASOS — entrada, framebuffer y reactor"
# El kernel es un binario freestanding: sin arnes de pruebas ni de benchmarks.
[[bin]]
name = "kernel"
path = "src/main.rs"
test = false
bench = false
doctest = false
[dependencies]
# --- Fase 1-2 :: arranque, framebuffer e interrupciones ---
bootloader_api = "0.11"
x86_64 = "0.15"
embedded-graphics = "0.8"
# --- Fase 3 :: heap dinamico, reactor asincrono y texto vectorial ---
linked_list_allocator = "0.10"
spin = "0.9"
crossbeam-queue = { version = "0.3", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3", default-features = false, features = ["alloc"] }
# `hashbrown` ACTIVA el modo `no_std` de fontdue; sin el recae en `std`.
fontdue = { version = "0.9", default-features = false, features = ["hashbrown"] }
# --- Fase 4 :: interprete WebAssembly del userspace aislado ---
# `wasmi` bare-metal: sin `std`, sin `wat`, sin `simd` (el target no tiene SSE).
wasmi = { version = "1.0", default-features = false, features = ["hash-collections"] }
# --- Fase 6 :: drivers de hardware — el disco virtio-blk sobre el bus PCI ---
# `virtio-drivers` bare-metal: el kernel implementa su `trait Hal` para el DMA.
virtio-drivers = { version = "0.13", default-features = false, features = ["alloc"] }
# --- Fase 6.1c :: el grafo de objetos direccionado por contenido ---
# `serde` da el rasgo de (de)serializacion; `postcard` lo materializa en un
# formato binario compacto, pensado para sistemas empotrados — el que viaja al
# disco. Ambos `no_std`, apoyados en `alloc`.
serde = { version = "1", default-features = false, features = ["alloc", "derive"] }
postcard = { version = "1", default-features = false, features = ["alloc"] }
# `blake3`: la funcion hash que da identidad a cada objeto. Se fuerza la
# implementacion ESCALAR pura (`pure` + los cuatro `no_*`): el target del kernel
# corre sin SSE, y un camino SIMD activado por deteccion en tiempo de ejecucion
# ejecutaria instrucciones que la CPU, sin `CR4.OSFXSR`, rechazaria con un #UD.
blake3 = { version = "1", default-features = false, features = [
"pure", "no_sse2", "no_sse41", "no_avx2", "no_avx512",
] }
# --- Fase 8 (preparación) :: el compositor ---
# `mirada-layout` es el motor de teselado del compositor de brahman —
# geometría pura (rectángulos, foco, Z-order), `no_std`, sin smithay ni
# Wayland. Vive en el monorepo brahman, en su PROPIO workspace; renaser
# lo enlaza por `path` cruzando la frontera. La feature `serde` queda
# APAGADA: el kernel computa el layout en memoria, no lo serializa.
mirada-layout = { path = "../../crates/modules/mirada/mirada-layout" }
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
+292
View File
@@ -0,0 +1,292 @@
// =============================================================================
// renaser :: kernel/src/almacen.rs — Fase 6.1c :: el grafo de objetos
// -----------------------------------------------------------------------------
// renaser rompe con POSIX tambien en el almacenamiento: aqui no hay un sistema
// de archivos plano —rutas, directorios, inodos—. Hay un GRAFO DIRIGIDO ACICLICO
// de objetos DIRECCIONADOS POR CONTENIDO.
//
// Un objeto es una carga util de bytes y una lista de aristas hacia otros
// objetos. Su IDENTIDAD no es un nombre ni un numero: es el hash BLAKE3 de su
// forma serializada. De ello se siguen dos propiedades que un FS jamas regala:
//
// * INTEGRIDAD — el hash verifica el contenido; un objeto corrupto se delata.
// * DEDUPLICACION — contenido identico produce hash identico; se almacena
// una sola vez, aunque mil aristas apunten a el.
//
// El disco se organiza como un LOG: el sector 0 es el superbloque —el ancla
// del grafo—, y tras el se anexan los registros de objetos, uno tras otro. Un
// indice en memoria (hash -> sector) se reconstruye al arrancar recorriendo el
// log. La serializacion la hace `postcard`: binaria, compacta, determinista.
// =============================================================================
use alloc::collections::BTreeMap;
use alloc::vec;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use spin::{Mutex, Once};
use crate::drivers::disco::{self, TAM_SECTOR};
/// Firma magica del superbloque — «RENASer GRaFo». Distingue un disco de
/// renaser de uno virgen o ajeno.
const MAGIA: [u8; 8] = *b"RENASGRF";
/// Version del formato en disco. Un disco con otra version se reformatea.
const VERSION: u32 = 1;
/// Techo del tamaño de un objeto serializado: 1 MiB. Acota los buferes de E/S
/// y permite descartar un registro corrupto sin intentar leer un disparate.
const MAX_OBJETO: usize = 1024 * 1024;
/// El identificador de un objeto: el hash BLAKE3 de su forma serializada. En un
/// almacen direccionado por contenido, la identidad ES el contenido.
pub type Hash = [u8; 32];
/// Un objeto del grafo: una carga util opaca y las aristas que lo enlazan con
/// otros objetos. Los `hijos` hacen del almacen un DAG —no un arbol, no una
/// lista—: un objeto puede ser hijo de muchos, y el direccionamiento por
/// contenido garantiza que cada contenido distinto se guarda una sola vez.
#[derive(Serialize, Deserialize, Clone)]
pub struct Objeto {
/// La carga util del objeto: bytes crudos, que el kernel no interpreta.
pub datos: Vec<u8>,
/// Los hashes de los objetos hijos: las aristas salientes del DAG.
pub hijos: Vec<Hash>,
}
/// El superbloque: el sector 0 del disco. Ancla el grafo entero — dice por
/// donde continua el log y cual es el objeto raiz.
#[derive(Serialize, Deserialize)]
struct SuperBloque {
/// Firma magica: debe ser [`MAGIA`].
magia: [u8; 8],
/// Version del formato: debe ser [`VERSION`].
version: u32,
/// Proximo sector libre del log — donde se anexara el siguiente objeto.
cursor: u64,
/// El objeto raiz del DAG: el punto de entrada que el userspace fija y lee.
raiz: Option<Hash>,
}
/// El estado vivo del almacen: el cursor del log, la raiz y el indice en
/// memoria que traduce cada hash al sector donde habita su registro.
struct Almacen {
/// Proximo sector libre del log.
cursor: u64,
/// El objeto raiz del DAG.
raiz: Option<Hash>,
/// Indice hash -> sector del registro. Se reconstruye al arrancar.
indice: BTreeMap<Hash, u64>,
/// Capacidad del disco, en sectores.
capacidad: u64,
}
/// El almacen global de renaser. Se funde una sola vez, en `init`.
static ALMACEN: Once<Mutex<Almacen>> = Once::new();
/// El fruto de fundar el almacen — para que el arranque deje constancia visual.
pub struct Resumen {
/// Capacidad del disco, en sectores.
pub capacidad: u64,
/// Numero de objetos hallados en el grafo.
pub objetos: usize,
/// ¿Tiene el grafo un objeto raiz?
pub raiz: bool,
/// ¿Se reformateo el disco (estaba virgen o era ajeno)?
pub formateado: bool,
}
/// Numero de sectores que ocupa un registro cuyo payload mide `longitud` bytes.
/// Cada registro en disco es: `[longitud: u32 LE][payload postcard][relleno 0]`.
fn sectores_registro(longitud: usize) -> u64 {
(4 + longitud).div_ceil(TAM_SECTOR) as u64
}
/// Funda el almacen de objetos: monta el disco, lee el superbloque y, si el
/// disco ya es de renaser, reconstruye el indice recorriendo el log; si es
/// virgen o ajeno, lo formatea. Toda falla se devuelve como `Err`.
pub fn init() -> Result<Resumen, &'static str> {
let capacidad = disco::montar()?;
if capacidad < 2 {
return Err("el disco es demasiado pequeño para un grafo");
}
// Leer el sector 0 e intentar interpretarlo como superbloque de renaser.
let mut sector0 = [0u8; TAM_SECTOR];
disco::leer_sectores(0, &mut sector0)?;
let (cursor, raiz, indice, formateado) =
match postcard::take_from_bytes::<SuperBloque>(&sector0) {
// Disco de renaser, con la version corriente: adoptar su grafo.
Ok((sb, _)) if sb.magia == MAGIA && sb.version == VERSION => {
let indice = reconstruir_indice(sb.cursor)?;
(sb.cursor, sb.raiz, indice, false)
}
// Disco virgen, ajeno o de otra version: empezar de cero. El log
// arranca en el sector 1, justo despues del superbloque.
_ => (1, None, BTreeMap::new(), true),
};
let objetos = indice.len();
let tiene_raiz = raiz.is_some();
let almacen = Almacen {
cursor,
raiz,
indice,
capacidad,
};
// Un disco recien formateado necesita su superbloque grabado de inmediato.
if formateado {
persistir(&almacen)?;
}
ALMACEN.call_once(|| Mutex::new(almacen));
Ok(Resumen {
capacidad,
objetos,
raiz: tiene_raiz,
formateado,
})
}
/// Recorre el log —del sector 1 al `cursor`— y reconstruye el indice
/// hash -> sector. Cada registro se rehashea: el indice se reconstruye, no se
/// confia. Un registro corrupto detiene el escaneo sin incendiar nada.
fn reconstruir_indice(cursor: u64) -> Result<BTreeMap<Hash, u64>, &'static str> {
let mut indice = BTreeMap::new();
let mut sector: u64 = 1;
while sector < cursor {
let payload = leer_registro(sector)?;
match payload {
// Un payload valido: hashearlo e indexarlo.
Some(payload) => {
let n = sectores_registro(payload.len());
let hash = *blake3::hash(&payload).as_bytes();
indice.insert(hash, sector);
sector += n;
}
// Cabecera a cero o longitud imposible: fin (o corrupcion) del log.
None => break,
}
}
Ok(indice)
}
/// Lee el registro que arranca en `sector` y devuelve su payload postcard
/// (sin la cabecera de longitud ni el relleno). `None` si la cabecera dice
/// longitud cero —fin del log— o una longitud imposible —corrupcion—.
fn leer_registro(sector: u64) -> Result<Option<Vec<u8>>, &'static str> {
let mut cabecera = [0u8; TAM_SECTOR];
disco::leer_sectores(sector, &mut cabecera)?;
let longitud =
u32::from_le_bytes([cabecera[0], cabecera[1], cabecera[2], cabecera[3]]) as usize;
if longitud == 0 || longitud > MAX_OBJETO {
return Ok(None);
}
let n = sectores_registro(longitud) as usize;
// Si el registro cabe en el sector ya leido, evitar una segunda lectura.
let payload = if n == 1 {
cabecera[4..4 + longitud].to_vec()
} else {
let mut buf = vec![0u8; n * TAM_SECTOR];
disco::leer_sectores(sector, &mut buf)?;
buf[4..4 + longitud].to_vec()
};
Ok(Some(payload))
}
/// Graba el superbloque —el ancla del grafo— en el sector 0.
fn persistir(almacen: &Almacen) -> Result<(), &'static str> {
let sb = SuperBloque {
magia: MAGIA,
version: VERSION,
cursor: almacen.cursor,
raiz: almacen.raiz,
};
let bytes = postcard::to_allocvec(&sb).map_err(|_| "no se pudo serializar el superbloque")?;
if bytes.len() > TAM_SECTOR {
return Err("el superbloque no cabe en un sector");
}
let mut sector0 = [0u8; TAM_SECTOR];
sector0[..bytes.len()].copy_from_slice(&bytes);
disco::escribir_sectores(0, &sector0)
}
/// Almacena un objeto y devuelve su hash. Direccionamiento por contenido en
/// estado puro: si un objeto de contenido identico ya existe, NO se reescribe —
/// se devuelve el hash que ya tenia. El grafo nunca guarda dos veces lo mismo.
pub fn almacenar(datos: Vec<u8>, hijos: Vec<Hash>) -> Result<Hash, &'static str> {
let objeto = Objeto { datos, hijos };
let bytes =
postcard::to_allocvec(&objeto).map_err(|_| "no se pudo serializar el objeto")?;
if bytes.is_empty() || bytes.len() > MAX_OBJETO {
return Err("el objeto tiene un tamaño invalido");
}
// La identidad del objeto: el hash de su forma serializada.
let hash = *blake3::hash(&bytes).as_bytes();
let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
let mut almacen = mutex.lock();
// ¿Ya esta en el grafo? Entonces no hay nada que grabar.
if almacen.indice.contains_key(&hash) {
return Ok(hash);
}
// Reservar los sectores del registro al final del log.
let n = sectores_registro(bytes.len());
if almacen.cursor + n > almacen.capacidad {
return Err("el grafo de objetos esta lleno");
}
let sector = almacen.cursor;
// Componer el registro: [longitud][payload][relleno a cero] y grabarlo.
let mut registro = vec![0u8; n as usize * TAM_SECTOR];
registro[0..4].copy_from_slice(&(bytes.len() as u32).to_le_bytes());
registro[4..4 + bytes.len()].copy_from_slice(&bytes);
disco::escribir_sectores(sector, &registro)?;
// El objeto ya esta en disco: avanzar el cursor, indexarlo y RE-anclar el
// superbloque. El orden importa — el superbloque se graba el ultimo, de
// modo que jamas apunte a un registro a medio escribir.
almacen.cursor += n;
almacen.indice.insert(hash, sector);
persistir(&almacen)?;
Ok(hash)
}
/// Recupera un objeto por su hash. `Ok(None)` si el hash no esta en el grafo.
pub fn recuperar(hash: &Hash) -> Result<Option<Objeto>, &'static str> {
let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
// Soltar el cerrojo del almacen ANTES de la E/S de disco —lenta, por
// sondeo—: el indice ya entrego el sector, y nada mas reclama el cerrojo.
let sector = match mutex.lock().indice.get(hash) {
Some(&s) => s,
None => return Ok(None),
};
let payload = leer_registro(sector)?.ok_or("registro de objeto corrupto")?;
// Verificacion de integridad: el contenido leido DEBE rehashear al hash
// pedido. Si no, el disco ha mentido — y se delata.
if *blake3::hash(&payload).as_bytes() != *hash {
return Err("el objeto no supero la verificacion de integridad");
}
let (objeto, _) = postcard::take_from_bytes::<Objeto>(&payload)
.map_err(|_| "no se pudo deserializar el objeto")?;
Ok(Some(objeto))
}
/// El hash del objeto raiz del grafo, si lo hay.
pub fn raiz() -> Option<Hash> {
ALMACEN.get().and_then(|mutex| mutex.lock().raiz)
}
/// Corona un objeto como raiz del grafo y ancla el cambio en el superbloque.
pub fn fijar_raiz(hash: Hash) -> Result<(), &'static str> {
let mutex = ALMACEN.get().ok_or("almacen no inicializado")?;
let mut almacen = mutex.lock();
almacen.raiz = Some(hash);
persistir(&almacen)
}
+103
View File
@@ -0,0 +1,103 @@
// =============================================================================
// renaser :: async_system/executor.rs — Fase 3 :: el reactor cooperativo
// -----------------------------------------------------------------------------
// El ejecutor es el corazon reactivo de renaser. Mantiene el censo de tareas
// vivas y una cola de las que estan listas para avanzar. Cuando no queda nada
// por hacer, no malgasta la CPU en un bucle ocupado: la duerme con `hlt`
// hasta que el proximo impulso de hardware la despierte.
// =============================================================================
use alloc::collections::{BTreeMap, VecDeque};
use alloc::sync::Arc;
use core::future::Future;
use core::task::{Context, Poll, Waker};
use spin::Mutex;
use x86_64::instructions::interrupts;
use super::task::{Task, TaskId};
use super::waker::{self, ColaListas};
/// El ejecutor cooperativo de renaser.
pub struct Executor {
/// Censo de todas las tareas vivas, indexadas por su identidad.
tareas: BTreeMap<TaskId, Task>,
/// Cola de tareas listas para su proximo avance.
cola_listas: ColaListas,
/// Cache de wakers: uno por tarea, reutilizado entre avances.
cache_wakers: BTreeMap<TaskId, Waker>,
}
impl Executor {
/// Crea un ejecutor vacio. Requiere que el heap ya este fundado.
pub fn nuevo() -> Executor {
Executor {
tareas: BTreeMap::new(),
cola_listas: Arc::new(Mutex::new(VecDeque::new())),
cache_wakers: BTreeMap::new(),
}
}
/// Da de alta una tarea nueva y la marca como lista para su primer avance.
pub fn spawn(&mut self, futuro: impl Future<Output = ()> + Send + 'static) {
let tarea = Task::nueva(futuro);
let id = tarea.id;
if self.tareas.insert(id, tarea).is_some() {
panic!("renaser :: TaskId duplicado — lo imposible ha ocurrido");
}
interrupts::without_interrupts(|| self.cola_listas.lock().push_back(id));
}
/// Avanza, hasta agotarla, la cola de tareas listas.
fn avanzar_listas(&mut self) {
loop {
// Sacar el siguiente id con las interrupciones acalladas: la cola
// es compartida con los wakers que se disparan desde las IRQ.
let siguiente = interrupts::without_interrupts(|| self.cola_listas.lock().pop_front());
let id = match siguiente {
Some(id) => id,
None => return,
};
let tarea = match self.tareas.get_mut(&id) {
Some(t) => t,
None => continue, // la tarea concluyo en una vuelta previa
};
// Un waker por tarea, reutilizado: reinyecta esta tarea en la cola.
let cola = self.cola_listas.clone();
let despertador = self
.cache_wakers
.entry(id)
.or_insert_with(|| waker::crear(id, cola));
let mut contexto = Context::from_waker(despertador);
if let Poll::Ready(()) = tarea.poll(&mut contexto) {
// La tarea termino: se retira del censo y se libera su waker.
self.tareas.remove(&id);
self.cache_wakers.remove(&id);
}
}
}
/// Si no queda trabajo, duerme la CPU hasta la proxima interrupcion. El
/// chequeo y el `hlt` se hacen con las interrupciones acalladas para que
/// ningun despertar se pierda en la rendija entre uno y otro.
fn dormir_si_inactivo(&self) {
interrupts::disable();
let hay_trabajo = !self.cola_listas.lock().is_empty();
if hay_trabajo {
// Llego trabajo justo ahora: reactivar y seguir sin dormir.
interrupts::enable();
} else {
// `sti; hlt` atomico: habilita y duerme sin condicion de carrera.
interrupts::enable_and_hlt();
}
}
/// Cede el hilo principal al reactor. No retorna jamas: desde aqui, renaser
/// vive del latir de sus interrupciones.
pub fn run(&mut self) -> ! {
loop {
self.avanzar_listas();
self.dormir_si_inactivo();
}
}
}
+16
View File
@@ -0,0 +1,16 @@
// =============================================================================
// renaser :: kernel/src/async_system — el reactor cooperativo (Fase 3 / 5)
// -----------------------------------------------------------------------------
// Aqui renaser rompe con el modelo de hilos pesados de Linux. No hay cambio
// de contexto en la CPU: las interrupciones de hardware no conmutan pilas,
// DESPIERTAN tareas. El kernel avanza cooperativamente, una tarea cede y la
// siguiente toma el relevo. Sobre estos cimientos corre, desde la Fase 5, el
// bytecode WASM aislado por software de la Fase 4: cada aplicacion es una
// tarea mas, y el `reloj` le marca el compas de sus fotogramas.
// =============================================================================
pub mod executor;
pub mod reloj;
pub mod task;
pub mod teclado;
pub mod waker;
+101
View File
@@ -0,0 +1,101 @@
// =============================================================================
// renaser :: async_system/reloj.rs — Fase 5 :: el compas de los fotogramas
// -----------------------------------------------------------------------------
// El temporizador (PIT, IRQ0) ya no solo despierta al ejecutor de su `hlt`:
// ahora marca el COMPAS del userspace. Cada pulso a 100 Hz es un fotograma —
// una oportunidad de cesion cooperativa. `EsperaFrame` convierte ese pulso de
// hardware en un `Future`: una tarea WASM hace su trabajo de un fotograma y
// `.await`-ea el siguiente, cediendo la CPU a sus vecinas mientras tanto.
// =============================================================================
use alloc::vec::Vec;
use core::future::Future;
use core::pin::Pin;
use core::sync::atomic::{AtomicU64, Ordering};
use core::task::{Context, Poll, Waker};
use spin::{Mutex, Once};
use x86_64::instructions::interrupts;
/// Pulsos del temporizador acumulados desde el arranque. Lo incrementa la IRQ0;
/// lo consulta `EsperaFrame`. Atomico: la IRQ y el hilo principal lo comparten.
static CONTADOR_PULSOS: AtomicU64 = AtomicU64::new(0);
/// Censo de wakers en espera del proximo fotograma. Vive en el heap —de ahi el
/// `Once`—; tras su `Mutex`, lo disputan el lado tarea y el manejador de IRQ0.
static EN_ESPERA: Once<Mutex<Vec<Waker>>> = Once::new();
/// Funda el censo de wakers del reloj. Requiere el heap ya activo; debe
/// invocarse una sola vez, antes de habilitar las interrupciones.
pub fn init() {
EN_ESPERA.call_once(|| Mutex::new(Vec::new()));
}
/// Punto de entrada DESDE el manejador de IRQ0. Avanza el contador de pulsos y
/// despierta a cuantas tareas aguardaban el fotograma. Deliberadamente breve y
/// libre de panicos: corre en contexto de interrupcion.
pub fn pulso() {
// `Release`: el avance del contador se publica ANTES de que los wakers
// reinyecten sus tareas; el `poll` que sigue lo leera con `Acquire` y vera,
// garantizado, el valor nuevo.
CONTADOR_PULSOS.fetch_add(1, Ordering::Release);
if let Some(censo) = EN_ESPERA.get() {
// En contexto de IRQ las interrupciones ya estan acalladas; tomar aqui
// el cerrojo no puede interbloquear, pues el lado tarea siempre lo toma
// con las interrupciones desactivadas (ver `inscribir`).
for waker in censo.lock().drain(..) {
waker.wake();
}
}
}
/// Numero de pulsos del temporizador desde el arranque.
fn pulsos() -> u64 {
CONTADOR_PULSOS.load(Ordering::Acquire)
}
/// Inscribe un waker en el censo de espera del proximo fotograma.
fn inscribir(waker: Waker) {
if let Some(censo) = EN_ESPERA.get() {
// El cerrojo lo disputa el manejador de IRQ0: tomarlo con las
// interrupciones acalladas hace IMPOSIBLE el interbloqueo.
interrupts::without_interrupts(|| censo.lock().push(waker));
}
}
/// Un `Future` que se resuelve en el proximo pulso del temporizador. Es la
/// unidad de cesion cooperativa del userspace: una tarea WASM hace su trabajo
/// de un fotograma y `.await`-ea un `EsperaFrame` para ceder hasta el siguiente.
pub struct EsperaFrame {
/// Pulso a partir del cual la espera se da por cumplida.
objetivo: u64,
}
impl EsperaFrame {
/// Crea una espera que se resolvera en el siguiente pulso del temporizador.
pub fn nueva() -> EsperaFrame {
EsperaFrame {
objetivo: pulsos() + 1,
}
}
}
impl Future for EsperaFrame {
type Output = ();
fn poll(self: Pin<&mut Self>, contexto: &mut Context<'_>) -> Poll<()> {
// Lectura optimista: si el fotograma ya llego, no hay nada que esperar.
if pulsos() >= self.objetivo {
return Poll::Ready(());
}
// Aun no: inscribir el waker y RE-VERIFICAR. Si un pulso se colo entre
// la lectura de arriba y la inscripcion, este segundo chequeo lo atrapa;
// sin el, el despertar se perderia hasta el pulso siguiente.
inscribir(contexto.waker().clone());
if pulsos() >= self.objetivo {
Poll::Ready(())
} else {
Poll::Pending
}
}
}
+45
View File
@@ -0,0 +1,45 @@
// =============================================================================
// renaser :: async_system/task.rs — Fase 3 :: la unidad de trabajo asincrono
// =============================================================================
use alloc::boxed::Box;
use core::future::Future;
use core::pin::Pin;
use core::sync::atomic::{AtomicU64, Ordering};
use core::task::{Context, Poll};
/// Identificador unico de una tarea. Autoincremental y atomico: dos tareas
/// jamas comparten identidad.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TaskId(u64);
impl TaskId {
/// Acuña un identificador nuevo, distinto de todos los anteriores.
fn nuevo() -> TaskId {
static SIGUIENTE: AtomicU64 = AtomicU64::new(0);
TaskId(SIGUIENTE.fetch_add(1, Ordering::Relaxed))
}
}
/// Una tarea asincrona: un `Future` encapsulado, anclado (`Pin`) en el heap
/// para que su direccion nunca cambie mientras avanza.
pub struct Task {
/// La identidad de la tarea, su nombre ante el ejecutor.
pub id: TaskId,
futuro: Pin<Box<dyn Future<Output = ()> + Send + 'static>>,
}
impl Task {
/// Envuelve un `Future` en una tarea con identidad propia.
pub fn nueva(futuro: impl Future<Output = ()> + Send + 'static) -> Task {
Task {
id: TaskId::nuevo(),
futuro: Box::pin(futuro),
}
}
/// Hace avanzar la tarea un paso. `Poll::Ready` significa que concluyo.
pub fn poll(&mut self, contexto: &mut Context) -> Poll<()> {
self.futuro.as_mut().poll(contexto)
}
}
@@ -0,0 +1,76 @@
// =============================================================================
// renaser :: async_system/teclado.rs — el canal de scancodes del teclado
// -----------------------------------------------------------------------------
// El manejador de IRQ1 es un mero PRODUCTOR: deposita cada scancode en colas
// lock-free, seguras frente a interrupciones. Los consumidores —las apps WASM,
// via la capacidad `sys_get_scancode`— las drenan sin bloquear.
//
// FASE 5 :: con varias apps concurrentes, una sola cola compartida no sirve:
// la primera en sondear le robaria la pulsacion a las demas. Por eso cada
// aplicacion abre su PROPIO canal y la IRQ1 DIFUNDE cada scancode a todos —
// cada app recibe su copia integra del flujo de entrada.
// =============================================================================
use alloc::sync::Arc;
use alloc::vec::Vec;
use crossbeam_queue::ArrayQueue;
use spin::{Mutex, Once};
use x86_64::instructions::interrupts;
/// Capacidad de la cola de scancodes de cada app. Holgada: nadie teclea tanto.
const CAPACIDAD_COLA: usize = 256;
/// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
pub type CanalTeclado = Arc<ArrayQueue<u8>>;
/// Censo de canales — uno por aplicacion del userspace. El manejador de IRQ1
/// difunde cada scancode a TODOS: asi cada app recibe su propia copia del
/// evento, sin que una le arrebate la pulsacion a otra.
static CANALES: Once<Mutex<Vec<CanalTeclado>>> = Once::new();
/// Funda el censo de canales del teclado. Requiere el heap ya activo; debe
/// invocarse una sola vez, antes de habilitar las interrupciones.
pub fn init() {
CANALES.call_once(|| Mutex::new(Vec::new()));
}
/// Crea un canal de teclado nuevo, AUN sin inscribir en la difusion. Cada
/// aplicacion reclama el suyo al empezar a cargarse.
pub fn crear_canal() -> CanalTeclado {
Arc::new(ArrayQueue::new(CAPACIDAD_COLA))
}
/// Inscribe un canal en el censo de difusion. Desde este instante, la IRQ1
/// empuja cada scancode tambien a este canal. Se invoca al final de la carga
/// de una app: una carga fallida no debe dejar canales huerfanos.
pub fn registrar_canal(canal: &CanalTeclado) {
if let Some(censo) = CANALES.get() {
// El cerrojo lo disputa el manejador de IRQ1: tomarlo con las
// interrupciones acalladas hace imposible el interbloqueo.
interrupts::without_interrupts(|| censo.lock().push(canal.clone()));
}
}
/// Da de baja un canal del censo de difusion. Lo invoca el `Drop` de una
/// aplicacion desalojada: la IRQ1 deja, de inmediato, de empujarle scancodes.
pub fn cerrar_canal(canal: &CanalTeclado) {
if let Some(censo) = CANALES.get() {
interrupts::without_interrupts(|| {
censo.lock().retain(|inscrito| !Arc::ptr_eq(inscrito, canal));
});
}
}
/// Punto de entrada DESDE el manejador de IRQ1. DIFUNDE el scancode a cuantos
/// canales haya abiertos. Deliberadamente breve y libre de panicos: corre en
/// contexto de interrupcion.
pub fn recibir_scancode(scancode: u8) {
if let Some(censo) = CANALES.get() {
for canal in censo.lock().iter() {
// Si un canal desborda, se descarta el scancode en silencio: mas
// vale perder una tecla que colapsar dentro de una interrupcion.
let _ = canal.push(scancode);
}
}
}
+55
View File
@@ -0,0 +1,55 @@
// =============================================================================
// renaser :: async_system/waker.rs — Fase 3 :: el despertador de tareas
// -----------------------------------------------------------------------------
// Un `Waker` es la promesa de que una tarea dormida volvera a ejecutarse. El
// nuestro es minimo: al invocarse, reinyecta el `TaskId` de su tarea en la
// cola de listas del ejecutor. Quien lo invoque —incluido un manejador de
// IRQ— vuelve a poner esa tarea en circulacion.
// =============================================================================
use alloc::collections::VecDeque;
use alloc::sync::Arc;
use alloc::task::Wake;
use core::task::Waker;
use spin::Mutex;
use super::task::TaskId;
/// La cola de tareas listas para avanzar, compartida entre el ejecutor y todos
/// los wakers que ha repartido.
pub type ColaListas = Arc<Mutex<VecDeque<TaskId>>>;
/// El despertador de UNA tarea concreta.
struct WakerTarea {
id: TaskId,
cola: ColaListas,
}
impl WakerTarea {
/// Reinyecta la tarea en la cola de ejecucion.
fn reinyectar(&self) {
// El spinlock de la cola lo tocan tanto el hilo principal como los
// manejadores de IRQ. Tomarlo con las interrupciones acalladas hace
// IMPOSIBLE el interbloqueo: ninguna IRQ puede interrumpir al hilo
// principal justo mientras este sostiene el cerrojo.
x86_64::instructions::interrupts::without_interrupts(|| {
self.cola.lock().push_back(self.id);
});
}
}
impl Wake for WakerTarea {
fn wake(self: Arc<Self>) {
self.reinyectar();
}
fn wake_by_ref(self: &Arc<Self>) {
self.reinyectar();
}
}
/// Crea un `Waker` estandar que, al invocarse, reinyecta `id` en `cola`.
pub fn crear(id: TaskId, cola: ColaListas) -> Waker {
Waker::from(Arc::new(WakerTarea { id, cola }))
}
+128
View File
@@ -0,0 +1,128 @@
// =============================================================================
// renaser :: kernel/src/baliza.rs — la red de seguridad visual del sistema
// -----------------------------------------------------------------------------
// Sin consola de texto que valga cuando el sistema cae: si renaser colapsa, lo
// DIBUJA. La baliza publica —de forma atomica y sin cerrojos— los datos
// minimos del framebuffer, de modo que los manejadores de fallo puedan pintar
// una franja de advertencia incluso cuando el resto del kernel ya no es fiable.
// =============================================================================
use core::panic::PanicInfo;
use core::ptr;
use core::sync::atomic::{AtomicPtr, AtomicU32, AtomicUsize, Ordering};
use crate::grafico::{escribir_pixel_volatil, Pantalla};
/// Datos del framebuffer expuestos a los manejadores de fallo. Todo son
/// atomicos: la baliza es intrinsecamente `Sync`, sin cerrojos.
pub(crate) struct BalizaPanico {
base: AtomicPtr<u8>,
paso_bytes: AtomicUsize,
ancho: AtomicUsize,
alto: AtomicUsize,
bytes_por_pixel: AtomicUsize,
/// Rojo de alerta de colapso, ya codificado al formato de la pantalla.
pixel_alerta: AtomicU32,
/// Naranja de agotamiento de memoria, ya codificado.
pixel_oom: AtomicU32,
}
impl BalizaPanico {
/// Baliza apagada: sin pantalla publicada todavia.
const fn apagada() -> BalizaPanico {
BalizaPanico {
base: AtomicPtr::new(ptr::null_mut()),
paso_bytes: AtomicUsize::new(0),
ancho: AtomicUsize::new(0),
alto: AtomicUsize::new(0),
bytes_por_pixel: AtomicUsize::new(0),
pixel_alerta: AtomicU32::new(0),
pixel_oom: AtomicU32::new(0),
}
}
/// Enciende la baliza con los datos de una pantalla viva.
pub(crate) fn encender(&self, pantalla: &Pantalla, pixel_alerta: u32, pixel_oom: u32) {
self.paso_bytes.store(pantalla.paso_bytes, Ordering::Relaxed);
self.ancho.store(pantalla.ancho, Ordering::Relaxed);
self.alto.store(pantalla.alto, Ordering::Relaxed);
self.bytes_por_pixel
.store(pantalla.bytes_por_pixel, Ordering::Relaxed);
self.pixel_alerta.store(pixel_alerta, Ordering::Relaxed);
self.pixel_oom.store(pixel_oom, Ordering::Relaxed);
// La base se publica de ultima, con semantica `Release`.
self.base.store(pantalla.base, Ordering::Release);
}
/// Pinta, directa y volatilmente, una banda horizontal sobre el framebuffer
/// fisico. Es la herramienta de los manejadores de fallo: no confia ni en
/// el lienzo, ni en el heap, ni en estructura dinamica alguna.
fn pintar_banda(&self, y0: usize, altura: usize, pixel: u32) {
let base = self.base.load(Ordering::Acquire);
if base.is_null() {
return;
}
let ancho = self.ancho.load(Ordering::Relaxed);
let alto = self.alto.load(Ordering::Relaxed);
let paso = self.paso_bytes.load(Ordering::Relaxed);
let bpp = self.bytes_por_pixel.load(Ordering::Relaxed);
let y_fin = (y0 + altura).min(alto);
let mut y = y0.min(alto);
while y < y_fin {
let fila = y * paso;
let mut x = 0;
while x < ancho {
// SEGURIDAD: (x, y) esta acotado por las dimensiones que la
// baliza publico desde una pantalla real.
unsafe {
escribir_pixel_volatil(base.add(fila + x * bpp), pixel, bpp);
}
x += 1;
}
y += 1;
}
}
/// Altura de la franja de advertencia: ~8 % de la pantalla.
fn franja(&self) -> usize {
let alto = self.alto.load(Ordering::Relaxed);
if alto == 0 {
0
} else {
(alto / 12).max(1)
}
}
}
/// Instancia global de la baliza. Comienza apagada y se enciende en el arranque.
pub(crate) static BALIZA_PANICO: BalizaPanico = BalizaPanico::apagada();
// =============================================================================
// MANEJADORES DE FALLO — cuando el sistema colapsa, lo DIBUJA
// =============================================================================
/// Si renaser colapsa, tatuamos una franja ROJA en lo alto del framebuffer.
#[panic_handler]
fn al_colapsar(_info: &PanicInfo) -> ! {
x86_64::instructions::interrupts::disable();
BALIZA_PANICO.pintar_banda(
0,
BALIZA_PANICO.franja(),
BALIZA_PANICO.pixel_alerta.load(Ordering::Relaxed),
);
crate::detener()
}
/// Si el heap se agota, tatuamos una franja NARANJA: un fallo distinto al
/// colapso, y distinguible de un vistazo.
#[alloc_error_handler]
fn al_agotar_memoria(_disposicion: core::alloc::Layout) -> ! {
x86_64::instructions::interrupts::disable();
BALIZA_PANICO.pintar_banda(
0,
BALIZA_PANICO.franja(),
BALIZA_PANICO.pixel_oom.load(Ordering::Relaxed),
);
crate::detener()
}
+186
View File
@@ -0,0 +1,186 @@
// =============================================================================
// renaser :: kernel/src/consola.rs — la superficie de texto e imagen
// -----------------------------------------------------------------------------
// La consola une el lienzo intermedio, la pantalla fisica y una pluma de
// escritura. Rasteriza cada glifo con `fontdue` al vuelo —el texto convertido,
// por fin, en dibujo— y tambien sabe volcar fotogramas crudos del userspace
// WASM. Es global y serializada tras un `Mutex`: las tareas escriben en ella.
// =============================================================================
use spin::{Mutex, Once};
use crate::grafico::{codificar, Color, Lienzo, Pantalla, RegionPantalla};
use crate::texto;
/// Margen del texto respecto al borde del lienzo, en pixeles.
const MARGEN: usize = 40;
/// Tamaño de la tipografia, en pixeles.
const TAM_FUENTE: f32 = 30.0;
/// Altura de avance de una linea de texto, en pixeles.
const ALTO_LINEA: usize = 40;
/// Posicion vertical de la primera linea base.
const BASE_INICIAL: usize = MARGEN + 30;
/// Interpola dos colores segun una cobertura: `0` => `fondo`, `255` => `tinta`.
fn mezclar(fondo: Color, tinta: Color, cobertura: u8) -> Color {
let canal = |a: u8, b: u8| -> u8 {
let c = cobertura as u16;
((a as u16 * (255 - c) + b as u16 * c) / 255) as u8
};
Color {
r: canal(fondo.r, tinta.r),
g: canal(fondo.g, tinta.g),
b: canal(fondo.b, tinta.b),
}
}
/// La consola grafica de renaser: doble bufer, pantalla fisica y pluma.
pub(crate) struct Consola {
lienzo: Lienzo,
pantalla: Pantalla,
/// Posicion horizontal de la pluma de escritura.
pluma_x: usize,
/// Linea base vertical de la pluma de escritura.
base_y: usize,
}
// SEGURIDAD: `Consola` encierra, via `Pantalla`, un puntero crudo al
// framebuffer. Ese puntero es valido durante toda la vida del kernel y todo
// acceso a la consola se serializa tras un `Mutex`. En un sistema de un solo
// nucleo, esto la hace segura de compartir entre el hilo principal y las tareas.
unsafe impl Send for Consola {}
impl Consola {
/// Crea una consola con la pluma en la esquina superior izquierda.
pub(crate) fn nueva(lienzo: Lienzo, pantalla: Pantalla) -> Consola {
Consola {
lienzo,
pantalla,
pluma_x: MARGEN,
base_y: BASE_INICIAL,
}
}
/// Lleva la pluma al inicio de la siguiente linea. Al llegar al fondo,
/// limpia el lienzo: una pizarra nueva.
fn nueva_linea(&mut self) {
self.pluma_x = MARGEN;
self.base_y += ALTO_LINEA;
if self.base_y + ALTO_LINEA >= self.lienzo.alto {
self.lienzo.limpiar(Color::LIENZO_EN_REPOSO);
self.base_y = BASE_INICIAL;
}
}
/// Escribe un caracter: rasteriza su glifo y avanza la pluma.
fn escribir_char(&mut self, caracter: char) {
if caracter == '\n' {
self.nueva_linea();
return;
}
let (metricas, cobertura) = texto::rasterizar(caracter, TAM_FUENTE);
// Salto de linea automatico al alcanzar el margen derecho.
if self.pluma_x + metricas.advance_width as usize + MARGEN > self.lienzo.ancho {
self.nueva_linea();
}
self.dibujar_glifo(&metricas, &cobertura);
self.pluma_x += metricas.advance_width as usize;
}
/// Escribe una cadena completa, caracter a caracter.
pub(crate) fn escribir(&mut self, texto: &str) {
for caracter in texto.chars() {
self.escribir_char(caracter);
}
}
/// Funde un mapa de cobertura de `fontdue` sobre el lienzo, en la pluma.
fn dibujar_glifo(&mut self, metricas: &fontdue::Metrics, cobertura: &[u8]) {
// Origen del glifo: la pluma desplazada por las metricas. El mapa de
// `fontdue` se recorre de arriba a abajo desde la cima del glifo.
let inicio_x = self.pluma_x as isize + metricas.xmin as isize;
let inicio_y = self.base_y as isize - metricas.ymin as isize - metricas.height as isize;
for fila in 0..metricas.height {
for col in 0..metricas.width {
let opacidad = cobertura[fila * metricas.width + col];
if opacidad == 0 {
continue; // pixel transparente: no toca el fondo
}
let x = inicio_x + col as isize;
let y = inicio_y + fila as isize;
if x < 0 || y < 0 {
continue;
}
let color = mezclar(Color::LIENZO_EN_REPOSO, Color::TEXTO, opacidad);
self.lienzo.pintar_pixel(x as usize, y as usize, color);
}
}
}
/// Compone un fotograma crudo del userspace WASM —pixeles `0x00RRGGBB`, con
/// sus limites ya verificados por el host— sobre la SUB-REGION asignada a su
/// aplicacion. Cada pixel se recodifica al formato nativo del framebuffer y
/// se deposita desplazado por `(region.x, region.y)`: una app jamas escribe
/// fuera de su ventana, y varias cohabitan el lienzo sin pisarse.
fn volcar_marco(&mut self, region: RegionPantalla, datos: &[u8]) {
for (indice, trozo) in datos.chunks_exact(4).enumerate() {
let columna = indice % region.ancho;
let fila = indice / region.ancho;
if fila >= region.alto {
break; // el fotograma excede el alto de la region: se ignora el resto
}
let x = region.x + columna;
let y = region.y + fila;
if x >= self.lienzo.ancho || y >= self.lienzo.alto {
continue; // recorte firme: nada se pinta fuera del lienzo
}
let p = u32::from_le_bytes([trozo[0], trozo[1], trozo[2], trozo[3]]);
let color = Color {
r: (p >> 16) as u8,
g: (p >> 8) as u8,
b: p as u8,
};
self.lienzo.pixeles[y * self.lienzo.ancho + x] =
codificar(self.lienzo.formato, color);
}
self.presentar();
}
/// Inunda una region entera con un color plano y la presenta. Es la baliza
/// de desalojo: cuando una aplicacion falla, su ventana se tatua de purpura.
fn pintar_region(&mut self, region: RegionPantalla, color: Color) {
self.lienzo
.rellenar_rect(region.x, region.y, region.ancho, region.alto, color);
self.presentar();
}
/// Vuelca el lienzo sobre la pantalla fisica.
pub(crate) fn presentar(&mut self) {
self.pantalla.presentar(&self.lienzo);
}
}
/// La consola global de renaser. Se funde en el arranque; despues, las tareas
/// asincronas y las capacidades del userspace escriben en ella tras su `Mutex`.
pub(crate) static CONSOLA: Once<Mutex<Consola>> = Once::new();
/// Puerta del kernel para la capacidad `sys_render_frame` del userspace WASM:
/// compone sobre la consola global un fotograma —cuyos limites el host ya
/// verifico matematicamente contra la memoria lineal del modulo— dentro de la
/// region de pantalla que el kernel asigno a esa aplicacion.
pub(crate) fn volcar_marco_wasm(region: RegionPantalla, datos: &[u8]) {
if let Some(consola) = CONSOLA.get() {
consola.lock().volcar_marco(region, datos);
}
}
/// Tatua la baliza de desalojo sobre la region de una aplicacion que el kernel
/// ha dado por terminada. El color delata la causa —purpura para una falla de
/// ejecucion o de combustible, amarillo palido para un desbordo de memoria—. Es
/// una advertencia NO fatal: la app muere, el kernel y sus vecinas siguen vivos.
pub(crate) fn pintar_desalojo(region: RegionPantalla, color: Color) {
if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_region(region, color);
}
}
+562
View File
@@ -0,0 +1,562 @@
// =============================================================================
// renaser :: kernel/src/drivers/disco.rs — Fase 6.2 :: el disco asincrono
// -----------------------------------------------------------------------------
// La Fase 6.1 hizo hablar al disco; pero lo hacia por SONDEO: el procesador se
// quedaba en un bucle de espera activa vigilando el «used ring» de virtio,
// incapaz de atender nada mas. La Fase 6.2 lo libera. La E/S de bloques pasa a
// ser REACTIVA, guiada por la interrupcion fisica del dispositivo:
//
// * `EsperaDisco` — un `Future` nativo: enviada la peticion, cede la CPU; la
// IRQ del disco lo despertara cuando el bloque este listo.
// * `atender_irq` — el punto al que salta el manejador de la IRQ del disco:
// reconoce la interrupcion en el dispositivo y despierta a quien aguardaba.
// * `bloquear_en` — el puente para los contextos SINCRONOS (el arranque, las
// capacidades WASM): lleva un `Future` de disco hasta su final durmiendo la
// CPU con `hlt` —jamas en espera activa una vez el sistema esta en marcha—.
//
// Subsisten de la Fase 6.1 el asignador de marcos por mapa de bits (con
// liberacion real) y `KernelHal`, el puente DMA hacia `virtio-drivers`.
// =============================================================================
use alloc::boxed::Box;
use alloc::vec;
use alloc::vec::Vec;
use core::future::Future;
use core::pin::Pin;
use core::ptr::NonNull;
use core::sync::atomic::{AtomicU64, AtomicU8, Ordering};
use core::task::{Context, Poll, Waker};
use spin::{Mutex, Once};
use virtio_drivers::device::blk::{BlkReq, BlkResp, VirtIOBlk, SECTOR_SIZE};
use virtio_drivers::transport::pci::bus::{Command, DeviceFunction, PciRoot};
use virtio_drivers::transport::pci::PciTransport;
use virtio_drivers::{BufferDirection, Hal, PhysAddr};
use x86_64::instructions::interrupts;
use super::pci::CamPuertos;
/// Tamaño de una pagina / marco fisico, en bytes.
const PAGINA: u64 = 4096;
/// Techo de marcos que gestiona el asignador de DMA: 4096 marcos => una arena
/// de 16 MiB. El DMA del disco —colas virtio y buferes rebote— necesita una
/// fraccion minima de eso; el techo solo acota el tamaño del mapa de bits.
const MAX_MARCOS: usize = 4096;
/// Vendor ID de VirtIO; Device IDs de un disco de bloques (transicional/moderno).
const VENDOR_VIRTIO: u16 = 0x1AF4;
const VIRTIO_BLK_IDS: [u16; 2] = [0x1001, 0x1042];
/// El tamaño de un sector, reexportado para el resto del kernel.
pub const TAM_SECTOR: usize = SECTOR_SIZE;
// =============================================================================
// EL OFFSET DE MEMORIA FISICA Y EL ASIGNADOR DE MARCOS
// =============================================================================
/// Desplazamiento al que el cargador mapeo toda la memoria fisica: una
/// direccion fisica `p` es accesible por el kernel en la virtual `p + offset`.
static OFFSET_FISICO: AtomicU64 = AtomicU64::new(0);
/// Asignador de marcos por MAPA DE BITS: gestiona una arena de RAM fisica
/// contigua y LIBERA. Cada bit representa un marco de 4 KiB; `1` es ocupado,
/// `0` libre. Un almacen de objetos vivo asigna y devuelve marcos sin descanso.
struct AsignadorMarcos {
/// Direccion fisica del primer marco gestionado, alineada a pagina.
base: u64,
/// Numero de marcos que abarca la arena.
total: usize,
/// Mapa de ocupacion: un bit por marco.
mapa: Vec<u64>,
}
impl AsignadorMarcos {
/// ¿Esta libre el marco `i`?
fn libre(&self, i: usize) -> bool {
(self.mapa[i / 64] >> (i % 64)) & 1 == 0
}
/// Marca el marco `i` como ocupado.
fn ocupar(&mut self, i: usize) {
self.mapa[i / 64] |= 1 << (i % 64);
}
/// Marca el marco `i` como libre.
fn soltar(&mut self, i: usize) {
self.mapa[i / 64] &= !(1u64 << (i % 64));
}
/// Reserva `paginas` marcos CONTIGUOS y devuelve la direccion fisica del
/// primero. `None` si no hay un hueco contiguo bastante grande.
fn asignar(&mut self, paginas: usize) -> Option<u64> {
if paginas == 0 || paginas > self.total {
return None;
}
let mut i = 0;
while i + paginas <= self.total {
// Buscar `paginas` marcos libres consecutivos a partir de `i`.
match (i..i + paginas).find(|&k| !self.libre(k)) {
// Un marco ocupado rompe la racha: reanudar tras el.
Some(ocupado) => i = ocupado + 1,
// Racha completa: ocuparla y entregar su direccion fisica.
None => {
for k in i..i + paginas {
self.ocupar(k);
}
return Some(self.base + (i as u64) * PAGINA);
}
}
}
None
}
/// Devuelve a la arena `paginas` marcos que arrancan en la direccion
/// fisica `fisica`. Direcciones ajenas a la arena se ignoran sin daño.
fn liberar(&mut self, fisica: u64, paginas: usize) {
if fisica < self.base {
return;
}
let inicio = ((fisica - self.base) / PAGINA) as usize;
for k in inicio..(inicio + paginas).min(self.total) {
self.soltar(k);
}
}
}
/// El asignador global de marcos para DMA. Se funde en `init`.
static ASIGNADOR: Once<Mutex<AsignadorMarcos>> = Once::new();
/// Funda el subsistema de disco: registra el offset de memoria fisica y forja
/// el asignador de marcos sobre la region de RAM libre que el cargador reporto.
/// Una sola vez, antes de montar el disco.
pub fn init(offset_fisico: u64, region_inicio: u64, region_fin: u64) {
OFFSET_FISICO.store(offset_fisico, Ordering::Relaxed);
let base = alinear_arriba(region_inicio, PAGINA);
let disponibles = region_fin.saturating_sub(base) / PAGINA;
let total = (disponibles as usize).min(MAX_MARCOS);
ASIGNADOR.call_once(|| {
Mutex::new(AsignadorMarcos {
base,
total,
mapa: vec![0u64; total.div_ceil(64)],
})
});
}
/// Redondea `valor` hacia arriba al multiplo de `alineacion` (potencia de dos).
fn alinear_arriba(valor: u64, alineacion: u64) -> u64 {
(valor + alineacion - 1) & !(alineacion - 1)
}
/// Reserva `paginas` marcos fisicos contiguos de 4 KiB y devuelve su direccion
/// fisica. Agotar la arena es un fallo del kernel, no recuperable aqui: el
/// rasgo `Hal` no admite que `dma_alloc` falle.
fn asignar_marcos(paginas: usize) -> u64 {
ASIGNADOR
.get()
.expect("DMA :: el asignador de marcos no esta fundado")
.lock()
.asignar(paginas)
.expect("DMA :: la arena de marcos fisicos se agoto")
}
/// Devuelve `paginas` marcos fisicos a la arena. El reverso de `asignar_marcos`.
fn liberar_marcos(fisica: u64, paginas: usize) {
if let Some(asignador) = ASIGNADOR.get() {
asignador.lock().liberar(fisica, paginas);
}
}
/// Traduce una direccion fisica a la virtual que el kernel puede desreferenciar.
fn a_virtual(fisica: u64) -> *mut u8 {
(fisica + OFFSET_FISICO.load(Ordering::Relaxed)) as *mut u8
}
// =============================================================================
// KernelHal — EL PUENTE ENTRE `virtio-drivers` Y LA MEMORIA DE renaser
// =============================================================================
/// La implementacion del rasgo `Hal` de `virtio-drivers`. Sin estado propio:
/// se apoya en el asignador de marcos y el offset fisico, ambos globales.
pub struct KernelHal;
// SEGURIDAD: cada metodo respeta su contrato — `dma_alloc` entrega marcos
// fisicos exclusivos, contiguos, alineados a pagina y a cero; `dma_dealloc` y
// `unshare` los devuelven a la arena; las traducciones de direccion son validas
// porque el cargador mapeo toda la memoria fisica.
unsafe impl Hal for KernelHal {
fn dma_alloc(paginas: usize, _direccion: BufferDirection) -> (PhysAddr, NonNull<u8>) {
let fisica = asignar_marcos(paginas);
let virtual_ = a_virtual(fisica);
// SEGURIDAD: `asignar_marcos` entrego `paginas` marcos exclusivos y
// contiguos; `virtual_` es su imagen valida y escribible en el mapeo
// de memoria fisica. El rasgo exige que las paginas vayan a cero.
unsafe {
core::ptr::write_bytes(virtual_, 0, paginas * PAGINA as usize);
}
(fisica, NonNull::new(virtual_).expect("DMA :: marco fisico nulo"))
}
unsafe fn dma_dealloc(fisica: PhysAddr, _virtual_: NonNull<u8>, paginas: usize) -> i32 {
// Con un asignador real, la liberacion ya NO es un gesto vacio: los
// marcos vuelven a la arena y quedan disponibles para el proximo DMA.
liberar_marcos(fisica, paginas);
0
}
unsafe fn mmio_phys_to_virt(fisica: PhysAddr, _tam: usize) -> NonNull<u8> {
// El cargador mapeo, como minimo, los primeros 4 GiB de memoria fisica;
// todo BAR MMIO de PCI cae dentro y es accesible en `fisica + offset`.
NonNull::new(a_virtual(fisica)).expect("MMIO :: direccion fisica nula")
}
unsafe fn share(bufer: NonNull<[u8]>, direccion: BufferDirection) -> PhysAddr {
let longitud = bufer.len();
let paginas = longitud.div_ceil(PAGINA as usize);
let fisica = asignar_marcos(paginas);
// Si el bufer viaja HACIA el dispositivo, copiarlo al area DMA rebote.
if !matches!(direccion, BufferDirection::DeviceToDriver) {
// SEGURIDAD: el rasgo garantiza que `bufer` apunta a `longitud`
// bytes validos; el area DMA recien reservada los abarca de sobra.
unsafe {
core::ptr::copy_nonoverlapping(
bufer.as_ptr() as *const u8,
a_virtual(fisica),
longitud,
);
}
}
fisica
}
unsafe fn unshare(fisica: PhysAddr, bufer: NonNull<[u8]>, direccion: BufferDirection) {
let longitud = bufer.len();
// Si el bufer viene DESDE el dispositivo, copiar el area DMA de vuelta.
if !matches!(direccion, BufferDirection::DriverToDevice) {
// SEGURIDAD: `fisica` lo entrego `share` para este mismo `bufer`;
// ambas regiones abarcan los `longitud` bytes que se copian.
unsafe {
core::ptr::copy_nonoverlapping(
a_virtual(fisica),
bufer.as_ptr() as *mut u8,
longitud,
);
}
}
// Devolver a la arena los marcos del area rebote.
liberar_marcos(fisica, longitud.div_ceil(PAGINA as usize));
}
}
// =============================================================================
// EL DISCO PERSISTENTE
// =============================================================================
/// El disco virtio-blk, ya montado. Envuelve al `VirtIOBlk` para poder ligarlo
/// a un `static`.
struct Disco(VirtIOBlk<KernelHal, PciTransport>);
// SEGURIDAD: `Disco` encierra punteros crudos a las colas virtio y al MMIO del
// dispositivo. renaser es un kernel de un solo nucleo y todo acceso al disco se
// serializa tras el `Mutex` global `DISCO`; jamas se comparte entre hilos
// reales. Todo acceso normal toma el cerrojo con las interrupciones acalladas,
// de modo que la IRQ del disco jamas lo encuentra ocupado.
unsafe impl Send for Disco {}
/// El disco global de renaser. Se monta una sola vez, en `montar`.
static DISCO: Once<Mutex<Disco>> = Once::new();
/// La linea de IRQ del disco, descubierta al montarlo. Vale 0 si el firmware no
/// enruto una linea util: en ese caso la E/S recae en el sondeo, con la red de
/// seguridad del temporizador.
static IRQ_DISCO: AtomicU8 = AtomicU8::new(0);
/// Cuenta de interrupciones del disco atendidas desde el arranque. Es el
/// testigo vivo de que la E/S asincrona late de verdad.
static PULSOS_DISCO: AtomicU64 = AtomicU64::new(0);
/// El waker de la (unica) espera de disco en curso. Las operaciones de disco se
/// serializan, de modo que una sola ranura basta. La IRQ del disco lo invoca.
static WAKER_DISCO: Mutex<Option<Waker>> = Mutex::new(None);
/// Enumera el bus PCI, localiza el disco virtio-blk, lo monta y lo deja tras el
/// `Mutex` global. Descubre ademas su linea de IRQ, registra el manejador y
/// abre la linea en el PIC: desde aqui el disco puede interrumpir. Devuelve la
/// capacidad del disco en sectores. Toda falla se devuelve como `Err`.
pub fn montar() -> Result<u64, &'static str> {
let mut raiz = PciRoot::new(CamPuertos);
// 1. Localizar el primer disco virtio-blk recorriendo el bus.
let mut hallado: Option<DeviceFunction> = None;
'busqueda: for bus in 0..=255u8 {
for (device_function, info) in raiz.enumerate_bus(bus) {
if info.vendor_id == VENDOR_VIRTIO && VIRTIO_BLK_IDS.contains(&info.device_id) {
hallado = Some(device_function);
break 'busqueda;
}
}
}
let device_function = hallado.ok_or("virtio-blk no hallado en el bus PCI")?;
// 2. Habilitar E/S, espacio de memoria y BUS-MASTER. Sin bus-master el
// dispositivo no puede iniciar transferencias DMA por su cuenta.
raiz.set_command(
device_function,
Command::IO_SPACE | Command::MEMORY_SPACE | Command::BUS_MASTER,
);
// 3. Montar el transporte PCI moderno de virtio y el dispositivo de bloques.
let transporte = PciTransport::new::<KernelHal, _>(&mut raiz, device_function)
.map_err(|_| "no se pudo montar el transporte PCI de virtio")?;
let mut disco = VirtIOBlk::<KernelHal, _>::new(transporte)
.map_err(|_| "no se pudo inicializar el dispositivo virtio-blk")?;
let capacidad = disco.capacity();
// 4. Pedir al dispositivo que EMITA una interrupcion al completar cada
// peticion — el corazon de la E/S asincrona de esta fase.
disco.enable_interrupts();
DISCO.call_once(|| Mutex::new(Disco(disco)));
// 5. Descubrir la linea de IRQ que el firmware asigno al dispositivo y
// enrutarla: registrar el manejador en la IDT y abrir la linea en el
// PIC. Las IRQ 0 y 1 son del temporizador y el teclado; un valor fuera
// de 2..15 (p. ej. 0xFF, «sin conexion») significa que no hay linea
// util — la E/S seguira funcionando, pero por sondeo.
let irq = super::pci::linea_irq(device_function);
if (2..=15).contains(&irq) {
crate::interrupts::registrar_irq_disco(irq);
crate::pic::desenmascarar(irq);
IRQ_DISCO.store(irq, Ordering::SeqCst);
}
Ok(capacidad)
}
/// La linea de IRQ del disco, si el firmware enruto una util.
pub fn irq() -> Option<u8> {
match IRQ_DISCO.load(Ordering::SeqCst) {
0 => None,
n => Some(n),
}
}
/// Numero de interrupciones del disco atendidas desde el arranque.
pub fn pulsos_disco() -> u64 {
PULSOS_DISCO.load(Ordering::Relaxed)
}
/// Punto de entrada DESDE el manejador de la IRQ del disco. Reconoce la
/// interrupcion en el dispositivo —leer su registro ISR baja la linea INTx— y
/// despierta a la tarea que aguardaba el bloque. Breve y libre de panicos:
/// corre en contexto de interrupcion.
pub fn atender_irq() {
// Estamos en contexto de interrupcion (IF=0). Todo acceso normal a `DISCO`
// toma su cerrojo con las interrupciones acalladas, de modo que aqui jamas
// esta ocupado: tomarlo no puede interbloquear.
if let Some(disco) = DISCO.get() {
let _ = disco.lock().0.ack_interrupt();
}
PULSOS_DISCO.fetch_add(1, Ordering::Relaxed);
if let Some(waker) = WAKER_DISCO.lock().take() {
waker.wake();
}
}
/// Inscribe el waker de la espera de disco en curso. Una sola ranura: las
/// operaciones de disco estan serializadas. El cerrojo lo disputa el manejador
/// de IRQ — tomarlo con las interrupciones acalladas hace imposible el bloqueo.
fn registrar_waker(waker: Waker) {
interrupts::without_interrupts(|| *WAKER_DISCO.lock() = Some(waker));
}
// =============================================================================
// EsperaDisco — UNA OPERACION DE BLOQUE COMO `Future`
// =============================================================================
/// Una transferencia de bloques en vuelo, expresada como `Future`. Posee sus
/// propios buferes DMA —el encabezado de peticion, la respuesta y los datos—,
/// que `virtio-drivers` mantiene prestados hasta que la operacion concluye.
///
/// Su `poll` envia la peticion la primera vez y, despues, comprueba el «used
/// ring»: si el dispositivo aun no ha terminado, inscribe el waker y cede; la
/// IRQ del disco lo reanudara. Una operacion a la vez — basta para renaser.
pub struct EsperaDisco {
/// Encabezado de la peticion virtio. En el heap: direccion estable mientras
/// el dispositivo lo tiene prestado, sin importar si el `Future` se mueve.
req: Box<BlkReq>,
/// Respuesta de estado del dispositivo. En el heap, por la misma razon.
resp: Box<BlkResp>,
/// Los datos: destino de una lectura, origen de una escritura.
buf: Vec<u8>,
/// Primer sector de la transferencia.
sector: u64,
/// `true` si la operacion escribe; `false` si lee.
es_escritura: bool,
/// Token que `virtio-drivers` devolvio al enviar la peticion. `None` hasta
/// que el primer `poll` la envia.
token: Option<u16>,
}
impl EsperaDisco {
/// Hace avanzar la operacion: la envia si aun no lo estaba y comprueba si el
/// dispositivo la completo. `None` => sigue en vuelo; `Some` => terminada.
fn avanzar(&mut self) -> Option<Result<Vec<u8>, &'static str>> {
// Todo el dialogo con el dispositivo, con las interrupciones acalladas:
// asi la IRQ del disco jamas encuentra ocupado el cerrojo de `DISCO`.
interrupts::without_interrupts(|| {
let disco = match DISCO.get() {
Some(disco) => disco,
None => return Some(Err("disco no montado")),
};
let mut guardia = disco.lock();
let blk = &mut guardia.0;
// 1. Enviar la peticion la primera vez que se sondea esta espera.
if self.token.is_none() {
// SEGURIDAD: `req`, `buf` y `resp` viven en esta `EsperaDisco`,
// que no se libera ni se accede por otra via hasta que el
// `complete_*` de mas abajo cierre la operacion.
let envio = unsafe {
if self.es_escritura {
blk.write_blocks_nb(
self.sector as usize,
&mut self.req,
self.buf.as_slice(),
&mut self.resp,
)
} else {
blk.read_blocks_nb(
self.sector as usize,
&mut self.req,
self.buf.as_mut_slice(),
&mut self.resp,
)
}
};
match envio {
Ok(token) => self.token = Some(token),
Err(_) => return Some(Err("no se pudo enviar la peticion al disco")),
}
}
let token = self.token.unwrap();
// 2. ¿Ha colocado el dispositivo este token en el «used ring»?
if blk.peek_used() != Some(token) {
return None; // aun en vuelo
}
// 3. Completado: recoger el resultado y desligar los buferes DMA.
// SEGURIDAD: se pasan los MISMOS buferes que recibio el `*_nb`.
let fin = unsafe {
if self.es_escritura {
blk.complete_write_blocks(token, &self.req, self.buf.as_slice(), &mut self.resp)
} else {
blk.complete_read_blocks(
token,
&self.req,
self.buf.as_mut_slice(),
&mut self.resp,
)
}
};
Some(match fin {
Ok(()) => Ok(core::mem::take(&mut self.buf)),
Err(_) => Err("la operacion de disco no se completo"),
})
})
}
}
impl Future for EsperaDisco {
/// Al terminar, una lectura entrega sus datos; una escritura, un vector
/// vacio. El error es siempre un mensaje estable.
type Output = Result<Vec<u8>, &'static str>;
fn poll(self: Pin<&mut Self>, contexto: &mut Context<'_>) -> Poll<Self::Output> {
// `EsperaDisco` es `Unpin` —solo `Box`, `Vec` y escalares—: el `Pin`
// no impone nada y el acceso mutable es directo.
let this = self.get_mut();
// Inscribir el waker ANTES de comprobar: si la IRQ se cuela entre la
// comprobacion y la inscripcion, el waker ya esta puesto y el despertar
// no se pierde — el mismo blindaje que usa `EsperaFrame` (ver `reloj`).
registrar_waker(contexto.waker().clone());
match this.avanzar() {
Some(resultado) => Poll::Ready(resultado),
None => Poll::Pending,
}
}
}
/// Prepara la LECTURA asincrona de `n_sectores` sectores desde `sector`. El
/// `Future` que devuelve no toca el disco hasta que se le sondea.
pub fn leer_bloques(sector: u64, n_sectores: usize) -> EsperaDisco {
EsperaDisco {
req: Box::new(BlkReq::default()),
resp: Box::new(BlkResp::default()),
buf: vec![0u8; n_sectores * TAM_SECTOR],
sector,
es_escritura: false,
token: None,
}
}
/// Prepara la ESCRITURA asincrona de `datos` a partir de `sector`. La longitud
/// de `datos` debe ser multiplo de un sector.
pub fn escribir_bloques(sector: u64, datos: Vec<u8>) -> EsperaDisco {
EsperaDisco {
req: Box::new(BlkReq::default()),
resp: Box::new(BlkResp::default()),
buf: datos,
sector,
es_escritura: true,
token: None,
}
}
// =============================================================================
// bloquear_en — EL PUENTE PARA LOS CONTEXTOS SINCRONOS
// =============================================================================
/// Lleva un `Future` de disco hasta su final desde un contexto SINCRONO — el
/// arranque, o una capacidad WASM, que no pueden `.await`-ear—. Mientras el
/// disco trabaja:
///
/// * si las interrupciones estan activas, duerme la CPU con `hlt`; la
/// despertara la IRQ del disco —o el temporizador, como red de seguridad—;
/// * si no —en el arranque, antes de habilitarlas—, sondea en bucle.
///
/// Asi, una vez el sistema esta en marcha, la espera de disco JAMAS malgasta
/// ciclos en espera activa.
fn bloquear_en<F: Future>(futuro: F) -> F::Output {
let mut futuro = core::pin::pin!(futuro);
let waker = Waker::noop();
let mut contexto = Context::from_waker(waker);
loop {
match futuro.as_mut().poll(&mut contexto) {
Poll::Ready(salida) => return salida,
Poll::Pending => {
if interrupts::are_enabled() {
x86_64::instructions::hlt();
} else {
core::hint::spin_loop();
}
}
}
}
}
/// Lee `buf.len() / 512` sectores consecutivos a partir de `sector`. Sincrono:
/// construido sobre la maquinaria asincrona via `bloquear_en`. El bufer debe
/// medir un multiplo entero de un sector.
pub fn leer_sectores(sector: u64, buf: &mut [u8]) -> Result<(), &'static str> {
let datos = bloquear_en(leer_bloques(sector, buf.len() / TAM_SECTOR))?;
buf.copy_from_slice(&datos);
Ok(())
}
/// Escribe `buf.len() / 512` sectores consecutivos a partir de `sector`.
/// Sincrono, sobre la misma maquinaria asincrona.
pub fn escribir_sectores(sector: u64, buf: &[u8]) -> Result<(), &'static str> {
bloquear_en(escribir_bloques(sector, buf.to_vec())).map(|_| ())
}
+15
View File
@@ -0,0 +1,15 @@
// =============================================================================
// renaser :: kernel/src/drivers — Fase 6.1 :: el puente hacia el hardware real
// -----------------------------------------------------------------------------
// Hasta aqui renaser solo tocaba el silicio que el firmware le servia en
// bandeja: el framebuffer GOP, el temporizador, el teclado. Los `drivers`
// abren la primera via hacia hardware que el kernel debe DESCUBRIR y reclamar
// por si mismo:
//
// * `pci` — acceso al espacio de configuracion del bus PCI (0xCF8/0xCFC).
// * `disco` — el disco virtio-blk: asignador de marcos DMA, `Hal` y la
// lectura, por sondeo, de su primer sector.
// =============================================================================
pub mod disco;
pub mod pci;
+76
View File
@@ -0,0 +1,76 @@
// =============================================================================
// renaser :: kernel/src/drivers/pci.rs — Fase 6.1 :: el acceso al bus PCI
// -----------------------------------------------------------------------------
// El cargador `bootloader` entrega el mapa de memoria y el framebuffer, pero
// NO un censo de perifericos: descubrir el disco es tarea del kernel. Aqui
// renaser habla con el espacio de configuracion del bus PCI a traves del
// mecanismo #1 —los puertos 0xCF8 (direccion) y 0xCFC (datos)—.
//
// Este modulo provee `CamPuertos`, una implementacion del rasgo
// `ConfigurationAccess` de `virtio-drivers`: la crate enumera el bus y mapea
// los BARs del dispositivo apoyandose en estas dos funciones de acceso.
// =============================================================================
use virtio_drivers::transport::pci::bus::{ConfigurationAccess, DeviceFunction};
use x86_64::instructions::port::Port;
/// Puerto de DIRECCION del mecanismo de configuracion PCI #1.
const CONFIG_ADDRESS: u16 = 0xCF8;
/// Puerto de DATOS del mecanismo de configuracion PCI #1.
const CONFIG_DATA: u16 = 0xCFC;
/// Compone la palabra de direccion del mecanismo #1: bit 31 de habilitacion,
/// bus, dispositivo, funcion y offset de registro alineado a dword.
fn direccion(device_function: DeviceFunction, offset: u8) -> u32 {
0x8000_0000
| ((device_function.bus as u32) << 16)
| ((device_function.device as u32) << 11)
| ((device_function.function as u32) << 8)
| ((offset as u32) & 0xFC)
}
/// Lee un registro de 32 bits del espacio de configuracion PCI.
fn leer(device_function: DeviceFunction, offset: u8) -> u32 {
// SEGURIDAD: 0xCF8/0xCFC son los puertos del mecanismo de configuracion
// PCI #1, fijos en la arquitectura PC. La direccion lleva su bit de
// habilitacion y el offset alineado a dword, como exige el protocolo.
unsafe {
Port::<u32>::new(CONFIG_ADDRESS).write(direccion(device_function, offset));
Port::<u32>::new(CONFIG_DATA).read()
}
}
/// Escribe un registro de 32 bits en el espacio de configuracion PCI.
fn escribir(device_function: DeviceFunction, offset: u8, dato: u32) {
// SEGURIDAD: vease `leer` — mismos puertos, mismo protocolo del 8259/PCI.
unsafe {
Port::<u32>::new(CONFIG_ADDRESS).write(direccion(device_function, offset));
Port::<u32>::new(CONFIG_DATA).write(dato);
}
}
/// Lee el registro «Interrupt Line» (byte bajo del offset 0x3C): la linea de
/// IRQ del PIC que el firmware UEFI enruto y asigno a este dispositivo. Es el
/// puente entre el descubrimiento PCI y el manejo de interrupciones (Fase 6.2).
pub fn linea_irq(device_function: DeviceFunction) -> u8 {
(leer(device_function, 0x3C) & 0xFF) as u8
}
/// Acceso al espacio de configuracion PCI por puertos de E/S — el mecanismo #1.
/// Es un tipo sin estado: toda la informacion viaja en cada llamada.
pub struct CamPuertos;
impl ConfigurationAccess for CamPuertos {
fn read_word(&self, device_function: DeviceFunction, register_offset: u8) -> u32 {
leer(device_function, register_offset)
}
fn write_word(&mut self, device_function: DeviceFunction, register_offset: u8, data: u32) {
escribir(device_function, register_offset, data);
}
unsafe fn unsafe_clone(&self) -> Self {
// `CamPuertos` no tiene estado: clonarlo es trivial y sin riesgo.
CamPuertos
}
}
+80
View File
@@ -0,0 +1,80 @@
// =============================================================================
// renaser :: kernel/src/gdt.rs — Fase 2 :: cimientos del manejo de fallos
// -----------------------------------------------------------------------------
// La GDT y el TSS no se ven jamas, pero sostienen todo lo demas. Su cometido
// esencial en renaser es reservar un stack de emergencia inquebrantable: si la
// pila del kernel se desborda, el manejador de doble fallo aterrizara sobre
// terreno firme en lugar de arrastrar al sistema hacia un fallo triple.
//
// Una sola verdad, instalada una sola vez, que PERSISTE bajo cada interrupcion.
// =============================================================================
use x86_64::instructions::segmentation::{Segment, CS, DS, ES, SS};
use x86_64::instructions::tables::load_tss;
use x86_64::structures::gdt::{Descriptor, GlobalDescriptorTable, SegmentSelector};
use x86_64::structures::tss::TaskStateSegment;
use x86_64::VirtAddr;
use crate::CeldaSync;
/// Indice, dentro de la Interrupt Stack Table del TSS, del stack reservado
/// para el manejador de doble fallo.
pub const IST_DOBLE_FALLO: u16 = 0;
/// Tamaño del stack de emergencia: 20 KiB. Holgura suficiente para diagnosticar
/// el fallo y encender la baliza sin volver a tocar la pila comprometida.
const TAM_STACK_EMERGENCIA: usize = 4096 * 5;
/// Stack de emergencia del doble fallo. Vive en `.bss`; la IST apuntara a su
/// extremo superior, pues la pila de x86_64 crece hacia direcciones menores.
static STACK_EMERGENCIA: CeldaSync<[u8; TAM_STACK_EMERGENCIA]> =
CeldaSync::nueva([0; TAM_STACK_EMERGENCIA]);
/// El Task State Segment. En 64 bits ya no describe «tareas»: lo conservamos
/// unicamente por su Interrupt Stack Table.
static TSS: CeldaSync<TaskStateSegment> = CeldaSync::nueva(TaskStateSegment::new());
/// La Global Descriptor Table propia de renaser.
static GDT: CeldaSync<GlobalDescriptorTable> = CeldaSync::nueva(GlobalDescriptorTable::new());
/// Instala una GDT propia con su TSS y arma el stack de emergencia.
///
/// Debe invocarse una sola vez, durante el arranque, ANTES de cargar la IDT:
/// la IDT inscribe en cada entrada el selector de codigo vigente en ese momento.
pub fn init() {
// --- 1. Inscribir en el TSS la cima del stack de emergencia. ---
// La pila crece hacia abajo: la IST exige la direccion MAS ALTA del bloque.
let cima_stack = VirtAddr::from_ptr(STACK_EMERGENCIA.puntero()) + TAM_STACK_EMERGENCIA as u64;
// SEGURIDAD: `init` corre una sola vez, en arranque secuencial de un solo
// hilo; nada mas referencia el TSS todavia.
unsafe {
(*TSS.puntero()).interrupt_stack_table[IST_DOBLE_FALLO as usize] = cima_stack;
}
// --- 2. Construir la GDT: codigo de kernel, datos de kernel y el TSS. ---
// SEGURIDAD: mismo argumento de unicidad; la GDT aun no esta cargada y
// ningun otro flujo posee referencias a estas celdas.
let gdt: &'static mut GlobalDescriptorTable = unsafe { &mut *GDT.puntero() };
let tss: &'static TaskStateSegment = unsafe { &*TSS.puntero() };
let sel_codigo: SegmentSelector = gdt.append(Descriptor::kernel_code_segment());
let sel_datos: SegmentSelector = gdt.append(Descriptor::kernel_data_segment());
let sel_tss: SegmentSelector = gdt.append(Descriptor::tss_segment(tss));
// --- 3. Activar la GDT y recargar TODOS los registros de segmento. ---
let gdt_estatica: &'static GlobalDescriptorTable = gdt;
gdt_estatica.load();
// SEGURIDAD: los tres selectores apuntan a entradas validas de la GDT
// recien cargada. Recargar SS/DS/ES es IMPRESCINDIBLE, no opcional: el
// cargador nos deja esos registros con selectores de SU tabla; si SS
// conservara el valor heredado, el primer `iretq` de una rutina de
// excepcion intentaria recargar un selector que en nuestra GDT ya no
// describe un segmento de datos, y degeneraria en un #GP.
unsafe {
CS::set_reg(sel_codigo);
SS::set_reg(sel_datos);
DS::set_reg(sel_datos);
ES::set_reg(sel_datos);
load_tss(sel_tss);
}
}
+325
View File
@@ -0,0 +1,325 @@
// =============================================================================
// renaser :: kernel/src/grafico.rs — el sustrato grafico del sistema
// -----------------------------------------------------------------------------
// En renaser el texto es un caso particular del dibujo, y el dibujo descansa
// sobre este modulo: el color, el framebuffer fisico (`Pantalla`) y el lienzo
// intermedio en RAM (`Lienzo`) que sostiene la tecnica de doble bufer.
// =============================================================================
use core::cell::UnsafeCell;
use core::ptr;
use core::sync::atomic::{AtomicBool, Ordering};
use bootloader_api::info::{FrameBuffer, FrameBufferInfo, PixelFormat};
use embedded_graphics::draw_target::DrawTarget;
use embedded_graphics::geometry::{OriginDimensions, Size};
use embedded_graphics::pixelcolor::{Rgb888, RgbColor};
use embedded_graphics::Pixel;
/// Ancho maximo de lienzo soportado (Full HD).
pub(crate) const ANCHO_MAX: usize = 1920;
/// Alto maximo de lienzo soportado (Full HD).
pub(crate) const ALTO_MAX: usize = 1080;
/// Capacidad del lienzo intermedio, en pixeles de 32 bits.
const PIXELES_MAX: usize = ANCHO_MAX * ALTO_MAX;
// =============================================================================
// COLOR — la unidad indivisible del lenguaje visual de renaser
// =============================================================================
/// Color de 24 bits, independiente del formato fisico del framebuffer.
#[derive(Clone, Copy)]
pub(crate) struct Color {
pub(crate) r: u8,
pub(crate) g: u8,
pub(crate) b: u8,
}
impl Color {
/// Reposo del lienzo: un indigo casi negro, sereno y persistente.
pub(crate) const LIENZO_EN_REPOSO: Color = Color {
r: 0x12,
g: 0x16,
b: 0x20,
};
/// Alerta de colapso: un rojo saturado, imposible de ignorar.
pub(crate) const ALERTA: Color = Color {
r: 0xD4,
g: 0x1E,
b: 0x2C,
};
/// Agotamiento de memoria (OOM): un naranja de advertencia.
pub(crate) const OOM: Color = Color {
r: 0xFF,
g: 0xA5,
b: 0x00,
};
/// Tinta del texto: un blanco suave, legible sobre el indigo.
pub(crate) const TEXTO: Color = Color {
r: 0xE8,
g: 0xEC,
b: 0xF4,
};
/// Desalojo de una aplicacion: un purpura inequivoco. Distinto del rojo de
/// colapso del kernel y del naranja de OOM — porque esto NO es un colapso:
/// es el kernel conteniendo a un inquilino discolo y siguiendo vivo.
pub(crate) const DESALOJO: Color = Color {
r: 0x8B,
g: 0x5C,
b: 0xF6,
};
/// Desalojo por desbordo de memoria: un amarillo palido. Distingue al
/// inquilino que revento su techo ESPACIAL del que agoto su tiempo (purpura).
pub(crate) const DESALOJO_MEMORIA: Color = Color {
r: 0xFF,
g: 0xFF,
b: 0xE0,
};
}
// =============================================================================
// REGION — la sub-superficie que el kernel asigna a cada aplicacion
// =============================================================================
/// Una sub-region rectangular de la pantalla, en pixeles. El kernel asigna una
/// a cada aplicacion del userspace: es, a la vez, su ventana al mundo y su
/// confinamiento — una app jamas pinta un pixel fuera de la suya.
#[derive(Clone, Copy)]
pub(crate) struct RegionPantalla {
/// Desplazamiento horizontal de la esquina superior izquierda.
pub(crate) x: usize,
/// Desplazamiento vertical de la esquina superior izquierda.
pub(crate) y: usize,
/// Ancho de la region, en pixeles.
pub(crate) ancho: usize,
/// Alto de la region, en pixeles.
pub(crate) alto: usize,
}
impl RegionPantalla {
/// Numero total de pixeles que abarca la region.
pub(crate) const fn pixeles(&self) -> usize {
self.ancho * self.alto
}
}
/// Traduce un [`Color`] logico al valor nativo de 32 bits que el framebuffer
/// espera, respetando el orden de canales que reporta el firmware UEFI.
pub(crate) fn codificar(formato: PixelFormat, color: Color) -> u32 {
let (r, g, b) = (color.r as u32, color.g as u32, color.b as u32);
match formato {
PixelFormat::Rgb => r | (g << 8) | (b << 16),
PixelFormat::Bgr => b | (g << 8) | (r << 16),
PixelFormat::U8 => (r * 54 + g * 183 + b * 19) >> 8,
PixelFormat::Unknown {
red_position,
green_position,
blue_position,
} => (r << red_position) | (g << green_position) | (b << blue_position),
_ => r | (g << 8) | (b << 16),
}
}
// =============================================================================
// ESCRITURA VOLATIL — la unica celula que toca memoria de video real
// =============================================================================
/// Deposita un pixel ya codificado en una direccion del framebuffer fisico.
/// Las escrituras son **volatiles**: el optimizador no puede elidirlas.
///
/// # Seguridad
///
/// `destino` debe apuntar a memoria de video valida y escribible para
/// `bytes_por_pixel` bytes, correctamente alineada para el ancho de escritura.
#[inline]
pub(crate) unsafe fn escribir_pixel_volatil(destino: *mut u8, valor: u32, bytes_por_pixel: usize) {
match bytes_por_pixel {
4 => unsafe { ptr::write_volatile(destino.cast::<u32>(), valor) },
3 => unsafe {
ptr::write_volatile(destino, valor as u8);
ptr::write_volatile(destino.add(1), (valor >> 8) as u8);
ptr::write_volatile(destino.add(2), (valor >> 16) as u8);
},
2 => unsafe { ptr::write_volatile(destino.cast::<u16>(), valor as u16) },
_ => unsafe { ptr::write_volatile(destino, valor as u8) },
}
}
// =============================================================================
// LIENZO INTERMEDIO — el corazon del doble bufer
// =============================================================================
/// Respaldo estatico del lienzo, alineado a pagina. Vive en `.bss`.
#[repr(align(4096))]
struct LienzoEstatico(UnsafeCell<[u32; PIXELES_MAX]>);
// SEGURIDAD: el acceso se serializa mediante `LIENZO_ENTREGADO`, que garantiza
// un unico prestamo mutable durante toda la vida del sistema.
unsafe impl Sync for LienzoEstatico {}
/// Memoria de respaldo del lienzo intermedio.
static MEMORIA_LIENZO: LienzoEstatico = LienzoEstatico(UnsafeCell::new([0u32; PIXELES_MAX]));
/// Centinela de entrega unica del lienzo.
static LIENZO_ENTREGADO: AtomicBool = AtomicBool::new(false);
/// Entrega — exactamente una vez — el prestamo mutable de la memoria del lienzo.
pub(crate) fn reclamar_memoria_lienzo() -> Option<&'static mut [u32]> {
if LIENZO_ENTREGADO.swap(true, Ordering::AcqRel) {
return None;
}
// SEGURIDAD: el `swap` anterior garantiza que este es el unico prestamo
// mutable de `MEMORIA_LIENZO` durante toda la ejecucion.
let arreglo: &'static mut [u32; PIXELES_MAX] = unsafe { &mut *MEMORIA_LIENZO.0.get() };
Some(arreglo.as_mut_slice())
}
/// Superficie de dibujo en RAM. Cada pixel se almacena ya codificado.
pub(crate) struct Lienzo {
pub(crate) pixeles: &'static mut [u32],
pub(crate) ancho: usize,
pub(crate) alto: usize,
pub(crate) formato: PixelFormat,
}
impl Lienzo {
/// Construye un lienzo sobre la memoria de respaldo reclamada.
pub(crate) fn nuevo(
memoria: &'static mut [u32],
ancho: usize,
alto: usize,
formato: PixelFormat,
) -> Lienzo {
Lienzo {
pixeles: memoria,
ancho,
alto,
formato,
}
}
/// Pinta un unico pixel. Las coordenadas fuera del lienzo se ignoran.
pub(crate) fn pintar_pixel(&mut self, x: usize, y: usize, color: Color) {
if x < self.ancho && y < self.alto {
self.pixeles[y * self.ancho + x] = codificar(self.formato, color);
}
}
/// Rellena un rectangulo, recortado con firmeza a los limites del lienzo.
pub(crate) fn rellenar_rect(
&mut self,
x0: usize,
y0: usize,
ancho: usize,
alto: usize,
color: Color,
) {
let valor = codificar(self.formato, color);
let x_ini = x0.min(self.ancho);
let y_ini = y0.min(self.alto);
let x_fin = (x0 + ancho).min(self.ancho);
let y_fin = (y0 + alto).min(self.alto);
let mut y = y_ini;
while y < y_fin {
let base = y * self.ancho;
let mut x = x_ini;
while x < x_fin {
self.pixeles[base + x] = valor;
x += 1;
}
y += 1;
}
}
/// Inunda el lienzo entero con un color plano.
pub(crate) fn limpiar(&mut self, color: Color) {
self.rellenar_rect(0, 0, self.ancho, self.alto, color);
}
}
// `embedded-graphics` ve el lienzo como un `DrawTarget`: sus primitivas
// vectoriales pueden dibujar directamente sobre el.
impl DrawTarget for Lienzo {
type Color = Rgb888;
type Error = core::convert::Infallible;
fn draw_iter<I>(&mut self, pixeles: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = Pixel<Self::Color>>,
{
for Pixel(punto, color) in pixeles {
if let (Ok(x), Ok(y)) = (usize::try_from(punto.x), usize::try_from(punto.y)) {
self.pintar_pixel(
x,
y,
Color {
r: color.r(),
g: color.g(),
b: color.b(),
},
);
}
}
Ok(())
}
}
impl OriginDimensions for Lienzo {
fn size(&self) -> Size {
Size::new(self.ancho as u32, self.alto as u32)
}
}
// =============================================================================
// PANTALLA — el framebuffer fisico GOP, envuelto en seguridad
// =============================================================================
/// Vista segura del framebuffer lineal entregado por el firmware UEFI.
pub(crate) struct Pantalla {
pub(crate) base: *mut u8,
pub(crate) ancho: usize,
pub(crate) alto: usize,
pub(crate) paso_bytes: usize,
pub(crate) bytes_por_pixel: usize,
}
impl Pantalla {
/// Adopta el framebuffer descrito por `info`. La memoria de video es
/// permanente, asi que conservar su puntero crudo es legitimo.
pub(crate) fn adoptar(framebuffer: &mut FrameBuffer, info: FrameBufferInfo) -> Pantalla {
let base = framebuffer.buffer_mut().as_mut_ptr();
Pantalla {
base,
ancho: info.width,
alto: info.height,
paso_bytes: info.stride * info.bytes_per_pixel,
bytes_por_pixel: info.bytes_per_pixel,
}
}
/// Vuelca el lienzo intermedio sobre la pantalla fisica de un solo gesto.
pub(crate) fn presentar(&mut self, lienzo: &Lienzo) {
let ancho = self.ancho.min(lienzo.ancho);
let alto = self.alto.min(lienzo.alto);
for y in 0..alto {
let fila_fisica = y * self.paso_bytes;
let fila_lienzo = y * lienzo.ancho;
for x in 0..ancho {
let pixel = lienzo.pixeles[fila_lienzo + x];
// SEGURIDAD: x e y estan acotados por las dimensiones reales
// del framebuffer; el desplazamiento cae siempre dentro de el.
unsafe {
let destino = self.base.add(fila_fisica + x * self.bytes_por_pixel);
escribir_pixel_volatil(destino, pixel, self.bytes_por_pixel);
}
}
}
}
}
+148
View File
@@ -0,0 +1,148 @@
// =============================================================================
// renaser :: kernel/src/interrupts.rs — Fase 2/3 :: la IDT y los reflejos
// -----------------------------------------------------------------------------
// La Interrupt Descriptor Table es la tabla de reflejos del procesador. ante
// cada excepcion o IRQ, la CPU abandona lo que hacia y salta a la entrada que
// le corresponde. renaser distingue tres naturalezas de impulso:
//
// * RECUPERABLE — el breakpoint (#BP): se atiende y la ejecucion prosigue.
// * FATAL — el resto de excepciones: se entregan al `panic!`.
// * HARDWARE — temporizador, teclado y disco: ya NO escriben estado
// rustico, sino que alimentan el reactor asincrono.
// =============================================================================
use core::sync::atomic::{AtomicU8, Ordering};
use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame, PageFaultErrorCode};
use crate::{gdt, pic, CeldaSync};
/// La Interrupt Descriptor Table propia de renaser.
static IDT: CeldaSync<InterruptDescriptorTable> =
CeldaSync::nueva(InterruptDescriptorTable::new());
/// Vector de la IDT asignado a la IRQ del disco. Vale 0 hasta que el montaje
/// del disco (Fase 6.2) descubra su linea de IRQ y la registre — ninguna IRQ
/// legitima del disco vive en el vector 0 (reservado a las excepciones).
static VECTOR_DISCO: AtomicU8 = AtomicU8::new(0);
/// Construye y activa la Interrupt Descriptor Table.
///
/// Debe invocarse una sola vez, durante el arranque, DESPUES de [`gdt::init`].
pub fn init() {
// SEGURIDAD: `init` corre una sola vez, en arranque secuencial de un solo
// hilo; nada mas referencia la IDT todavia.
let idt: &'static mut InterruptDescriptorTable = unsafe { &mut *IDT.puntero() };
// --- Excepciones de CPU ---
idt.breakpoint.set_handler_fn(reflejo_breakpoint);
idt.invalid_opcode.set_handler_fn(reflejo_opcode_invalido);
idt.divide_error.set_handler_fn(reflejo_division);
idt.general_protection_fault
.set_handler_fn(reflejo_proteccion_general);
idt.page_fault.set_handler_fn(reflejo_fallo_pagina);
// El doble fallo se atiende SIEMPRE sobre el stack de emergencia del TSS:
// ni un desbordamiento de la pila del kernel impedira su diagnostico.
// SEGURIDAD: el indice IST referido fue armado previamente por `gdt::init`.
unsafe {
idt.double_fault
.set_handler_fn(reflejo_doble_fallo)
.set_stack_index(gdt::IST_DOBLE_FALLO);
}
// --- Interrupciones de hardware, ya remapeadas por el PIC ---
idt[pic::VECTOR_TEMPORIZADOR].set_handler_fn(irq_temporizador);
idt[pic::VECTOR_TECLADO].set_handler_fn(irq_teclado);
let idt_estatica: &'static InterruptDescriptorTable = idt;
idt_estatica.load();
}
/// Registra el manejador de la IRQ del disco virtio-blk en la entrada de la IDT
/// que corresponde a `irq` (Fase 6.2). Lo invoca el montaje del disco, una vez
/// descubierta la linea de IRQ que el firmware le asigno.
///
/// Se llama durante el arranque secuencial, con las interrupciones aun
/// desactivadas y la linea todavia enmascarada en el PIC: la entrada que se
/// escribe no puede dispararse mientras se escribe.
pub fn registrar_irq_disco(irq: u8) {
let vector = pic::vector_irq(irq);
VECTOR_DISCO.store(vector, Ordering::SeqCst);
// SEGURIDAD: el arranque es secuencial y de un solo hilo; las interrupciones
// estan desactivadas y la linea del disco, enmascarada. La IDT ya esta
// cargada (`init` hizo `lidt`), pero la CPU relee cada entrada en cada
// interrupcion: modificar esta entrada en memoria surte efecto de inmediato.
let idt: &'static mut InterruptDescriptorTable = unsafe { &mut *IDT.puntero() };
idt[vector].set_handler_fn(irq_disco);
}
// =============================================================================
// REFLEJOS DE EXCEPCION — las rutinas a las que la CPU salta ante cada fallo
// =============================================================================
/// #BP — Breakpoint. Excepcion RECUPERABLE: se atiende sin ruido y, al
/// retornar, la CPU reanuda la ejecucion justo tras la instruccion `int3`.
extern "x86-interrupt" fn reflejo_breakpoint(_marco: InterruptStackFrame) {}
/// #UD — Opcode invalido. Fatal: la corriente de instrucciones es incoherente.
extern "x86-interrupt" fn reflejo_opcode_invalido(marco: InterruptStackFrame) {
panic!("EXCEPCION FATAL :: opcode invalido (#UD)\n{marco:#?}");
}
/// #DE — Error de division. Fatal.
extern "x86-interrupt" fn reflejo_division(marco: InterruptStackFrame) {
panic!("EXCEPCION FATAL :: error de division (#DE)\n{marco:#?}");
}
/// #GP — Fallo de proteccion general. Fatal.
extern "x86-interrupt" fn reflejo_proteccion_general(marco: InterruptStackFrame, codigo: u64) {
panic!("EXCEPCION FATAL :: proteccion general (#GP) codigo={codigo:#x}\n{marco:#?}");
}
/// #PF — Fallo de pagina. Fatal en esta fase (sin memoria virtual dinamica).
extern "x86-interrupt" fn reflejo_fallo_pagina(
marco: InterruptStackFrame,
codigo: PageFaultErrorCode,
) {
let direccion = x86_64::registers::control::Cr2::read_raw();
panic!("EXCEPCION FATAL :: fallo de pagina (#PF) en {direccion:#x} {codigo:?}\n{marco:#?}");
}
/// #DF — Doble fallo. Fatal e irreversible: por definicion, diverge. Se ejecuta
/// sobre el stack de emergencia del TSS, nunca sobre la pila comprometida.
extern "x86-interrupt" fn reflejo_doble_fallo(marco: InterruptStackFrame, _codigo: u64) -> ! {
panic!("EXCEPCION FATAL :: doble fallo (#DF)\n{marco:#?}");
}
// =============================================================================
// IMPULSOS DE HARDWARE — productores de eventos para el reactor asincrono
// =============================================================================
/// IRQ0 — Temporizador. Desde la Fase 5 marca el compas del userspace: cada
/// pulso avanza el `reloj` y despierta a las tareas que aguardaban su fotograma.
extern "x86-interrupt" fn irq_temporizador(_marco: InterruptStackFrame) {
crate::async_system::reloj::pulso();
pic::fin_de_interrupcion(pic::VECTOR_TEMPORIZADOR);
}
/// IRQ1 — Teclado. Ya no escribe estado rustico: lee el scancode crudo y lo
/// entrega al reactor asincrono, que despertara a la tarea del teclado.
extern "x86-interrupt" fn irq_teclado(_marco: InterruptStackFrame) {
// SEGURIDAD: 0x60 es el puerto de datos del controlador PS/2, fijo en la
// arquitectura PC. Leerlo, ademas, libera al controlador para el siguiente.
let scancode: u8 = unsafe { x86_64::instructions::port::Port::new(0x60).read() };
crate::async_system::teclado::recibir_scancode(scancode);
pic::fin_de_interrupcion(pic::VECTOR_TECLADO);
}
/// IRQ del disco — virtio-blk (Fase 6.2). El disco ha terminado una
/// transferencia: `atender_irq` reconoce la interrupcion en el dispositivo
/// —lo que libera su linea— y despierta a la tarea que aguardaba el bloque.
/// Asi la E/S de disco deja de bloquear el planificador por sondeo.
extern "x86-interrupt" fn irq_disco(_marco: InterruptStackFrame) {
crate::drivers::disco::atender_irq();
// El EOI se cierra DESPUES de reconocer al dispositivo: una IRQ legada de
// PCI es de nivel — anunciar el fin sin haber bajado la linea la reavivaria.
pic::fin_de_interrupcion(VECTOR_DISCO.load(Ordering::SeqCst));
}
+342
View File
@@ -0,0 +1,342 @@
// =============================================================================
// renaser :: kernel/src/main.rs — el punto de entrada y la orquestacion
// -----------------------------------------------------------------------------
// Aqui no nace una terminal: nace una superficie. renaser es un kernel
// asincrono de Espacio de Direccionamiento Unico (SASOS) que rompe con el
// paradigma POSIX — sin TTYs, sin archivos planos, sin capas GNU.
//
// Este archivo es deliberadamente delgado: solo el arranque y el cableado.
// Cada subsistema vive en su propio modulo (ver `ARCHITECTURE.md`):
//
// grafico — color, framebuffer fisico y lienzo de doble bufer.
// consola — superficie de texto/imagen; rasteriza glifos con fontdue.
// baliza — red de seguridad visual; manejadores de panico y de OOM.
// sync — `CeldaSync`, la celda de inicializacion unica.
// gdt — GDT + TSS + stack de emergencia del doble fallo.
// interrupts — IDT, excepciones de CPU e interrupciones de hardware.
// pic — el par 8259 remapeado y el temporizador (PIT).
// drivers — descubrimiento de hardware y E/S de disco asincrona por
// interrupcion: el bus PCI y el virtio-blk (Fases 6.1, 6.2).
// almacen — el grafo de objetos direccionado por contenido (Fase 6.1c).
// memory — el heap dinamico del kernel (`#[global_allocator]`).
// async_system — el reactor cooperativo: ejecutor, tareas, wakers, teclado
// y el reloj que marca el compas de los fotogramas (Fase 5).
// texto — rasterizacion de tipografia vectorial (fontdue).
// wasm — el runtime WebAssembly, la matriz de capacidades y el
// escudo de combustible que acota el tiempo de cada app.
// =============================================================================
#![no_std] // Prohibido `std`: bajo nosotros no hay sistema operativo alguno.
#![no_main] // El punto de entrada lo define el cargador, no la convencion C.
#![feature(abi_x86_interrupt)] // ABI de las rutinas de excepcion (Fase 2).
#![feature(alloc_error_handler)] // Manejador propio de agotamiento de heap (Fase 3).
#![deny(unsafe_op_in_unsafe_fn)] // Cada operacion `unsafe` se justifica explicitamente.
extern crate alloc; // El heap esta vivo: `alloc::*` queda disponible (Fase 3).
use alloc::format;
use bootloader_api::config::{BootloaderConfig, Mapping};
use bootloader_api::info::{FrameBufferInfo, MemoryRegionKind, MemoryRegions, PixelFormat};
use bootloader_api::{entry_point, BootInfo};
use spin::Mutex;
// --- Subsistemas del kernel ---
mod almacen;
mod async_system;
mod baliza;
mod consola;
mod drivers;
mod gdt;
mod grafico;
mod interrupts;
mod memory;
mod pic;
mod sync;
mod texto;
mod wasm;
// Reexportaciones para que los submodulos conserven rutas `crate::` estables.
pub(crate) use consola::volcar_marco_wasm;
pub(crate) use sync::CeldaSync;
use async_system::executor::Executor;
use baliza::BALIZA_PANICO;
use consola::{Consola, CONSOLA};
use grafico::{
codificar, reclamar_memoria_lienzo, Color, Lienzo, Pantalla, RegionPantalla, ALTO_MAX,
ANCHO_MAX,
};
/// El modulo WASM del userspace, empotrado en el binario del kernel para esta
/// fase de pruebas. Es un `.wasm` puro, compilado aparte para `wasm32`. La
/// Fase 5 lo instancia DOS veces —el mismo bytecode, dos regiones distintas—
/// para demostrar la multitarea cooperativa sobre el espacio unico.
static APP_WASM: &[u8] = include_bytes!("../assets/app.wasm");
/// La aplicacion DISCOLA: un modulo WASM cuyo `tick` cae en un bucle cerrado y
/// jamas retorna. Existe para una sola cosa — demostrar que el guardarrail de
/// combustible la fulmina sin despeinar al kernel ni a sus vecinas.
static DISCOLA_WASM: &[u8] = include_bytes!("../assets/discola.wasm");
/// La aplicacion GLOTONA: un modulo WASM que reclama memoria lineal sin freno.
/// Demuestra el guardarrail ESPACIAL — el techo de memoria la desaloja con la
/// baliza amarilla, gemela de la purpura del desalojo por combustible.
static GLOTONA_WASM: &[u8] = include_bytes!("../assets/glotona.wasm");
/// La aplicacion CRONISTA: la primera ciudadana del userspace que escribe en el
/// almacenamiento PERSISTENTE. En cada arranque graba un objeto en el grafo
/// —enlazado al del arranque anterior—, lo corona como raiz y pinta una celda
/// por cada arranque registrado. El disco recuerda; la cuenta sobrevive a los
/// reinicios. Demuestra las capacidades `sys_object_*` de la Fase 6.1c.
static CRONISTA_WASM: &[u8] = include_bytes!("../assets/cronista.wasm");
/// Configuracion que el cargador `bootloader` aplicara antes de cedernos la CPU.
static CONFIG_ARRANQUE: BootloaderConfig = {
let mut config = BootloaderConfig::new_default();
// Pedimos la memoria fisica mapeada: cimiento para futuras fases.
config.mappings.physical_memory = Some(Mapping::Dynamic);
config
};
// `entry_point!` genera el simbolo `_start`, valida la firma de `kernel_main`
// y nos transfiere el control con seguridad de tipos.
entry_point!(kernel_main, config = &CONFIG_ARRANQUE);
/// Detiene la CPU de forma definitiva: `hlt` la duerme hasta una interrupcion.
pub(crate) fn detener() -> ! {
loop {
x86_64::instructions::hlt();
}
}
/// Tarea cooperativa de una aplicacion WASM. En cada pulso del reloj le concede
/// un `tick` —un fotograma de trabajo— y cede la CPU hasta el siguiente; entre
/// medias corren sus vecinas. Si la app falla o agota su combustible, se la
/// DESALOJA: se tatua su region con la baliza de purpura y la tarea concluye.
/// El ejecutor la retira del censo, su memoria se libera, el kernel sigue vivo.
async fn tarea_aplicacion(mut app: wasm::AplicacionWasm) {
loop {
async_system::reloj::EsperaFrame::nueva().await;
if let Err(falla) = app.tick() {
// El color de la baliza delata la causa: purpura si agoto su tiempo
// o aborto, amarillo si reviento su techo de memoria.
consola::pintar_desalojo(app.region(), falla.color_baliza());
return;
}
}
}
/// FASE 6.2 — la prueba viva de la E/S asincrona. Esta tarea del reactor lee el
/// sector 0 del disco SIN bloquear: cede la CPU mientras el disco trabaja —las
/// apps siguen pintando entre tanto— y la IRQ del disco la reanuda cuando el
/// bloque esta listo. Deja en la consola el resultado y cuantas interrupciones
/// de disco se atendieron — el testigo de que la E/S por sondeo quedo atras.
async fn tarea_sonda_disco() {
let resultado = drivers::disco::leer_bloques(0, 1).await;
let Some(consola) = CONSOLA.get() else {
return;
};
let mut consola = consola.lock();
match resultado {
Ok(_) => consola.escribir(&format!(
"disco :: sonda asincrona OK -- {} IRQ de disco atendidas\n",
drivers::disco::pulsos_disco(),
)),
Err(motivo) => {
consola.escribir(&format!("disco :: sonda asincrona fallida -- {motivo}\n"))
}
}
consola.presentar();
}
/// Da vida a una aplicacion del userspace: la carga en su region y, si lo
/// logra, la despacha como tarea cooperativa del reactor. Una carga fallida se
/// salda pintando su region con la baliza de desalojo — el kernel no se inmuta.
fn encender_app(ejecutor: &mut Executor, bytecode: &'static [u8], region: RegionPantalla) {
match wasm::AplicacionWasm::cargar(bytecode, region) {
Ok(app) => ejecutor.spawn(tarea_aplicacion(app)),
Err(_) => consola::pintar_desalojo(region, Color::DESALOJO),
}
}
/// Localiza la mayor region de RAM libre que el cargador reporto — la cantera
/// de la que el DMA del disco tomara sus marcos fisicos.
fn mayor_region_usable(regiones: &MemoryRegions) -> Option<(u64, u64)> {
regiones
.iter()
.filter(|region| matches!(region.kind, MemoryRegionKind::Usable))
.map(|region| (region.start, region.end))
.max_by_key(|&(inicio, fin)| fin - inicio)
}
/// FASE 6.1c — funda el grafo de objetos. Monta el disco virtio-blk, lee o
/// forja el superbloque, reconstruye el indice recorriendo el log y deja
/// constancia visual: cuantos sectores tiene el disco, cuantos objetos viven
/// ya en el grafo y si el arranque encontro —o no— una raiz de la que tirar.
fn informar_almacen() {
// Fundar el almacen ANTES de tomar el cerrojo de la consola: el montaje
// del disco hace E/S por sondeo y nada de ello reclama la consola.
let resultado = almacen::init();
let Some(consola) = CONSOLA.get() else {
return;
};
let mut consola = consola.lock();
match resultado {
Ok(resumen) => {
let estado = if resumen.formateado {
"disco formateado"
} else {
"grafo montado"
};
consola.escribir(&format!(
"almacen :: {} :: {} sectores :: {} objetos :: raiz {}\n",
estado,
resumen.capacidad,
resumen.objetos,
if resumen.raiz { "presente" } else { "ausente" },
));
}
Err(motivo) => {
consola.escribir(&format!("almacen :: fallo :: {motivo}\n"));
}
}
// FASE 6.2 — dejar constancia de la linea de IRQ por la que el disco
// anunciara, ya sin sondeo, el fin de cada transferencia.
match drivers::disco::irq() {
Some(irq) => {
consola.escribir(&format!("disco :: virtio-blk en IRQ {irq} -- E/S asincrona\n"))
}
None => consola.escribir("disco :: IRQ no enrutada -- E/S por sondeo\n"),
}
consola.presentar();
}
// =============================================================================
// PUNTO DE ENTRADA DEL KERNEL
// =============================================================================
fn kernel_main(boot_info: &'static mut BootInfo) -> ! {
// --- 1. Recuperar el framebuffer GOP que el firmware nos confio. ---
let framebuffer = match boot_info.framebuffer.as_mut() {
Some(fb) => fb,
None => detener(),
};
let info: FrameBufferInfo = framebuffer.info();
let formato: PixelFormat = info.pixel_format;
let pantalla = Pantalla::adoptar(framebuffer, info);
// Datos para la sonda de disco (Fase 6.1b): el offset al que el cargador
// mapeo la memoria fisica y la mayor region de RAM libre para el DMA.
let offset_fisico = boot_info.physical_memory_offset.into_option();
let region_dma = mayor_region_usable(&boot_info.memory_regions);
// --- 2. Encender la baliza: la red de seguridad visual va primero. ---
BALIZA_PANICO.encender(
&pantalla,
codificar(formato, Color::ALERTA),
codificar(formato, Color::OOM),
);
// --- 3. Cimientos de fallos e interrupciones (Fases 2.0 y 2.1). ---
gdt::init();
interrupts::init();
pic::init();
// --- 4. FASE 3 :: fundar el heap. A partir de aqui, `alloc` esta vivo. ---
memory::init();
// --- 5. Con el heap activo, fundar lo que depende de el: el canal de
// scancodes, el reloj de fotogramas y la tipografia vectorial. ---
async_system::teclado::init();
async_system::reloj::init();
texto::init();
// --- 6. Construir el lienzo y la consola; pintar el rotulo inicial,
// ya rasterizado por fontdue, y publicar la consola globalmente. ---
let memoria = match reclamar_memoria_lienzo() {
Some(m) => m,
None => detener(),
};
let mut lienzo = Lienzo::nuevo(
memoria,
info.width.min(ANCHO_MAX),
info.height.min(ALTO_MAX),
formato,
);
lienzo.limpiar(Color::LIENZO_EN_REPOSO);
let mut consola = Consola::nueva(lienzo, pantalla);
consola.escribir("renaser :: fase 6.2 -- E/S de disco asincrona por interrupcion\n");
consola.presentar();
CONSOLA.call_once(|| Mutex::new(consola));
// --- 6.5. FASE 6.1c :: fundar el subsistema de disco y, sobre el, el grafo
// de objetos: enumerar el bus PCI, montar el transporte virtio-blk,
// y leer o forjar el superbloque del almacen direccionado por
// contenido. El kernel adquiere, por fin, una memoria que perdura. ---
match (offset_fisico, region_dma) {
(Some(offset), Some((inicio, fin))) => {
drivers::disco::init(offset, inicio, fin);
informar_almacen();
}
_ => {
if let Some(consola) = CONSOLA.get() {
let mut consola = consola.lock();
consola.escribir("virtio-blk :: omitido -- memoria fisica sin mapear\n");
consola.presentar();
}
}
}
// --- 7. FASE 5/6 :: levantar el reactor y poblar el userspace con CINCO
// aplicaciones WASM concurrentes, cada una en su propia region:
//
// * App 1 — instancia de hello_wasm, a la izquierda, gobernada
// por el teclado.
// * App 2 — segunda instancia del MISMO bytecode, a la derecha:
// un unico modulo en la RAM unificada, dos vidas aisladas.
// * App discola — su `tick` es un bucle cerrado: el escudo de
// COMBUSTIBLE la desaloja (baliza purpura) en su 1er fotograma.
// * App glotona — reclama memoria sin freno: el escudo ESPACIAL
// la desaloja (baliza amarilla) en su 1er fotograma.
// * App cronista — escribe en el GRAFO DE OBJETOS persistente:
// cada arranque deja un objeto enlazado al anterior y pinta una
// celda por arranque. El disco recuerda entre reinicios.
//
// Las interrupciones se habilitan AHORA: el temporizador marcara el
// compas de los fotogramas y la IRQ del teclado difundira cada
// scancode a los canales que las apps consultan. ---
let mut ejecutor = Executor::nuevo();
encender_app(
&mut ejecutor,
APP_WASM,
RegionPantalla { x: 100, y: 120, ancho: 480, alto: 560 },
);
encender_app(
&mut ejecutor,
APP_WASM,
RegionPantalla { x: 700, y: 120, ancho: 480, alto: 560 },
);
encender_app(
&mut ejecutor,
DISCOLA_WASM,
RegionPantalla { x: 60, y: 700, ancho: 360, alto: 80 },
);
encender_app(
&mut ejecutor,
GLOTONA_WASM,
RegionPantalla { x: 460, y: 700, ancho: 360, alto: 80 },
);
encender_app(
&mut ejecutor,
CRONISTA_WASM,
RegionPantalla { x: 860, y: 700, ancho: 360, alto: 80 },
);
// FASE 6.2 :: una tarea mas del reactor — no una app WASM— que sondea el
// disco de forma ASINCRONA: la demostracion de que la IRQ del disco
// conduce la E/S sin detener a las aplicaciones visuales.
ejecutor.spawn(tarea_sonda_disco());
x86_64::instructions::interrupts::enable();
ejecutor.run();
}
+42
View File
@@ -0,0 +1,42 @@
// =============================================================================
// renaser :: kernel/src/memory/allocator.rs — Fase 3 :: el asignador global
// -----------------------------------------------------------------------------
// Reutilizamos un algoritmo probado (`linked_list_allocator`) en lugar de
// arriesgar un asignador propio a bugs de fragmentacion. El heap es una region
// estatica que PERSISTE durante toda la vida del kernel.
// =============================================================================
use linked_list_allocator::LockedHeap;
use crate::CeldaSync;
/// 64 MiB de heap para el kernel. La arquitectura estimaba 16, pero `fontdue`,
/// al analizar una tipografia real, exige mas holgura; el manejador de OOM —la
/// franja naranja— fue justamente lo que delato esa cota demasiado corta.
const TAM_HEAP: usize = 64 * 1024 * 1024;
/// Region de respaldo del heap, alineada a pagina, residente en `.bss`. Su
/// campo solo se alcanza via puntero crudo —asi lo exige `GlobalAlloc`—, de ahi
/// el `allow(dead_code)`: la memoria se usa, aunque no por una via «normal».
#[repr(align(4096))]
#[allow(dead_code)]
struct RegionHeap([u8; TAM_HEAP]);
/// La memoria fisica del heap. Nace a ceros, no engorda el binario.
static REGION_HEAP: CeldaSync<RegionHeap> = CeldaSync::nueva(RegionHeap([0u8; TAM_HEAP]));
/// El asignador global. Todo `alloc::*` —`Box`, `Vec`, `BTreeMap`, `Arc`...—
/// se apoya, en silencio, sobre este.
#[global_allocator]
static ASIGNADOR: LockedHeap = LockedHeap::empty();
/// Funda el heap del kernel. Debe invocarse UNA sola vez, en el arranque,
/// antes del primer uso de cualquier estructura de `alloc`.
pub fn init() {
let inicio: *mut u8 = REGION_HEAP.puntero().cast::<u8>();
// SEGURIDAD: la region es estatica, de uso exclusivo del asignador, vive
// tanto como el kernel, y `init` se invoca una unica vez en el arranque.
unsafe {
ASIGNADOR.lock().init(inicio, TAM_HEAP);
}
}
+11
View File
@@ -0,0 +1,11 @@
// =============================================================================
// renaser :: kernel/src/memory — la memoria dinamica del kernel (Fase 3)
// -----------------------------------------------------------------------------
// El heap no es, en renaser, una utilidad pasiva al estilo C. Existe para
// sostener algo vivo: la cola de futuros y los reactores asincronos sobre los
// que, fase a fase, se ejecutara el bytecode WASM aislado por software.
// =============================================================================
pub mod allocator;
pub use allocator::init;
+147
View File
@@ -0,0 +1,147 @@
// =============================================================================
// renaser :: kernel/src/pic.rs — Fase 2.1 :: el par 8259 y el metronomo
// -----------------------------------------------------------------------------
// El controlador de interrupciones 8259 (PIC) nace, por herencia historica,
// con sus vectores solapados sobre los de las excepciones de CPU. Antes de
// encender las interrupciones HAY que remapearlo: desplazar sus vectores
// fuera del rango 0..31. Aqui tambien programamos el PIT, el temporizador
// que dara a renaser su latido regular — el origen de su fluidez.
// =============================================================================
use x86_64::instructions::port::Port;
// --- Puertos del par de 8259 (maestro + esclavo en cascada). ---
const CMD_MAESTRO: u16 = 0x20;
const DATOS_MAESTRO: u16 = 0x21;
const CMD_ESCLAVO: u16 = 0xA0;
const DATOS_ESCLAVO: u16 = 0xA1;
// --- Puertos del temporizador de intervalos programable (PIT). ---
const PIT_COMANDO: u16 = 0x43;
const PIT_CANAL0: u16 = 0x40;
/// Frecuencia base del cristal del PIT, en Hz.
const PIT_BASE_HZ: u32 = 1_193_182;
/// Orden de «fin de interrupcion» que el PIC espera tras cada IRQ atendida.
const EOI: u8 = 0x20;
/// Vector base del PIC maestro tras el remapeo (justo encima de las 32
/// excepciones reservadas de la arquitectura).
const OFFSET_MAESTRO: u8 = 0x20;
/// Vector base del PIC esclavo tras el remapeo.
const OFFSET_ESCLAVO: u8 = 0x28;
/// Vector de la IRQ0 — el temporizador (PIT).
pub const VECTOR_TEMPORIZADOR: u8 = OFFSET_MAESTRO; // 0x20
/// Vector de la IRQ1 — el teclado.
pub const VECTOR_TECLADO: u8 = OFFSET_MAESTRO + 1; // 0x21
/// Remapea el par 8259 y programa el PIT a 100 Hz.
///
/// Debe invocarse una sola vez, tras cargar la IDT y ANTES de habilitar las
/// interrupciones con `sti`.
pub fn init() {
remapear();
configurar_temporizador(100);
}
/// Reprograma los vectores del 8259 fuera del rango de las excepciones de CPU.
fn remapear() {
// SEGURIDAD: estos son los puertos de E/S estandar del 8259 en la
// arquitectura PC; la secuencia ICW1..ICW4 es su protocolo de inicio.
unsafe {
let mut cmd_m = Port::<u8>::new(CMD_MAESTRO);
let mut dat_m = Port::<u8>::new(DATOS_MAESTRO);
let mut cmd_e = Port::<u8>::new(CMD_ESCLAVO);
let mut dat_e = Port::<u8>::new(DATOS_ESCLAVO);
// Escribir en un puerto inerte da al 8259 el respiro que necesita
// entre palabras de control en hardware antiguo.
let mut respiro = Port::<u8>::new(0x80);
// ICW1 — iniciar la secuencia: en cascada, con ICW4 presente.
cmd_m.write(0x11u8);
respiro.write(0u8);
cmd_e.write(0x11u8);
respiro.write(0u8);
// ICW2 — el remapeo en si: desplazar los vectores lejos de 0..31.
dat_m.write(OFFSET_MAESTRO);
respiro.write(0u8);
dat_e.write(OFFSET_ESCLAVO);
respiro.write(0u8);
// ICW3 — declarar el cableado de la cascada (el esclavo en la IRQ2).
dat_m.write(0b0000_0100u8);
respiro.write(0u8);
dat_e.write(0b0000_0010u8);
respiro.write(0u8);
// ICW4 — modo 8086.
dat_m.write(0x01u8);
respiro.write(0u8);
dat_e.write(0x01u8);
respiro.write(0u8);
// Mascaras: el maestro deja pasar la IRQ0 (temporizador) y la IRQ1
// (teclado); todo lo demas, en silencio hasta que renaser lo reclame.
dat_m.write(0b1111_1100u8);
dat_e.write(0b1111_1111u8);
}
}
/// Programa el canal 0 del PIT para que emita la IRQ0 a `frecuencia_hz`.
fn configurar_temporizador(frecuencia_hz: u32) {
let divisor = (PIT_BASE_HZ / frecuencia_hz) as u16;
// SEGURIDAD: 0x43 y 0x40 son los puertos estandar del PIT en el PC.
unsafe {
let mut comando = Port::<u8>::new(PIT_COMANDO);
let mut canal0 = Port::<u8>::new(PIT_CANAL0);
// Canal 0, acceso lobyte+hibyte, modo 3 (generador de onda cuadrada).
comando.write(0x36u8);
canal0.write((divisor & 0xFF) as u8);
canal0.write((divisor >> 8) as u8);
}
}
/// Notifica al PIC el «fin de interrupcion» de la IRQ recien atendida. Sin
/// este aviso, el PIC jamas volveria a emitir esa interrupcion.
pub fn fin_de_interrupcion(vector: u8) {
// SEGURIDAD: enviar EOI al puerto de comandos del PIC tras atender su IRQ
// es el cierre obligatorio del protocolo del 8259.
unsafe {
// Si la IRQ provino del esclavo, ambos PIC deben recibir el EOI.
if vector >= OFFSET_ESCLAVO {
Port::<u8>::new(CMD_ESCLAVO).write(EOI);
}
Port::<u8>::new(CMD_MAESTRO).write(EOI);
}
}
/// Vector de la IDT que corresponde a una linea de IRQ (0..15). El remapeo dejo
/// las 16 lineas del par 8259 en vectores contiguos desde `OFFSET_MAESTRO`: la
/// IRQ8 cae en `OFFSET_ESCLAVO`, que es justo `OFFSET_MAESTRO + 8`.
pub fn vector_irq(irq: u8) -> u8 {
OFFSET_MAESTRO + irq
}
/// Levanta la mascara de una linea de IRQ — el PIC empezara a emitirla. Si la
/// linea vive en el PIC esclavo (8..15), abre tambien la cascada del maestro
/// (la IRQ2), sin la cual el esclavo jamas alcanzaria a la CPU.
///
/// Debe invocarse en el arranque, antes de habilitar las interrupciones.
pub fn desenmascarar(irq: u8) {
// SEGURIDAD: 0x21 y 0xA1 son los puertos de mascara del par 8259 en la
// arquitectura PC; leer-modificar-escribir es la via de tocar una linea
// sola sin perturbar a las demas.
unsafe {
if irq < 8 {
let mut datos = Port::<u8>::new(DATOS_MAESTRO);
let mascara = datos.read();
datos.write(mascara & !(1 << irq));
} else {
let mut datos_e = Port::<u8>::new(DATOS_ESCLAVO);
let mascara_e = datos_e.read();
datos_e.write(mascara_e & !(1 << (irq - 8)));
// La cascada: el esclavo entrega sus IRQ al maestro por la IRQ2.
let mut datos_m = Port::<u8>::new(DATOS_MAESTRO);
let mascara_m = datos_m.read();
datos_m.write(mascara_m & !(1 << 2));
}
}
}
+29
View File
@@ -0,0 +1,29 @@
// =============================================================================
// renaser :: kernel/src/sync.rs — la celda de inicializacion unica
// -----------------------------------------------------------------------------
// Las estructuras globales del kernel (GDT, TSS, IDT, el heap...) nacen una
// sola vez, durante el arranque secuencial y de un solo hilo, y despues solo
// se leen. `CeldaSync` envuelve ese unico `unsafe` en una abstraccion comun:
// un contrato de unicidad que el codigo de arranque garantiza por construccion.
// =============================================================================
use core::cell::UnsafeCell;
/// Celda `Sync` para estado global de inicializacion unica.
pub(crate) struct CeldaSync<T>(UnsafeCell<T>);
// SEGURIDAD: cada celda se escribe una sola vez, durante el arranque, antes de
// que existan interrupciones o concurrencia; despues es de solo lectura.
unsafe impl<T> Sync for CeldaSync<T> {}
impl<T> CeldaSync<T> {
/// Crea una celda con su valor inicial.
pub(crate) const fn nueva(valor: T) -> Self {
CeldaSync(UnsafeCell::new(valor))
}
/// Puntero crudo al contenido. Quien lo usa asume el contrato de unicidad.
pub(crate) fn puntero(&self) -> *mut T {
self.0.get()
}
}
+36
View File
@@ -0,0 +1,36 @@
// =============================================================================
// renaser :: kernel/src/texto.rs — Fase 3 :: el texto como caso del dibujo
// -----------------------------------------------------------------------------
// Con el heap activo, el texto deja de ser un mapa de bits estatico. Una
// tipografia vectorial se empotra en el binario y se rasteriza glifo a glifo,
// bajo demanda: el texto se vuelve, literalmente, un caso particular del
// dibujo — la promesa fundacional de renaser.
// =============================================================================
use alloc::vec::Vec;
use fontdue::{Font, FontSettings, Metrics};
use spin::Once;
/// La tipografia vectorial, empotrada en el propio binario del kernel.
static FUENTE_TTF: &[u8] = include_bytes!("../assets/font.ttf");
/// La fuente ya analizada. Se funde una sola vez, tras activar el heap.
static FUENTE: Once<Font> = Once::new();
/// Analiza la tipografia empotrada. Requiere el heap ya activo.
pub fn init() {
FUENTE.call_once(|| {
Font::from_bytes(FUENTE_TTF, FontSettings::default())
.expect("renaser :: la tipografia empotrada es invalida")
});
}
/// Rasteriza un glifo al vuelo: devuelve sus metricas de colocacion y un mapa
/// de cobertura (un byte de opacidad, 0..=255, por cada pixel del glifo).
pub fn rasterizar(caracter: char, tam_px: f32) -> (Metrics, Vec<u8>) {
FUENTE
.get()
.expect("renaser :: la tipografia no fue fundida")
.rasterize(caracter, tam_px)
}
+349
View File
@@ -0,0 +1,349 @@
// =============================================================================
// renaser :: kernel/src/wasm/env.rs — Fase 4/5/6 :: la matriz de capacidades
// -----------------------------------------------------------------------------
// El aislamiento de renaser no descansa en `int 0x80` ni en `sysenter`: no hay
// vectores de syscall. Una aplicacion WASM solo puede hacer aquello para lo
// que el kernel le haya inyectado una FUNCION DEL HOST. Esta matriz concede:
//
// * sys_render_frame — componer un fotograma en su region de pantalla;
// * sys_get_scancode — consultar su canal de teclado;
// * sys_object_put — grabar un objeto en el grafo (Fase 6.1c);
// * sys_object_datos — leer la carga util de un objeto;
// * sys_object_hijo — recorrer las aristas del DAG;
// * sys_object_raiz — leer la raiz del grafo;
// * sys_object_fijar_raiz — coronar un objeto como raiz.
//
// GUARDARRAIL: el kernel valida MATEMATICAMENTE todo puntero que el modulo le
// entrega contra los limites reales de su memoria lineal. No se confia en que
// el runtime lo haga; se verifica aqui, antes de leer o escribir un solo byte.
//
// DOS CLASES DE FALLO. Un puntero fuera de limites es CULPA DE LA APP: se
// devuelve un `Error` que la ABORTA (el kernel la captura y la desaloja). Un
// fallo del almacenamiento —disco, objeto inexistente— NO es culpa de la app:
// se le devuelve un codigo de error negativo, y la app decide que hacer.
// =============================================================================
use wasmi::{Caller, Error, Extern, Linker, Memory, StoreLimits};
use crate::almacen::Hash;
use crate::async_system::teclado::CanalTeclado;
use crate::grafico::RegionPantalla;
/// El estado del host adscrito al `Store` de una aplicacion: cuanto necesita
/// una capacidad para servir a ESA app y a ninguna otra — su region de pantalla,
/// su canal de teclado y sus cuotas de recursos. Dos apps jamas comparten nada.
pub(crate) struct ContextoCapacidades {
/// La sub-region de pantalla asignada a la aplicacion.
pub(crate) region: RegionPantalla,
/// El canal de teclado propio de la aplicacion.
pub(crate) canal: CanalTeclado,
/// El techo de recursos de la aplicacion — hoy, su memoria lineal maxima.
/// `wasmi` lo consulta en cada `memory.grow` via `Store::limiter`.
pub(crate) limites: StoreLimits,
}
/// Recupera la memoria lineal exportada por el modulo. Que no la exporte es un
/// modulo mal formado: se aborta.
fn obtener_memoria(caller: &Caller<'_, ContextoCapacidades>) -> Result<Memory, Error> {
caller
.get_export("memory")
.and_then(Extern::into_memory)
.ok_or_else(|| Error::new("WASM :: el modulo no exporta su memoria lineal"))
}
/// VALIDACION INFRANQUEABLE DE LIMITES. Comprueba que `[ptr, ptr + len)` cae
/// entera dentro de la memoria lineal `m` y devuelve ese sub-slice. Un rango
/// que se desborde aborta la app — el `Error` se traduce en una trampa de WASM.
fn rango<'a>(m: &'a [u8], ptr: u32, len: usize, fallo: &'static str) -> Result<&'a [u8], Error> {
let inicio = ptr as usize;
match inicio.checked_add(len) {
Some(fin) if fin <= m.len() => Ok(&m[inicio..fin]),
_ => Err(Error::new(fallo)),
}
}
/// Lee un hash de 32 bytes de la memoria lineal, con sus limites verificados.
fn leer_hash(m: &[u8], ptr: u32, fallo: &'static str) -> Result<Hash, Error> {
let bytes = rango(m, ptr, 32, fallo)?;
let mut hash = [0u8; 32];
hash.copy_from_slice(bytes);
Ok(hash)
}
/// Inyecta en el enlazador la matriz de capacidades del modulo WASM. Todo lo
/// que no se defina aqui le queda, al modulo, fisicamente fuera de alcance.
///
/// Devuelve `Err` si una capacidad no se pudo enlazar — un fallo del kernel,
/// no de la app; aun asi se propaga como `Result` para no incendiar nada.
pub(crate) fn enlazar_capacidades(
enlazador: &mut Linker<ContextoCapacidades>,
) -> Result<(), Error> {
// --- CAPACIDAD 1 :: sys_render_frame(ptr, len) ---
// El modulo entrega (ptr, len) hacia su PROPIA memoria lineal; el kernel
// valida esos limites y, solo entonces, compone el fotograma DENTRO de la
// region asignada a la app.
enlazador.func_wrap(
"renaser",
"sys_render_frame",
|caller: Caller<'_, ContextoCapacidades>, ptr: u32, len: u32| -> Result<(), Error> {
let region = caller.data().region;
// El fotograma debe medir EXACTAMENTE los pixeles de la region. Un
// tamaño distinto delata a una app que pinta fuera de su ventana:
// se aborta antes de tocar un byte.
let esperado = region.pixeles() * 4;
if len as usize != esperado {
return Err(Error::new(
"WASM :: sys_render_frame con un fotograma ajeno a la region asignada",
));
}
let memoria = obtener_memoria(&caller)?;
let datos: &[u8] = memoria.data(&caller);
// VALIDACION INFRANQUEABLE: si (ptr, len) se sale de la memoria
// lineal del modulo, se aborta la app —no el kernel—.
let marco = rango(
datos,
ptr,
len as usize,
"WASM :: sys_render_frame desbordo la memoria lineal del modulo",
)?;
// Limites verificados: la region es segura de leer y componer.
crate::volcar_marco_wasm(region, marco);
Ok(())
},
)?;
// --- CAPACIDAD 2 :: sys_get_scancode() -> u32 ---
// Expone, sin bloquear, el siguiente scancode del canal PROPIO de la app.
enlazador.func_wrap(
"renaser",
"sys_get_scancode",
|caller: Caller<'_, ContextoCapacidades>| -> u32 {
caller.data().canal.pop().unwrap_or(0) as u32
},
)?;
// --- CAPACIDAD 3 :: sys_object_put(datos, datos_len, hijos, hijos_cnt, salida) -> i32 ---
// Graba un objeto en el grafo. El modulo entrega, en su memoria lineal, la
// carga util y un arreglo de `hijos_cnt` hashes de 32 bytes (las aristas).
// El kernel escribe el hash resultante —la identidad del objeto— en
// `salida`. Devuelve 0 si el objeto se grabo (o ya existia), -1 si el
// almacenamiento fallo.
enlazador.func_wrap(
"renaser",
"sys_object_put",
|mut caller: Caller<'_, ContextoCapacidades>,
datos_ptr: u32,
datos_len: u32,
hijos_ptr: u32,
hijos_cnt: u32,
salida: u32|
-> Result<i32, Error> {
let memoria = obtener_memoria(&caller)?;
// --- Leer las entradas de la memoria lineal, con limites firmes. ---
let (datos, hijos) = {
let m = memoria.data(&caller);
let datos = rango(
m,
datos_ptr,
datos_len as usize,
"WASM :: sys_object_put desbordo la memoria lineal (datos)",
)?
.to_vec();
// El arreglo de hijos: `hijos_cnt` hashes contiguos de 32 bytes.
let bytes_hijos = (hijos_cnt as usize).checked_mul(32).ok_or_else(|| {
Error::new("WASM :: sys_object_put con un conteo de hijos imposible")
})?;
let crudo = rango(
m,
hijos_ptr,
bytes_hijos,
"WASM :: sys_object_put desbordo la memoria lineal (hijos)",
)?;
let mut hijos: alloc::vec::Vec<Hash> =
alloc::vec::Vec::with_capacity(hijos_cnt as usize);
for trozo in crudo.chunks_exact(32) {
let mut h = [0u8; 32];
h.copy_from_slice(trozo);
hijos.push(h);
}
// Verificar que el hash de salida cabe ANTES de tocar el disco.
rango(
m,
salida,
32,
"WASM :: sys_object_put desbordo la memoria lineal (salida)",
)?;
(datos, hijos)
};
// --- Grabar. Un fallo del almacen NO es culpa de la app: -1. ---
match crate::almacen::almacenar(datos, hijos) {
Ok(hash) => {
let m = memoria.data_mut(&mut caller);
m[salida as usize..salida as usize + 32].copy_from_slice(&hash);
Ok(0)
}
Err(_) => Ok(-1),
}
},
)?;
// --- CAPACIDAD 4 :: sys_object_datos(hash, salida, capacidad) -> i32 ---
// Copia la carga util del objeto `hash` en `salida`. Devuelve el numero de
// bytes copiados, o -1 si el objeto no existe, -2 si `capacidad` no basta,
// -3 si el almacenamiento fallo.
enlazador.func_wrap(
"renaser",
"sys_object_datos",
|mut caller: Caller<'_, ContextoCapacidades>,
hash_ptr: u32,
salida: u32,
capacidad: u32|
-> Result<i32, Error> {
let memoria = obtener_memoria(&caller)?;
let hash = {
let m = memoria.data(&caller);
leer_hash(
m,
hash_ptr,
"WASM :: sys_object_datos desbordo la memoria lineal (hash)",
)?
};
let objeto = match crate::almacen::recuperar(&hash) {
Ok(Some(objeto)) => objeto,
Ok(None) => return Ok(-1),
Err(_) => return Ok(-3),
};
if objeto.datos.len() > capacidad as usize {
return Ok(-2);
}
// Verificar que el destino cabe, y solo entonces copiar.
{
let m = memoria.data(&caller);
rango(
m,
salida,
objeto.datos.len(),
"WASM :: sys_object_datos desbordo la memoria lineal (salida)",
)?;
}
let n = objeto.datos.len();
let m = memoria.data_mut(&mut caller);
m[salida as usize..salida as usize + n].copy_from_slice(&objeto.datos);
Ok(n as i32)
},
)?;
// --- CAPACIDAD 5 :: sys_object_hijo(hash, indice, salida) -> i32 ---
// Recorre las aristas del DAG. Devuelve el NUMERO de hijos del objeto
// `hash`; si `indice` es valido, ademas escribe el hash de ese hijo en
// `salida`. Devuelve -1 si el objeto no existe, -3 si el almacen fallo.
enlazador.func_wrap(
"renaser",
"sys_object_hijo",
|mut caller: Caller<'_, ContextoCapacidades>,
hash_ptr: u32,
indice: u32,
salida: u32|
-> Result<i32, Error> {
let memoria = obtener_memoria(&caller)?;
let hash = {
let m = memoria.data(&caller);
leer_hash(
m,
hash_ptr,
"WASM :: sys_object_hijo desbordo la memoria lineal (hash)",
)?
};
let objeto = match crate::almacen::recuperar(&hash) {
Ok(Some(objeto)) => objeto,
Ok(None) => return Ok(-1),
Err(_) => return Ok(-3),
};
let total = objeto.hijos.len();
// Si el indice apunta a un hijo real, entregar su hash.
if let Some(hijo) = objeto.hijos.get(indice as usize) {
{
let m = memoria.data(&caller);
rango(
m,
salida,
32,
"WASM :: sys_object_hijo desbordo la memoria lineal (salida)",
)?;
}
let m = memoria.data_mut(&mut caller);
m[salida as usize..salida as usize + 32].copy_from_slice(hijo);
}
Ok(total as i32)
},
)?;
// --- CAPACIDAD 6 :: sys_object_raiz(salida) -> i32 ---
// Escribe en `salida` el hash de la raiz del grafo. Devuelve 1 si hay
// raiz, 0 si el grafo aun no tiene ninguna.
enlazador.func_wrap(
"renaser",
"sys_object_raiz",
|mut caller: Caller<'_, ContextoCapacidades>, salida: u32| -> Result<i32, Error> {
let memoria = obtener_memoria(&caller)?;
match crate::almacen::raiz() {
Some(hash) => {
{
let m = memoria.data(&caller);
rango(
m,
salida,
32,
"WASM :: sys_object_raiz desbordo la memoria lineal (salida)",
)?;
}
let m = memoria.data_mut(&mut caller);
m[salida as usize..salida as usize + 32].copy_from_slice(&hash);
Ok(1)
}
None => Ok(0),
}
},
)?;
// --- CAPACIDAD 7 :: sys_object_fijar_raiz(hash) -> i32 ---
// Corona el objeto `hash` como raiz del grafo. Devuelve 0 si se logro, -3
// si el almacenamiento fallo.
enlazador.func_wrap(
"renaser",
"sys_object_fijar_raiz",
|caller: Caller<'_, ContextoCapacidades>, hash_ptr: u32| -> Result<i32, Error> {
let memoria = obtener_memoria(&caller)?;
let hash = {
let m = memoria.data(&caller);
leer_hash(
m,
hash_ptr,
"WASM :: sys_object_fijar_raiz desbordo la memoria lineal (hash)",
)?
};
match crate::almacen::fijar_raiz(hash) {
Ok(()) => Ok(0),
Err(_) => Ok(-3),
}
},
)?;
Ok(())
}
+200
View File
@@ -0,0 +1,200 @@
// =============================================================================
// renaser :: kernel/src/wasm — Fase 4/5 :: el escudo de aislamiento (WASM)
// -----------------------------------------------------------------------------
// Aqui renaser sustituye las costosas fronteras de hardware (la MMU, los
// anillos de la CPU) por limites MATEMATICOS sobre el bytecode. Una aplicacion
// WebAssembly se ejecuta en su propia memoria lineal; sus unicas puertas al
// exterior son las capacidades que el enlazador del host le concede. Lo que
// no este importado no existe: no hay camino fisico para ejecutarlo.
//
// FASE 5 :: el aislamiento deja de ser solo ESPACIAL (memoria) y pasa a ser
// tambien TEMPORAL (tiempo de CPU). Cada `tick` se ejecuta con un presupuesto
// estricto de COMBUSTIBLE (fuel): si una app lo agota —un bucle infinito, un
// trabajo desmedido—, el runtime lanza una trampa, el kernel recupera el mando
// y la desaloja. Ningun modulo, por discolo que sea, secuestra el procesador.
// =============================================================================
pub mod env;
use wasmi::{
CompilationMode, Config, Engine, Linker, Module, Store, StoreLimitsBuilder, TrapCode, TypedFunc,
};
use crate::grafico::{Color, RegionPantalla};
use env::ContextoCapacidades;
/// Combustible concedido a `init`. Cubre con holgura el pintado inicial del
/// fondo de una region a pantalla casi completa — un gasto unico, de arranque.
const FUEL_ARRANQUE: u64 = 20_000_000;
/// Combustible concedido a cada `tick`. Sobra para un fotograma honesto (unos
/// cientos de miles de operaciones); una app en bucle infinito lo agota en
/// milisegundos y es desalojada. Este numero ES el techo temporal del userspace.
const FUEL_FOTOGRAMA: u64 = 2_000_000;
/// Techo de memoria lineal por aplicacion: 4 MiB. Un modulo que intente crecer
/// su memoria mas alla es desalojado — el aislamiento ESPACIAL del userspace,
/// gemelo del techo TEMPORAL que impone el combustible.
const TECHO_MEMORIA: usize = 4 * 1024 * 1024;
/// Por que el kernel da por terminada —desaloja— una aplicacion WASM.
#[derive(Clone, Copy)]
pub enum FallaApp {
/// El modulo no se pudo cargar, validar, enlazar o instanciar.
Carga,
/// La aplicacion agoto su combustible dentro de un `tick`: bucle infinito
/// o trabajo desmedido. El guardarrail TEMPORAL en accion.
SinCombustible,
/// La aplicacion intento crecer su memoria lineal mas alla de su cuota.
/// El guardarrail ESPACIAL en accion.
SinMemoria,
/// La aplicacion ejecuto una trampa: acceso fuera de limites, instruccion
/// `unreachable`, una capacidad violada... su propio codigo la abortó.
Trampa,
}
impl FallaApp {
/// El color de la baliza de desalojo segun la causa de la falla: amarillo
/// palido si reviento su techo de memoria, purpura para cualquier otra.
pub fn color_baliza(self) -> Color {
match self {
FallaApp::SinMemoria => Color::DESALOJO_MEMORIA,
_ => Color::DESALOJO,
}
}
}
/// Una aplicacion WebAssembly viva: su estado PERSISTE entre fotogramas. A
/// diferencia de la Fase 4 —que instanciaba y cedia el control de un gesto—,
/// aqui la instancia se conserva y el kernel la hace avanzar `tick` a `tick`.
pub struct AplicacionWasm {
/// El almacen: todo el estado de ESTA instancia — su memoria lineal, sus
/// globales y el contexto de capacidades con su region de pantalla.
almacen: Store<ContextoCapacidades>,
/// El punto de entrada de fotograma, ya resuelto y con seguridad de tipos.
/// `TypedFunc` es un asa autosuficiente dentro del `Store`: conservada esta,
/// el handle de la `Instance` no aporta nada y no se retiene.
func_tick: TypedFunc<(), ()>,
/// La region de pantalla de la app — su ventana, y donde se tatua su baliza
/// de desalojo si llega a fallar.
region: RegionPantalla,
}
impl AplicacionWasm {
/// Carga, valida, instancia y arranca una aplicacion WASM aislada, ligada a
/// una region de pantalla. Si algo falla en el camino, se devuelve la falla
/// en lugar de incendiar el kernel.
///
/// El nuevo ABI del userspace exige dos exportaciones: `init` —invocada una
/// sola vez, aqui— y `tick` —un fotograma de trabajo, invocada despues por
/// el reactor en cada pulso del reloj.
pub fn cargar(bytecode: &[u8], region: RegionPantalla) -> Result<AplicacionWasm, FallaApp> {
// 1. El motor, con metricas de combustible y compilacion ANTICIPADA: la
// traduccion del modulo ocurre ahora, de modo que el `fuel` mida
// despues solo EJECUCION, jamas compilacion diferida.
let mut config = Config::default();
config.consume_fuel(true);
config.compilation_mode(CompilationMode::Eager);
let motor = Engine::new(&config);
// 2. Validar y traducir el modulo — ya instrumentado con fuel.
let modulo = Module::new(&motor, bytecode).map_err(|_| FallaApp::Carga)?;
// 3. El almacen, con el contexto de capacidades de ESTA app: su region
// de pantalla, su canal de teclado y su techo de memoria. El canal
// se crea ahora pero se inscribe en la difusion de la IRQ1 al final,
// ya con la app cargada: una carga fallida no deja canales huerfanos.
let canal = crate::async_system::teclado::crear_canal();
let limites = StoreLimitsBuilder::new()
.memory_size(TECHO_MEMORIA)
// Una expansion denegada se convierte en TRAMPA, no en un -1 que la
// app pudiera ignorar: asi el kernel la captura y la desaloja.
.trap_on_grow_failure(true)
.build();
let mut almacen = Store::new(
&motor,
ContextoCapacidades {
region,
canal,
limites,
},
);
// Ligar el limitador de recursos: `wasmi` lo consultara en cada
// `memory.grow`, tambien durante la instanciacion.
almacen.limiter(|contexto| &mut contexto.limites);
// Dotar de combustible ANTES de instanciar: la instanciacion no debe
// quedarse a cero y abortar.
almacen.set_fuel(FUEL_ARRANQUE).map_err(|_| FallaApp::Carga)?;
// 4. El enlazador y la matriz de capacidades (ver `env`).
let mut enlazador: Linker<ContextoCapacidades> = Linker::new(&motor);
env::enlazar_capacidades(&mut enlazador).map_err(|_| FallaApp::Carga)?;
// 5. Instanciar, resolviendo las importaciones contra las capacidades.
let instancia = enlazador
.instantiate_and_start(&mut almacen, &modulo)
.map_err(|_| FallaApp::Carga)?;
// 6. Resolver los dos puntos del ABI de fotograma: `init` y `tick`.
let func_init = instancia
.get_typed_func::<(), ()>(&almacen, "init")
.map_err(|_| FallaApp::Carga)?;
let func_tick = instancia
.get_typed_func::<(), ()>(&almacen, "tick")
.map_err(|_| FallaApp::Carga)?;
// 7. Arranque unico: `init` prepara el estado inicial de la aplicacion.
almacen.set_fuel(FUEL_ARRANQUE).map_err(|_| FallaApp::Carga)?;
func_init
.call(&mut almacen, ())
.map_err(|_| FallaApp::Carga)?;
// 8. Con la app ya cargada e instanciada, inscribir su canal de teclado
// en la difusion de la IRQ1: desde aqui recibe cada pulsacion.
crate::async_system::teclado::registrar_canal(&almacen.data().canal);
Ok(AplicacionWasm {
almacen,
func_tick,
region,
})
}
/// Hace avanzar la aplicacion un fotograma. Recarga su presupuesto de
/// combustible y le cede el control con `tick`. Si la app lo agota o ejecuta
/// una trampa, el kernel recupera el mando y la falla se devuelve para que
/// la tarea proceda al desalojo. El kernel nunca pierde el control.
pub fn tick(&mut self) -> Result<(), FallaApp> {
// Recargar el deposito: cada fotograma parte con su techo intacto.
self.almacen
.set_fuel(FUEL_FOTOGRAMA)
.map_err(|_| FallaApp::Trampa)?;
match self.func_tick.call(&mut self.almacen, ()) {
Ok(()) => Ok(()),
// `as_trap_code` da un codigo publico y univoco para cada causa:
// `OutOfFuel` pliega toda variante de agotamiento de combustible;
// `GrowthOperationLimited` es la cuota de memoria denegada.
Err(error) => match error.as_trap_code() {
Some(TrapCode::OutOfFuel) => Err(FallaApp::SinCombustible),
Some(TrapCode::GrowthOperationLimited) => Err(FallaApp::SinMemoria),
_ => Err(FallaApp::Trampa),
},
}
}
/// La region de pantalla asignada a la aplicacion.
pub fn region(&self) -> RegionPantalla {
self.region
}
}
/// Reconciliacion del ciclo de vida. Cuando una `AplicacionWasm` muere —porque
/// fue desalojada y su tarea concluyo—, su canal de teclado debe darse de baja
/// de la difusion de la IRQ1. Sin esto, el manejador de interrupciones seguiria
/// empujando scancodes a una cola muerta: una fuga lenta pero segura.
impl Drop for AplicacionWasm {
fn drop(&mut self) {
crate::async_system::teclado::cerrar_canal(&self.almacen.data().canal);
}
}
+14
View File
@@ -0,0 +1,14 @@
# =============================================================================
# renaser :: fijacion del toolchain
# -----------------------------------------------------------------------------
# Nightly por dos razones que PERSISTEN: las dependencias de artefacto
# (`-Zbindeps`) y el script de construccion de la crate `bootloader`, que
# compila su propio cargador UEFI/BIOS y necesita `rust-src` + `llvm-tools`.
# El kernel en si ya NO necesita `build-std`: usa el target nativo precompilado.
# =============================================================================
[toolchain]
channel = "nightly"
components = ["rust-src", "llvm-tools", "rustfmt", "clippy"]
targets = ["x86_64-unknown-none", "x86_64-unknown-uefi"]
profile = "minimal"