Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proka Kernel

Kernel Tests Rust Nightly License: MIT Documentation

欢迎阅读 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.rsBuddy System 实现
memory/paging/mod.rs分页支持
memory/paging/vmm.rs虚拟内存管理
memory/heap.rs内核堆管理
memory/error.rs内存错误类型

中断子系统

Gdt Idt

Apic

驱动框架

文件系统架构

Vfs

内核文件系统 (KernFS)

多任务处理

EFI加载器

调度器 (Scheduler)

Proka 内核的调度器负责管理和切换线程,是实现并发和多任务处理的核心组件。我们采用了一种高度模块化和可插拔的设计,允许在运行时或编译时轻松替换调度算法。

设计理念

调度器的核心设计目标是 解耦 和 灵活性。

  • 抽象与实现分离:通过 Scheduler Trait 定义调度器的标准行为,将接口与具体的调度算法(如优先级调度、时间片轮转等)完全分开。
  • 可插拔性:内核持有一个全局的 Scheduler trait object (Box<dyn Scheduler>)。这意味着可以动态替换调度器实现,为测试、性能分析或特定应用场景下的优化提供了极大的便利。
  • 进程与线程分离:明确了进程 (Process) 作为资源(内存、文件描述符)的所有者,而线程 (Thread) 作为被调度的执行单元。

进程切换 vs 线程切换

调度器在 schedule_next 过程中会执行以下逻辑:

  1. 线程上下文切换:始终执行,保存旧线程寄存器,加载新线程寄存器。
  2. 地址空间切换:仅在 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),用于在切换时保存和恢复状态。
    • 内核栈。

具体实现

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, ()>
}

创建流程:

  1. 分配新的 PID
  2. 创建 PCB 并初始化资源
  3. 将新进程添加到父进程的 children 列表
  4. 返回新进程的 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 进程处理

当进程终止时:

  1. 进程状态变为 Zombie
  2. 退出码被保存
  3. 进程被添加到 zombie queue
  4. 父进程通过 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 用户态服务

服务名称当前位置可迁移至用户态
ProcessServiceproc:/内核态⚠️ 部分功能(需要调度器支持)
MemoryServicemem:/内核态⚠️ 部分功能(需要页表操作)
ConsoleServiceconsole:/内核态✅ 可迁移
FilesystemServicefs:/未实现✅ 用户态设计
DeviceServicedev:/未实现✅ 用户态设计

为什么某些服务保留在内核态?

  1. MemoryService: 需要直接操作页表,修改地址空间映射
  2. 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

消息类型

类型值说明
Exit0退出当前进程
GetPid1获取当前进程 ID
Spawn2创建新进程 (未实现)
Wait3等待子进程 (未实现)

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

消息类型

类型值说明
Mmap0映射内存
Munmap1取消映射
Brk2调整堆边界

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 = 0x1
  • PROT_WRITE = 0x2
  • PROT_EXEC = 0x4

映射标志:

  • MAP_PRIVATE = 0x02
  • MAP_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

消息类型

类型值说明
Putc0输出字符
Getc1输入字符 (未实现)
Write2输出字符串
Read3读取字符串 (未实现)

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.rsIPC 调用入口
service/mod.rs服务注册和分发
service/types.rsIPC 类型定义
service/process.rs进程服务
service/memory.rs内存服务
service/console.rs控制台服务
ipc/mod.rsIPC 消息传递、服务注册

ELF 加载器

为了运行用户态程序和加载内核模块,内核需要能够解析并加载 ELF64 格式的可执行文件和共享库。

概述

Proka Kernel 使用 elf_loader crate 实现 ELF 加载功能,并通过自定义的 KernelMmap trait 实现内核态内存映射。

处理步骤

  1. 头部解析:验证魔数 (0x7F 'E' 'L' 'F') 和架构信息。
  2. 段映射 (Segments):遍历程序头表 (Program Header Table),将 PT_LOAD 段映射到地址空间。
  3. 重定位:处理重定位表,修正符号地址。
  4. 符号解析:解析动态符号表,查找符号地址。

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();
    // 使用页表进行映射...
}
}

注意事项

  1. 地址空间:确保使用内核空间的 canonical 地址(0xFFFF_XXXX_XXXX_XXXX)
  2. TLB 刷新:映射完成后必须刷新 TLB
  3. 内存对齐:所有映射地址必须页对齐(4KB)
  4. 错误处理:正确处理内存分配失败的情况

相关文件

文件功能
libs/elf.rsELF 加载器和 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 链。
  • 回溯逻辑:
    1. 捕获当前 RBP。
    2. 跳过第一层(Panic 处理器自身)。
    3. 循环迭代:读取 [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 的 log facade 实现。
  • 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

规则:

  1. 每个目录必须有 mod.rs 文件
  2. 使用 pub(crate) 限制内部可见性
  3. 仅在 lib.rs 中公开必要的 API
  4. 避免模块间的循环依赖

注释与文档

对于注释,我们建议使用英文编写,以确保项目的国际化,使全世界的开发者都能够理解和维护代码。

单行注释:

#![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 要求:

  1. 每个 unsafe 块必须有 // SAFETY: 注释
  2. 说明为什么是安全的以及调用者需要满足的条件
  3. 验证所有不变量和前置条件

示例:

#![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 保护共享状态
  • 避免裸的全局变量
  • 使用 LazyLock_static 或 once_cell 进行初始化

贡献与提交约定

Git 提交规范

遵循 Conventional Commits 规范

代码审查标准

审查要点:

  1. 代码是否符合本规范
  2. 是否有充分的测试覆盖
  3. 文档是否完整
  4. 性能影响是否评估
  5. 错误处理是否恰当
  6. 安全考虑是否充分

审查流程:

  1. 创建 Pull Request
  2. 至少需要一名核心维护者审查
  3. 所有 CI 测试必须通过
  4. 解决所有审查意见
  5. 获得批准后合并

测试要求

单元测试:

  • 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 项目的开发基础。所有贡献者应:

  1. 阅读并理解:在编写代码前仔细阅读本规范
  2. 持续遵循:在开发过程中持续检查是否符合规范
  3. 积极反馈:如发现规范问题,请提出改进建议

规范的目的是提高代码质量、保持一致性并降低维护成本。通过共同遵守这些约定,我们可以构建一个更加健壮和可维护的内核。

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 编译参数。
  • 链接内核二进制文件。

构建流程

  1. 预处理:生成配置头文件。
  2. 内核编译:cargo build 编译 Rust 源码为 ELF。
  3. Initrd 创建:打包 assets 中的必要文件。
  4. 镜像合成:将 Limine 引导程序、内核 ELF 和 Initrd 合并为 ISO。

配置系统 (Anaxa Builder)

Proka Kernel 借鉴了 Linux 内核的配置思路,使用 Kconfig.toml 文件定义内核的可配置选项,并提供类似 menuconfig 的交互界面。我们的配置系统基于 Anaxa Builder,一个用于动态配置生成的 Rust 库(没错,也是我们开发的!)。

配置定义

我们的根配置文件为kernel/Kconfig.toml,该配置文件定义了内核各个子系统的分配置文件引用。

如:

{{#include ../../kernel/Kconfig.toml}}

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或false
  • int: 整数型,值为任意整数
  • string: 字符串型,值为任意字符串
  • hex: 十六进制型,值为任意十六进制数
  • choice: 选择型,值为用户选择的选项

rust_type字段定义了配置项在 Rust 中的类型,他会覆盖type字段的默认映射,但是,rust_type字段将会进行验证,如果类型不匹配,将会报错。以下为配置项类型的默认映射:

  • bool: bool
  • int: i64
  • string: &str
  • hex: u64
  • choice: &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。

附录

Glossary

参考资料