202509
systems

Building an OS Kernel from Scratch in Rust: Bootloader, Memory Management, Process Scheduling, and Interrupt Handling

A hands-on guide to implementing key OS components in Rust for x86 bare-metal, including bootloader setup, paging, simple scheduling, and interrupts with GDB support.

在裸机x86环境下用Rust从零构建操作系统内核,是低级系统编程的经典实践。本文聚焦引导加载器、内存管理、进程调度和中断处理的核心实现,提供工程化参数和调试要点。通过Phil Opp的开源系列,我们可以高效验证每个组件,避免常见陷阱。

引导加载器:从BIOS到内核入口

引导加载器是OS启动的桥梁,确保内核代码正确加载到内存并跳转执行。在Rust中,使用bootloader crate简化这一过程。它基于GRUB-like机制,但针对裸机优化,支持自定义目标三元组如x86_64-unknown-none。

关键参数:

  • Cargo.toml中指定bootloader = { version = "0.9", features = ["map_physical_memory"] },启用物理内存映射以访问页表。
  • 自定义.json目标文件:rustup target add x86_64-blog_os.json,定义panic=abort、no_std和linker脚本,设置入口点为_start。
  • 构建命令:cargo bootimage,确保生成可引导的disk.img。

落地清单:

  1. 初始化Cargo项目:cargo new --bin kernel,添加no_std和no_main属性。
  2. 实现_start:使用extern "C" fn _start() -> ! { init(); hlt_loop(); },调用GDT和IDT初始化。
  3. 验证:qemu-system-x86_64 -drive format=raw,file=bootimage-kernel.bin,观察"Hello World"输出。
  4. GDB调试:target remote :1234,设置断点于_start,监控CR3寄存器变化。

证据显示,bootloader crate处理了BIOS/UEFI兼容性和页表初步映射,减少手动汇编需求(来源:Phil Opp博客)。

内存管理:分页与堆分配

x86_64架构使用4级页表实现虚拟内存隔离。内核需从物理帧访问页表,实现地址翻译和动态分配。

核心实现:

  • 启用map_physical_memory特性,将物理内存映射到虚拟偏移(如0xffff_8000_0000_0000),通过BootInfo获取偏移。
  • OffsetPageTable:unsafe fn init(physical_memory_offset: VirtAddr) -> OffsetPageTable<'static>,从CR3读取L4表。
  • 翻译函数:mapper.translate_addr(virt),支持4KiB页和巨大页(2MiB),返回PhysAddr或None。
  • 堆分配:BootInfoFrameAllocator从memory_map迭代Usable区域,分配PhysFrame。使用linked_list_allocator::LockedHeap,init_heap以0x_4444_4444_0000起始1GiB。

参数阈值:

  • 页大小:默认4KiB,巨大页需HugeFrame支持,TLB刷新用mapper.flush()。
  • 分配器设计:BumpAllocator简单但碎片化;LinkedListAllocator支持释放,固定块分配优化小对象。
  • 风险:页表别名导致UB,限制为单一OffsetPageTable实例,避免多&mut。

清单:

  1. 解析BootInfo.memory_map,过滤Usable帧。
  2. 创建映射:mapper.map_to(page, frame, Flags::PRESENT | WRITABLE),flush后测试VGA缓冲重映射。
  3. 集成alloc:use alloc::vec::Vec; let v = Vec::new(); v.push(1); 验证动态增长。
  4. GDB:info registers rsp,watch CR3变化;bt追踪页故障栈。

实践证明,分页引入内存安全,heap分配启用Vec/Box等std-like功能(来源:x86_64 crate文档)。

进程调度:协作式多任务

简单调度从协作式async/await开始,利用Future poll实现非抢占多任务。Rust核心库支持no_std环境下的Future,无需线程即可并发I/O。

实现要点:

  • Task:Pin<Box<dyn Future<Output=()> + Send>>,使用AtomicU64生成唯一ID。
  • Executor:BTreeMap<TaskId, Task>存储,Arc<ArrayQueue>队列。spawn插入任务,run_ready_tasks poll直到Pending。
  • Wake:TaskWaker实现Wake trait,wake_by_ref推送ID到队列,避免Arc克隆开销。
  • 调度策略:FIFO via VecDeque,优先级需扩展BTreeMap排序。

参数:

  • 队列容量:100,溢出panic或丢弃低优先级任务。
  • hlt idle:interrupts::disable(); if queue.empty() { enable_and_hlt(); } else { enable(); },原子检查避免丢失中断。
  • 扩展:工作窃取支持多核,阈值如任务>核心数时负载均衡。

清单:

  1. 实现poll:Pin::as_mut(&mut self.future).poll(cx),返回Poll<()>。
  2. 键盘任务:ScancodeStream impl Stream,poll_next注册AtomicWaker,add_scancode wake。
  3. 测试:spawn(async { println!("Task1"); }) 和 print_keypresses,观察交替输出。
  4. GDB:watch task_queue,step poll调用,验证waker通知。

证据:async/await状态机零成本,pinning防自引用移动,确保安全并发(来源:Rust async书)。

中断处理:PIC与IDT集成

中断桥接硬件与内核,8259 PIC聚合信号,重映射IRQ 0-7/8-15至32-47,避免异常冲突。

配置:

  • ChainedPics::new(32, 40),initialize()设置ICW1-4。
  • IDT:idt[InterruptIndex::Timer.as_usize()].set_handler_fn(timer_handler),load()加载。
  • EOI:PICS.lock().notify_end_of_interrupt(idx),特定于主/从PIC。

参数:

  • 优先级:PIC支持IRQ优先,timer高优先避免时钟漂移。
  • 延迟:中断禁用<1us,without_interrupts(|| { ... })包裹锁。
  • 键盘:Port<0x60>.read()获取scancode,pc-keyboard crate解码Unicode。

清单:

  1. enum InterruptIndex { Timer=32, Keyboard=33 },set_handler_fn。
  2. handler:read port,EOI,process(如print '.')。
  3. 死锁修复:interrupts::without_interrupts(|| writer.lock().write())。
  4. GDB:handle SIGTRAP stop,info interrupts查看向量,x/16i handler地址。

中断启用sti()后,timer每~1193182Hz tick,键盘IRQ1实时响应(来源:OSDev wiki)。

GDB调试:裸机追踪

GDB支持x86裸机,结合QEMU -s -S启动,target remote localhost:1234连接。

要点:

  • .gdbinit:set architecture i386:x86-64,target remote :1234,add-symbol kernel。
  • 断点:break _start,continue,si单步,x/10i $pc查看汇编。
  • 监控:info registers,watch CR3*,bt回溯栈。
  • 页表:print (PageTable)$cr3,x86_64::VirtAddr::new(0xb8000)翻译。

风险:-O优化混淆,use -g -C debuginfo=2构建;多核需info threads。

通过这些组件,Rust内核实现安全、高效OS基础。未来扩展至抢占调度和多核,利用APIC替换PIC提升性能。总字数约1200,实践优先,确保可落地。