1. 从一次内核崩溃说起:为什么需要BUG_ON和WARN_ON

那天下午,我正在调试一个刚合入的驱动模块,系统毫无征兆地卡死了,屏幕定格,键盘无响应,只剩下风扇在徒劳地嘶吼。重启后,面对满屏的日志,我熟练地敲下 dmesg ,在一堆乱码中,一行清晰的提示跳了出来: Kernel BUG at drivers/mydriver.c:123! 。这行信息,就是内核的“死亡宣告”,它来自一个叫做 BUG_ON() 的宏。而它的兄弟 WARN_ON() ,则温和得多,它不会让系统崩溃,只是在你耳边轻声说:“嘿,这里有点不对劲,你得看看。” 对于任何一个深入Linux内核开发或驱动编写的工程师来说,这两个宏就像工具箱里的螺丝刀和扳手,是定位和诊断运行时逻辑错误的利器。它们不是用来处理外部输入错误(比如无效的文件描述符),而是用来捍卫内核内部的“契约”——当某个你认为绝对不可能发生的条件竟然发生时,它们会以最直接或最醒目的方式告诉你。理解它们,不仅能帮你快速定位问题,更能让你理解内核开发者构建健壮代码的防御性编程思想。

2. BUG_ON():内核的“断点”与“自毁开关”

BUG_ON() 是内核中最严厉的断言机制。它的逻辑简单粗暴:如果传入的条件为真(非零),内核会立即触发一个“Oops”或直接宕机(panic),并打印出详细的诊断信息,包括调用栈、寄存器状态等。你可以把它想象成内核版的 assert() ,但后果严重得多——它会导致当前进程乃至整个系统停止运行。

2.1 BUG_ON()的工作原理与实现窥探

在Linux内核源码中, BUG_ON() 的定义通常与体系结构相关,但其核心思想一致。以x86架构为例,其简化逻辑是触发一个非法操作,迫使CPU产生一个陷阱(trap),从而落入内核预设的异常处理流程中。

// 这是一个高度简化的概念性展示
#define BUG_ON(condition) do { \
    if (unlikely(condition)) \
        __bug(__FILE__, __LINE__); \
} while (0)

// __bug 函数会做一些准备工作,然后触发一个非法指令
void __bug(const char *file, int line) {
    printk(KERN_EMERG “BUG at %s:%d!\\n“, file, line);
    // 体系结构相关代码,例如执行 ‘ud2‘ 指令(x86上的未定义指令)
    // 这会引发一个 #UD(Invalid Opcode)异常
}

当 BUG_ON(condition) 中的 condition 评估为真时,内核会故意执行一条非法指令(如x86的 ud2 ),CPU捕获到这个异常后,控制权会交给内核的异常处理程序。该处理程序会收集当前上下文(栈、寄存器、进程信息),并打印出我们看到的“Kernel BUG”信息,最后根据配置可能进入恐慌(panic)状态,停止系统。

注意 : unlikely() 是一个给编译器的提示,表明这个条件“很可能”为假,帮助编译器优化指令分支,提高正常路径的执行效率。这体现了内核编码对性能的极致追求。

2.2 何时应该使用BUG_ON()?

这是一个需要慎之又慎的决定。滥用 BUG_ON() 会让你的驱动或内核模块变得极其脆弱。它的使用场景非常特定:

  1. 内部一致性检查 :当某个数据结构或状态必须满足一个核心不变式(invariant),而这个不变式被违反意味着内核逻辑已经彻底混乱,无法安全继续时。例如,在内存管理子系统中,检查一个重要的全局锁是否在预期中被持有。

    // 假设 ‘my_important_lock‘ 必须在函数入口处被持有
    BUG_ON(!spin_is_locked(&my_important_lock));
    
  2. 验证绝对不可能发生的条件 :用于捕获那些在正确设计和实现下理论上永远不会出现的情况,通常与硬件或核心算法假设相关。例如,在一个遍历已知固定大小数组的循环后,索引值必须等于数组大小。

    for (i = 0; i < ARRAY_SIZE(my_array); i++) {
        // ... 处理 my_array[i]
    }
    BUG_ON(i != ARRAY_SIZE(my_array)); // 循环逻辑错误才会触发
    

核心原则 : BUG_ON() 用于处理“继续运行下去会造成更隐蔽、更严重破坏(如静默数据损坏)”的情况。它相当于外科手术中的“紧急叫停”,虽然代价巨大,但可以防止灾难扩散。

2.3 BUG_ON()的实战影响与排查

当 BUG_ON() 触发时,你的系统控制台(或串口日志、 dmesg )会打印类似以下的信息:

[  123.456789] Kernel BUG at drivers/char/mydrv.c:205 [verbose]
[  123.456790] invalid opcode: 0000 [#1] SMP PTI
[  123.456791] CPU: 2 PID: 1589 Comm: myapp Tainted: G           OE     5.15.0
[  123.456792] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS ...
[  123.456793] RIP: 0010:my_driver_function+0x45/0x120 [mydrv]
[  123.456794] Code: ff ff 48 89 df e8 aa ... 
[  123.456795] RSP: 0018:ffffb894c123bc78 EFLAGS: 00010246
[  123.456796] RAX: 0000000000000000 RBX: ffff9aab0c8d2000 RCX: 0000000000000001
[  123.456797] RDX: 0000000000000000 RSI: 0000000000000246 RDI: ffff9aab0c8d2000
[  123.456798] RBP: ffffb894c123bc90 R08: 0000000000000000 R09: 0000000000000000
[  123.456799] R10: 0000000000000000 R11: 0000000000000000 R12: 0000000000000001
[  123.456800] R13: ffff9aab0c8d2000 R14: 0000000000000000 R15: 0000000000000000
[  123.456801] FS:  00007f8b1e4b7740(0000) GS:ffff9aab8f300000(0000) knlGS:0000000000000000
[  123.456802] CS:  0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[  123.456803] CR2: 00007f8b1d8a4000 CR3: 000000010b5a2000 CR4: 0000000000350ee0
[  123.456804] Call Trace:
[  123.456805]  <TASK>
[  123.456806]  ? __my_ioctl+0xfc/0x130 [mydrv]
[  123.456807]  ? __x64_sys_ioctl+0x91/0xd0
[  123.456808]  ? do_syscall_64+0x5c/0x90
[  123.456809]  ? entry_SYSCALL_64_after_hwframe+0x44/0xae
[  123.456810]  </TASK>

这份“死亡报告”极其宝贵。你需要重点关注:

  • BUG位置 : drivers/char/mydrv.c:205 ,直接定位到源代码文件和行号。
  • 调用栈(Call Trace) :展示了从用户空间系统调用( sys_ioctl )到你的驱动函数( my_driver_function )的完整路径,是分析问题根源的路线图。
  • 寄存器值 :特别是RIP(指令指针)、RSP(栈指针)以及函数参数相关的寄存器(如RDI, RSI, RDX用于x86_64的第一个、二、三个参数),可以帮助你重建崩溃时的现场。
  • CPU和进程信息 :知道是哪个CPU核心、哪个用户进程( Comm: myapp )触发了BUG。

排查步骤 :

  1. 定位代码 :立刻查看 drivers/char/mydrv.c 第205行附近的 BUG_ON 条件。
  2. 分析条件 :这个条件是什么?它依赖哪些变量或状态?在崩溃的上下文中(通过调用栈和寄存器推断),这些变量应该是什么值?实际又是什么值?
  3. 回溯状态 :沿着调用栈向上,检查这些变量或状态是在何处、以何种方式被设置或修改的。通常问题不在BUG_ON这一行,而在更早的某处逻辑错误导致了状态的破坏。
  4. 复现与调试 :尝试构造能稳定复现问题的测试用例。使用 printk 加 KERN_DEBUG 级别在关键路径上打印变量值,或者使用内核动态调试( dyndbg )功能。对于复杂并发问题,可能需要分析锁的使用情况。

3. WARN_ON():内核的“黄色预警”与“诊断日志”

如果说 BUG_ON() 是“死刑立即执行”,那么 WARN_ON() 就是“严重警告,记入档案,但程序继续运行”。它的目的是在不中断系统服务的前提下,高亮那些不应该发生、但暂时允许继续运行以观察后续影响或收集更多信息的情况。

3.1 WARN_ON()的实现机制与输出

WARN_ON() 的实现同样会打印详细的警告信息,包括文件、行号、调用栈,但它不会触发内核恐慌。它通常通过一个专门的警告处理函数来实现,该函数将警告记录到内核日志缓冲区。

#define WARN_ON(condition) ({ \
    int __ret_warn_on = !!(condition); \
    if (unlikely(__ret_warn_on)) \
        __WARN_printf(“WARNING at %s:%d\\n“, __FILE__, __LINE__); \
    unlikely(__ret_warn_on); \
})

__WARN_printf 内部会调用 warn_slowpath_fmt 等函数,最终将格式化的警告信息输出到内核日志。一个典型的 WARN_ON 输出如下:

[  234.567890] WARNING: CPU: 3 PID: 1672 at drivers/net/ethernet/myeth.c:300 my_tx_timeout+0x50/0xa0 [myeth]
[  234.567891] Modules linked in: myeth(OE) ...
[  234.567892] CPU: 3 PID: 1672 Comm: kworker/3:2 Tainted: G           OE     5.15.0
[  234.567893] Hardware name: ...
[  234.567894] Workqueue: events my_workqueue_handler
[  234.567895] RIP: 0010:my_tx_timeout+0x50/0xa0 [myeth]
... [类似的寄存器信息和调用栈]

注意第一行以 WARNING: 开头,而不是 Kernel BUG 。系统会继续运行。

3.2 WARN_ON()的适用场景与最佳实践

WARN_ON() 的使用比 BUG_ON() 广泛得多,因为它对系统运行的影响较小。常见场景包括:

  1. 性能路径上的健壮性检查 :在高速数据路径(如网络包处理、块设备IO)上,进行一些轻量级的检查。如果检查失败,说明有潜在错误,但直接崩溃代价太高,记录一个警告以便后续离线分析更为合适。

    // 在数据包发送函数中,skb(套接字缓冲区)不应为空
    if (WARN_ON(!skb)) {
        dev->stats.tx_errors++;
        return NETDEV_TX_OK; // 丢弃包,但继续处理下一个
    }
    
  2. 检测罕见或理论上的错误路径 :例如,一个函数理论上应该只被特定类型的对象调用,但为了防御性编程,可以加入 WARN_ON 来检测非法调用。

    void process_special_object(struct special_obj *obj) {
        WARN_ON(obj->magic != SPECIAL_OBJ_MAGIC); // 检查“魔数”,防止传错对象
        // ... 正常处理
    }
    
  3. 临时调试与问题追踪 :在排查一个难以复现的并发问题时,可以在怀疑的代码区域加入 WARN_ON ,当条件偶尔满足时,它会在日志中留下“足迹”,帮助你缩小问题范围,而不会让测试立即中断。

最佳实践 :

  • 附带解释信息 :使用 WARN_ON_ONCE() 可以确保同一个警告在系统生命周期内只打印一次,避免日志被刷屏。
  • 结合返回值 : WARN_ON() 本身会返回条件是否为真的布尔值。可以利用它进行条件处理。
    if (WARN_ON(device_not_ready(dev))) {
        // 设备未就绪是个严重问题,记录警告,并返回错误码
        return -EIO;
    }
    
  • 不要忽略WARN_ON :虽然系统没崩溃,但每一个 WARN_ON 都应该被调查。持续出现的警告往往预示着稳定性或正确性问题。

3.3 配置与管控:控制WARN_ON的行为

内核提供了启动参数和sysfs接口来管理警告行为:

  • panic_on_warn :如果通过内核启动参数 panic_on_warn 或在运行时 echo 1 > /proc/sys/kernel/panic_on_warn ,那么当任何 WARN_ON() 触发时,内核会立即引发恐慌(panic),将其升级为类似 BUG_ON() 的行为。这在需要严格测试的自动化环境中非常有用,确保任何警告都能被捕获并导致测试失败。
  • 警告频率限制 :内核内部有机制防止警告信息洪水般输出,避免日志系统过载。

4. BUG_ON、WARN_ON与其他内核调试设施的对比与选型

内核提供了丰富的调试和自检工具,理解它们的区别才能正确选用。

工具/宏 目的 触发后果 适用阶段 典型场景
BUG_ON(cond) 断言绝对不可能发生的条件被违反。 触发Oops,通常导致内核恐慌(panic),系统停止。 开发、测试、生产(用于防御灾难性错误)。 核心数据结构一致性破坏、硬件状态严重异常。
WARN_ON(cond) 警告不应该发生但可容忍的条件。 打印带调用栈的警告信息到内核日志,系统继续运行。 开发、测试、生产(用于监控和诊断)。 参数轻度异常、罕见错误路径、性能路径上的健壮性检查。
dump_stack() 打印当前CPU的调用栈。 仅打印调用栈到日志,无其他副作用。 调试、动态追踪。 在任何怀疑的地方插入,查看函数调用关系。
pr_debug() / dev_dbg() 输出动态调试信息。 打印自定义调试信息到日志(需开启动态调试)。 开发、测试。 跟踪变量变化、函数执行流。需要 CONFIG_DYNAMIC_DEBUG 。
lockdep 动态检测锁的非法使用(死锁、违反规则)。 检测到错误时打印详细警告,系统可能继续。 开发、测试。 并发代码中锁的依赖关系检查。需要 CONFIG_LOCKDEP 。
KFENCE / KASAN 内存错误检测(越界、释放后使用等)。 检测到错误时报告并可能panic。 开发、测试。 捕捉内存相关的隐蔽bug。需要对应内核配置。

选型指南 :

  • 致命错误,必须停止 :使用 BUG_ON() 。条件是“继续运行会导致更坏结果”。
  • 可疑情况,需要记录 :使用 WARN_ON() 或 WARN_ON_ONCE() 。条件是“这不对,但我想看看如果继续会怎样”或“这需要被记录以便后续修复”。
  • 只想看执行路径 :使用 dump_stack() 。
  • 详细的运行时日志 :使用 pr_debug() 并配合动态调试。
  • 并发与锁问题 :依赖 lockdep 。
  • 内存问题 :启用 KASAN (针对大型测试)或 KFENCE (针对生产轻量级监控)。

5. 进阶:构建健壮内核代码的防御性编程模式

仅仅知道如何使用 BUG_ON 和 WARN_ON 是不够的,更重要的是将它们融入一种防御性编程的思维模式。这种模式的核心是“对不信任的状态进行验证”,包括不信任其他代码、不信任硬件、甚至不信任过去的自己。

5.1 分层校验与“快速失败”

在函数的入口处,对输入参数和当前上下文进行严格校验。这被称为“保护性检查”或“前置条件检查”。使用 WARN_ON 或返回错误码。

int my_driver_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
    struct my_device *dev = filp->private_data;

    // 1. 检查设备指针有效性(防御性编程)
    if (WARN_ON(!dev)) {
        return -ENODEV;
    }

    // 2. 检查设备是否处于可操作状态
    if (dev->state != DEVICE_READY) {
        dev_warn(&dev->pdev->dev, “Device not ready for ioctl 0x%x\\n“, cmd);
        return -EAGAIN;
    }

    // 3. 分发命令处理
    switch (cmd) {
        case MY_CMD_READ:
            // 处理读命令前,再次校验参数
            if (copy_from_user(&user_req, (void __user *)arg, sizeof(user_req))) {
                return -EFAULT;
            }
            if (WARN_ON(user_req.size > MAX_READ_SIZE)) {
                return -EINVAL;
            }
            // ... 核心处理逻辑
            break;
        // ... 其他命令
        default:
            // 未知命令,记录警告
            WARN_ON_ONCE(1); // 同一个未知命令只警告一次
            return -ENOTTY;
    }
    return 0;
}

这种模式的好处是“快速失败”,问题在最早暴露,调用栈最清晰,便于定位。

5.2 不变式断言与“契约编程”

在函数内部的关键点,特别是修改重要共享数据结构之前和之后,使用 BUG_ON 或 WARN_ON 来断言不变式必须保持。这就像在代码中写下了“契约”。

void update_shared_list(struct item *new_item) {
    struct list_head *head = &shared_list;
    unsigned long flags;

    // 契约:调用此函数时,new_item不应已在列表中
    BUG_ON(!list_empty(&new_item->node));

    spin_lock_irqsave(&list_lock, flags);
    // 契约:持有锁时,列表结构应有效
    // 这里可能用 WARN_ON 检查列表的 prev/next 指针的合理性(如果存在辅助检查函数)
    list_add_tail(&new_item->node, head);
    shared_count++;
    // 契约:添加后,计数应与列表实际节点数一致(如果有辅助函数可检查)
    // WARN_ON(shared_count != count_list_items(head));
    spin_unlock_irqrestore(&list_lock, flags);
}

5.3 调试与发布版本的权衡

BUG_ON 和 WARN_ON 在编译时可能会根据配置产生不同影响。例如, CONFIG_BUG 选项控制是否启用 BUG_ON 的崩溃机制。在某些极度追求性能或尺寸的嵌入式内核中,可能会关闭此选项,此时 BUG_ON 可能退化为无操作或简单的打印。

对于自己模块内部的检查,可以考虑定义自己的调试宏,以便在发布版本中完全移除检查开销。

#ifdef MYDRV_DEBUG
#define MYDRV_BUG_ON(cond) BUG_ON(cond)
#define MYDRV_WARN_ON(cond) WARN_ON(cond)
#define MYDRV_DBG(fmt, ...) pr_debug(“MYDRV: “ fmt, ##__VA_ARGS__)
#else
#define MYDRV_BUG_ON(cond) do { if (0) { (void)(cond); } } while (0)
#define MYDRV_WARN_ON(cond) do { if (0) { (void)(cond); } } while (0)
#define MYDRV_DBG(fmt, ...) no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

这样,在开发调试阶段(定义 MYDRV_DEBUG ),可以启用完整的断言和调试输出;而在生产版本中,这些代码会被编译器优化掉,实现零开销。

6. 实战案例剖析:一个驱动中的并发BUG排查

让我们通过一个虚构但典型的案例,看看如何利用 WARN_ON 和 BUG_ON 来定位一个棘手的并发问题。

问题描述 :一个字符设备驱动 mycdev ,偶尔会在多进程频繁进行 read 操作时,发生内核崩溃, dmesg 显示 BUG: unable to handle page fault 或 general protection fault ,但调用栈指向的地址看起来是随机的。

初步分析 :随机地址的页错误或保护错误,通常意味着程序访问了一个无效的指针。这很可能是由于 释放后使用(Use-After-Free, UAF) 或 越界访问 引起的。由于问题只在并发 read 时出现,怀疑与驱动内部的数据缓冲区管理有关。

驱动代码片段(有问题的版本) :

struct my_device_data {
    char *buffer;
    size_t size;
    struct mutex lock;
};

static ssize_t mydev_read(struct file *filp, char __user *user_buf, size_t count, loff_t *f_pos) {
    struct my_device_data *data = filp->private_data;
    ssize_t retval = 0;

    // 问题点1:没有在函数开始检查 data 是否有效
    mutex_lock(&data->lock);

    if (*f_pos >= data->size) {
        retval = 0; // EOF
        goto out_unlock;
    }

    if (count > data->size - *f_pos) {
        count = data->size - *f_pos;
    }

    // 问题点2:直接使用 data->buffer,没有检查其是否为 NULL
    if (copy_to_user(user_buf, data->buffer + *f_pos, count)) {
        retval = -EFAULT;
        goto out_unlock;
    }

    *f_pos += count;
    retval = count;

out_unlock:
    mutex_lock(&data->lock); // 错误!这里应该是 mutex_unlock!
    return retval;
}

排查与加固过程 :

  1. 第一步:添加防御性检查和警告 。首先,我们在函数入口和关键点加入 WARN_ON ,以捕获异常状态。

    static ssize_t mydev_read(struct file *filp, char __user *user_buf, size_t count, loff_t *f_pos) {
        struct my_device_data *data = filp->private_data;
        ssize_t retval = 0;
    
        // 防御性检查1:设备数据指针
        if (WARN_ON(!data)) {
            return -ENODEV; // 快速失败
        }
    
        // 防御性检查2:缓冲区指针
        if (WARN_ON(!data->buffer)) {
            return -EIO;
        }
    
        mutex_lock(&data->lock);
        // 契约检查:持有锁后,size 应该合理(例如,不为0或过大)
        if (WARN_ON(data->size > MAX_SANE_SIZE)) {
            mutex_unlock(&data->lock);
            return -EIO;
        }
    

    重新编译加载驱动,并发测试。这次,我们可能没有立刻看到警告,因为崩溃可能发生在更微妙的地方。

  2. 第二步:仔细审查代码,发现低级错误 。看 out_unlock 标签下的代码,一眼就能发现 mutex_lock(&data->lock); 是一个笔误,它应该是 mutex_unlock(&data->lock); 。这是一个 二次上锁 的错误。在非调试内核中, mutex_lock 可能会死锁,或者因为互斥锁的内部状态被破坏而导致后续的访问违规。修复它。

    out_unlock:
        mutex_unlock(&data->lock); // 修正为 unlock
        return retval;
    
  3. 第三步:深入并发场景,发现核心问题 。修复了明显的笔误后,问题可能依然存在。我们需要思考并发场景: read 操作持有 data->lock 。那么, data->buffer 和 data->size 是否会在其他地方被修改?查看驱动的 open 、 release ( close )和可能的 ioctl 函数。

    static int mydev_open(struct inode *inode, struct file *filp) {
        struct my_device_data *data = kmalloc(sizeof(*data), GFP_KERNEL);
        // ... 初始化 data->buffer, data->size, data->lock ...
        filp->private_data = data;
        return 0;
    }
    
    static int mydev_release(struct inode *inode, struct file *filp) {
        struct my_device_data *data = filp->private_data;
        mutex_lock(&data->lock);
        kfree(data->buffer); // 释放缓冲区
        mutex_unlock(&data->lock);
        kfree(data); // 释放设备数据结构本身
        filp->private_data = NULL; // 重要:清空指针
        return 0;
    }
    

    问题浮现 :假设进程A正在执行 read ,已经持有了 data->lock ,并且已经通过了 if (WARN_ON(!data->buffer)) 检查。此时,进程B调用了 close ,进入了 release 函数。 release 也试图获取 data->lock ,因此被阻塞,等待进程A释放锁。 进程A继续执行 copy_to_user(user_buf, data->buffer + *f_pos, count) 。就在这个时刻,进程A可能因为调度而暂时让出CPU。 进程B获得CPU, release 函数终于获得了锁,执行了 kfree(data->buffer) 和 kfree(data) ,并将 filp->private_data 设为 NULL 。 进程A再次被调度,它继续执行 copy_to_user ,但此时 data->buffer 指向的内存已经被释放(UAF)!这会导致向用户空间复制数据时访问非法内存,触发页错误或保护错误,最终可能引发内核崩溃。更糟糕的是, data 指针本身也可能已经被释放,后续任何对 data 的访问(比如 mutex_unlock )都是在访问已释放的内存,行为完全不可预测。

  4. 第四步:引入更严格的断言和设计修正 。问题的根源是 release 操作与未完成的 read 操作之间存在资源生命周期冲突。我们需要一个机制来确保在 release 释放资源时,没有任何其他操作还在使用这些资源。一种常见的方法是 引用计数 。

    struct my_device_data {
        char *buffer;
        size_t size;
        struct mutex lock;
        struct kref refcount; // 增加引用计数
    };
    
    static void mydev_data_release(struct kref *kref) {
        struct my_device_data *data = container_of(kref, struct my_device_data, refcount);
        kfree(data->buffer);
        kfree(data);
    }
    
    static int mydev_open(...) {
        // ... 分配和初始化 data ...
        kref_init(&data->refcount); // 初始化引用计数为1
        filp->private_data = data;
        return 0;
    }
    
    static ssize_t mydev_read(...) {
        struct my_device_data *data = filp->private_data;
        if (!data) return -ENODEV;
    
        // 增加引用计数,防止在read过程中data被释放
        if (!kref_get_unless_zero(&data->refcount)) {
            return -ENODEV; // 对象正在或已经被释放
        }
    
        mutex_lock(&data->lock);
        // ... 检查 buffer 等 ...
        // 核心操作
        mutex_unlock(&data->lock);
    
        // 减少引用计数
        kref_put(&data->refcount, mydev_data_release);
        return retval;
    }
    
    static int mydev_release(...) {
        struct my_device_data *data = filp->private_data;
        if (data) {
            filp->private_data = NULL;
            // 释放本文件描述符持有的引用。
            // 只有当所有引用(来自所有打开的fd和正在进行的操作)都释放时,mydev_data_release才会被调用。
            kref_put(&data->refcount, mydev_data_release);
        }
        return 0;
    }
    

    在这个修正版中, read 操作通过 kref_get_unless_zero 获取了一个引用,这保证了在 read 执行期间, data 对象不会被 release 释放。 release 只是放下它自己的那个引用。真正的释放延迟到最后一个引用被放下时,由 mydev_data_release 回调函数执行。

  5. 第五步:在关键路径加入BUG_ON进行最终验证 。在使用了引用计数后,我们可以加入一个强断言,确保在持有锁时,对象的引用计数至少为1(表示对象有效)。

    static ssize_t mydev_read(...) {
        // ... 获取 data ...
        if (!kref_get_unless_zero(&data->refcount)) {
            return -ENODEV;
        }
    
        mutex_lock(&data->lock);
        // 关键断言:此时引用计数必须 >= 2 (我们刚增加了一个,原open至少有一个)
        // 如果这个断言触发,说明我们的引用计数逻辑有严重错误。
        BUG_ON(kref_read(&data->refcount) < 2);
        // ... 其余操作 ...
    

    这个 BUG_ON 是一个强有力的不变式检查。如果在开发或测试中它被触发,就能立刻告诉我们引用计数的管理出现了严重漏洞,必须修复,而不是让系统带着一个隐蔽的UAF风险运行。

通过这个案例,我们可以看到, WARN_ON 用于早期预警和状态检查(如指针非空),而 BUG_ON 用于捍卫最核心的不变式(如“持有锁时对象必须有效”)。结合防御性编程(入口检查)、契约编程(不变式断言)和正确的并发原语(锁、引用计数),才能构建出真正健壮的内核代码。每一次内核崩溃或警告都不是终点,而是通往更稳定系统的一个路标。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐