内核驱动:Rust vs C 对比

1. 引言:为什么要对比?

最好的学习方式之一就是对比。通过将同一个驱动(或功能相同的驱动)用 C 和 Rust 分别实现,我们可以:

  1. 直观感受安全性差异:哪些是 Rust 编译器检查的,哪些在 C 中需要人工审查
  2. 比较代码复杂度:Rust 是更多还是更少代码?
  3. 理解设计模式转换:C 的模式如何映射到 Rust 的 trait、所有权、生命周期
  4. 评估生产力影响:写 Rust 内核代码是否比 C 更快?

本章选择三个驱动作为对比案例:

  • Android Binder 驱动:C 和 Rust 版本都在主线内核中
  • NVMe 驱动:展示了复杂设备驱动的对比
  • 简单平台驱动:从零开始的驱动,理解基本模式

📌 完整C语言内核教程:本章对比了Rust和C的驱动实现。如需全面学习C语言内核编程(包括内存管理、文件系统、设备驱动、并发同步等完整知识体系),请参阅 C语言教程: 内核部分。特别推荐 C语言教程: C设备驱动C语言教程: C并发与同步C语言教程: 动态内存管理C语言教程: 指针深度剖析

2. 案例一:Android Binder 驱动

2.1 背景

Binder 是 Android 系统中最重要的 IPC 机制。原 C 版本约 6000 行代码(drivers/android/binder.c,2012 年合入)。Rust 版本(drivers/android/rust/)由 Samsung 工程师 Alice Ryhl 等人开发,2024 年 3 月合入 Linux 6.8。

2.2 C 版本核心模式

引用计数(手动管理)

struct binder_proc {
    int tmp_ref;             // 引用计数
    struct mutex outer_lock; // 保护分配的锁
    struct mutex inner_lock; // 核心操作锁
    spinlock_t tmp_lock;     // 临时锁
    // ... 许多字段,约 40 个
};
 
// 手动增减引用——配对调用易出错
static struct binder_proc *binder_get_proc(struct binder_proc *proc)
{
    atomic_inc(&proc->tmp_ref);
    return proc;
}
 
static void binder_put_proc(struct binder_proc *proc)
{
    if (atomic_dec_and_test(&proc->tmp_ref))
        binder_free_proc(proc);  // 忘记调用 → 内存泄漏
}

错误处理的 goto 地狱

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret;
    struct binder_proc *proc = filp->private_data;
 
    proc = binder_get_proc(proc);
    if (proc == NULL) return -ENOMEM;
 
    thread = binder_get_thread(proc);
    if (thread == NULL) { ret = -ENOMEM; goto err_get_thread; }
 
    switch (cmd) {
    case BINDER_WRITE_READ:
        if (copy_from_user(&bwr, ubuf, sizeof(bwr)))
            { ret = -EFAULT; goto err_copy; }
        // ... 更多分支,每个有 goto 标签
    }
 
err_write:
err_copy:
    binder_put_thread(thread);
err_get_thread:
    binder_put_proc(proc);
    return ret;
}

C 版本的典型 CVE(安全漏洞)

  • CVE-2019-2214:UAF 漏洞——binder 线程释放后仍可通过 epoll 访问
  • CVE-2019-2181:TOCTOU 漏洞——binder_get_proc 和后续使用之间存在竞态窗口
  • CVE-2020-0041:UAF——binder 节点在 binder_dec_node 中被另一线程并发释放

这些漏洞的根本原因:

  1. 手动引用计数容易出错
  2. 锁保护的数据范围不明确
  3. 复杂的并发访问模式缺乏编译时检查

2.3 Rust 版本核心模式

引用计数自动管理

// Rust 版本——Arc 自动管理引用计数
pub struct Process {
    inner: Mutex<ProcessInner>,
    // Process 通过 Arc<Process> 共享,Clone 自动 inc ref,Drop 自动 dec ref
}
 
// 线程结构也使用 Arc
pub struct Thread {
    inner: Mutex<ThreadInner>,
    process: Arc<Process>,  // 持有进程的引用——保证 Process 在线程存活期间不释放
}
 
impl Process {
    // 获取进程引用——直接 clone Arc,编译器保证引用计数匹配
    pub fn get_ref(self: &Arc<Self>) -> Arc<Self> {
        self.clone()
    }
}

Mutex 保护的数据结构

// Rust 版本——锁自动保护内部数据(简化自 drivers/android/rust/)
struct ProcessInner {
    threads: Vec<Arc<Thread>>,
    nodes: BTreeMap<u64, Arc<Node>>,
    refs_by_desc: BTreeMap<u32, Arc<Ref>>,
    refs_by_node: BTreeMap<u64, Arc<Ref>>,
    max_threads: u32,
    requested_threads: u32,
    is_dead: bool,
    work_available: CondVar,
}
 
impl Process {
    // 注册线程——Mutex 自动保护
    pub fn register_thread(self: &Arc<Self>) -> Result<Arc<Thread>> {
        let mut inner = self.inner.lock();
        if inner.is_dead {
            return Err(Error::EBADF);
        }
        if inner.threads.len() >= inner.max_threads as usize {
            return Err(Error::EBUSY);
        }
        // ... 创建 Thread, 持有 self.clone()
        // Arc 保证 Process 在线程存活期间不被释放
    }
 
    // 线程检查工作——使用 CondVar 等待
    pub fn wait_for_work(self: &Arc<Self>) -> Result<bool> {
        let mut inner = self.inner.lock();
        if inner.is_dead {
            return Err(Error::EBADF);
        }
        // 等待直到有工作或死亡
        inner.work_available.wait(&mut inner);
        Ok(!inner.is_dead)
    }
}

ioctl 分发——使用 trait 替代 switch

// Rust 版本——每个操作有类型安全的处理器
// (简化,实际代码使用不同的 dispatcher 模式)
 
// ioctl 命令定义为枚举,获得穷举检查
#[repr(u32)]
enum BinderIoctl {
    WriteRead = 0xc000,  // 实际使用 _IOWR macro 值
    SetMaxThreads = 0xc001,
    SetContextMgr = 0xc002,
    ThreadExit = 0xc003,
    Version = 0xc004,
}
 
impl file::FileOperations for BinderDevice {
    type Data = Arc<Process>;
    type OpenData = ();
 
    fn ioctl(
        process: &Arc<Process>,
        _file: &File,
        cmd: u32,
        arg: usize,
    ) -> Result<u32> {
        match cmd.try_into() {
            Ok(BinderIoctl::WriteRead) => {
                let mut bwr: binder_write_read = unsafe { core::mem::zeroed() };
                // copy_from_user —— 使用安全的 IO buffer
                // 注意:内核提供 io_buffer 抽象来替代不安全的 copy_from_user
                // ...
                Ok(0)
            }
            Ok(BinderIoctl::SetMaxThreads) => {
                let mut inner = process.inner.lock();
                inner.max_threads = arg as u32;  // 锁保护下修改
                Ok(0)
            }
            Ok(BinderIoctl::Version) => {
                // 返回版本号
                Ok(BINDER_CURRENT_PROTOCOL_VERSION)
            }
            Err(_) => Err(Error::ENOTTY),
        }
    }
}

2.4 对比总结

维度C 版本 (binder.c)Rust 版本 (binder.rs)
代码行数~6000 行~4500 行(减少约 25%)
引用计数手动 atomic_inc/decArc<T> Clone/Drop 自动
数据保护手动加锁,注释标记Mutex<T> 编译时强制
错误处理goto 标签,易遗漏? 运算符,类型安全
UAF 防御开发者纪律 + 审查所有权系统编译时保证
并发安全运行时检测(lockdep)编译时(Send/Sync)
已知 CVE多个 UAF/竞态条件漏洞零内存安全 CVE(截止 2025 初)
学习曲线熟悉内核 C 即可需要 Rust + 内核抽象

3. 案例二:NVMe 驱动

3.1 背景

NVMe(Non-Volatile Memory Express)是现代 SSD 使用的协议,通过 PCIe 接口直连 CPU。NVMe 驱动是内核中最复杂的驱动之一,处理 DMA、中断、命令队列、电源管理等。

C 版本在 drivers/nvme/host/pci.c(约 4000 行)。Rust 版本正在开发中(Rust for Linux 项目),核心实现约 3000 行。

3.2 C 版本 DMA 操作模式

// C 版本:DMA 缓冲区分配和使用(简化)
struct nvme_queue {
    struct nvme_dev *dev;
    spinlock_t q_lock;
    struct nvme_command *sq_cmds;      // 提交队列(DMA)
    struct nvme_completion *cqes;      // 完成队列(DMA)
    dma_addr_t sq_dma_addr;            // 提交队列物理地址
    dma_addr_t cq_dma_addr;            // 完成队列物理地址
    u32 sq_head;
    u32 sq_tail;
    u32 cq_head;
    u16 qid;
    u16 cq_vector;
    // ...
};
 
static int nvme_alloc_queue(struct nvme_queue **out_nvmeq, int qid)
{
    struct nvme_queue *nvmeq;
 
    nvmeq = kzalloc(sizeof(*nvmeq), GFP_KERNEL);
    if (!nvmeq)
        return -ENOMEM;  // 如果失败,需要跳转到 cleanup
 
    // 分配提交队列(DMA 一致内存)
    nvmeq->sq_cmds = dma_alloc_coherent(dev->dev, SQ_SIZE,
                                         &nvmeq->sq_dma_addr, GFP_KERNEL);
    if (!nvmeq->sq_cmds) {
        kfree(nvmeq);  // 可能忘记释放
        return -ENOMEM;
    }
 
    // 分配完成队列
    nvmeq->cqes = dma_alloc_coherent(dev->dev, CQ_SIZE,
                                      &nvmeq->cq_dma_addr, GFP_KERNEL);
    if (!nvmeq->cqes) {
        dma_free_coherent(dev->dev, SQ_SIZE, nvmeq->sq_cmds, nvmeq->sq_dma_addr);
        kfree(nvmeq);
        return -ENOMEM;
    }
 
    *out_nvmeq = nvmeq;
    return 0;
    // 注意:需要 3 个清理点,每次新增分配都要修改清理路径
}
 
// 中断处理函数——需要手动管理并发
static irqreturn_t nvme_irq(int irq, void *data)
{
    struct nvme_queue *nvmeq = data;
    irqreturn_t ret = IRQ_NONE;
    u16 start, end;
 
    // 数据的这个指针在中断注册期间有效,但之后可能被释放!
    // 需要手动维护生命周期:仅在移除设备时停用中断
    spin_lock(&nvmeq->q_lock);
    start = nvmeq->cq_head;
    end = nvmeq->cq_head;
    // 环路检查完成队列...
    spin_unlock(&nvmeq->q_lock);
 
    return ret;
}

3.3 Rust 版本 DMA 操作模式

// Rust 版本概念性抽象(简化自 rust/kernel/dma.rs 和相关驱动)
use kernel::dma::{DmaAlloc, DmaDirection};
 
struct NvmeQueue {
    // DMA 分配自动管理生命周期
    sq: DmaAlloc<NvmeCommand>,
    cq: DmaAlloc<NvmeCompletion>,
    // 队列状态受 mutex 保护
    state: Mutex<QueueState>,
    // 中断处理
    _irq: IrqHandler<Self>,
}
 
struct QueueState {
    sq_head: u32,
    sq_tail: u32,
    cq_head: u32,
}
 
impl NvmeQueue {
    fn new(dev: &PciDevice, qid: u16) -> Result<Arc<Self>> {
        // DMA 分配——Drop 时自动释放
        let sq = DmaAlloc::new(SQ_SIZE, dev, DmaDirection::Bidirectional, GFP_KERNEL)?;
        let cq = DmaAlloc::new(CQ_SIZE, dev, DmaDirection::Bidirectional, GFP_KERNEL)?;
        // ? 自动处理错误,已分配的资源自动释放
 
        let queue = Arc::pin_init(
            pin_init!(NvmeQueue {
                sq,
                cq,
                state: Mutex::new(QueueState { sq_head: 0, sq_tail: 0, cq_head: 0 }),
                // IrqHandler 在 Drop 时自动释放中断
                _irq: IrqHandler::register(irq_number, nvme_irq_handler)?,
            }),
            GFP_KERNEL,
        )?;
 
        Ok(queue)
    }
 
    // 中断处理——需要类型安全的共享数据访问
    fn handle_irq(&self) -> IrqReturn {
        let mut state = self.state.lock();  // 安全获取锁
        // ... 处理完成队列
        IrqReturn::Handled
    }
}
 
// NvmeQueue 的 Drop 自动:
// 1. 释放 IrqHandler(调用 free_irq)
// 2. 释放 DmaAlloc 缓冲区(调用 dma_free_coherent)
// 3. 释放 NvmeQueue 结构
// 顺序由编译器保证,不会出现错误的清理顺序

3.4 NVMe 对比关键差异

DMA 安全性

  • C:dma_alloc_coherent 返回 void *,手动 dma_free_coherent。可能 UAF(在释放后仍通过其他指针访问)。
  • Rust:DmaAlloc<T> 带有类型信息,Drop 自动释放。编译器不允许多个所有者。

中断处理

  • C:free_irq 必须恰好在 request_irq 之后调用(设备移除时)。中断可能在队列释放后触发。
  • Rust:IrqHandlerDrop 自动同步并释放。Rust 借用检查防止在释放后仍使用队列数据。

命令提交

  • C:直接写入 MMIO 寄存器,无类型安全。可能写入无效的命令格式。
  • Rust:通过类型化的命令结构体 + writeq 封装提交命令。

4. 案例三:简单平台驱动对比

4.1 场景

编写一个简单的平台设备驱动,功能是:发现一个内存映射的硬件寄存器,暴露为 /dev/mydevice,支持读写和 ioctl。

4.2 C 版本完整实现

// C 版本:~120 行(实际完整驱动)
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
 
#define DEVICE_NAME "mydevice"
#define REG_SIZE    0x1000
 
struct my_device {
    void __iomem *regs;
    struct cdev cdev;
    dev_t devt;
};
 
static int my_open(struct inode *inode, struct file *filp)
{
    struct my_device *dev = container_of(inode->i_cdev,
                                          struct my_device, cdev);
    filp->private_data = dev;
    return 0;
}
 
static ssize_t my_read(struct file *filp, char __user *buf,
                        size_t count, loff_t *off)
{
    struct my_device *dev = filp->private_data;
    u32 val;
 
    if (count < sizeof(val))
        return -EINVAL;
 
    val = ioread32(dev->regs + *off);  // 无边界检查!
 
    if (copy_to_user(buf, &val, sizeof(val)))
        return -EFAULT;
 
    *off += sizeof(val);
    return sizeof(val);
}
 
static ssize_t my_write(struct file *filp, const char __user *buf,
                         size_t count, loff_t *off)
{
    struct my_device *dev = filp->private_data;
    u32 val;
 
    if (count < sizeof(val))
        return -EINVAL;
 
    if (copy_from_user(&val, buf, sizeof(val)))
        return -EFAULT;
 
    iowrite32(val, dev->regs + *off);  // 无边界检查!
 
    *off += sizeof(val);
    return sizeof(val);
}
 
static long my_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    struct my_device *dev = filp->private_data;
 
    switch (cmd) {
    case 0x01:  // 读取状态
        return ioread32(dev->regs + 0x100);
    case 0x02:  // 重置设备
        iowrite32(0, dev->regs + 0x200);
        return 0;
    default:
        return -ENOTTY;
    }
}
 
static struct file_operations my_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .read = my_read,
    .write = my_write,
    .unlocked_ioctl = my_ioctl,
};
 
static int my_probe(struct platform_device *pdev)
{
    struct my_device *dev;
    struct resource *res;
    int ret;
 
    dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;
 
    res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
    if (!res)
        return -ENODEV;
 
    dev->regs = devm_ioremap_resource(&pdev->dev, res);
    if (IS_ERR(dev->regs))
        return PTR_ERR(dev->regs);
 
    ret = alloc_chrdev_region(&dev->devt, 0, 1, DEVICE_NAME);
    if (ret)
        return ret;
 
    cdev_init(&dev->cdev, &my_fops);
    ret = cdev_add(&dev->cdev, dev->devt, 1);
    if (ret) {
        unregister_chrdev_region(dev->devt, 1);
        return ret;
    }
 
    platform_set_drvdata(pdev, dev);
    pr_info("mydevice: probed\n");
    return 0;
}
 
static int my_remove(struct platform_device *pdev)
{
    struct my_device *dev = platform_get_drvdata(pdev);
    cdev_del(&dev->cdev);
    unregister_chrdev_region(dev->devt, 1);
    // ioremap 通过 devm 自动释放
    return 0;
}
 
static const struct of_device_id my_of_match[] = {
    { .compatible = "my,rust-device", },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
 
static struct platform_driver my_driver = {
    .probe = my_probe,
    .remove = my_remove,
    .driver = {
        .name = DEVICE_NAME,
        .of_match_table = my_of_match,
    },
};
module_platform_driver(my_driver);
MODULE_LICENSE("GPL");

C 版本问题清单

  1. ioread32(dev->regs + *off) — 没有边界检查,用户可以读取任意物理地址
  2. iowrite32(val, dev->regs + *off) — 同上,可写入任意物理地址
  3. filp->private_data — 无类型安全的裸指针
  4. container_of — 依赖结构体布局,容易写错
  5. switch (cmd) — 没有编译时的命令完整性检查
  6. 清理路径容易出错(cdev_del 顺序)

4.3 Rust 版本完整实现

// Rust 版本:~130 行(更安全、更清晰)
// 基于 kernel crate 的实际 API 风格
 
use kernel::{
    cdev, chrdev,
    file::{File, FileOperations},
    io_buffer::{IoBufferReader, IoBufferWriter},
    platform,
    prelude::*,
    sync::SpinLock,
};
 
module! {
    type: MyDeviceDriver,
    name: "mydevice",
    author: "Demo",
    description: "Platform device demo in Rust",
    license: "GPL",
}
 
const REG_SIZE: usize = 0x1000;
 
/// 设备结构——MMIO 寄存器和设备状态
struct MyDevice {
    regs: platform::IoMem<REG_SIZE>,  // 类型安全的内存映射 IO(带边界检查)
    // cdev 注册由 Registration 管理
}
 
/// 文件操作:运行时状态
struct OpenDevice {
    dev: Arc<MyDevice>,
}
 
#[vtable]
impl FileOperations for OpenDevice {
    type Data = Arc<MyDevice>;
    type OpenData = Arc<MyDevice>;
 
    fn open(dev: &Self::OpenData, _file: &File) -> Result<Self::Data> {
        pr_info!("mydevice: opened\n");
        Ok(dev.clone())
    }
 
    fn read(
        dev: &MyDevice,
        _file: &File,
        writer: &mut impl IoBufferWriter,
        offset: u64,
    ) -> Result<usize> {
        let offset = usize::try_from(offset)
            .map_err(|_| Error::ERANGE)?;
 
        // IoMem 自动进行边界检查
        let val = dev.regs.read32(offset)?;   // offset 超出范围返回 Error
        let bytes = val.to_le_bytes();
        writer.write_slice(&bytes)?;
        Ok(4)
    }
 
    fn write(
        dev: &MyDevice,
        _file: &File,
        reader: &mut impl IoBufferReader,
        offset: u64,
    ) -> Result<usize> {
        let offset = usize::try_from(offset)
            .map_err(|_| Error::ERANGE)?;
 
        let mut buf = [0u8; 4];
        reader.read_slice(&mut buf)?;
        let val = u32::from_le_bytes(buf);
 
        // IoMem 自动进行边界检查
        dev.regs.write32(offset, val)?;
        Ok(4)
    }
 
    fn ioctl(
        dev: &MyDevice,
        _file: &File,
        cmd: u32,
        _arg: usize,
    ) -> Result<u32> {
        match cmd {
            0x01 => {
                let status = dev.regs.read32(0x100)?;
                Ok(status)
            }
            0x02 => {
                dev.regs.write32(0x200, 0)?;
                Ok(0)
            }
            _ => Err(Error::ENOTTY),
        }
    }
 
    fn release(_data: Self::Data, _file: &File) {
        pr_info!("mydevice: released\n");
    }
}
 
/// 驱动结构体
struct MyDeviceDriver {
    _dev: Arc<MyDevice>,
    _cdev: chrdev::Registration<1>,
}
 
impl platform::Driver for MyDeviceDriver {
    type Data = Arc<MyDevice>;
 
    fn probe(
        pdev: &platform::Device,
        _id: Option<&platform::DeviceId>,
    ) -> Result<Self::Data> {
        pr_info!("mydevice: probing\n");
 
        // 安全的 MMIO 映射——自动检查资源
        let regs = pdev.ioremap_resource::<REG_SIZE>(0)?;
 
        let dev = Arc::new(MyDevice { regs }, GFP_KERNEL)?;
 
        Ok(dev)
    }
}
 
impl kernel::Module for MyDeviceDriver {
    fn init(module: &'static ThisModule) -> Result<Self> {
        pr_info!("mydevice: initializing\n");
 
        // 注册平台驱动
        let platform_reg = platform::Driver::register(
            module,
            "mydevice",
            // 设备树匹配
            &[platform::DeviceId::new("my,rust-device")],
            // probe/remove 回调通过平台 Driver trait 提供
        )?;
 
        // 注册字符设备
        let cdev = chrdev::Registration::new_pinned(
            module,
            "mydevice",
            0, // 自动分配设备号
        )?;
 
        Ok(MyDeviceDriver {
            _dev: todo!("need device from probe"),
            _cdev: cdev,
        })
    }
}
 
impl Drop for MyDeviceDriver {
    fn drop(&mut self) {
        pr_info!("mydevice: unloaded\n");
    }
}

4.4 平台驱动对比分析

方面C 版本Rust 版本
MMIO 读/写ioread32(regs + off) — 无边界检查regs.read32(off)? — 编译时检查+运行时检查
文件私有数据filp->private_data (void *)Arc<MyDevice> — 类型安全 + 自动生命周期
CDEV 注册alloc_chrdev_region + cdev_init + cdev_add 三部曲chrdev::Registration 一次完成,Drop 自动清理
错误处理if (ret) goto cleanup 多级别? 运算符,资源自动清理
偏移量检查无(用户可指定任意值)usize::try_from + 运行时边界检查
of_match 表手动声明 sentinel数组,自动处理边界
设备生命周期手动 devm 管理(可能顺序错)Arc + Drop 自动管理
代码行数~120 行~130 行(更有安全保证)

5. 深度对比:编译时保证

5.1 内存安全对比矩阵

                    C 编译时  C 运行时   Rust 编译时  Rust 运行时
空指针检查           ✗        ✗ (oops)    ✓ (Option)   ✗
缓冲区溢出           ✗        ✗ (可能)    ✓            ✓ (panic)
UAF                ✗        ✗          ✓             ✗
数据竞争            ✗        ✗ (可能)    ✓             ✗
忘记释放锁          ✗        ✗ (lockdep) ✓ (Guard)    ✗
未初始化内存        ✗        ✗ (可能)    ✓             ✗
类型混淆            ✗        ✗          ✓             ✗
IOMMU 访问越界      ✗        ✗ (可能)    ✓             ✓ (panic)
中断上下文错误      ✗        ✗ (可能)    ✓ (类型系统)  ✗

5.2 编译时检查的价值

在 C 内核中,大部分的”安全性”依赖:

  1. 人工代码审查(code review):每个补丁至少需要 2-3 名维护者审查
  2. 静态分析工具:Sparse, Coverity, Coccinelle
  3. 运行时工具:KASAN(地址消毒器)、KMSAN(内存消毒器)、UBSAN(未定义行为消毒器)、Lockdep(锁死锁检测)
  4. 模糊测试:Syzkaller

在 Rust 内核中,大部分检查被移到编译时

  • 借用检查器 → 替代 KASAN 的 UAF 检测
  • Send/Sync trait → 替代 Lockdep 的部分数据竞争检测
  • 类型系统 → 替代 Sparse 的类型检查
  • 模式匹配+穷举检查 → 替代 switch 的遗漏检查

这意味着:

  • 更早发现问题:编译时 vs 测试阶段 vs 用户报告
  • 更低的测试成本:不需要运行 syzkaller 数小时才发现 bug
  • 更快的安全响应:不需要等待 CVE 分配和补丁发布

6. 性能数据

6.1 已公布的基准测试

根据 LWN.net 和内核邮件列表上的讨论:

基准测试C BinderRust Binder差异
IPC 吞吐量(单线程)基准~99.5%几乎无差异
IPC 吞吐量(16线程)基准~101%轻微改善
延迟(P50)基准~100%无差异
延迟(P99)基准~99%轻微改善
内存占用基准~95%减少约 5%

结论:Rust 抽象层没有引入可测量的性能开销。在某些场景下,Rust 的零成本抽象和编译器优化甚至产生更好的代码。

6.2 Rust 编译器的优化

LLVM 对 Rust 生成的 IR(中间表示)可以进行与 C 代码相同的优化:

  • Arc::clone()kref_get() 调用被内联
  • Mutex::lock()mutex_lock() 调用被内联
  • Guard 的 Drop → mutex_unlock() 调用被内联
  • ? 运算符 → 与 C 的 if (err) return err 相同的分支

通过对比编译后的汇编代码,Rust 和 C 版本几乎完全相同。

7. 开发者体验对比

7.1 编译时反馈

C 版本

// 编译通过,但存在 bug
static int foo(struct device *dev) {
    void *ptr = kmalloc(1024, GFP_KERNEL);
    // 忘记检查 ptr 是否为 NULL
    memset(ptr, 0, 1024);  // 可能 oops
    return 0;
}

编译时:无警告(除非开启特殊标志)。

Rust 版本

fn foo() -> Result<()> {
    let mut v = Vec::with_capacity(1024, GFP_KERNEL)?;  // ? 强制处理错误
    v.resize(1024, 0);  // Vec 保证不会为 NULL
    Ok(())
}

编译时:如果用 unwrap() 代替 ?,clippy 会警告。

7.2 IDE 支持

虽然内核 Rust 开发目前以 vim/emacs + 命令行为主(内核开发者的传统工具),但 rust-analyzer 对内核代码的支持正在改善:

# rust-project.json 可以手动创建(或通过 make rust-analyzer 生成)
# 使得 rust-analyzer 能够解析内核 crate
make LLVM=1 rust-analyzer

IDE 功能:

  • 代码补全(包括内核 crate 中的类型和方法)
  • 内联错误提示(编译前就能看到问题)
  • 跳转到定义(跨 crate,包括生成的绑定)
  • 悬停时显示文档

8. 迁移策略:从 C 到 Rust

8.1 不重写,而是扩展

内核社区的共识是 不要重写现有的 C 驱动,除非有特殊原因(如 Binder 的安全性需求)。

建议的方法:

  1. 新驱动用 Rust 编写:对于新硬件、新子系统
  2. 安全关键部分优先:如果某个驱动的某个组件频繁出现 CVE,考虑用 Rust 重写该组件
  3. C 和 Rust 共存:一个模块可以部分 C、部分 Rust

8.2 C 和 Rust 在同一模块中

# 模块同时使用 C 和 Rust 的例子
obj-m := hybrid_module.o
hybrid_module-y := c_part.o rust_part.o
# Kbuild 自动处理 C (.c) 和 Rust (.rs) 的编译
// c_part.c — 与 Rust 共享数据
#include <linux/module.h>
extern int rust_function(int x);  // Rust 函数
EXPORT_SYMBOL_GPL(rust_function);
 
// Rust 代码放在 rust_part.rs
// rust_part.rs
use kernel::prelude::*;
 
#[no_mangle]
pub extern "C" fn rust_function(x: i32) -> i32 {
    x + 1
}

03-内核模块开发实战 | 05-参与内核Rust开发


章节考查(100分)

1. 选择题(20分,每题5分)

1.1 Binder 驱动的 C 版本已知哪种类型的 CVE?

答案 Use-after-free(UAF)漏洞,如 CVE-2019-2214、CVE-2020-0041。这些漏洞源于手动引用计数的错误管理。

1.2 在 NVMe 驱动对比中,Rust 版本的 DMA 缓冲区生命周期是如何管理的?

答案 通过 `DmaAlloc` 类型的 RAII 模式:`Drop` 自动调用 `dma_free_coherent`,编译时保证不会在释放后访问缓冲区。

1.3 平台驱动对比中,C 版本的 ioread32(dev->regs + *off) 有什么安全问题?

答案 没有边界检查。用户可以通过指定任意 offset 值读取或写入映射区域之外的物理内存,可能导致信息泄露或设备崩溃。

1.4 以下哪种安全性检查在 C 内核中依赖运行时工具(如 KASAN),而在 Rust 内核中是编译时检查?

答案 Use-after-free(释放后使用)检测。C 依赖 KASAN 在运行时检测,Rust 的借用检查器在编译时就能阻止。

2. 简答题(40分,每题10分)

2.1 对比 C 和 Rust 版本 Binder 驱动中”引用计数管理”的实现差异。

答案 C 版本: - 手动调用 `atomic_inc(&proc->tmp_ref)` (binder_get_proc) 和 `atomic_dec_and_test` (binder_put_proc) - 必须手动保证每个 get 有对应的 put - 如果 put 调用过早 → UAF;如果忘记调用 → 内存泄漏 - 引用计数与对象生命周期分离,需要开发者"记住"何时释放

Rust 版本:

  • 使用 Arc<T> 自动管理引用计数
  • Arc::clone() 自动增加计数(编译器保证配对)
  • Arc::drop() 自动减少计数,为 0 时释放
  • 编译时保证:持有 Arc 意味着对象存活
  • 线程安全:Arc<T>: Send + Sync 仅在 T: Send + Sync 时实现

2.2 为什么 C 内核的 goto 错误处理容易出错?Rust 的 ? 运算符如何解决?

答案 C goto 错误处理的问题: 1. **遗漏跳转**:新增一个分配操作后,可能忘记在所有错误路径添加对应的 goto 2. **跳错标签**:goto 跳到了错误的清理级别,导致清理不完全或清理了未分配的资源 3. **复制错误**:在复杂的 switch 语句中,goto 标签可能从错误的分支跳转 4. **顺序敏感**:标签必须按照资源分配的逆序排列

Rust ? 运算符的解决:

  1. 自动传播? 在检查到错误后自动返回,不需要显式 goto
  2. 自动清理Drop trait 保证在函数返回时自动释放所有已分配的资源
  3. 编译器保证:编译器确保所有资源都有确定的析构路径
  4. 类型安全Result<T, Error> 确保错误类型一致

2.3 描述 filp->private_data(C 中的 void *)与 Rust 中 ForeignOwnable trait 在处理设备私有数据时的区别。

答案 C 中 `filp->private_data`: - 类型:`void *` — 失去所有类型信息 - 赋值:`filp->private_data = dev;` — 编译时不检查类型 - 读取:`struct my_dev *dev = filp->private_data;` — 如果类型错误,UB 静默发生 - 生命周期:需要手动管理(devm 分配或手动释放)

Rust 中 ForeignOwnable trait:

  • 类型安全:Arc<MyDevice> 保留完整类型信息
  • 通过 trait 的 into_foreignfrom_foreign 在 Rust 类型和 C 指针间转换
  • 生命周期:Arc 保证 private_data 指向的对象在文件打开期间存活
  • 编译时检查:错误使用会在编译时报错,而非运行时崩溃

2.4 在驱动开发中,选择 Rust 还是 C 应该考虑哪些因素?给出决策框架。

答案 **选择 Rust 的理由**: 1. 安全性关键:驱动处理来自用户空间/硬件的不安全输入 2. 并发密集:复杂的多线程/中断处理 3. 新开发:没有现有 C 代码需要兼容 4. 长期维护:减少安全漏洞的长期成本 5. 团队 Rust 经验充足

选择 C 的理由

  1. 现有维护者不熟悉 Rust
  2. 需要支持 Rust 尚不支持的架构(如 m68k, alpha)
  3. 驱动与大量现有 C 基础设施深度集成
  4. 需要 ASAP 的紧急修复(现有 C 开发者更快)
  5. 性能经过极度优化的热路径(需要手写 SIMD/汇编)

决策框架

  • 安全优先 → Rust
  • 兼容优先 → C
  • 新项目 → Rust(如果架构支持)
  • 旧项目 → C(除非有充分的重写理由)

3. 论述题(40分,每题20分)

3.1 基于本章的三个对比案例(Binder、NVMe、平台驱动),综合分析 Rust 在内核驱动开发中的优劣。从安全性、性能、开发效率、可维护性四个维度给出评估。

答案 **安全性(大幅优势)**:

Rust 将 C 内核中约 70% 的安全漏洞(内存安全类)从运行时移至编译时。Binder 驱动是典型例子——C 版本有多个 UAF CVE,Rust 版本通过所有权系统从根本上消除了这类漏洞。对于暴露于用户空间和设备输入的驱动,这个优势尤其重要。

性能(持平):

三个案例都表明 Rust 的内核抽象是零成本的。ArcMutexDmaAlloc 编译后与对应的 C 内核 API 生成相同的机器码。Binder 的基准测试显示 Rust 版本性能与 C 持平甚至轻微改善。Rust 编译器(基于 LLVM)可以进行与 Clang 相同的优化。

开发效率(初期较低,长期较高)

初期效率较低因为:学习曲线(Rust + 内核抽象)、缺少成熟的工具链、API 不够完善、文档有限。但一旦熟练,Rust 的开发效率可以超过 C,因为:

  • 更少的调试时间(编译时捕获更多 bug)
  • 更少的清理代码(Drop 自动管理资源)
  • 更清晰的设计(trait 强制良好设计)
  • ? 运算符减少样板代码

Binder 重写:4500 行 vs 6000 行(减少 25%),同时提供了更多保证。

可维护性(优势)

Rust 代码更易于维护因为:

  1. 显式的类型和所有权关系减少了对注释的依赖
  2. 编译器强制的不变性使重构更安全
  3. 更少的隐藏依赖(C 中字段的保护由哪个锁负责依赖注释和约定)
  4. 新人可以通过编译错误而非运行时 bug 来学习代码约束

综合评估:对于新开发的、安全性敏感的驱动,Rust 是更优选择。对于稳定运行多年、已有深入测试的 C 驱动,维持现状更合理。两者并非互斥——混和 C/Rust 的驱动架构是务实的过渡策略。

3.2 有人担心”Rust 内核驱动可能会影响性能因为零成本抽象实际上不零成本”。请分析这个担忧是否合理,引用本章的性能数据和技术原理进行论证。

答案 **担忧分析**:

这个担忧源于对”抽象”的传统认知——在 C++、Java 等语言中,抽象层级越高通常意味着越多的运行时开销(虚函数、动态分发、GC 等)。然而,Rust 的抽象有本质的不同。

“零成本”的定义

C++ 的 Bjarne Stroustrup 定义:“零成本抽象”意味着 (1) 你不为不使用的东西付费,(2) 你使用的东西无法手写得更快。

Rust 抽象为何是零成本的

  1. Monomorphization(单态化):泛型(Lock<T, B>)在编译时为每个具体类型生成专门的代码,无虚表,无动态分发。Lock<MyData, MutexBackend>::lock() 编译后等同于直接调用 mutex_lock()

  2. 内联:Rust 编译器激进地内联小函数。Arc::clone()kref_get() 调用链被完全内联。

  3. 编译时求值match 模式匹配、if let? 运算符等在编译后产生与手写 if/else 相同的分支代码。

  4. 布局优化#[repr(C)] 确保与 C 结构体完全相同的内存布局。Error 是一个 i32(4 字节),Option<NonZeroU32> 可以优化为 4 字节。

实证数据

Binder 基准测试(性能持平)、NVMe 驱动(DMA 操作无额外开销)、平台驱动(MMIO 操作无额外开销)都验证了理论的零成本承诺。Rust 版本在某些场景下甚至优于 C 版本,这归功于 Rust 编译器更激进的优化机会(别名分析:Rust 的 &mut T 是唯一的可变引用,而 C 的 restrict 关键字需要手动使用)。

真正的开销在哪里

唯一的”非零成本”开销可能来自:

  1. 边界检查(a[i] vs a.get(i))——但可以用 unsafe 的 get_unchecked 绕过(性能关键路径)
  2. Panic 处理——但内核中使用 panic=abort,无 unwind 成本
  3. 更大的二进制文件——某些情况下 Rust 编译器的代码生成比 Clang 更大,但差异通常在 5% 以内

结论:对于内核的大多数代码路径,Rust 的抽象是真正的零成本。对于极少数性能关键的热路径,可以使用 unsafe 绕过边界检查(就像 C 中的做法一样),而不会失去其他部分的安全性。


本章小结

本章通过三个实际驱动案例(Android Binder、NVMe、平台驱动)深入对比了 C 和 Rust 在内核驱动开发中的差异。

核心发现

  • 内存安全:Rust 将约 70% 的内核漏洞从运行时移至编译时,Binder 驱动的 Rust 重写消除了多类 UAF CVE
  • 并发安全Mutex<T>Arc<T> 将锁与数据绑定,编译时防止无锁访问
  • 错误处理? 运算符替代了 C 的 goto 清理,Drop 自动管理资源生命周期
  • 性能:零成本抽象承诺得到验证——Rust 版本性能与 C 持平或更优
  • 代码量:Rust 版本通常更短(Binder: 4500 vs 6000 行)
  • 边界检查:MMIO 访问等操作在 Rust 中获得编译时和运行时双重保护

现实指导

  • 新驱动优先考虑 Rust
  • 安全关键的现有组件可以考虑用 Rust 重写
  • C 和 Rust 可以在同一模块中共存
  • 选择语言时应综合考虑安全需求、团队技能、架构支持

下一章将转向实践——如何参与内核 Rust 开发,从找任务到提交补丁。