内核开发中的语言选择:C、C++ 与 Rust 的运行时与标准库

操作系统内核开发与应用程序开发的核心区别之一,在于运行时与内存管理模型的约束。本文从运行时大小、内存管理模型和标准库依赖三个方面,分析 C、C++、Rust 在内核开发中的差异。

运行时大小问题

C 运行时

C 的运行时几乎可以忽略不计:

C++ 运行时

C++ 的运行时较大,原因是:

Rust 运行时

Rust 介于两者之间:

内存管理的核心区别

内存管理模型的差异是另一关键因素。

C++ 的内存管理问题

  1. 构造函数和析构函数
    class Device {
     Resource* res;
    public:
     Device() { res = allocate_resource(); }  // 可能失败
     ~Device() { release_resource(); }        // 异常可能发生
    };
    
    • 构造函数无法返回错误码(只能用异常)
    • 析构函数中不能抛出异常
    • 对象生命周期由编译器自动管理,但在内核中这往往是不可预测的
  2. 异常处理
    void driver_function() {
     Device d;  // 构造
     // 如果这里发生异常,d 的析构函数会自动调用
     // 但在内核中,这种隐式控制流是危险的
    }
    
    • 异常展开需要复杂的栈回溯
    • 增加了二进制文件大小
    • 实时性无法保证
  3. RAII 的局限性
    • RAII 假设资源释放是确定性的、无错的
    • 内核中可能需要延迟释放、异步释放
    • 硬件资源的释放可能非常复杂
  4. 模板元编程
    template<typename T>
    class RingBuffer {
     T buffer[256];  // 类型在编译时确定
     // 但在内核中,可能需要根据硬件配置动态选择类型
    };
    
    • 过度依赖模板会导致代码膨胀
    • 难以处理动态硬件配置

C 的内存管理优势

  1. 显式控制
    struct device *dev = kmalloc(sizeof(*dev), GFP_KERNEL);
    if (!dev)
     return -ENOMEM;
    dev->ops = &device_ops;
    // 所有操作都是显式的,没有隐藏的控制流
    
  2. 错误处理直接
    int init_device(struct device *dev) {
     int ret;
     ret = init_resource_a(dev);
     if (ret)
         return ret;
     ret = init_resource_b(dev);
     if (ret) {
         cleanup_resource_a(dev);
         return ret;
     }
     return 0;
    }
    
    • 所有错误路径都清晰可见
    • 没有隐式的资源释放
  3. 内存布局可预测
    struct packet {
     uint32_t len;
     char data[0];  // 灵活数组成员
    };  // 内存布局完全由程序员控制
    

Rust 的创新解决方案

Rust 通过所有权系统和生命周期来平衡安全性和控制力:

struct Device {
    resource: Resource,
}

impl Device {
    fn new() -> Result<Self, Error> {
        let res = Resource::new()?;  // 显式错误处理
        Ok(Device { resource: res })
    }
}  // Drop trait 提供确定性析构,但比 C++ 更可控

// 所有权确保资源只有一个所有者
fn use_device(dev: Device) {  // 获得所有权
    // 使用设备
}  // 这里自动释放,但行为是确定的

Rust 解决了 C++ 的几个关键问题:

  1. 无异常:使用 Result 类型进行显式错误处理
  2. 所有权系统:资源释放是确定性的
  3. 零成本抽象:无运行时开销
  4. 内存安全:编译时检查,无 GC 开销

为什么内核不能使用标准库

1. 标准库依赖操作系统服务

标准库本质上是操作系统功能的封装:

// 标准库的实现依赖系统调用
// std::fs::File::open("test.txt") 最终会调用:
// Linux: openat() 系统调用
// Windows: NtCreateFile() 系统调用

// 但在内核中:
// 1. 没有文件系统(或文件系统实现不同)
// 2. 没有当前工作目录的概念
// 3. 没有用户态/内核态的转换机制

2. 内核需要裸机环境

// 用户态程序可以这样:
#include <stdio.h>
int main() {
    printf("Hello\n");  // 依赖操作系统的标准输出
    return 0;
}

// 内核只能这样:
void kernel_entry() {
    // 没有 main 函数,没有标准库
    // 需要直接操作硬件
    char *video_memory = (char*)0xb8000;
    *video_memory = 'H';  // 直接写入显存
}

各语言在没有标准库时的表现

C 语言:裸机编程的典范

// 内核中常见的 C 代码
static void serial_putc(char c) {
    // 直接操作硬件寄存器
    while (!(inb(COM1 + 5) & 0x20));
    outb(COM1, c);
}

// 自己实现需要的功能
void* memcpy(void* dest, const void* src, size_t n) {
    char* d = dest;
    const char* s = src;
    while (n--) *d++ = *s++;
    return dest;
}

C 语言的特点:

C++:标准库依赖严重

// 不能用的 C++ 特性:
#include <vector>      // 需要动态内存分配和异常
#include <string>      // 需要内存分配和字符处理
#include <iostream>    // 需要操作系统支持
#include <thread>      // 需要线程库支持
#include <mutex>       // 需要同步原语

// 即使不用标准库,语言特性本身也有问题:
class Device {
    std::string name;  // 错误:string 需要标准库
public:
    Device() { /* 构造函数不能失败? */ }
    ~Device() { /* 析构函数不能抛异常? */ }
};

// 尝试不用标准库:
class Device {
    char name[32];  // 固定大小,但不够灵活
    int fd;
public:
    Device() : fd(-1) {}  // 两阶段构造(anti-pattern)
    bool init(const char* n) { /* 真正的初始化 */ }
    void deinit() { /* 手动释放 */ }
};
// 但这违背了 RAII 原则

C++ 的问题:

Rust:no_std 模式2

// 指定不使用标准库
#![no_std]

// 只能使用 core 库(无操作系统依赖)
use core::panic::PanicInfo;

// 需要自己处理 panic
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
    loop {}
}

// 需要自己实现内存分配(如果需要)
#[global_allocator]
static ALLOCATOR: MyAllocator = MyAllocator;

// 可以安全地使用大部分语言特性
#[repr(C)]
struct Device {
    base_addr: usize,
    irq: u32,
}

impl Device {
    const fn new() -> Self {  // const fn 可以在编译时执行
        Device { base_addr: 0, irq: 0 }
    }

    fn read_reg(&self, offset: usize) -> u32 {
        // 直接操作内存映射 IO
        unsafe { (self.base_addr as *const u32).add(offset).read_volatile() }
    }
}

Rust 的优势2

实际代码对比

实现一个简单的串口驱动

C 版本

// serial.h
struct serial_port {
    uint16_t port;
    int initialized;
};

void serial_init(struct serial_port *sp, uint16_t port);
void serial_putc(struct serial_port *sp, char c);

// serial.c
void serial_init(struct serial_port *sp, uint16_t port) {
    sp->port = port;
    sp->initialized = 1;
    outb(port + 1, 0x00);  // 关闭中断
    outb(port + 3, 0x80);  // 设置波特率
    outb(port + 0, 0x03);
    outb(port + 1, 0x00);
    outb(port + 3, 0x03);
    outb(port + 2, 0xC7);
    outb(port + 4, 0x0B);
}

void serial_putc(struct serial_port *sp, char c) {
    while ((inb(sp->port + 5) & 0x20) == 0);
    outb(sp->port, c);
}

C++ 版本(有问题)

// 尝试用 C++ 风格
class SerialPort {
private:
    uint16_t port;
    bool initialized;

public:
    SerialPort(uint16_t port) : port(port) {
        // 构造函数中初始化,但如果失败?
        init();  // 不能返回错误码
    }

    ~SerialPort() {
        // 析构函数中清理
    }

    void putc(char c) {
        while ((inb(port + 5) & 0x20) == 0);
        outb(port, c);
    }

private:
    void init() {
        // 如果这里失败,只能抛异常
        // 但内核中不能使用异常
        outb(port + 1, 0x00);
        // ...
    }
};

Rust 版本(内存映射 I/O 风格):

#![no_std]

use core::ptr::{read_volatile, write_volatile};

#[repr(C)]
pub struct SerialPort {
    port: u16,
    initialized: bool,
}

impl SerialPort {
    pub fn new(port: u16) -> Result<Self, &'static str> {
        let mut sp = SerialPort {
            port,
            initialized: false,
        };
        sp.init()?;
        Ok(sp)
    }

    fn init(&mut self) -> Result<(), &'static str> {
        unsafe {
            write_volatile((self.port + 1) as *mut u8, 0x00);
            write_volatile((self.port + 3) as *mut u8, 0x80);
            write_volatile((self.port + 0) as *mut u8, 0x03);
            write_volatile((self.port + 1) as *mut u8, 0x00);
            write_volatile((self.port + 3) as *mut u8, 0x03);
            write_volatile((self.port + 2) as *mut u8, 0xC7);
            write_volatile((self.port + 4) as *mut u8, 0x0B);
        }
        self.initialized = true;
        Ok(())
    }

    pub fn putc(&self, c: u8) {
        unsafe {
            while (read_volatile((self.port + 5) as *const u8) & 0x20) == 0 {}
            write_volatile(self.port as *mut u8, c);
        }
    }
}

上述 Rust 示例为内存映射 I/O 风格(例如常见于 ARM 等平台);在 x86 上 COM 口为端口 I/O,需使用 inb/outb 或 x86_64::instructions::port::Port 等封装。

标准库 vs no_std 的生态差异

可用功能对比

功能 标准库 no_std 说明
Vec/String 需要内存分配器
Box/Rc/Arc ⚠️ 需要内存分配器
HashMap 需要随机数源
println! 需要 IO
文件操作 需要文件系统
线程 需要调度器
Mutex ⚠️ 需要原子操作支持
迭代器 纯语言特性
match 语言特性
trait 语言特性
闭包 语言特性

实际影响

在裸机环境中:

实际内核开发的选择

总结

从运行时与内存管理看,C++ 不适合内核开发的主要原因在于内存管理模型的差异:异常处理、隐式构造/析构、RAII 等与内核需要的确定性和显式控制相冲突;Rust 则用所有权系统在零成本抽象与内存安全之间取得折中。从标准库看,内核不能使用标准库:C 失去的很少(语言本身不依赖库),C++ 失去核心优势(STL、异常、部分 RAII),Rust 失去便利性(集合类型、格式化输出)但保留安全性。因此 Linux 选择 C(简单、可控、最小依赖,Rust 作为补充逐步引入6),Windows 内核主要用 C、部分驱动用 C++ 且限制特性,Redox 选择 Rust(no_std 提供安全性与表达能力的最佳平衡4)。

References

  1. Linux Kernel Source (torvalds/linux) - 官方内核源码(C 为主,含 Rust 子系统) 

  2. The Embedded Rust Book - no_std - Rust 裸机/内核开发中的 no_std 与 core 库说明  2 3 4

  3. Rust RFC 1184: Stabilize no_std - no_std 稳定化与 libcore 范围 

  4. Redox OS - 使用 Rust no_std 编写的操作系统  2 3

  5. Linux Kernel - Rust support - 内核 Rust 支持说明(仅链接 libcore,无 std) 

  6. Rust for Linux - 内核内 Rust 支持项目与文档  2

My Github Page: https://github.com/liweinan

Powered by Jekyll and Theme by solid

If you have any question want to ask or find bugs regarding with my blog posts, please report it here:
https://github.com/liweinan/liweinan.github.io/issues