架构总览¶
本文档介绍 NoobKernel 的整体设计思想、模块划分和核心概念,帮助读者建立对内核结构的全局认识。
内核是什么?¶
在深入架构之前,先理解内核的本质。
内核的角色¶
操作系统内核是硬件和应用程序之间的中介:
┌─────────────────────────────────────┐
│ 应用程序 │
│ 浏览器、编辑器、Shell... │
├─────────────────────────────────────┤
│ 系统调用接口 │
│ open, read, write, fork... │
├─────────────────────────────────────┤
│ 内核 │
│ 进程管理、内存管理、文件系统... │
├─────────────────────────────────────┤
│ 硬件抽象层 │
│ 驱动、中断控制器... │
├─────────────────────────────────────┤
│ 硬件 │
│ CPU、内存、磁盘、网卡... │
└─────────────────────────────────────┘
内核的核心职责¶
- 资源管理:CPU 时间、内存、设备
- 抽象硬件:提供统一的接口
- 隔离保护:进程间互不干扰
- 并发支持:让多个任务"同时"运行
内核的特权¶
内核运行在最高特权级(RISC-V 的 S-Mode),可以: - 访问所有内存 - 执行特权指令 - 配置硬件
这种特权是双刃剑:强大的能力也意味着更大的责任和风险。
NoobKernel 的设计哲学¶
教学优先¶
NoobKernel 的目标是教学,不是生产。设计选择遵循:
- 简单优于完美:选择简单的实现,即使效率不是最优
- 清晰优于紧凑:代码结构清晰,注释充分
- 渐进优于一步到位:先实现核心功能,逐步扩展
参考 Linux,但简化¶
Linux 是成熟的内核,设计经过验证。NoobKernel 参考其架构,但: - 去除复杂特性(抢占、RCU 等) - 简化数据结构(简单调度器代替 CFS) - 减少代码量(约 5000 行 vs Linux 的数百万行)
RISC-V 优先¶
NoobKernel 专门为 RISC-V 设计,利用其特性: - 简洁的特权级模型 - 规范的中断机制 - 灵活的 CSR
这比移植一个 x86 内核到 RISC-V 更自然。
模块划分¶
功能模块¶
| 模块 | 职责 | 关键问题 |
|---|---|---|
| boot | 启动初始化 | 如何从硬件过渡到内核? |
| mm | 内存管理 | 如何高效分配内存?如何隔离地址空间? |
| hal | 硬件抽象 | 如何屏蔽硬件差异? |
| trap | 中断处理 | 如何响应异步事件? |
| task | 进程管理 | 如何切换进程?如何调度? |
| sync | 同步机制 | 如何避免并发冲突? |
| fs | 文件系统 | 如何组织持久化数据? |
| misc | 基础库 | 提供通用数据结构 |
模块间的依赖关系¶
boot(启动)
│
┌─────────────┼─────────────┐
│ │ │
misc hal trap
│ │ │
│ │ ┌──────┴──────┐
│ │ │ │
sync ←───────→ mm ←──→ task ←──────→ fs
│ │ │ │
└─────────────┴──────┴─────────────┘
理解依赖关系的重要性: - 初始化顺序必须遵守依赖关系 - 修改一个模块可能影响依赖它的模块 - 调试时可以从底层模块开始
关键模块详解¶
内存管理(mm)¶
内存管理是内核最基础的子系统。设计要点:
- 分层分配:Slab 处理小对象,Buddy 处理大块
- 虚拟内存:Sv39 页表,恒等映射
- 缓存层:bcache 缓存磁盘块
为什么分层?因为单一分配器无法兼顾所有场景。
进程管理(task)¶
进程是执行单元。设计要点:
- 进程结构:记录进程的所有信息
- 上下文切换:保存/恢复寄存器
- 调度器:决定下一个运行谁
关键挑战:切换进程时要保证状态完整恢复。
中断处理(trap)¶
中断是硬件通知内核的方式。设计要点:
- 入口代码:汇编保存寄存器
- 分发处理:根据中断类型分发
- 调度触发:定时器中断驱动调度
关键设计:用 gp 寄存器快速定位 trapframe。
文件系统(fs)¶
文件系统组织持久化数据。设计要点:
- VFS 框架:统一接口,支持多种文件系统
- 对象模型:inode、dentry、file
- 缓存加速:dentry 缓存、bcache
关键思想:通过抽象层隔离具体实现。
核心概念¶
上下文切换¶
上下文切换是进程管理的心脏。核心问题:
- 保存什么:callee-saved 寄存器 + CSR
- 如何切换:修改寄存器,改变执行流
- 如何恢复:加载保存的状态
关键洞察:切换本质上是"改变返回地址",让 ret 指令跳到新进程的代码。
中断与调度¶
中断是驱动调度的机制:
定时器中断 → 设置调度标志 → 从中断返回时检查 → 触发调度
为什么不在中断中直接调度?因为: - 中断上下文有限制 - 需要完整保存/恢复流程 - 灵活性更好
锁与中断¶
锁和中断的关系微妙:
获取锁 → 关中断 → 保护临界区 → 开中断 → 释放锁
为什么?因为中断处理函数可能也要获取锁,导致死锁。
虚拟内存¶
虚拟内存提供隔离和抽象:
- 隔离:每个进程独立的地址空间
- 保护:权限控制(只读、不可执行)
- 抽象:连续的虚拟地址,不连续的物理内存
当前实现:内核使用恒等映射(虚拟地址 = 物理地址),简化管理。
数据流示例¶
读取文件的数据流¶
应用程序: read(fd, buf, size)
│
▼ 系统调用(待实现)
│
VFS: 根据文件描述符找到 file 结构
│ 调用 file->f_op->read
▼
文件系统: 转换文件位置到块号
│ 调用 bcache 读取块
▼
bcache: 查缓存,未命中则读磁盘
│ 调用块设备驱动
▼
块设备驱动: 发送 I/O 请求
│ 等待中断
▼
硬件: 磁盘返回数据
进程切换的数据流¶
定时器中断
│
▼ kernelvec(汇编)
│ 保存寄存器到 ktrapframe
│ 调用 kerneltrap
▼
kerneltrap(C)
│ handle_timer
│ 设置 need_resched
│ 检查 need_resched
│ 调用 sched_yield
▼
sched_yield
│ 当前进程入队
│ 取出下一个进程
│ context_switch
▼
context_switch(汇编)
│ 保存旧进程上下文
│ 加载新进程上下文
│ ret(跳到新进程)
▼
新进程继续执行
与 xv6 的比较¶
| 方面 | NoobKernel | xv6 |
|---|---|---|
| 内存分配 | Buddy + Slab | 简单 kalloc |
| 文件系统 | VFS + ramfs | 直接实现 |
| 调度器 | FIFO | FIFO |
| 代码量 | ~5000 行 | ~8000 行 |
| 特点 | 强调模块化、内存管理 | 全功能、易上手 |
学习路径建议¶
初学者路径¶
- 理解启动流程:从硬件到内核,理解初始化过程
- 理解中断机制:中断是内核与硬件交互的核心
- 理解进程切换:上下文切换是并发的关键
- 理解内存管理:分配器设计体现了工程权衡
- 理解文件系统:VFS 框架体现了抽象的力量
进阶路径¶
- 阅读源码,对照文档
- 修改内核,观察效果
- 添加功能,如系统调用
- 移植到新平台
设计权衡总结¶
为什么选择这些设计?¶
| 设计 | 优点 | 缺点 |
|---|---|---|
| Buddy + Slab | 高效、低碎片 | 实现复杂 |
| VFS 框架 | 可扩展 | 增加抽象层 |
| FIFO 调度 | 简单公平 | 无优先级 |
| 恒等映射 | 简单 | 无隔离 |
| 单核 | 无锁竞争 | 性能受限 |
未来改进方向¶
- 抢占式调度:提高响应性
- 用户态支持:系统调用、进程隔离
- 多核支持:SMP、锁优化
- 持久化文件系统:ext2、FAT32
思考题¶
- 如果所有模块都用一个大锁保护,会有什么问题?
- 为什么内存管理要在中断处理之前初始化?
- 文件系统可以不用 VFS 吗?会有什么问题?
- 如何判断一个设计选择是"足够好"而不是"完美"?
下一步¶
根据兴趣选择学习路径: - 内存管理:深入内存分配 - 中断处理:理解异步机制 - 进程管理:理解并发 - 文件系统:理解数据组织