SudoOS 是一个从零构建(From Scratch)、面向现代 x86-64 架构的宏内核(Monolithic Kernel)操作系统。本项目摒弃了传统的 32 位过渡模式,直接通过开源bootloader项目 Limine 引导协议 进入 64 位长模式(Long Mode),旨在实现一个具备抢占式多任务调度、虚拟内存管理、用户态隔离以及交互式 Shell 的完整内核生态。
- 主流 x86-64 长模式:系统直接运行在 64 位模式下,完全利用现代 CPU 的通用寄存器和指令集,突破了 4GB 内存限制,支持 NX(No-Execute)位保护,。
- Limine 高级引导协议:采用业界前沿的 Limine Bootloader,支持 HHDM(Higher Half Direct Map)特性,实现了内核空间的高地址映射,为内核与用户空间的隔离奠定了基础,。
- 硬件抽象层 (HAL):实现了 GDT/IDT 的重构与管理,完善的中断上下文保存(Context Save/Restore)机制,支持异常(Exception)与硬件中断(IRQ)的精确分发,。
- 标准四级页表 (4-Level Paging):完整实现了 PML4 -> PDPT -> PD -> PT 的四级分页机制,支持 48 位虚拟地址空间,精确管理用户态与内核态的内存映射。
- 物理内存管理 (PMM):基于 Bitmap(位图) 算法的高效物理页分配器,通过解析 BIOS/UEFI 提供的内存映射图(E820/UEFI MemMap)动态管理物理内存资源。
- 虚拟内存管理 (VMM):支持缺页异常处理,实现了内核堆(Kernel Heap)的动态分配(
kmalloc/kfree),支持 VMA(虚拟内存区域)管理,为每个进程提供独立的地址空间。
- 1:1 线程模型:实现了基于 PCB(进程控制块)的内核级线程管理,支持线程的创建、等待、退出和资源回收。
- 时间片轮转调度 (Round-Robin):基于 PIT 时钟中断实现抢占式调度器。系统能够强制剥夺耗时进程的 CPU 使用权,保证了交互式任务的响应速度和系统的公平性,。
- 完整的生命周期管理:设计了清晰的进程状态机(READY, RUNNING, ZOMBIE),并实现了 Exit-Zombie-Reap 机制,彻底解决了僵尸进程回收和内核栈释放的死锁难题。
- Ring 0 / Ring 3 特权级隔离:实现了从内核态到用户态的安全切换(
iretq),用户程序在受限的 Ring 3 级别运行,无法直接破坏内核数据。 - ELF64 可执行文件加载器:内核内置 ELF 解析器,能够动态解析并加载标准的 ELF64 二进制文件,支持
.text,.data,.bss段的内存映射。 - 交互式 Shell 与 Libc:
- 移植并实现了一个轻量级的 Libc(包含
stdio,string,stdlib)。 - 开发了功能完备的 SudoOS Shell,支持
ls,cd,cat,run等十余种命令,支持参数解析和命令历史。
- 移植并实现了一个轻量级的 Libc(包含
- 系统调用接口 (System Calls):通过
int 0x80软中断实现了标准的系统调用接口,支持文件读写、进程控制、内存申请等核心功能。
- 内存文件系统 (In-Memory FS):实现了一个类 VFS 接口的 RamFS,支持目录树结构解析、文件读写(Read/Write)、目录遍历(Getdents)和权限管理,能够模拟真实的文件系统操作体验。
- 自动化构建:使用 GNU Make 构建了模块化的编译系统,支持一键编译内核、用户库和 ISO 镜像打包。
- 跨平台兼容:支持 BIOS 与 UEFI 双模引导(通过 Limine),可在 QEMU 模拟器及真实物理机上运行。
SudoOS/
├── .gitignore # Git 版本控制忽略配置
├── flake.nix # Nix 环境配置文件(用于开发环境构建)
├── GNUmakefile # 项目顶层 Makefile,负责编译内核、用户程序和制作 ISO
├── limine.conf # Limine 引导加载程序配置文件,指定内核路径和协议
├── README.md # 项目说明文档
├── sudoOS.iso # 最终构建出的操作系统 ISO 镜像
├── boot/ # 引导加载程序相关文件
│ └── limine/ # Limine Bootloader 二进制文件及模块 (UEFI/BIOS)
├── kernel/ # 操作系统内核源码目录
│ ├── GNUmakefile # 内核子项目 Makefile
│ ├── get-deps # 脚本:用于获取内核依赖(如 Limine)
│ ├── linker.lds # 链接脚本,定义内核内存布局 (.text, .data 等)
│ ├── bin/ # 内核编译产物目录
│ │ └── kernel # 编译链接完成的内核 ELF 可执行文件
│ └── src/ # 内核源代码
│ ├── main.c # 内核入口 kmain,负责初始化各个子系统
│ ├── limine.h # Limine 引导协议定义头文件
│ ├── arch/ # 硬件架构相关代码 (x86_64)
│ │ ├── gdt.c/h # 全局描述符表 (GDT) 初始化与刷新
│ │ ├── gdt.S # 汇编辅助:加载 GDT
│ │ ├── idt.c/h # 中断描述符表 (IDT) 初始化
│ │ ├── interrupts.c # C 语言中断处理函数 (Exception/IRQ Handler)
│ │ ├── isr.S # 中断服务程序汇编入口 (Stub),保存现场
│ │ ├── switch.S/h # 线程上下文切换汇编实现 (switch_to)
│ │ ├── timer.c/h # PIT (Programmable Interval Timer) 时钟驱动
│ │ ├── trap.h # 中断与异常向量号定义
│ │ └── x86_64.h # 架构相关宏定义与端口操作
│ ├── drivers/ # 设备驱动程序
│ │ ├── console.c/h # 基于 Framebuffer 的图形控制台驱动
│ │ ├── drivers.h # 驱动子系统通用头文件
│ │ ├── font.h # 内置位图字体数据
│ │ ├── io.h # I/O 端口读写辅助函数
│ │ └── keyboard.c/h # PS/2 键盘驱动与扫描码解析
│ ├── fs/ # 文件系统
│ │ └── ramfs.c/h # 内存文件系统 (RamFS) 实现 (open, read, write)
│ ├── lib/ # 内核通用库函数
│ │ ├── elf.h # ELF64 文件格式结构体定义
│ │ ├── list.h # 双向循环链表实现
│ │ ├── std.h # 标准整数类型与宏定义
│ │ └── string.c/h # 字符串与内存操作函数 (memcpy, strlen 等)
│ ├── mm/ # 内存管理子系统
│ │ ├── debug_mm.c/h # 内存调试辅助工具
│ │ ├── paging.c/h # 四级页表管理与虚拟地址映射
│ │ ├── pmm.c/h # 物理内存管理器 (基于 Bitmap)
│ │ └── vmm.c/h # 虚拟内存区域 (VMA) 与 mm_struct 管理
│ └── proc/ # 进程管理与调度
│ ├── entry.S # 用户态/内核态切换汇编跳板 (Trampoline)
│ ├── proc.c/h # 进程控制块 (PCB) 管理与 ELF 加载器
│ └── sche.c/h # 轮转调度器 (Round-Robin Scheduler) 实现
└── usr/ # 用户态空间代码
├── shell.c # 交互式 Shell 源码 (系统启动后的第一个程序)
├── usrmain.c # 用户程序主入口 (调用 Shell)
├── bin/ # 用户程序编译产物
│ └── user.elf # 编译好的用户态可执行文件
└── lib/ # 用户态 C 运行时库 (Mini Libc)
├── stdio.c/h # 标准输入输出库 (printf, gets)
├── stdlib.c/h # 标准工具库 (malloc, atoi, exit)
├── string.c/h # 用户态字符串库
└── syscall.c/h # 系统调用封装层 (syscall wrapper)
由于本项目开发过程主要在github上,以下是github的提交记录。
- 2026-01-04 18:52 - modify readme.md [a19ba9c]
- 2026-01-04 18:21 - file 1.0updated use [9652a90]
- 2026-01-04 18:11 - file 1.0updated [c20924e]
- 2026-01-04 18:07 - file finished [330b102]
- 2026-01-03 19:55 - Merge pull request #14 from Sudo-666/b1 [1dee968]
- 2026-01-03 19:54 - shell first-char-no-display fixed [1a2d6d4]
- 2026-01-03 17:29 - shell added [391073e]
- 2026-01-03 14:54 - common update [6315773]
- 2026-01-03 14:45 - more syscall funcs added [be6efae]
- 2026-01-01 00:24 - Merge pull request #13 from Sudo-666/b1 [a547a7c]
- 2026-01-01 00:22 - add docs [a5501c0]
- 2026-01-01 00:17 - create user process [90fecd1]
- 2025-12-30 19:32 - Merge branch 'main' into b1 [79c9936]
- 2025-12-30 19:32 - add mm docs [5504d10]
- 2025-12-30 19:30 - keyboard and console drivers updated [5956e3e]
- 2025-12-30 19:13 - add proc docs [ebecb7f]
- 2025-12-30 18:59 - Merge branch 'main' into b1 [9893083]
- 2025-12-30 18:59 - add proc exit [f31c385]
- 2025-12-30 18:47 - schedule() function and timer eoi logic corrected [c853b71]
- 2025-12-30 17:50 - Merge pull request #12 from Sudo-666/b1 [7487bec]
- 2025-12-30 17:49 - modify proc [e6790a9]
- 2025-12-29 21:56 - Merge pull request #11 from Sudo-666/b1 [c172b1d]
- 2025-12-29 21:53 - Merge branch 'main' into b1 [0364526]
- 2025-12-29 21:51 - fix conflict [ef4f770]
- 2025-12-29 21:47 - add proc and schedule [0f00d30]
- 2025-12-29 21:45 - gdt and usr makefile corrected [4746728]
- 2025-12-29 18:54 - Merge pull request #10 from Sudo-666/b1 [e96c6e3]
- 2025-12-29 18:51 - fix main and init proc [418d282]
- 2025-12-29 13:32 - add flake in b1 [9d379f8]
- 2025-12-29 13:30 - rm flake in main [6cfe89f]
- 2025-12-29 13:25 - add my nixos-develoment flake config [f58c50d]
- 2025-12-29 13:19 - add my nixos-develoment flake config [a89546d]
- 2025-12-29 13:17 - add my nixos-develoment flake config [8be74a2]
- 2025-12-29 13:16 - build linux dev env [603b641]
- 2025-12-22 23:03 - Merge pull request #9 from Sudo-666/b1 [2b6ee2c]
- 2025-12-22 23:00 - switched to user mode! and added docs [7cf7dec]
- 2025-12-21 20:17 - merged [1f84ef6]
- 2025-12-21 20:16 - nothing [c66512c]
- 2025-12-21 20:15 - daily update [d3e54c7]
- 2025-12-21 17:51 - gdt and basic timer added [1481683]
- 2025-12-21 17:47 - Merge pull request #8 from Sudo-666/b1 [54e4865]
- 2025-12-21 17:47 - add kstack init [2a1da05]
- 2025-12-19 23:10 - Merge pull request #7 from Sudo-666/b1 [e9efed8]
- 2025-12-19 22:46 - add kheap manager kmalloc & kfree [26e971a]
- 2025-12-18 23:47 - add kaddr memory layout [fe06bf5]
- 2025-12-18 14:37 - Merge branch 'main' into b1 merge idt [62295b8]
- 2025-12-18 14:32 - update readme [a9504b0]
- 2025-12-15 19:38 - Combination [8d6d1a6]
- 2025-12-15 19:29 - 加入基本中断系统 [b92b624]
- 2025-12-15 19:22 - 系统中断实现 [8a25179]
- 2025-12-10 14:43 - add memlayout image [d7d5d13]
- 2025-12-10 14:42 - add memlayout image [68d30c5]
- 2025-12-10 14:33 - init paging [124113a]
- 2025-12-10 14:32 - init paing [c8c54be]
- 2025-12-08 23:00 - add alloc_page & free_page [59e52c9]
- 2025-12-08 21:12 - Merge pull request #4 from Sudo-666/b1 [77fc2a1]
- 2025-12-08 20:54 - kprintf function added [960c87b]
- 2025-12-08 20:28 - add pmm_init [4b4c532]
- 2025-12-05 22:39 - 重构 [ea8b834]
- 2025-12-05 21:54 - pull latest [6218aa1]
- 2025-12-05 21:51 - modify debugmm [cf3111e]
- 2025-12-05 21:39 - partly uint64 functions added [8448b89]
- 2025-12-05 21:05 - modify debugmm [f455a38]
- 2025-12-05 20:26 - Merge pull request #2 from Sudo-666/b1 [1955985]
- 2025-12-05 20:24 - finish conflict [91181ff]
- 2025-12-05 20:21 - Merge branch 'b1' of github.com:Sudo-666/SudoOS into b1 [2abdb8e]
- 2025-12-05 20:20 - kprint debug mmap done! [68689de]
- 2025-12-05 20:17 - Remove ignored files from .gitignore [9ef8b21]
- 2025-12-05 19:06 - kprint debug mmap done! [ab96d21]
- 2025-12-02 21:53 - restructured [827401b]
- 2025-12-02 21:48 - restructured [4f1b32c]
- 2025-12-02 20:52 - add mac/linux platform for compile kern [c8681eb]
- 2025-12-02 19:55 - a smallest kern [0193bcf]
- 2025-12-01 19:20 - Daily init [334a12c]
- 2025-12-01 18:46 - Update README.md [57534f6]
- 2025-12-01 18:36 - Create README.md [8865eb7]
- 2025-12-01 18:23 - First commit via HTTPS [607e082]
SudoOS 不仅仅是一个简单的教学内核,它是一个麻雀虽小,五脏俱全的现代化操作系统原型。从底层的四级页表到上层的 Shell 交互,从内核的内存分配到用户态的进程调度,每一行代码都体现了对计算机系统底层原理的精准把控和工程实践能力。
在编译和运行 SudoOS 之前,请确保你的开发环境(Linux 或 macOS)已安装以下工具:
- 编译器:
- Linux:
gcc,ld,make(通常包含在build-essential中)。 - macOS: 需要交叉编译器
x86_64-elf-gcc和x86_64-elf-ld(可通过 Homebrew 安装)。
- Linux:
- 构建工具:
xorriso(用于打包 ISO 镜像)。 - 模拟器:
qemu-system-x86_64(用于运行系统)。 - 网络: 构建脚本会自动从 GitHub 下载 Limine Bootloader,请保持网络连接。
项目根目录提供了自动化的 Makefile,支持一键构建与运行。
- 默认运行 (推荐): 编译内核并使用 QEMU (BIOS 模式) 启动。
make run
- UEFI 模式运行: 需要系统安装了 OVMF 固件。
make run-uefi
- 仅编译 ISO: 生成
sudoOS.iso但不启动模拟器。make all
- 清理构建: 删除所有编译产物。
make clean
系统启动后会自动进入 SudoOS Shell,你可以通过键盘与系统交互。
SudoOS 的图形控制台 (console.c) 内置了历史缓冲区,支持查看之前的输出内容。
- 上翻页 (Scroll Up): 按下键盘上的
PageUp键。- 效果: 屏幕内容向下滚动,显示更早之前的历史日志(如内核启动时的初始化信息)。
- 下翻页 (Scroll Down): 按下键盘上的
PageDown键。- 效果: 屏幕内容向上滚动,返回最新的输出视图。
- 自动复位: 当你在 Shell 中输入任何新字符或按下回车时,视图会自动强制跳转回最底部,确保你能看到最新的输入内容。
Shell 支持基础的文件系统操作和系统调用测试。
| 命令 | 示例 | 说明 |
|---|---|---|
| help | help |
显示帮助菜单和系统调用号列表。 |
| ls | ls /usr |
列出目录内容。 |
| cd | cd bin |
切换当前目录。 |
| cat | cat README.txt |
查看文件内容。 |
| clear | clear |
清空屏幕(视觉清空,历史记录仍保留)。 |
| run | run <系统调用号> |
系统调用调试器。强烈推荐!可测试系统调用。交互式调用内核功能(如输入 0 测试 SYS_READ)。 |
如果你在开发过程中需要调试内核:
- 修改 QEMU 参数: 在
GNUmakefile中,将QEMUFLAGS修改为包含-d int,可以在终端看到中断日志。 - 查看中断: 如果系统卡死,QEMU 控制台通常会打印
cpu_reset或异常中断号(如int 13GP,int 14PF)。
用 C 编写的一个符合 Limine 标准的小型 x86-64 内核,并使用 Limine 引导加载程序启动
-
Limine 本质只是引导程序(bootloader),它的工作是:
- 加载你的内核 ELF 文件(bin/sudoOS)
- 切换到你指定的 CPU 状态(x86_64 long mode)
- 为你准备好一些必要的信息(内存地图、帧缓冲、SMBIOS...)
- 跳转到你的内核入口函数 kmain()
-
内核地址空间:0xffffffff80000000 ~ 0xffffffffffffffff
================================================================================
阶段 1: Limine 引导交接后 (Limine Page Tables)
================================================================================
[ 虚拟地址空间 (Virtual Address Space) ] [ 物理地址空间 (Physical Address Space) ]
(64-bit Address Space, Canonical Form)
0xFFFFFFFFFFFFFFFF +----------------------+ +----------------------+ <--- Max RAM
| | | |
| Kernel Image Map | mapped to | 可用 RAM (Free) |
Kernel Virtual Base| (.text, .data, ...) | ----------> |----------------------|
(e.g. 0xFFFFFFFF8..+----------------------+ | Limine Reclaimable |
| | | (Memmap, Stack, etc) |
| | |----------------------|
| | | |
| ... | | Kernel Image Phys | <--- Kernel Phys Base
| | | (.text, .data, ...) |
| | |----------------------|
| | | |
| | | 可用 RAM (Free) |
+----------------------+ | |
| | +----------------------+ 0x00000000
| HHDM (Limine) | mapped to
HHDM Start | All Physical Memory | ----------> [覆盖整个物理内存 0 - Max]
(e.g. 0xFFFF8......+----------------------+
| |
0x00... +----------------------+
| (包含 1:1 映射) |
| (Limine 可能会映射 |
| 前 4GB,但不保证) |
+----------------------+
================================================================================
阶段 2: kmain 初始化后 (SudoOS Custom Page Tables)
================================================================================
[ 虚拟地址空间 (Virtual Address Space) ] [ 物理地址空间 (Physical Address Space) ]
(由 PMM 管理)
0xFFFFFFFFFFFFFFFF +----------------------+ +----------------------+ <--- Max RAM
| | | |
| Kernel Image Map | mapped to | 可用 RAM (Free) |
Kernel Virtual Base| (RWX 权限重设/分离) | ----------> |----------------------|
(e.g. 0xFFFFFFFF8..+----------------------+ | PMM Bitmap | <--- pmm_init 分配
| | | (管理所有页面的状态) |
| | |----------------------|
| | | 可用 RAM (Free) |
| ... | |----------------------|
| | | Kernel PML4 (New) | <--- paging_init 分配
| | | (及 PDPT, PD 等) |
| | |----------------------|
| | | Limine Reclaimable |
+----------------------+ | (此时数据可能仍存在) |
| | |----------------------|
| HHDM (SudoOS) | mapped to | Kernel Image Phys |
HHDM Start | All Physical Memory | ----------> | (.text, .data, ...) |
(e.g. 0xFFFF8......| (重映射,用于内核访问| |----------------------|
| 任意物理地址) | | |
+----------------------+ | 可用 RAM (Free) |
| | | |
| (Limine 的旧映射被 | | (0号页通常保留/Busy) |
| 丢弃,低地址空间 | +----------------------+ 0x00000000
| 目前为空/User Space)|
0x00... +----------------------+
- 读取memmap(物理内存探测),检测物理内存的情况
- memmap结构
struct limine_memmap_response {
uint64_t entry_count; // 条目数量
struct limine_memmap_entry **entries; // 指向 entries[] 数组
};
struct limine_memmap_entry {
uint64_t base; // 物理地址起点
uint64_t length; // 长度
uint64_t type; // 区域类型
};- 物理内存的类型
LIMINE_MEMMAP_USABLE // 可以用来分配给物理内存管理
LIMINE_MEMMAP_RESERVED // 不可用(可能被固件、BIOS 占用)
LIMINE_MEMMAP_ACPI_RECLAIMABLE // ACPI 信息区,可在解析 ACPI 后使用
LIMINE_MEMMAP_ACPI_NVS // ACPI NVS(不可动)
LIMINE_MEMMAP_BOOTLOADER_RECLAIMABLE
LIMINE_MEMMAP_KERNEL_AND_MODULES // 内核自身所在内存
LIMINE_MEMMAP_FRAMEBUFFER // 显存-
建立过程
- 获取物理地址最高位。
- 计算将物理地址空间总共分成多少页,从而计算Bitmap大小。
- 遍历mmap,找到一个空闲区域放置
- 通过pa+hhdm获取bitmap的虚拟地址,并初始化
- 再次遍历mmap,这次对每一个空闲区域进行分页并设置
-
Bitmap每一项为
uint8_t类型,8位无符号整数的每一位对应一个物理页框,总共对应8个物理页框,共8*4kB大小。 -
Bitmap的i位对应物理地址
i*PAGE_SIZE -
对Bitmap的bit位进行操作:
static inline void bit_set(size_t bit);
static inline void bit_unset(size_t bit);
static inline bool bit_test(size_t bit);- 内存分配接口
// 分配一页
uint64_t pmm_alloc_page();
// 释放一页
void pmm_free_page(uint64_t pa);虚拟地址 (Virtual Address) 内存区域 (Memory Region) 作用与存放内容
+----------------------------+-----------------------------------+------------------------------------------+
| 0xFFFFFFFF81000000 (或更高) | 内核栈区 (Kernel Stacks) | 存放各进程/线程的内核态运行栈 (kmalloc 分配) |
+----------------------------+-----------------------------------+------------------------------------------+
| 0xFFFFFFFF80000000 | 内核镜像区 (Kernel Image) | 存放 .text, .rodata, .data, .bss (IDT) |
+----------------------------+-----------------------------------+------------------------------------------+
| ... | 未使用 | 预留空间 |
+----------------------------+-----------------------------------+------------------------------------------+
| 0xFFFFB00000000000 | MMIO / Vmalloc 映射区 | 映射硬件寄存器或不连续物理内存 |
+----------------------------+-----------------------------------+------------------------------------------+
| 0xFFFF900000000000 | 内核堆区 (Kernel Heap) | kmalloc 管理的区域,用于动态分配小对象 |
+----------------------------+-----------------------------------+------------------------------------------+
| 0xFFFF800000000000 (HHDM) | 物理内存直接映射区 (HHDM Area) | 直接访问所有物理内存 (如 Bitmap 数组) |
+----------------------------+-----------------------------------+------------------------------------------+
| ... | 用户态空间 | 存放进程的用户代码、数据、栈 (Ring 3) |
+----------------------------+-----------------------------------+------------------------------------------+
- 起始地址:
HHDM_OFFSET(由 Limine 协议提供)。 - 存放内容: 整个物理内存的副本。Bitmap 数组 应该通过该区域访问。
- 作用: 内核可以通过
虚拟地址 = 物理地址 + HHDM_OFFSET的简单公式直接操作物理内存,无需反复建立新页表。这对 PMM 和页表项(PTE/PDE)本身的修改至关重要。
- 起始地址:
0xFFFF900000000000。 - 存放内容: 存放内核运行期间动态申请的对象,如进程控制块 (PCB)、缓冲区、动态分配的数据结构。
- 作用: 通过双向循环链表,在此区域内进行细粒度的字节分配。当空间不足时,调用
pmm_alloc_page申请页框并由vmm_map_page映射到此区域。
- 起始地址:
0xFFFFB00000000000。 - 存放内容: 硬件寄存器映射(如显存 Framebuffer、APIC 寄存器)或不连续物理页组成的连续虚拟内存。
- 作用: 提供一个逻辑上连续的窗口来访问硬件或零散内存。
-
起始地址:
0xFFFFFFFF80000000(由链接脚本linker.lds定义)。 -
存放内容:
-
.text:内核代码。 -
.rodata:只读常量。 -
.data&.bss:全局变量。IDT (中断描述符表) 和 GDT 通常静态分配在这里。 -
作用: 这是内核的“核心大本营”。
paging_init会根据kernel_addr_request的结果将链接好的 ELF 段映射到物理内存。
- 起始地址: 紧跟在内核镜像之后,例如
0xFFFFFFFF81000000。 - 存放内容: 每个进程独立的内核态栈。
- 作用: 处理中断或系统调用时,CPU 切换到的临时工作空间。每个栈之间通常留一个不映射的“保护页”,防止一个进程的栈溢出后破坏另一个进程。
堆管理器定义了以下关键的内存布局参数,位于 src/mm/pmm.h 中:
- 基地址 (
KERNEL_HEAP_BASE):0xFFFF900000000000。这是堆在虚拟地址空间中的起始位置。 - 页大小 (
PAGE_SIZE):4096字节。 - 对齐方式: 所有分配的内存大小均向上对齐到 8字节。
- 最小切割阈值 (
MIN_SPLIT):16字节。当空闲块剩余空间小于此值时,不再进行切割,以减少碎片。
堆内存管理基于双向循环链表,链表中包含所有内存块(包括已分配和空闲的)。每个内存块由一个头部结构 kheap_pghdr_t 描述。
定义于 src/mm/pmm.h:
typedef struct {
list_node_t node; // 链表节点 (prev, next),用于连接前后物理相邻的内存块
uint64_t size; // 数据区大小 (不包含头部本身的大小)
bool is_free; // 状态标志:true=空闲, false=已占用
} kheap_pghdr_t;HEADER_SIZE:sizeof(kheap_pghdr_t),即每个内存块的固有开销。- 链表组织: 所有内存块按照虚拟地址顺序链接在全局链表
kheap_list中。这使得kfree可以通过检查node.prev和node.next快速找到物理上相邻的块进行合并。
采用 First-Fit (首次适应) 算法:
- 对齐: 将请求大小
size向上对齐至 8 字节。 - 搜索: 遍历
kheap_list,寻找第一个满足is_free == true且size >= request_size的块。 - 扩容: 如果遍历结束仍未找到合适的块,则计算所需页数并调用
kheap_expand扩容堆空间,随后递归重试分配。 - 切割 (Splitting): 如果找到的块大小远大于请求大小(剩余空间
>= HEADER_SIZE + MIN_SPLIT),则将该块分裂为两个块:
- 前半部分为已分配块。
- 后半部分为新的空闲块,并插入到链表中。
- 标记: 将目标块的
is_free设为false并返回数据区指针。
支持 即时合并 (Immediate Coalescing):
- 定位: 根据传入指针回退
HEADER_SIZE字节找到块头。 - 标记: 将块状态设为
is_free = true。 - 向后合并: 检查
node.next。如果下一个块存在且空闲,并且地址连续,则将其吞并(增加当前块大小,从链表移除下一个块)。 - 向前合并: 检查
node.prev。如果前一个块存在且空闲,并且地址连续,则将当前块并入前一个块。
当堆空间不足时,自动向高地址扩展:
- 物理分配: 调用
pmm_alloc_page申请新的物理页。 - 虚拟映射: 调用
vmm_map_page将新页映射到kheap_top指向的虚拟地址,属性为PTE_PRESENT | PTE_RW。 - 初始化: 在新页起始处构建
kheap_pghdr_t,初始状态设为is_free = 0(占用)。 - 链表插入: 使用
list_add_before将新块插入到kheap_list尾部。 - 触发合并: 调用
kfree释放这个新块。利用kfree的合并逻辑,如果新页与之前的堆顶在物理/逻辑上连续,它们会自动合并成一个更大的空闲块。 - 更新边界:
kheap_top指针增加PAGE_SIZE。
以下接口声明在 src/mm/pmm.h 中:
- 功能: 初始化内核堆管理器。
- 参数:
init_pages- 初始预分配的物理页数量。 - 实现细节: 初始化
kheap_list哨兵节点,设定kheap_top为基地址,并调用kheap_expand进行初始扩容。
- 功能: 申请内核堆内存。
- 参数:
size- 请求的字节数。 - 返回: 成功返回指向数据区的指针,失败返回
NULL。 - 依赖:
first_fit,kheap_expand。
- 功能: 释放堆内存。
- 参数:
ptr- 由kmalloc返回的指针。如果为NULL则直接返回。 - 注意: 包含双重释放检查(虽然当前实现仅打印日志或直接返回,建议在调试模式下Panic)。
- 将一个 48位 的虚拟地址(Virtual Address),切分成 4 段索引(每段 9 位)和 1 段偏移量(12 位)
- 虽然指针是 64 位的,但标准的 4 级分页只使用了低 48 位。高 16 位必须进行符号扩展(Canonical Form,通常全是0或全是1)。
63 48 47 39 38 30 29 21 20 12 11 0
+-------------+---------+---------+---------+---------+------------+
| Sign Extend | PML4 | PDPT | PD | PT | Offset |
| (不可用) | Index | Index | Index | Index | |
+-------------+---------+---------+---------+---------+------------+
| | | | | |
必须为全0 9 bits 9 bits 9 bits 9 bits 12 bits
或全1 (0~511) (0~511) (0~511) (0~511) (0~4095)
-
每一级页表都刚好占用 4KB(一个物理页),并且包含 512 个条目(Entries)。每个条目 8 字节(64位)。
-
CR3 寄存器: 存储顶级页表(PML4)的物理基地址
CR3 Register (存放 PML4 物理地址)
|
v
+----------------------+
| PML4 Table | <-- 1. 用虚拟地址 Bits 39-47 (PML4 Index)
| (Page Map Level 4) | 找到第 N 个条目 (PML4E)
+----------------------+
|
| PML4E 中包含下一级表的物理地址
v
+----------------------+
| PDPT Table | <-- 2. 用虚拟地址 Bits 30-38 (PDPT Index)
| (Page Dir Pointer) | 找到第 N 个条目 (PDPTE)
+----------------------+
|
| PDPTE 中包含下一级表的物理地址
v
+----------------------+
| PD Table | <-- 3. 用虚拟地址 Bits 21-29 (PD Index)
| (Page Directory) | 找到第 N 个条目 (PDE)
+----------------------+
|
| PDE 中包含下一级表的物理地址
v
+----------------------+
| PT Table | <-- 4. 用虚拟地址 Bits 12-20 (PT Index)
| (Page Table) | 找到第 N 个条目 (PTE)
+----------------------+
|
| PTE 中包含最终物理页框的基地址 (Frame Address)
v
+------------------+
| Physical Page |
| (4KB Frame) |
+------------------+
^
|
+--- 5. 加上虚拟地址 Bits 0-11 (Offset) = 最终物理地址
无论是 PML4E, PDPTE, PDE 还是 PTE,它们的结构在硬件上高度相似。 这是一个标准的 64位 页表项结构图:
63 (NX) 51 12 11 9 8 7 6 5 4 3 2 1 0
+-----------+------------+------+-+-+-+-+-+-+-+-+-+
| NX Bit | Physical | Avail|G|S|D|A|P|P|U|R|P|
| (No Exec) | Address | | |Z| | |C|W|/|/| |
| | (Frame) | | | | | |D|T|S|W| |
+-----------+------------+------+-+-+-+-+-+-+-+-+-+
| | | | | | | | | | | +-> Present (1=存在)
| | | | | | | | | | +---> Read/Write (1=可写)
| | | | | | | | | +-----> User/Super (1=用户态)
| | | | | | | | +-------> Write Through
| | | | | | | +---------> Cache Disable
| | | | | | +-----------> Accessed (CPU自动置1)
| | | | | +-------------> Dirty (仅PT有效, 写入后置1)
| | | | +---------------> Page Size (1=2MB大页)
| | | +-----------------> Global (TLB刷新不清除)
| | +-----------------------> Available (OS自定义使用)
| +---------------------------------> 下一级表的物理基地址 (4KB对齐)
+----------------------------------------------> No Execute (1=禁止执行代码)
本设计实现了一个基于 1:1 模型 的内核级线程(Kernel Thread)管理系统。内核线程是操作系统调度的基本单位,拥有独立的内核栈和硬件上下文,但共享内存地址空间(mm_struct)。
- PCB 管理:基于
pcb_t结构体管理进程/线程状态。 - 抢占式调度:基于时间片的轮转调度算法(Round-Robin)。
- 生命周期管理:支持线程创建、运行、退出(僵尸态)和资源回收。
- 上下文切换:基于软件的栈切换与硬件状态保存。
pcb_t 是核心数据结构,定义于 proc.h。
| 字段 | 类型 | 说明 |
|---|---|---|
pid |
int |
唯一进程标识符,由 next_pid 自增生成。 |
name |
char[] |
线程名称,最大长度 32 字节。 |
proc_state |
proc_state_t |
当前状态 (RUNNING, READY, BLOCKED, ZOMBIE)。 |
rsp |
uint64_t |
上下文切换时的内核栈顶指针。 |
kstack_base |
uint64_t |
内核栈的基地址(低地址),用于释放内存。 |
context |
context_t* |
指向栈上保存的寄存器上下文的指针。 |
mm |
mm_struct* |
内存地址空间指针(内核线程共享父进程的 mm)。 |
parent |
pcb_t* |
父进程指针。 |
proc_list_node |
list_node_t |
链接到全局进程链表 proc_list。 |
sched_node |
list_node_t |
链接到就绪队列 ready_queue。 |
exit_code |
int |
线程退出时的返回值。 |
proc_list:包含系统中所有的 PCB(包括运行中、就绪、阻塞和僵尸进程)。ready_queue:仅包含处于PROC_READY状态,等待被 CPU 调度的进程。
graph LR
Create((创建)) --> READY
READY -->|schedule| RUNNING
RUNNING -->|时间片耗尽| READY
RUNNING -->|kthread_exit| ZOMBIE
ZOMBIE -->|free_proc| Destroy((销毁))
- READY: 已分配资源,在
ready_queue中等待调度。 - RUNNING: 当前正在 CPU 上执行(
current_proc指向该线程)。 - ZOMBIE: 线程已退出,但内核栈和 PCB 尚未回收。
- BLOCKED: (预留) 等待外部事件。
为了解决“线程无法释放当前正在使用的栈”这一悖论,采用了 Exit-Zombie-Reap 模型:
- Exit: 线程调用
kthread_exit,状态变为ZOMBIE,移出就绪队列,但保留内核栈,随后主动让出 CPU。 - Reap: 父进程或 Idle 线程通过
free_proc清理ZOMBIE状态的线程,释放其物理内存和 PCB。
功能:初始化进程管理子系统,创建 Idle 进程。 实现步骤:
- 初始化全局链表
proc_list和ready_queue。 - 通过
alloc_new_pcb分配 Idle 进程的 PCB。 - 设置 Idle 进程名为 "idle",状态为
PROC_RUNNING。 - 将
current_proc和idle_proc指向该 PCB。 - 将 Idle 进程加入
proc_list。
功能:创建一个新的内核线程,并伪造其上下文使其能被 schedule 调度。
参数:
parent: 父进程 PCB。name: 线程名。kthread_func: 线程入口函数指针。arg: 传递给入口函数的参数。
实现步骤:
- 分配 PCB:调用
alloc_new_pcb,分配内存并清零。 - 继承资源:共享父进程的
mm指针。 - 分配内核栈:调用
kstack_init(KSTACK_SIZE)分配物理页并映射,记录kstack_base。 - 伪造上下文 (Context Forging):
- 计算栈顶
rsp。 - 在栈顶预留
context_t空间。 - 设置
context->rip指向kernel_thread_entry(汇编跳板)。 - 设置
context->rdi为arg(作为第一个参数)。 - 设置
context->rbx为kthread_func(被跳板调用)。
- 加入队列:将新线程状态设为
PROC_READY,加入proc_list和ready_queue。
位置:proc/entry.S
设计意图:统一所有内核线程的启动和退出行为。
流程:
sti:开中断(新线程启动时默认开中断)。call *%rbx:调用实际的线程函数(创建时存入rbx)。call kthread_exit:如果线程函数返回,自动调用退出函数,防止跑飞。
位置:proc/sche.c
功能:基于时间片的轮转调度。
实现步骤:
- 保护现场:读取
RFLAGS保存中断状态,执行cli()关中断。 - 处理当前进程 (
prev):
- 若为
RUNNING且时间片耗尽 (<=0):状态改为READY,重置时间片,移至ready_queue尾部。 - 若时间片未耗尽:直接返回 (
sti恢复中断)。
- 选择下一个进程 (
next):
- 若
ready_queue为空:选择idle_proc。 - 若不为空:取出队头节点,从队列移除,获取其 PCB。
- 上下文切换:
- 若
prev != next: - 设置
next为RUNNING,更新current_proc。 - TSS 更新:
set_tss_stack(next->rsp),确保下一次中断能正确找到内核栈。 - 页表切换:若
mm不同,执行lcr3。 - 汇编切换:
switch_to(&prev->context, next->context)。
- 恢复现场:恢复中断状态 (
sti)。
功能:终止当前线程运行。 实现步骤:
cli()关中断。- 将当前进程状态设为
PROC_ZOMBIE。 - 设置
exit_code。 - 关键:调用
schedule()。调度器会发现当前进程不再是RUNNING且不在就绪队列中,从而不再调度它,永久切出。
功能:回收僵尸进程占用的物理资源。 注意:此函数不能由目标进程自己调用。 实现步骤:
- 检查进程状态是否为
PROC_ZOMBIE。 - 从
proc_list和ready_queue(如果有) 中移除节点。 - 释放内核栈:调用
kstack_free释放对应的物理页框。 - 释放 PCB:调用
kfree。
SudoOS 支持两种类型的执行流,它们共享相同的 PCB 结构,但内存视图不同:
- 内核线程 (Kernel Thread):
- 运行级别: Ring 0。
- 内存空间: 共享内核的 PML4 页表,没有独立的用户空间映射。
- 创建方式:
kthread_create()。 - 典型例子:
Idle进程(PID 0)。
- 用户进程 (User Process):
- 运行级别: 用户态运行在 Ring 3,陷入内核后运行在 Ring 0。
- 内存空间: 拥有独立的
mm_struct和 PML4 页表。拥有独立的虚拟地址空间(代码段、数据段、堆、栈)。 - 创建方式:
create_user_process()加载 ELF 文件。 - 典型例子:
init进程(PID 1)。
- 算法: 时间片轮转调度 (Round-Robin)。
- 机制:
src/proc/sche.c维护一个ready_queue(双向循环链表)。 - 触发:
- 主动调度: 进程阻塞或退出时调用
schedule()。 - 抢占调度: 时钟中断 (
timer_callback) 递减time_slice,减为 0 时强制调用schedule()。
- 主动调度: 进程阻塞或退出时调用
本模块负责管理用户进程的虚拟内存。
- 生命周期管理:创建 (
mm_create) 和销毁 (mm_destroy) 进程的地址空间。 - 内核共享:确保所有用户进程的页表高半部分(Kernel Space)正确映射到内核,使中断和系统调用能正常工作。
- 区域管理 (VMA):记录用户空间中哪些地址是合法的(代码段、数据段、堆、栈),以及它们的权限。
代表一段连续的虚拟地址范围(例如:代码段 0x400000 - 0x401000)。
// src/mm/vmm.h
struct vma_struct {
list_node_t list_node; // 链表节点,用于串联该进程的所有 VMA
struct mm_struct *mm; // 反向指针,指向所属的 mm_struct
uint64_t vm_start; // 起始虚拟地址 (Page Aligned)
uint64_t vm_end; // 结束虚拟地址 (vm_start + size)
uint64_t vm_flags; // 权限标志 (VM_READ, VM_WRITE, VM_EXEC 等)
uint64_t vm_file_offset; // (可选) 如果是从文件加载的,记录文件偏移
// struct file *vm_file; // (进阶) 如果是内存映射文件 mmap
};代表一个进程完整的地址空间。
// src/mm/vmm.h
struct mm_struct {
pg_table_t* pml4; // 【关键】该进程的顶级页表虚拟地址
uint64_t pml4_pa; // 【新增】顶级页表的物理地址 (方便 context switch 时 load cr3)
list_node_t vma_list; // VMA 链表头
struct vma_struct* mmap_cache; // 最近一次访问的 VMA (缓存优化查询速度)
// 数据统计
int map_count; // VMA 的数量
int ref_count; // 引用计数 (用于多线程共享 mm)
// 关键段边界 (用于 brk, stack extend 等)
uint64_t start_code, end_code;
uint64_t start_data, end_data;
uint64_t start_brk, brk; // 堆的起始和当前顶端
uint64_t start_stack; // 栈底
};// ================= API =================
// 1. 创建一个新的地址空间 (用于 fork 或 exec)
struct mm_struct * mm_alloc();
// 2. 销毁地址空间 (用于 exit)
void mm_free(struct mm_struct *mm);
// 4. 在地址空间中映射一段区域 (用于 load_elf)
// 包含:申请 VMA + 申请物理页 + 修改页表映射
bool mm_map_range(struct mm_struct *mm, uint64_t va, size_t size, uint64_t vm_flags);- 加载内核: Limine 将
kernelELF 文件加载到物理内存。 - 协议请求: 处理
limine_requests(HHDM, Memmap, Framebuffer 等)。 - 进入内核: 跳转到内核入口点
kmain(src/main.c)。此时 CPU 处于 64 位长模式,分页已开启,但使用的是 Limine 提供的临时页表。
内核接管硬件控制权,建立自己的执行环境。
- GDT & IDT 初始化:
gdt_init(): 建立新的 GDT,定义内核代码段/数据段、用户代码段/数据段以及 TSS。idt_init(): 建立中断描述符表,重映射 PIC (8259A),注册异常处理函数(如 Page Fault)和系统调用(Int 0x80)。
- NX 位开启: 调用
enable_nx()开启页表不可执行位支持,防止缓冲区溢出攻击。 - 内存管理初始化 (
mm_init):
- PMM: 解析 Limine Memmap,初始化物理页分配器(位图法)。
- Paging: 创建内核自己的 PML4 页表 (
kernel_pml4),映射内核代码、HHDM 区域。关键动作:切换 CR3 到内核页表。 - Heap: 初始化内核堆 (
kmalloc)。 - Stack: 重新分配并切换到一个更大的内核栈。
- 进程子系统初始化 (
proc_init):
- 创建 Idle 进程 (PID 0)。这是系统第一个 PCB,代表内核主循环。
- 设置
current_proc = idle。
- 加载初始用户程序:
- 从 Limine Module 请求中获取
init程序(ELF 格式)的数据。 - 调用
init_userproc()->create_user_process()。
这是最复杂的环节之一,涉及内存视图的构建和 ELF 加载。
- 分配 PCB:
alloc_new_pcb()分配内存,PID = 1。 - 创建地址空间 (
mm_alloc):
- 分配一个新的 PML4。
- 复制内核映射: 将内核的高半核映射(256-511 项)复制到新页表。保证陷入内核时能访问内核代码。
- 加载 ELF (
load_elf):
- 临时切换页表: 执行
lcr3(proc->mm->pml4_pa)。因为我们要往用户地址(如0x400000)写数据,必须激活目标页表。 - 解析 Segment: 遍历 ELF Program Header,找到
PT_LOAD段。 - 映射内存:
mm_map_range分配物理页并映射到虚拟地址(设置VM_WRITE权限)。 - 拷贝数据:
memcpy将指令和数据从 ELF 镜像拷贝到物理内存。 - 恢复页表: 切回原来的 CR3。
- 分配栈:
- 用户栈: 映射
USER_STACK_TOP向下的区域。 - 内核栈:
kstack_init分配 16KB,用于该进程在内核态执行(中断/Syscall)。
- 构建伪造的上下文 (Context Forging):
- 我们需要“欺骗”调度器,让它以为这个进程是“被暂停”的。
- 在内核栈顶构建
context_t。 context->rip=user_entry(一个汇编跳板函数)。context->r12= ELF 入口点 (e.g.,0x400000)。context->r13= 用户栈顶。proc->context指向这个伪造的栈顶。
- 入队: 将 PID 1 加入
ready_queue。
此时,CPU 还在运行 kmain 的 idle 线程。
- 开启中断:
kmain调用sti。 - 触发调度:
kmain调用schedule()。 - 上下文切换 (
switch_to):
- 调度器选中 PID 1。
- 切换 CR3: 加载 PID 1 的页表。此时用户空间的映射(0x400000 等)变得可见。
- 切换 TSS RSP0: 设置
tss.rsp0为 PID 1 的内核栈底。这至关重要,否则下次中断发生时 CPU 无处保存状态。 switch_to保存 Idle 的寄存器,加载 PID 1 的寄存器。ret指令弹出rip。此时 CPU 跳转到user_entry。
- 执行跳板函数 (
user_entry):
- 从
r12读取 ELF 入口地址。 - 从
r13读取用户栈地址。 - 调用
enter_user_mode(entry, stack)。
- 特权级切换 (
iretq):
-
enter_user_mode手动在栈上构建 中断返回帧 (Interrupt Return Stack Frame): -
SS(User Data Selector, 0x23) -
RSP(用户栈顶) -
RFLAGS(开启 IF 中断位) -
CS(User Code Selector, 0x1B) -
RIP(ELF 入口点) -
执行
iretq。 -
CPU 硬件动作: 从栈中弹出上述值,将
CPL(当前特权级) 从 0 变为 3,跳转到用户代码。
至此,第一个用户进程 init 开始运行,屏幕打印 "Hello, User World!"。
当用户程序调用 print 时:
- 用户态: 执行
int 0x80指令。 - 硬件:
- 检查 IDT 第 128 项。
- 切换到 Ring 0。
- 从 TSS 读取
RSP0,切换到内核栈。 - 压入用户态的 SS, RSP, RFLAGS, CS, RIP。
- 内核态 (
isr_stub->isr_handler):
pushall保存通用寄存器。- 调用 C 函数
syscall_handler。 - 根据
rax执行逻辑(如kprint)。
- 返回:
popall恢复寄存器。iretq返回用户态。
+-------------------------+ 0xFFFFFFFF FFFFFFFF
| |
| Kernel Image | .text, .data (Higher Half)
| |
+-------------------------+ 0xFFFF8000 00000000 (HHDM Start)
| |
| Direct Map (HHDM) | 所有物理内存的直接映射
| |
+-------------------------+
| ... |
+-------------------------+ 0x00007FFF FFFFFFFF (User Space End)
| |
| User Stack | 0x00000000 80000000 (向下生长)
| | |
| v |
| |
| ^ |
| | |
| User Heap | (动态分配)
| |
+-------------------------+
| User Code | 0x00000000 00400000 (ELF Load Addr)
+-------------------------+ 0x00000000 00000000
SudoOS 的架构层代码位于 kernel/src/arch/,专门针对 x86_64 架构实现。该层负责屏蔽底层硬件细节,提供中断管理、特权级切换、上下文保存与恢复等核心机制。
源文件: kernel/src/arch/gdt.c, kernel/src/arch/gdt.S
头文件: kernel/src/arch/gdt.h
虽然 x86_64 弱化了分段机制,但 GDT 仍然是切换特权级(Ring 0 <-> Ring 3)和加载 TSS 的必要条件。
内核定义了以下 5 个关键段描述符(加上 Null 段共 6 个):
- Null Descriptor: 索引 0,必须为全 0。
- Kernel Code (0x08): 64位代码段,DPL=0,用于内核指令执行。
- Kernel Data (0x10): 数据段,DPL=0,用于内核数据访问。
- User Data (0x23): 数据段,DPL=3,用于用户栈和堆。注意选择子低 3 位为
11b(RPL=3)。 - User Code (0x1B): 64位代码段,DPL=3,用于用户程序执行。
- TSS (Task State Segment): 系统段,用于保存中断时的栈指针。
在 x86_64 中,TSS 不再用于保存任务上下文(寄存器等),其唯一的核心用途是保存 RSP0。
- 场景: 当 CPU 处于 Ring 3(用户态)并接收到中断或系统调用时,必须切换到 Ring 0。
- 机制: 硬件会自动从 TR 寄存器指向的 TSS 中读取
RSP0字段,并将栈指针RSP切换到该地址。 - 代码体现:
gdt.c中的write_tss和set_tss_stack函数负责动态更新这个栈地址(通常在进程调度schedule()时更新为下一个进程的内核栈基址)。
源文件: kernel/src/arch/idt.c
头文件: kernel/src/arch/idt.h, kernel/src/arch/trap.h
IDT 定义了 CPU 如何响应异常(Exception)和外部中断(Interrupt)。
SudoOS 将 256 个中断向量划分为三个区域:
- 异常 (0 - 31): CPU 内部错误。
- 例如:
#PF (14)页错误,#GP (13)一般保护性错误,#DF (8)双重错误。
- 例如:
- 外部中断 (32 - 47): 由 PIC (8259A) 芯片重映射而来。
IRQ 0-> 向量 32 (时钟)IRQ 1-> 向量 33 (键盘)
- 系统调用 (128 / 0x80): 用户态陷入内核的软件中断入口。
idt_init() 函数执行以下步骤:
- 重映射 PIC: 将主片偏移设为 32,从片偏移设为 40,防止 IRQ 与 CPU 异常冲突。
- 填充 IDT: 循环设置 256 个门描述符,指向
isr_stub_table中的汇编入口。 - 加载 IDTR: 执行
lidt指令。
汇编入口: kernel/src/arch/isr.S
C 处理函数: kernel/src/arch/interrupts.c
SudoOS 采用“汇编存根 + C 分发器”的两级处理模型。
对于每个中断向量,汇编宏 ISR_NOERRCODE 或 ISR_ERRCODE 会生成一个微型入口:
isr_stub_0:
push 0 ; 压入伪错误码 (为了栈对齐)
push 0 ; 压入中断号
jmp isr_common_stubisr_common_stub 负责保存现场(Context Save):
- 保存寄存器:
pushall宏将r15-r8,rbp,rdi,rsi,rdx,rcx,rbx,rax全部压栈。 - 切换段寄存器: 将
ds,es设为内核数据段 (0x10)。 - 调用 C 函数:
call isr_handler,此时栈顶指针rsp作为参数传递给 C 函数(指向registers_t结构体)。 - 恢复现场: 函数返回后,
popall恢复所有寄存器,最后iretq返回。
isr_handler(registers_t* regs) 是所有中断的统一 C 入口:
- 异常分发: 如果
int_no < 32,打印错误信息(如 Page Fault 地址)并暂停系统 (hcf),或者调用注册的异常处理回调。 - IRQ 分发: 如果
int_no >= 32,调用interrupt_handlers[int_no]中注册的驱动回调(如timer_callback或keyboard_handler)。处理完毕后发送 EOI (End of Interrupt) 信号给 PIC。
源文件: kernel/src/arch/switch.S
这是多任务调度的核心引擎,用于在两个内核线程之间切换执行流。
C 原型:void switch_to(struct context **prev, struct context *next);
由于这是主动切换(Cooperative),编译器生成的代码已经处理了 Caller-saved 寄存器。switch_to 只需要保存 Callee-saved 寄存器:
switch_to:
; 1. 保存当前进程 (prev) 的上下文到它的内核栈
push rbx
push rbp
push r12
push r13
push r14
push r15
; 2. 将当前的栈指针 (rsp) 保存到 prev->context 变量中
; rdi 是第一个参数 (struct context **prev)
mov [rdi], rsp
; 3. 加载下一个进程 (next) 的栈指针
; rsi 是第二个参数 (struct context *next)
mov rsp, rsi
; 4. 从新进程的栈中恢复上下文
pop r15
pop r14
pop r13
pop r12
pop rbp
pop rbx
; 5. 跳转执行
; ret 指令会弹出栈顶的 RIP (即新进程上次调用 switch_to 后的返回地址)
ret源文件: kernel/src/arch/timer.c
头文件: kernel/src/arch/timer.h
- 硬件: 编程 8253/8254 PIT (Programmable Interval Timer) 芯片。
- 配置: 使用模式 3 (方波发生器),频率设置为 100Hz (10ms 一个 Tick)。
- 中断处理: 注册
timer_callback到 IRQ 0。- 该函数不仅增加系统 tick 计数。
- 更重要的是调用
schedule()检查当前进程时间片 (current_proc->time_slice),如果耗尽则触发抢占。
头文件: kernel/src/arch/x86_64.h
提供了 C 语言无法直接表达的汇编指令封装:
- 端口 I/O:
outb,inb,io_wait。 - 控制寄存器:
read_cr2()(读取缺页地址),write_cr3()(切换页表),read_rflags()(读取中断状态)。 - MSR 操作:
rdmsr,wrmsr(用于开启 NX 位或系统调用相关设置)。
SudoOS 目前实现了两个核心驱动:基于 Framebuffer 的图形控制台 (console.c) 和 PS/2 键盘驱动 (keyboard.c)。
位于 kernel/src/drivers/ 目录。
源文件: kernel/src/drivers/console.c
SudoOS 摒弃了传统的 VGA 文本模式 (0xB8000),直接运行在高分辨率图形模式(如 1920x1080, 32位色深)下。这意味着内核必须像画图软件一样,通过修改内存中的像素数据来“画”出每一个字符。
屏幕在操作系统眼中,本质上只是一块巨大的内存区域(Framebuffer)。
- 物理内存映射: Limine 引导协议在启动时会告诉内核这块显存的物理地址 (
g_fb->address)、宽度 (width)、高度 (height) 以及每行占用的字节数 (pitch)。 - 定位像素: 要在坐标
(x, y)画一个点,必须计算它在内存中的线性偏移量。
代码深度解析 (put_pixel_scaled):
在 console.c 中,绘制像素的核心逻辑如下:
// 1. 获取显存基地址 (转换为 uint32_t* 指针,因为每个像素占 4 字节/32位)
uint32_t* fb_addr = (uint32_t*)g_fb->address;
// 2. 计算显存的“跨度” (Pitch)
// Pitch 是显存中一行的总字节数(可能包含硬件对齐填充)。
// 除以 4 是为了将其转换为“以像素为单位”的步长。
uint32_t pitch_pixels = g_fb->pitch / 4;
// 3. 绘制逻辑 (支持 SCALE 放大倍数)
// 我们不仅画一个点,而是画一个 SCALE * SCALE 的矩形块,这样字体看起来更大。
for (int dy = 0; dy < SCALE; dy++) {
// 计算目标行的内存指针:
// 基址 + (起始Y + 偏移Y) * 每行像素数 + 起始X
uint32_t* row_ptr = fb_addr + (start_y + dy) * pitch_pixels + start_x;
for (int dx = 0; dx < SCALE; dx++) {
// 直接写入颜色值 (例如白色 0xFFFFFFFF)
row_ptr[dx] = color;
}
}由于是图形模式,没有硬件帮我们显示 "A" 这个字。我们需要内置一套位图字库 (font.h)。
绘制流程 (draw_char_on_screen):
- 获取字模: 读取字符对应的 8x8 位图数组 (
font8x8_basic[c])。 - 扫描位图: 双重循环遍历这 64 个点。
- 着色:
- 如果某一位是
1: 画前景色 (FG_COLOR, 白色)。 - 如果某一位是
0: 画背景色 (BG_COLOR, 黑色)。这一步至关重要,否则文字会重叠在旧内容上。
- 如果某一位是
驱动维护了一个 history_buffer(字符数组),用于记录最近 100 行的文本内容。
- 逻辑滚动: 当光标
g_cursor_y超过屏幕行数时,并不立即移动显存数据(因为太慢),而是通过g_view_offset控制视图。 - 物理刷新 (
console_refresh): 当需要翻页或发生剧烈变动时,console_refresh会根据当前的history_buffer和g_view_offset重新计算并绘制整个屏幕的每一个字符。这比显存拷贝 (memcpy) 慢,但逻辑更清晰。
源文件: kernel/src/drivers/keyboard.c
SudoOS 使用中断驱动的方式处理键盘输入,避免了 CPU 轮询带来的资源浪费。
- 端口:
0x60(数据端口)。 - 中断:
IRQ 1(映射到 IDT 向量 33)。
每当你按下一个键,键盘控制器就会向 CPU 发送一个中断信号,触发以下流程:
- 读取扫描码:
uint8_t scancode = inb(0x60); // 从 IO 端口直接读取硬件数据
- 特殊键处理:
- 断开码 (Break Code): 如果
scancode & 0x80为真(最高位是1),说明是按键松开(Key Up),通常忽略(除非是 Shift 键松开)。 - Shift 键: 检测到
0x2A/0x36(按下) 设置g_shift = true;检测到0xAA/0xB6(松开) 设置g_shift = false。 - 翻页键: 检测到 PageUp (
0x49),调用console_scroll(10)实现终端向上滚屏查看历史。
- 断开码 (Break Code): 如果
- 字符映射:
使用查找表
kmap_low(无Shift) 或kmap_up(有Shift) 将扫描码转换为 ASCII 字符。例如扫描码0x1E会被转换为字符'a'或'A'。 - 环形缓冲区 (Circular Buffer):
解析出的字符被存入
kbuf。这是一个生产者-消费者模型:中断处理函数是生产者,用户态的read()系统调用是消费者。
源文件: kernel/src/drivers/io.h
x86 架构使用独立的 I/O 地址空间来与外设通信。C 语言本身不支持 in/out 指令,因此必须通过内联汇编封装。
// 向端口 port 写入一个字节 val
// "a"(val) -> 将 val 放入 al 寄存器
// "Nd"(port) -> 将 port 放入 dx 寄存器
static inline void outb(uint16_t port, uint8_t val) {
__asm__ volatile ( "outb %0, %1" : : "a"(val), "Nd"(port) );
}
// 从端口 port 读取一个字节
static inline uint8_t inb(uint16_t port) {
uint8_t ret;
__asm__ volatile ( "inb %1, %0" : "=a"(ret) : "Nd"(port) );
return ret;
}SudoOS 实现了一个简单的内存文件系统 (In-Memory Filesystem),称为 RamFS。它不是基于磁盘的持久化存储,而是利用内核堆内存动态模拟文件和目录结构。
源文件: src/fs/ramfs.c
RamFS 摒弃了复杂的块设备驱动层,直接使用数组存储文件节点(Inode 模拟)。
- 节点存储:
static ramfs_node_t files[MAX_FILES];(最大支持 64 个文件/目录) - 全局打开文件表:
static file_t global_file_table[MAX_SYSTEM_OPEN_FILES];(系统级文件句柄池)
文件节点 (ramfs_node_t):
代表文件系统中的一个实体(文件或目录)。
typedef struct {
char name[32]; // 文件名
int type; // 类型:RAMFS_TYPE_DIR (目录) 或 RAMFS_TYPE_FILE (文件)
int parent_idx; // 父目录索引 (用于支持 .. 跳转)
int inode_idx; // 自身在 files 数组中的索引
uint8_t* content; // 文件内容指针 (指向 kmalloc 分配的内存)
uint64_t size; // 文件大小
bool is_used; // 分配标记
} ramfs_node_t;文件句柄 (file_t):
代表进程打开的一个文件实例,记录读写偏移量。
typedef struct {
ramfs_node_t* node; // 指向底层文件节点
uint64_t offset; // 当前读写指针位置
int ref_count; // 引用计数
} file_t;系统内置了路径解析器 resolve_path,支持绝对路径(/usr/bin)和相对路径(../lib),以及特殊符号 . 和 ..。
内核启动时,ramfs_init 会自动构建以下目录结构并将模拟文件写入内存:
/
├── init (Pid 1 用户进程镜像)
└── usr
├── bin (存放用户可执行程序)
| └── user.elf (实际 Shell 程序)
├── lib (库文件目录)
├── shell.c (Shell 源代码预览)
└── usrmain.c (用户主程序源码预览)
RamFS 实现了类似 POSIX 的标准文件操作接口:
open: 解析路径,分配文件句柄 (fd)。支持O_CREAT标志创建新文件。read / write: 基于内存的拷贝 (memcpy)。写入时若超过预分配大小会自动更新文件 size。getdents64: 读取目录项,返回linux_dirent64结构,供ls命令使用。mkdir: 创建新目录。chdir: 修改进程 PCB 中的cwd_inode,实现工作目录切换。getcwd: 通过回溯parent_idx直到根目录,反向构建当前路径字符串。
SudoOS 提供了一个基于 libc 的用户环境和一个交互式 Shell。
位于 /usr/bin/user.elf,是系统启动后运行的第一个交互程序。
源文件: usr/shell.c
Shell 支持以下常用命令:
| 命令 | 格式 | 描述 |
|---|---|---|
| ls | ls [path] |
列出目录内容,区分目录(带 [])和文件。 |
| cd | cd <path> |
切换当前工作目录。 |
| pwd | pwd |
显示当前工作目录的全路径。 |
| mkdir | mkdir <path> |
创建新目录。 |
| touch | touch <file> |
创建空文件。 |
| cat | cat <file> |
打印文件内容到终端。 |
| echo | echo <text> [> file] |
输出文本,支持简单的重定向写入文件。 |
| clear | clear |
清屏。 |
| run | run <syscall_id> |
内核调试工具:交互式调用任意系统调用。 |
run 命令允许开发者绕过 libc 封装,直接通过汇编指令 int 0x80 触发系统调用,用于测试内核响应。
- 用法示例:
run 0(测试 SYS_READ) -> Shell 会提示输入 fd 和长度。
用户程序通过寄存器传递参数触发软中断。
源文件: usr/lib/syscall.c, usr/lib/syscall.h
- 中断号:
0x80 - ABI:
rax(调用号),rdi,rsi,rdx,r10,r8(参数 1-5)。
| ID | 宏名称 (syscall.h) | 功能 |
|---|---|---|
| 0 | SYS_READ |
从文件描述符读取数据 |
| 1 | SYS_WRITE |
向文件描述符写入数据 |
| 2 | SYS_OPEN |
打开或创建文件 |
| 3 | SYS_CLOSE |
关闭文件描述符 |
| 39 | SYS_GETPID |
获取当前进程 ID |
| 57 | SYS_FORK |
创建子进程 (Copy-on-Write / Deep Copy) |
| 59 | SYS_EXECVE |
执行新程序 |
| 60 | SYS_EXIT |
终止进程 |
| 79 | SYS_GETCWD |
获取当前工作目录 |
| 80 | SYS_CHDIR |
改变工作目录 |
| 83 | SYS_MKDIR |
创建目录 |
| 217 | SYS_GETDENTS64 |
获取目录项列表 |
源文件: src/proc/proc.c (配合 src/lib/elf.h)
内核具备解析 ELF64 格式可执行文件的能力:
- 验证: 检查 ELF Magic Header 确认文件格式。
- 解析: 遍历 Program Headers,寻找
PT_LOAD类型的段。 - 加载: 根据段信息(FileSiz, MemSiz)调用
mm_map_range分配物理页,并映射到用户虚拟地址空间。 - BSS 处理: 如果 MemSiz > FileSiz,将多余部分(BSS)清零。
- 入口跳转: 读取 ELF Header 中的 Entry Point,设置为新进程上下文的
rip。