Proka Kernel
欢迎阅读 Proka Kernel 开发文档。Proka Kernel 是一个为 ProkaOS 设计的基于 Rust 的内核,旨在探索 x86_64 裸机上的内存安全与模块化设计。
Proka Kernel 由 RainSTR Studio 的年轻开发者们共同维护。
愿景与基本理念
略
项目愿景与特性
项目愿景
对于此项目,我们希望实现一个简单、可靠的操作系统内核,同时提供丰富的功能和特性。
作为一名中国人,我们希望这个项目能够为中国的开源社区做出贡献,同时也能够为中国的信息技术发展做出贡献。
项目特性
我们这个项目最先的目标是实现一个简单的操作系统内核,同时提供基本的功能和特性。
将来随着我们技术的不断改善,我们可能会实现自己的文件系统、可执行文件格式、专属于此内核的引导器等,以实现此系统的国产化,推动中国的信息技术发展。
技术目标
快速上手
略
环境配置
核心构建工具
- Rust Toolchain:
nightly版本,目标平台x86_64-unknown-none。 - GCC: 用于编译 C 代码。
- Make: 构建自动化。
- Anaxa Builder: 用于解析相关配置,并编译内核。
模拟与镜像工具
- QEMU: 内核模拟运行。
- xorriso: 创建 ISO 镜像。
- cpio: 创建 initrd 镜像。
编译与运行
编译内核
# 如果是debug(dev)模式的话,直接写make all即可
make all
# 如果是release(prod)模式的话,需要给PROFILE=release。
PROFILE=release make all
创建 ISO 镜像
# 与上同理,debug(dev)模式直接写make iso即可,release(prod)模式需要给PROFILE=release。
# 这里以release模式为例,哪个模式下封装哪个模式的镜像。
PROFILE=release make iso
在 QEMU 中运行
# 为了调试,使用此命令时将直接以debug(dev)模式运行。
make run
Debugging
内核架构
本章节深入探讨 Proka Kernel 的内部实现,从引导协议到各核心系统的协作。
微内核重构路线图 (Roadmap)
Proka 目前正处于从混合内核 (Hybrid) 向微内核 (Microkernel) 演进的中后期阶段。
已完成 (Done)
- 强隔离的多任务模型:基于 TCB/PCB 的进程管理,集成硬件级页表切换。
- 高性能同步原语:实现无分配等待队列、自适应自旋和公平自旋锁 (Ticket Lock)。
- 微内核 IPC 框架:初步实现基于服务发现的消息传递机制。
进行中 (In Progress)
- 系统调用 (Syscall) 硬件集成:配置
IA32_LSTAR实现 Ring 3 到 Ring 0 的快速跳转。 - ELF64 加载器:从文件系统加载用户态可执行程序并建立地址空间。
待处理 (Backlog)
- 服务外迁:将键盘驱动、串口输出等搬移到独立的 Ring 3 进程。
- VFS 服务化:将 VFS 逻辑从内核剥离,通过 IPC 处理文件请求。
- 用户态权限管理 (Capabilities):实现细粒度的资源访问控制。
引导流程
内存管理架构
Proka Kernel 采用分层内存管理架构,支持内核空间和用户空间的内存隔离与管理。
架构概览
┌─────────────────────────────────────────────────────────────┐
│ 用户空间 (User Space) │
│ 0x0000_0000_0000 - 0x0000_7FFF_FFFF_FFFF (128TB) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 栈 (Stack) - 向下增长 │ │
│ │ mmap 区域 (Memory Mappings) │ │
│ │ 堆 (Heap) - 向上增长 │ │
│ │ 程序段 (Text/Data/BSS) │ │
│ └─────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 内核空间 (Kernel Space) │
│ 0xFFFF_8000_0000_0000 - 0xFFFF_FFFF_FFFF_FFFF (128TB) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ HHDM (Higher Half Direct Mapping) │ │
│ │ 内核堆 (Kernel Heap) │ │
│ │ 内核代码/数据 (Kernel Text/Data/BSS) │ │
│ │ ELF 加载区域 (ELF Loading Region) │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
物理内存管理
帧分配器 (Frame Allocator)
使用 Buddy System 算法管理物理内存帧:
- 位置:
kernel/src/memory/frame/buddy.rs - 特点: 支持连续物理内存分配,高效处理内存碎片
- 接口:
allocate_frame(),deallocate_frame()
#![allow(unused)]
fn main() {
// 获取全局帧分配器
let mut frame_allocator = FRAME_ALLOCATOR;
// 分配单个帧
let frame = frame_allocator.allocate_frame()?;
// 释放帧
frame_allocator.deallocate_frame(frame);
}
虚拟内存管理
VmArea 抽象
VmArea (Virtual Memory Area) 表示一段连续的虚拟内存区域:
#![allow(unused)]
fn main() {
pub struct VmArea {
pub start: VirtAddr, // 起始地址
pub end: VirtAddr, // 结束地址(不包含)
pub flags: PageTableFlags, // 页表权限标志
pub area_type: VmAreaType, // 区域类型
}
pub enum VmAreaType {
Text, // 代码段
Rodata, // 只读数据
Data, // 数据段
Heap, // 堆
Stack, // 栈
Mmap, // 内存映射
KernelText, KernelRodata, KernelData, KernelBss, KernelHeap,
}
}
MemorySet
MemorySet 管理一个地址空间的所有 VMA 和页表:
#![allow(unused)]
fn main() {
pub struct MemorySet {
areas: Vec<VmArea>,
page_table: OffsetPageTable<'static>,
}
}
用户空间内存管理
用户空间内存布局
地址范围 用途
────────────────────────────────────────────────
0x0000_0000_0000 - 0x0000_000F_FFFF Null guard (1MB)
0x0000_0010_0000 - ... 程序代码/数据
0x0000_1000_0000 - 0x0000_7F9F_FFFF 用户堆 (向上增长)
0x0000_7FA0_0000 - 0x0000_7FFF_FFFF mmap 区域 (256MB)
0x0000_7FC0_0000 - 0x0000_8000_0000 用户栈 (向下增长, 4MB)
创建用户地址空间
#![allow(unused)]
fn main() {
// 创建新的用户进程地址空间
let memory_set = MemorySet::new_user(&mut frame_allocator)?;
}
内存映射 API
mmap_anon - 匿名内存映射
#![allow(unused)]
fn main() {
/// 在指定地址映射匿名内存
pub fn mmap_anon(
&mut self,
addr: Option<VirtAddr>, // 首选地址(None 表示自动选择)
size: usize, // 映射大小
flags: PageTableFlags, // 权限标志
) -> Result<VirtAddr, MemoryError>
}
munmap - 取消映射
#![allow(unused)]
fn main() {
/// 取消内存映射并释放物理帧
pub fn munmap(
&mut self,
addr: VirtAddr,
size: usize,
) -> Result<(), MemoryError>
}
堆管理
#![allow(unused)]
fn main() {
/// 扩展用户堆
pub fn expand_user_heap(&mut self, new_end: VirtAddr) -> Result<(), MemoryError>
/// 收缩用户堆
pub fn shrink_user_heap(&mut self, new_end: VirtAddr) -> Result<(), MemoryError>
/// 获取当前堆边界
pub fn heap_break(&self) -> VirtAddr
}
内核 ELF 加载区域
为支持内核态动态库加载,预留了专用的 ELF 加载区域:
起始地址: 0xFFFF_FFFF_8100_0000
大小: 1GB
分配方式: Bump 分配器
页表管理
页表标志
常用标志组合:
| 用途 | 标志 |
|---|---|
| 内核代码 | PRESENT |
| 内核数据 | PRESENT | WRITABLE | NO_EXECUTE |
| 用户代码 | PRESENT | USER_ACCESSIBLE |
| 用户数据 | PRESENT | USER_ACCESSIBLE | WRITABLE | NO_EXECUTE |
TLB 刷新
修改页表后需要刷新 TLB:
#![allow(unused)]
fn main() {
x86_64::instructions::tlb::flush_all();
}
错误处理
#![allow(unused)]
fn main() {
pub enum MemoryError {
AreaOverlap, // 区域重叠
AreaNotFound, // 区域未找到
FrameAllocationFailed, // 帧分配失败
MappingFailed, // 映射失败
}
}
相关文件
| 文件 | 功能 |
|---|---|
memory/frame/mod.rs | 帧分配器接口 |
memory/frame/buddy.rs | Buddy System 实现 |
memory/paging/mod.rs | 分页支持 |
memory/paging/vmm.rs | 虚拟内存管理 |
memory/heap.rs | 内核堆管理 |
memory/error.rs | 内存错误类型 |
中断子系统
Gdt Idt
Apic
驱动框架
文件系统架构
Vfs
内核文件系统 (KernFS)
多任务处理
EFI加载器
调度器 (Scheduler)
Proka 内核的调度器负责管理和切换线程,是实现并发和多任务处理的核心组件。我们采用了一种高度模块化和可插拔的设计,允许在运行时或编译时轻松替换调度算法。
设计理念
调度器的核心设计目标是 解耦 和 灵活性。
- 抽象与实现分离:通过
SchedulerTrait 定义调度器的标准行为,将接口与具体的调度算法(如优先级调度、时间片轮转等)完全分开。 - 可插拔性:内核持有一个全局的
Schedulertrait object (Box<dyn Scheduler>)。这意味着可以动态替换调度器实现,为测试、性能分析或特定应用场景下的优化提供了极大的便利。 - 进程与线程分离:明确了进程 (Process) 作为资源(内存、文件描述符)的所有者,而线程 (Thread) 作为被调度的执行单元。
进程切换 vs 线程切换
调度器在 schedule_next 过程中会执行以下逻辑:
- 线程上下文切换:始终执行,保存旧线程寄存器,加载新线程寄存器。
- 地址空间切换:仅在
old_pid != new_pid时执行。此时会向Cr3寄存器写入新进程的vspace(页表基址),实现硬件级的地址空间隔离。
核心抽象
调度器模块围绕以下几个核心抽象构建:
Scheduler Trait
所有调度算法都必须实现 Scheduler trait (kernel/src/process/scheduler.rs),它定义了调度器的标准接口:
init(): 初始化调度器,创建空闲线程 (idle thread)。schedule(): 核心调度函数,决定下一个要运行的线程。create_kernel_thread()/create_user_thread(): 创建新的内核或用户线程。terminate_thread(): 终止一个线程,将其标记为待回收状态。block_ipc()/unblock(): 改变线程状态以支持阻塞操作(如 IPC)。get_thread()/get_thread_mut(): 获取线程控制块 (TCB) 的引用。
SchedulerError
一个统一的错误枚举,覆盖了所有调度操作可能遇到的问题,例如 MaxThreadsReached 或 ThreadNotFound。
进程与线程模型
-
进程 (
ProcessControlBlock, PCB): 定义在kernel/src/process/process.rs。它是资源管理的单位,拥有:- 独立的虚拟内存空间 (
MemorySet)。 - 文件描述符表 (
fds)。 - 当前工作目录 (
cwd)。 - 一个或多个线程。
- 独立的虚拟内存空间 (
-
线程 (
ThreadControlBlock, TCB): 定义在kernel/src/process/thread.rs。它是CPU调度的基本单位,包含:- 唯一的线程ID (
Tid) 和所属进程的ID (Pid)。 - 线程状态(如
Running,Runnable,BlockedIpc)。 - 寄存器上下文 (
Context),用于在切换时保存和恢复状态。 - 内核栈。
- 唯一的线程ID (
具体实现
Proka 内核目前内置了两种调度器实现,位于 kernel/src/process/schedulers/ 目录下。
1. 优先级调度器 (PriorityScheduler)
这是内核默认的调度器,实现了基于优先级的抢占式调度。
- 工作原理: 调度器维护一个由 256 个队列组成的
PriorityQueue,每个队列对应一个优先级(0-255,0为最高)。调度时,总会从最高优先级的非空队列中选择下一个线程来执行。 - 公平性: 为了防止低优先级线程“饿死“ (starvation),
PriorityQueue的出队逻辑包含一个简单的公平性机制:每隔一定次数的调度,会尝试从较低优先级的队列中选择线程。 - 适用场景: 适用于需要区分任务重要性的场景,确保高优先级任务(如驱动程序、中断处理)能够及时响应。
2. 时间片轮转调度器 (RoundRobinScheduler)
这是一个更简单的、非优先级的调度器实现。
- 工作原理: 所有可运行的线程被放在一个先进先出 (FIFO) 队列中。每次时钟中断 (
timer_tick) 触发调度时,当前线程被移到队尾,队列头部的线程成为下一个运行的线程。 - 适用场景: 作为可插拔调度器架构的简单示例,适用于所有任务优先级相同的场景。
集成与使用
- 初始化: 内核在
kernel_main函数中通过调用scheduler::init()来初始化默认的PriorityScheduler。可以通过scheduler::set_scheduler()函数来替换为其他调度器。 - 时钟中断 (
timer_tick): APIC Timer 的中断处理程序会调用scheduler::timer_tick(),这会触发一次抢占式调度,确保没有线程可以无限期地占用CPU。 - 主动让出 (
yield_thread): 线程可以主动调用scheduler::yield_thread()来放弃CPU,触发一次调度,让其他线程运行。 - 阻塞与唤醒: 当线程需要等待资源(例如,等待一个 IPC 消息)时,调度器会将其状态设置为
Blocked并从就绪队列中移除。当资源可用时,其他代码(如 IPC 子系统)会调用scheduler::unblock()来唤醒该线程,使其重新进入_就绪队列。
进程管理 (Process Management)
Proka 内核的进程管理子系统负责管理进程的生命周期、资源分配和父子关系。进程是资源(内存、文件描述符)的所有者,而线程是 CPU 调度的基本单位。
设计理念
- 进程-线程分离:进程拥有资源,线程执行代码
- 层级关系:进程之间存在父子关系,形成进程树
- 资源管理:进程负责管理其资源(内存空间、文件描述符)
- 生命周期管理:进程创建、执行、终止、回收的完整生命周期
核心概念:进程 vs 线程
在 Proka 的微内核演进路线中,分清进程与线程的边界至关重要:
| 特征 | 进程 (Process) | 线程 (Thread) |
|---|---|---|
| 本质 | 资源分配单位(容器) | 执行调度单位(打工者) |
| 内存隔离 | 每个进程有独立页表 (vspace),地址空间硬隔离 | 同一进程内的线程共享页表 |
| 切换开销 | 高(需切换 Cr3 寄存器并刷新 TLB) | 低(仅需保存/恢复通用寄存器) |
| 数据结构 | ProcessControlBlock (PCB) | ThreadControlBlock (TCB) |
进程 (Process):“房产证与账本”
进程定义在 kernel/src/process/process.rs。它不直接跑代码,而是负责“占有”资源。
- 地址空间:通过
MemorySet持有独立的四级页表(PML4)。 - 资源清单:记录打开的文件描述符 (
fds)、工作目录 (cwd) 和子进程。
线程 (Thread):“具体的打工实体”
线程定义在 kernel/src/process/thread.rs。它是真正占用 CPU 时间的代码实体。
- 执行状态:持有 CPU 寄存器上下文 (
Context) 和执行栈。 - 调度状态:处于
Running,Runnable或BlockedSync等状态。
核心数据结构
ProcessControlBlock (PCB)
进程控制块定义在 kernel/src/process/process.rs:
#![allow(unused)]
fn main() {
pub struct ProcessControlBlock {
pub pid: Pid, // 进程 ID
pub ppid: Pid, // 父进程 ID
pub status: ProcessStatus, // 进程状态
pub exit_code: Option<i32>, // 退出码
pub memory_set: Arc<Mutex<MemorySet>>, // 地址空间
pub fds: Arc<Mutex<Vec<Option<Arc<File>>>>>, // 文件描述符表
pub cwd: String, // 当前工作目录
pub threads: Vec<Tid>, // 线程列表
pub main_thread_tid: Option<Tid>, // 主线程 ID
pub children: Vec<Pid>, // 子进程列表
pub signal_mask: u64, // 信号掩码
}
}
ProcessStatus
进程状态枚举:
- Ready: 进程已创建,等待第一个线程运行
- Running: 至少有一个线程正在运行
- Blocked: 进程被阻塞(如等待子进程)
- Zombie: 进程已终止,等待父进程回收
进程生命周期
1. 进程创建
#![allow(unused)]
fn main() {
// 创建新进程
pub fn create_process(&mut self, ppid: Pid, memory_set: MemorySet) -> Result<Pid, ()>
}
创建流程:
- 分配新的 PID
- 创建 PCB 并初始化资源
- 将新进程添加到父进程的 children 列表
- 返回新进程的 PID
2. 进程执行
进程本身不执行代码,而是通过其线程执行:
#![allow(unused)]
fn main() {
// 创建用户线程(属于某个进程)
pub fn create_user_thread(
&mut self,
pid: Pid, // 所属进程
entry_point: usize,
user_stack_top: usize,
priority: u8,
name: Option<&str>,
) -> Result<Tid, SchedulerError>
}
3. 进程终止
当进程的最后一个线程终止时,进程变为 Zombie 状态:
#![allow(unused)]
fn main() {
// 调度器在线程终止时调用
fn terminate_thread(&mut self, tid: Tid) -> Result<(), SchedulerError> {
// ... 终止线程 ...
// 通知进程管理器
if pcb.remove_thread(tid) {
// 最后一个线程退出,进程变为 Zombie
pcb.status = ProcessStatus::Zombie;
pcb.exit_code = Some(0);
}
}
}
4. 进程回收
父进程通过 waitpid 系统调用回收子进程:
#![allow(unused)]
fn main() {
pub fn wait_child(&mut self, pid: Pid, target_pid: Option<Pid>) -> Option<(Pid, i32)>
}
资源管理
文件描述符管理
#![allow(unused)]
fn main() {
// 分配新的文件描述符
pub fn alloc_fd(&mut self) -> Option<Fd>
// 打开文件
pub fn open_file(&mut self, file: Arc<File>) -> Option<Fd>
// 获取文件
pub fn get_file(&self, fd: Fd) -> Option<Arc<File>>
// 关闭文件描述符
pub fn close_fd(&mut self, fd: Fd) -> bool
// 复制文件描述符
pub fn dup_fd(&mut self, old_fd: Fd) -> Option<Fd>
// 复制到指定位置
pub fn dup2_fd(&mut self, old_fd: Fd, new_fd: Fd) -> Option<Fd>
}
内存管理
进程的地址空间通过 MemorySet 管理:
#![allow(unused)]
fn main() {
pub struct ProcessControlBlock {
pub memory_set: Arc<Mutex<MemorySet>>,
// ...
}
}
MemorySet 包含:
- 页表(Page Table)
- 虚拟内存区域列表(VMA)
- 堆区域管理
进程间关系
父子关系
- 每个进程(除了 init)都有一个父进程
- 父进程可以通过
waitpid等待子进程终止 - 子进程终止时,父进程会收到通知(通过 zombie queue)
进程树
PID 0 (kernel)
├── PID 1 (init)
│ ├── PID 2 (shell)
│ │ ├── PID 3 (ls)
│ │ └── PID 4 (cat)
│ └── PID 5 (daemon)
└── PID 6 (kthread)
系统调用
进程相关
| 系统调用 | 功能 | 参数 |
|---|---|---|
sys_exit | 退出当前进程 | exit_code: i32 |
sys_getpid | 获取当前进程 ID | - |
sys_getppid | 获取父进程 ID | - |
sys_waitpid | 等待子进程终止 | pid: Pid, exit_code_ptr: *mut i32 |
sys_fork | 创建子进程(未实现) | - |
sys_execve | 执行新程序(未实现) | path, argv, envp |
线程相关
| 系统调用 | 功能 | 参数 |
|---|---|---|
sys_gettid | 获取当前线程 ID | - |
sys_create_thread | 创建新线程 | entry_point, stack_top, priority |
sys_exit_thread | 退出当前线程 | - |
sys_yield | 让出 CPU | - |
文件描述符相关
| 系统调用 | 功能 | 参数 |
|---|---|---|
sys_open | 打开文件(未实现) | path, flags |
sys_close | 关闭文件描述符 | fd: Fd |
sys_dup | 复制文件描述符 | old_fd: Fd |
sys_dup2 | 复制到指定位置 | old_fd, new_fd: Fd |
目录相关
| 系统调用 | 功能 | 参数 |
|---|---|---|
sys_getcwd | 获取当前工作目录 | buf, size |
sys_chdir | 改变当前工作目录 | path |
实现细节
内核进程 (PID 0)
- 特殊的内核进程,所有内核线程都属于它
- 拥有内核地址空间
- 在系统启动时创建
Zombie 进程处理
当进程终止时:
- 进程状态变为 Zombie
- 退出码被保存
- 进程被添加到 zombie queue
- 父进程通过
waitpid回收
如果父进程先终止:
- 子进程被重新分配给 init 进程(PID 1)
- init 进程负责回收这些孤儿进程
地址空间切换
当调度器切换线程时,如果新旧线程属于不同进程,需要切换地址空间:
#![allow(unused)]
fn main() {
if old_pid != new_pid {
// Switch address space
let new_pcb = process::lock().get_process(new_pid).unwrap();
let pml4_addr = new_pcb.memory_set.lock().page_table.level_4_table().addr();
unsafe {
x86_64::registers::control::Cr3::write(
PhysFrame::from_start_address_unchecked(pml4_addr),
Cr3Flags::empty(),
);
}
}
}
待完善内容
-
fork系统调用实现 -
execve系统调用实现 - 信号机制
- 进程组与会话
- 资源限制 (rlimit)
- 进程间通信 (IPC) 机制
IPC 服务架构
Proka Kernel 采用微内核架构,所有系统操作通过 IPC(进程间通信)消息传递完成。
架构概览
┌─────────────────────────────────────────────────────────────┐
│ 用户空间 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────────┐ │
│ │ 用户程序 │ │ 用户程序 │ │ 文件服务 │ │ 设备服务 │ │
│ │ │ │ │ │ (fs:/) │ │ (dev:/) │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └───────┬─────────┘ │
└───────┼────────────┼────────────┼───────────────┼───────────┘
│ syscall │ │ IPC 消息 │ IPC 消息
└─────┬──────┴────────────┴───────┬───────┘
│ │
v v
┌─────────────────────────────────────────────────────────────┐
│ 内核空间 │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ IPC 调度器 (service::dispatch) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────┼─────────────────┐ │
│ v v v │
│ ┌────────────┐ ┌────────────┐ ┌────────────┐ │
│ │ proc:/ │ │ mem:/ │ │ console:/ │ │
│ │ 进程服务 │ │ 内存服务 │ │ 控制台服务 │ │
│ │ (内核态) │ │ (内核态) │ │ (内核态) │ │
│ └────────────┘ └────────────┘ └────────────┘ │
│ │
│ 注: fs:/ 和 dev:/ 可迁移到用户态成为用户空间服务 │
└─────────────────────────────────────────────────────────────┘
微内核设计原则
内核态 vs 用户态服务
| 服务 | 名称 | 当前位置 | 可迁移至用户态 |
|---|---|---|---|
| ProcessService | proc:/ | 内核态 | ⚠️ 部分功能(需要调度器支持) |
| MemoryService | mem:/ | 内核态 | ⚠️ 部分功能(需要页表操作) |
| ConsoleService | console:/ | 内核态 | ✅ 可迁移 |
| FilesystemService | fs:/ | 未实现 | ✅ 用户态设计 |
| DeviceService | dev:/ | 未实现 | ✅ 用户态设计 |
为什么某些服务保留在内核态?
- MemoryService: 需要直接操作页表,修改地址空间映射
- ProcessService: 需要操作调度器数据结构
迁移路径
阶段 1 (当前): 所有服务在内核态
↓
阶段 2: Console/FS/Device 迁移到用户态
↓
阶段 3: 仅保留最小内核 (IPC + 调度 + 基础内存)
命名服务注册
服务名称格式
服务通过 URI 风格的名称注册:
<type>://
| 服务名 | ServiceId | 说明 |
|---|---|---|
proc:/ | 0 | 进程管理服务 |
mem:/ | 1 | 内存管理服务 |
console:/ | 2 | 控制台服务 |
fs:/ | 3 | 文件系统服务 |
dev:/ | 4 | 设备管理服务 |
注册内核态服务
内核服务在 service::init() 中自动注册:
#![allow(unused)]
fn main() {
// kernel/src/service/mod.rs
pub fn init() {
register_kernel_service_locked(&mut registry, Box::new(ProcessService::new()));
register_kernel_service_locked(&mut registry, Box::new(MemoryService::new()));
register_kernel_service_locked(&mut registry, Box::new(ConsoleService::new()));
}
}
注册用户态服务
用户态服务进程启动后注册自己:
// 用户态服务进程
fn main() {
// 创建消息队列
ipc::create_queue(my_tid);
// 注册服务名
service::register_user_service("fs:/", my_tid).unwrap();
// 服务循环
loop {
let msg = ipc::recv(None, None).unwrap();
handle_request(msg);
}
}
服务发现
通过名称查找服务:
#![allow(unused)]
fn main() {
// 查找服务
let service_id = service::lookup_service("fs:/"); // 返回 ServiceId
// 或通过 IPC 模块查找
let tid = ipc::lookup_service("fs:/"); // 返回 TID
// 检查是否为内核服务
if ipc::is_kernel_service(tid) {
// 使用 service::dispatch 路由到内核服务
} else {
// 直接发送 IPC 消息到用户态服务
}
}
IPC 调用约定
寄存器传递
| 寄存器 | 用途 |
|---|---|
| RAX | 固定为 0(IPC_CALL) |
| RDI | 服务 ID |
| RSI | 载荷指针 |
| RDX | 保留 (0) |
| R10 | 消息类型 |
| R8 | 载荷指针 (备用) |
| R9 | 载荷大小 |
| RAX | 返回值 |
返回值
- 成功: 返回值在 RAX 中
- 失败: RAX 最高位为 1,低 63 位为错误码
if (result >> 63) {
// 错误: error_code = result & 0x7FFFFFFFFFFFFFFF
} else {
// 成功: retval = result
}
服务定义
服务 ID
#![allow(unused)]
fn main() {
pub enum ServiceId {
Process = 0, // 进程管理
Memory = 1, // 内存管理
Console = 2, // 控制台 I/O
FileSystem = 3, // 文件系统
Device = 4, // 设备管理
}
}
消息格式
#![allow(unused)]
fn main() {
pub struct IpcRequestHeader {
pub service: u16, // 目标服务 ID
pub msg_type: u16, // 消息类型
pub flags: u32, // 标志 (保留)
pub payload_size: u64, // 载荷大小
}
pub struct IpcResponseHeader {
pub status: i64, // 状态码 (0=成功)
pub retval: u64, // 返回值
pub payload_size: u64, // 响应载荷大小
}
}
ProcessService
服务名: proc:/, 服务 ID: 0
消息类型
| 类型 | 值 | 说明 |
|---|---|---|
| Exit | 0 | 退出当前进程 |
| GetPid | 1 | 获取当前进程 ID |
| Spawn | 2 | 创建新进程 (未实现) |
| Wait | 3 | 等待子进程 (未实现) |
Exit
// 载荷: 4 字节退出码 (小端序)
uint8_t payload[4] = { code & 0xFF, (code >> 8) & 0xFF, ... };
ipc_call(SERVICE_PROCESS, PROCESS_EXIT, payload, 4);
GetPid
uint64_t pid = ipc_call(SERVICE_PROCESS, PROCESS_GETPID, NULL, 0);
MemoryService
服务名: mem:/, 服务 ID: 1
消息类型
| 类型 | 值 | 说明 |
|---|---|---|
| Mmap | 0 | 映射内存 |
| Munmap | 1 | 取消映射 |
| Brk | 2 | 调整堆边界 |
Mmap
// 载荷: 32 字节
// [0-7] addr (u64)
// [8-15] size (u64)
// [16-23] prot (u64)
// [24-31] flags (u64)
void *mem = (void *)ipc_call(SERVICE_MEMORY, MEMORY_MMAP, payload, 32);
保护标志:
PROT_READ = 0x1PROT_WRITE = 0x2PROT_EXEC = 0x4
映射标志:
MAP_PRIVATE = 0x02MAP_ANONYMOUS = 0x20
Munmap
// 载荷: 16 字节
// [0-7] addr (u64)
// [8-15] size (u64)
ipc_call(SERVICE_MEMORY, MEMORY_MUNMAP, payload, 16);
Brk
// 载荷: 8 字节
// [0-7] new_brk (u64)
uint64_t brk = ipc_call(SERVICE_MEMORY, MEMORY_BRK, payload, 8);
ConsoleService
服务名: console:/, 服务 ID: 2
消息类型
| 类型 | 值 | 说明 |
|---|---|---|
| Putc | 0 | 输出字符 |
| Getc | 1 | 输入字符 (未实现) |
| Write | 2 | 输出字符串 |
| Read | 3 | 读取字符串 (未实现) |
Putc
uint8_t payload[1] = { (uint8_t)c };
ipc_call(SERVICE_CONSOLE, CONSOLE_PUTC, payload, 1);
Write
ipc_call(SERVICE_CONSOLE, CONSOLE_WRITE, str, strlen(str));
错误码
#![allow(unused)]
fn main() {
pub mod error {
pub const EINVAL: i64 = 22; // 无效参数
pub const ENOSYS: i64 = 38; // 功能未实现
pub const ESRCH: i64 = 3; // 进程不存在
pub const ENOMEM: i64 = 12; // 内存不足
pub const ESRV: i64 = 100; // 无效服务
pub const ESRVUNAVAIL: i64 = 101; // 服务不可用
}
}
示例
#include "ipc_test.c"
void _start(void) {
// 输出字符串
console_write("Hello, World!\n");
// 获取 PID
uint64_t pid = proc_getpid();
// 分配内存
void *mem = mem_alloc(4096);
// 退出
proc_exit(0);
}
相关文件
| 文件 | 功能 |
|---|---|
syscall/mod.rs | IPC 调用入口 |
service/mod.rs | 服务注册和分发 |
service/types.rs | IPC 类型定义 |
service/process.rs | 进程服务 |
service/memory.rs | 内存服务 |
service/console.rs | 控制台服务 |
ipc/mod.rs | IPC 消息传递、服务注册 |
ELF 加载器
为了运行用户态程序和加载内核模块,内核需要能够解析并加载 ELF64 格式的可执行文件和共享库。
概述
Proka Kernel 使用 elf_loader crate 实现 ELF 加载功能,并通过自定义的 KernelMmap trait 实现内核态内存映射。
处理步骤
- 头部解析:验证魔数 (
0x7F 'E' 'L' 'F') 和架构信息。 - 段映射 (Segments):遍历程序头表 (Program Header Table),将
PT_LOAD段映射到地址空间。 - 重定位:处理重定位表,修正符号地址。
- 符号解析:解析动态符号表,查找符号地址。
KernelMmap 实现
KernelMmap 实现了 elf_loader::os::Mmap trait,提供内核态内存映射支持:
内存分配区域
ELF 加载使用内核空间专用区域:
起始地址: 0xFFFF_FFFF_8100_0000
大小: 1GB
分配方式: Bump 分配器
接口实现
#![allow(unused)]
fn main() {
impl Mmap for KernelMmap {
/// 通用内存映射
unsafe fn mmap(
addr: Option<usize>,
len: usize,
prot: ProtFlags,
flags: MapFlags,
offset: usize,
fd: Option<isize>,
need_copy: &mut bool,
) -> Result<*mut c_void>;
/// 匿名内存映射
unsafe fn mmap_anonymous(
addr: usize,
len: usize,
prot: ProtFlags,
flags: MapFlags,
) -> Result<*mut c_void>;
/// 取消映射
unsafe fn munmap(addr: *mut c_void, len: usize) -> Result<()>;
/// 修改内存保护属性
unsafe fn mprotect(
addr: *mut c_void,
len: usize,
prot: ProtFlags,
) -> Result<()>;
/// 预留地址空间
unsafe fn mmap_reserve(
addr: Option<usize>,
len: usize,
use_file: bool,
) -> Result<*mut c_void>;
}
}
权限标志转换
#![allow(unused)]
fn main() {
fn prot_to_flags(prot: ProtFlags) -> PageTableFlags {
let mut flags = PageTableFlags::PRESENT;
if prot.contains(ProtFlags::PROT_WRITE) {
flags |= PageTableFlags::WRITABLE;
}
if !prot.contains(ProtFlags::PROT_EXEC) {
flags |= PageTableFlags::NO_EXECUTE;
}
flags
}
}
使用示例
加载共享库
#![allow(unused)]
fn main() {
use crate::libs::elf::{load_elf, test_load_elf};
// 加载动态库
let lib = load_elf("mylib.so")?;
// 获取符号
let add_func = unsafe { lib.get::<fn(i32, i32) -> i32>("add")? };
// 调用函数
let result = add_func(1, 2); // 返回 3
}
编译共享库
使用 GCC 编译适用于内核加载的共享库:
# 最小化编译(不链接 libc)
gcc -shared -fPIC -nostdlib -o mylib.so mylib.c
# 示例源码
cat > mylib.c << 'EOF'
int add(int a, int b) {
return a + b;
}
EOF
加载流程详解
1. 文件读取
#![allow(unused)]
fn main() {
let f = VFS.open(path)?;
let file_data = f.read_all()?;
let data = file_data.as_slice();
}
2. 创建加载器
#![allow(unused)]
fn main() {
let mut loader = Loader::new().with_mmap::<KernelMmap>();
}
3. 加载和重定位
#![allow(unused)]
fn main() {
let lib = loader
.load_dylib(ElfBinary::new(path, data))?
.relocator()
.relocate()?;
}
内核内存集访问
ELF 加载器需要访问内核页表进行映射。由于进程管理器初始化时会获取内核内存集,加载器通过以下方式访问:
#![allow(unused)]
fn main() {
// 优先通过进程管理器获取内核进程的内存集
if let Some(pcb) = process::process::lock().get_process(0) {
let pcb_lock = pcb.lock();
let mut ms = pcb_lock.memory_set.lock();
// 使用页表进行映射...
}
}
注意事项
- 地址空间:确保使用内核空间的 canonical 地址(
0xFFFF_XXXX_XXXX_XXXX) - TLB 刷新:映射完成后必须刷新 TLB
- 内存对齐:所有映射地址必须页对齐(4KB)
- 错误处理:正确处理内存分配失败的情况
相关文件
| 文件 | 功能 |
|---|---|
libs/elf.rs | ELF 加载器和 KernelMmap 实现 |
memory/paging/vmm.rs | 虚拟内存管理 |
fs/vfs/mod.rs | 文件系统接口 |
待完善内容
- 文件映射支持(非匿名映射)
- 动态链接器 (Interpreters) 支持
- 辅助向量 (Auxiliary Vector) 传递
- 符号版本控制
内核安全与异常处理
确保内核在面对异常情况时能够安全地崩溃 (Panic) 或优雅地恢复。
异常捕获
- CPU Exceptions:处理除零、缺页 (Page Fault)、通用保护异常等。
- Stack Guards:利用分页机制在内核栈底设置不可访问页。
Panic 处理
- Stack Trace:通过解析符号表或 DWARF 数据生成堆栈回溯。
- Dump 状态:将 CPU 寄存器和关键内存信息输出到串口/屏幕。
待完善内容
- 针对 Rust
panic!宏的内核自定义实现。 - 多核环境下的 Panic 广播(停止所有核心)。
异常处理 (Panic System)
Proka Kernel 采用了一套健壮的异常处理机制,旨在系统发生不可恢复错误时,提供清晰的诊断信息并保护硬件状态。
核心设计
当内核触发 panic! 或发生未捕获的 CPU 异常(如 Page Fault)时,系统会进入 Panic 流程。
1. 状态锁定与现场捕获
在进入 Panic 处理函数的第一时间,内核会执行以下操作:
- 禁用中断:通过
cli指令防止嵌套中断干扰。 - 寄存器快照:捕获触发 Panic 瞬间的通用寄存器(RAX, RBX, RCX…)和关键控制寄存器(RIP, RFLAGS, RSP, RBP)。
- 异常信息存储:如果是 CPU 中断引起的 Panic,中断处理程序会将异常类型和错误码写入全局的
EXCEPTION_INFO锁中。
2. 栈回溯 (Stack Backtrace)
为了帮助开发者定位错误,内核实现了基于帧指针(Frame Pointer)的调用栈回溯。
- 实现机制:通过在编译选项中强制开启帧指针 (
-C force-frame-pointers=yes),使编译器在每个函数起始处维护RBP链。 - 回溯逻辑:
- 捕获当前
RBP。 - 跳过第一层(Panic 处理器自身)。
- 循环迭代:读取
[RBP + 8]作为返回地址,读取[RBP]作为上一层帧指针。
- 捕获当前
- 安全性:
- 限制最大深度为 16 层。
- 验证地址是否处于内核高半区内存空间。
- 检查地址对齐及链的增长方向,防止死循环。
输出界面
Proka Kernel 提供双重 Panic 信息输出:
图形化蓝屏 (BSoD)
如果帧缓冲(Framebuffer)可用,内核会切换到专用的 Panic 控制台(功能简陋),并在屏幕上绘制:
- 错误原因:Panic 宏提供的字符串消息。
- 源代码位置:触发错误的文件名 and 行号。(依赖于Rust的core,可能不是真正的源码位置)
- 系统状态:开机时长、关键寄存器状态。
- 通用寄存器:十六进制显示的寄存器列表。
- 调用栈:回溯得到的地址列表。
串口输出
所有 Panic 信息也会同步发送到串口 (Serial Port),方便通过开发机日志捕捉。
调试建议
当看到栈回溯地址时,可以使用 addr2line 工具配合内核 ELF 文件进行解析:
# 将 0xffffffff80001234 替换为实际的回溯地址
addr2line -e output/kernel 0xffffffff80001234
测试模式下的 Panic
在单元测试模式下,Panic 处理器会重定向到测试框架。它会打印错误信息并执行 long_jmp 跳回测试调度器,从而允许测试套件继续运行后续测试项,而不是直接挂起整个系统。
具体组件与硬件实现
本章节详细介绍 Proka Kernel 中具体硬件设备的驱动实现与组件细节。
目录
人机交互设备
Input
时间与计时器
Graphics
日志与控制台
Proka Kernel 提供了一个统一的日志系统,支持多种输出后端,包括 VGA 文本模式、帧缓冲区以及串口。
架构
- Logger 核心:基于 Rust 的
logfacade 实现。 - Console Trait:定义了底层的字符/字符串输出接口。
- 输出驱动:
- VGA Text Mode (Legacy)
- Early Serial (COM1)
- FB Console (基于图形帧缓冲区)
使用示例
#![allow(unused)]
fn main() {
log::info!("Kernel initialized successfully");
log::warn!("Low memory detected: {} KB left", free_kb);
}
待完善内容
- 滚动缓冲区支持。
- 不同的日志级别颜色显示。
- 运行时动态切换输出后端。
同步原语 (Synchronization)
Proka 内核提供了一套高性能的同步原语,专为微内核架构下的高频 IPC 和多核竞争场景优化。
设计目标
- 去内存分配化:同步对象在运行过程中不触发任何堆内存分配(如
Vec)。 - 高性能 IPC:支持自适应自旋,降低短时间锁竞争的上下文切换开销。
- 多核公平性:底层自旋锁采用 Ticket Lock 算法,确保 CPU 核心间的公平竞争。
核心组件
1. Mutex (优先级继承互斥锁)
用于保护独占资源。
- 快速路径 (Fast-path):在无竞争情况下,通过一次原子操作即可完成加锁。
- 自适应自旋 (Adaptive Spinning):在进入阻塞状态前,线程会先尝试短时间自旋。如果锁被很快释放,则避免了昂贵的调度开销。
- 优先级继承 (Priority Inheritance):自动提升持有锁的低优先级线程,防止优先级反转。
- 无分配等待队列:利用调度器的
block_sync机制,将线程阻塞在对象地址上,彻底移除Vec<Tid>。
2. RwLock (读写锁)
支持多个读者或单个写者。
- 写者优先 (Writer-Preferring):引入
WRITE_PENDING标志位。当有写者等待时,新的读者将被强制阻塞,防止写者饥饿。 - 公平性:写者在释放锁后,会通过
unblock_sync唤醒所有等待者。
3. SpinLock (公平自旋锁)
用于内核底层、中断上下文等无法阻塞的场景。
- Ticket Lock 实现:
- 每个加锁请求都会领取一个序号。
- 只有当前序号与服务序号匹配时才能获取锁。
- 优点:严格保证了 FIFO 顺序,彻底解决了传统自旋锁导致的 CPU 核心饥饿问题。
4. Semaphore & Condvar
- Semaphore:计数信号量,用于限流或资源计数。
- Condvar:条件变量,允许线程在特定条件满足前挂起。
- 实现原理:均已重构为基于调度器同步 ID 的机制。
调度器集成
同步原语通过以下接口与调度器深度绑定:
#![allow(unused)]
fn main() {
// 阻塞在特定同步 ID 上(通常是对象的内存地址)
scheduler::block_sync(sync_id: u64);
// 唤醒所有阻塞在特定同步 ID 上的线程
scheduler::unblock_sync(sync_id: u64);
}
这种设计使得同步原语本身变得非常“薄”,核心逻辑(如等待者调度)完全由调度器中心化管理。
贡献指引
Testing
代码规范与约定
本文档定义了 Proka Kernel 项目的编码规范、命名约定和代码风格。所有贡献者都应遵守这些约定以保持代码库的一致性和可维护性。
代码格式
所有 Rust 代码必须遵循 Rustfmt 标准格式:
make fmt
具体规则:
- 4 个空格缩进(无制表符)
- 行宽限制 100 个字符
- 使用
rustfmt默认配置 - 导入按标准库、外部库、内部模块分组排序
命名约定
遵循 Rust 社区标准:
| 项目 | 约定 | 示例 |
|---|---|---|
| 模块 | 蛇形命名法(snake_case) | interrupts, memory_management |
| 结构体 | 大驼峰命名法(PascalCase) | FrameAllocator, InterruptDescriptorTable |
| 枚举 | 大驼峰命名法(PascalCase) | MemoryError, DriverType |
| 函数 | 蛇形命名法(snake_case) | allocate_frame, handle_interrupt |
| 变量 | 蛇形命名法(snake_case) | frame_count, current_process |
| 常量 | 全大写蛇形命名法(SCREAMING_SNAKE_CASE) | BASE_REVISION, PAGE_SIZE |
| 类型参数 | 大驼峰命名法(PascalCase) | T, E, P, Buf |
模块组织
kernel/src/
├── lib.rs # 公共 API 导出
├── main.rs # 内核入口点
└── module_name/
├── mod.rs # 模块声明和内部重导出
├── submodule1.rs
└── submodule2.rs
规则:
- 每个目录必须有
mod.rs文件 - 使用
pub(crate)限制内部可见性 - 仅在
lib.rs中公开必要的 API - 避免模块间的循环依赖
注释与文档
对于注释,我们建议使用英文编写,以确保项目的国际化,使全世界的开发者都能够理解和维护代码。
单行注释:
#![allow(unused)]
fn main() {
// Use the simple comment to explain the complex logic
let frame = allocator.allocate()?; // If allocation fails, return an error
}
多行注释:
#![allow(unused)]
fn main() {
/*
* Complex algorithm description
* Second line description
*/
}
文档注释:
#![allow(unused)]
fn main() {
/// Allocate a physical frame
///
/// # Arguments
/// - `allocator`: The frame allocator instance
/// - `count`: The number of frames to allocate
///
/// # Returns
/// Returns the address of the allocated frame, or an error
///
/// # Safety Requirements
/// The caller must ensure that the frame allocator is initialized
///
/// # Examples
/// ```
/// let frame = allocate_frames(&mut allocator, 1)?;
/// ```
pub fn allocate_frames(allocator: &mut FrameAllocator, count: usize) -> Result<FrameAddress, MemoryError> {
// ...
}
}
内联汇编注释:
#![allow(unused)]
fn main() {
// SAFETY: Must ensure that the page table address is valid and properly aligned
unsafe {
asm!("mov cr3, {}", in(reg) page_table_addr);
}
}
内存安全与 unsafe 代码
安全第一原则:
- 优先编写安全的 Rust 代码
unsafe块必须最小化并有充分理由
unsafe 要求:
- 每个
unsafe块必须有// SAFETY:注释 - 说明为什么是安全的以及调用者需要满足的条件
- 验证所有不变量和前置条件
示例:
#![allow(unused)]
fn main() {
/// Set the current page table
///
/// # Arguments
/// - `page_table_addr`: The address of the page table to set as the current page table
///
/// # Safety Requirements
/// - `page_table_addr` must point to a valid page table
/// - The page table must have the correct permission bits set
/// - The caller must ensure that no invalid memory will be accessed after this function returns
pub unsafe fn set_page_table(page_table_addr: usize) {
// SAFETY: The caller must ensure that `page_table_addr` points to a valid page table structure,
// and that no invalid memory will be accessed after this function returns.
asm!("mov cr3, {}", in(reg) page_table_addr);
}
}
全局状态:
- 使用
Mutex、RwLock或Atomic保护共享状态 - 避免裸的全局变量
- 使用
lazy_static或once_cell进行初始化
贡献与提交约定
Git 提交规范
遵循 Conventional Commits 规范
代码审查标准
审查要点:
- 代码是否符合本规范
- 是否有充分的测试覆盖
- 文档是否完整
- 性能影响是否评估
- 错误处理是否恰当
- 安全考虑是否充分
审查流程:
- 创建 Pull Request
- 至少需要一名核心维护者审查
- 所有 CI 测试必须通过
- 解决所有审查意见
- 获得批准后合并
测试要求
单元测试:
- Rust: 使用
#[test_case]属性,即在测试函数前添加#[test_case]宏(不是#[test]!!!)
测试文件位置:
- Rust 测试与源码在同一文件(使用
#[cfg(test)])
示例:
#![allow(unused)]
fn main() {
// 这里假定你是在proka-kernel(lib/main)下写的代码,即kernel/src/lib.rs已经
// mod了你的module。
//
// 此时你便可以直接使用`#[test_case]`宏来编写测试用例。
// 这里定义一个功能
pub struct MyStruct {
pub field: u32,
}
impl MyStruct {
/// Initilaize a new `MyStruct` with the given field value.
pub fn new(field: u32) -> Self {
Self { field }
}
/// Return the value of the `field` field.
pub fn some_method(&self) -> u32 {
self.field
}
}
// 这里编写测试用例
#[cfg(test)]
mod tests {
use super::*;
/// Test the `new` method.
#[test_case]
fn test_new() {
let my_struct = MyStruct::new(114514);
assert_eq!(my_struct.field, 114514);
}
/// Test the `some_method` method.
#[test_case]
fn test_some_method() {
let my_struct = MyStruct::new(114514);
assert_eq!(my_struct.some_method(), 114514);
}
}
}
总结
本规范是 Proka Kernel 项目的开发基础。所有贡献者应:
- 阅读并理解:在编写代码前仔细阅读本规范
- 持续遵循:在开发过程中持续检查是否符合规范
- 积极反馈:如发现规范问题,请提出改进建议
规范的目的是提高代码质量、保持一致性并降低维护成本。通过共同遵守这些约定,我们可以构建一个更加健壮和可维护的内核。
New-Driver
项目目录结构
Proka Kernel 的代码组织遵循模块化原则。
.
├── assets/ # 引导配置、initrd 资源及固件 (OVMF)
├── docs/ # mdBook 文档源码
├── kernel/ # 内核核心源码 (Rust)
│ ├── src/ # 源代码
│ │ ├── drivers/ # 硬件驱动
│ │ ├── memory/ # 内存管理 (Paging, Allocator)
│ │ ├── interrupts/ # IDT, GDT, APIC
│ │ └── ...
│ └── Makefile # 内核特定构建逻辑
├── scripts/ # 开发与构建辅助脚本
├── Makefile # 根目录 Makefile (总控)
└── book.toml # mdBook 配置文件
内核核心源码
内核核心源码位于 kernel 目录下。Rust项目所必须的Cargo.toml同样再次目录下,因此需在kernel目录下执行cargo命令(部分命令已通过makefile进行了封装)。
src目录下的模块包含:
drivers: 硬件驱动模块memory: 内存管理模块interrupts: 中断模块fs: 文件系统模块process: 进程模块libs: 通用工具及杂项模块
Scripts
脚本位于 scripts 目录下。用于存储构建与开发过程中所使用的脚本。一般为bash或python脚本。该目录不应含任何可执行二进制文件。
test_runner.sh:测试运行器,用于在QEMU中运行内核测试font: 该目录下为已弃用的BMF格式字体生成脚本及其文件格式定义,因为该格式已不再使用。
构建系统 (Makefile)
项目采用 Makefile 作为顶层调度器,配合 Cargo 完成 Rust 内核的编译。
顶层 Makefile
位于项目根目录,负责:
- 调度内核编译。
- 调用
xorriso生成 ISO。 - 管理
scripts下的辅助工具。 - 启动 QEMU。
内核 Makefile
位于 kernel/Makefile,专注于:
- 处理
cargo编译参数。 - 链接内核二进制文件。
构建流程
- 预处理:生成配置头文件。
- 内核编译:
cargo build编译 Rust 源码为 ELF。 - Initrd 创建:打包
assets中的必要文件。 - 镜像合成:将 Limine 引导程序、内核 ELF 和 Initrd 合并为 ISO。
配置系统 (Anaxa Builder)
Proka Kernel 借鉴了 Linux 内核的配置思路,使用 Kconfig.toml 文件定义内核的可配置选项,并提供类似 menuconfig 的交互界面。我们的配置系统基于 Anaxa Builder,一个用于动态配置生成的 Rust 库(没错,也是我们开发的!)。
配置定义
我们的根配置文件为kernel/Kconfig.toml,该配置文件定义了内核各个子系统的分配置文件引用。
如:
title = "Kernel Configuration"
[menu]
# The core configuration, which is at "src" root.
core = "src"
# The memory configuration.
memory = "src/memory"
# The library configuration.
library = "src/libs"
output = "src/output"
# The process/scheduler configuration.
process = "src/process"
menu下的配置项会生成一个菜单项,其显示名称会优先使用其引用的文件的title字段,否则使用他的key。
配置项定义
每一个配置项均为[[config]]的子项,其完整示例定义如下:
[[config]]
name = "PAGE_SIZE" # 配置项名称(必填)
type = "int" # 配置项类型(必填)
default = 4096 # 默认值(选填)
desc = "The Page Size (Unit: Bytes)." # 描述(选填)
help = "The size of per page." # 帮助信息(选填)
rust_type = "usize" # Rust 类型(选填)
options = ["RR", "FIFO", "CFS"] # 选项(仅适用于 choice 类型)
depends_on = "PAG_STRATEGY || PAGE_REC" # 依赖项(选填)
range = [1, 1024] # 取值范围(仅适用于 int 类型)
regex = "^[a-zA-Z0-9_]+$" # 正则表达式(仅适用于 string 类型)
feature = ["paas"] # 对应需启用的cargo feature(仅适用于 bool 类型,选填)
type字段定义了配置项的类型,目前支持以下类型:
bool: 布尔型,值为true或falseint: 整数型,值为任意整数string: 字符串型,值为任意字符串hex: 十六进制型,值为任意十六进制数choice: 选择型,值为用户选择的选项
rust_type字段定义了配置项在 Rust 中的类型,他会覆盖type字段的默认映射,但是,rust_type字段将会进行验证,如果类型不匹配,将会报错。以下为配置项类型的默认映射:
bool:boolint:i64string:&strhex:u64choice:&str
desc字段定义了配置项的描述,将会在菜单中显示,并且也会作为其生成的Rust代码的文档字符串。help将在TUI的配置项详细页中显示。
depends_on字段定义了配置项的依赖项表达式(evalexpr语法),如果依赖项表达式求值结果为true,则当前配置项将显示。
range字段定义了配置项的取值范围(使用>=或<=),仅适用于int类型。
regex字段定义了配置项的匹配正则表达式,仅适用于string类型。
配置引用定义
[[menu]]
a = "./a"
a下的Kconfig.toml文件将作为子配置项引用,并自动添加到配置文件中,从而绕过自动的按文件结构构建配置树。
条件编译
每一个类型为bool的配置项,都会向rustc传递与其名称相同的cfg选项,因此你可以在代码中通过#[cfg(xxx)]来进行条件编译,不应使用#[cfg(feature = "xxx")]进行条件编译,feature只应控制条件依赖。
配置检查
配置检查直接使用make check,他会同时运行cargo check和cargo anaxa config-check,以检查配置项的合法性和依赖关系是否正确。
TUI配置界面
配置界面使用make menuconfig启动,会启动一个TUI界面,用户可以通过键盘输入来配置内核。
配置界面的详细使用方法请参考Anaxa Builder README。
CI/CD 与工作流
我们使用 GitHub Actions 来确保代码质量。
自动化流水线
每当有代码推送到 main 分支或提交 PR 时,CI 会触发以下流程:
- 代码检查:运行
cargo fmt和cargo clippy。 - 内核构建:尝试编译
x86_64-unknown-none目标。 - 测试运行:在 headless 模式下运行自动化测试脚本。
配置文件
流水线定义在 .github/workflows/test.yml。