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

一个统一的错误枚举,覆盖了所有调度操作可能遇到的问题,例如 MaxThreadsReachedThreadNotFound

进程与线程模型

  • 进程 (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, RunnableBlockedSync 等状态。

核心数据结构

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);
}
}

全局状态

  • 使用 MutexRwLockAtomic 保护共享状态
  • 避免裸的全局变量
  • 使用 lazy_staticonce_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 目录下。用于存储构建与开发过程中所使用的脚本。一般为bashpython脚本。该目录不应含任何可执行二进制文件。

  • 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,该配置文件定义了内核各个子系统的分配置文件引用。

如:

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: 布尔型,值为truefalse
  • 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 checkcargo anaxa config-check,以检查配置项的合法性和依赖关系是否正确。

TUI配置界面

配置界面使用make menuconfig启动,会启动一个TUI界面,用户可以通过键盘输入来配置内核。

配置界面的详细使用方法请参考Anaxa Builder README

CI/CD 与工作流

我们使用 GitHub Actions 来确保代码质量。

自动化流水线

每当有代码推送到 main 分支或提交 PR 时,CI 会触发以下流程:

  • 代码检查:运行 cargo fmtcargo clippy
  • 内核构建:尝试编译 x86_64-unknown-none 目标。
  • 测试运行:在 headless 模式下运行自动化测试脚本。

配置文件

流水线定义在 .github/workflows/test.yml

附录

Glossary

参考资料