4. 内核模块编程进阶

基础模块只能完成加载与卸载,而真实驱动往往需要“可配置、可复用、可异步、可响应硬件、可安全并发”。本章围绕这五个进阶能力展开,它们共同构成 Linux 内核模块从“能跑”走向“可交付”的关键台阶。

一、核心原理:五个进阶能力的分工

模块参数让模块在加载时接受外部配置,避免把硬件地址、调试开关写死在代码里;符号导出让模块之间形成依赖,把通用能力封装成可被其他模块调用的接口;内核线程与工作队列解决“不能长时间占用调用上下文”的问题,把耗时或可能睡眠的工作推迟执行;中断处理负责对硬件事件做出快速响应;并发控制则保证多核、中断、软中断同时访问共享数据时的一致性。

这五者并非孤立:中断处理中常需要把下半部交给工作队列,工作队列又常由内核线程承载,而共享数据访问必须加锁。理解它们的协作关系,比单独记忆 API 更重要。

内核模块进阶能力协作关系 模块参数 module_param / sysfs 符号导出 EXPORT_SYMBOL 并发控制 mutex / spinlock 内核线程 kthread_run / kthread_stop 工作队列 schedule_work / 延迟工作 中断处理 request_irq / 上半部+下半部 共享数据 多核 / 中断 / 软中断

二、方法流程:从参数到并发的落地步骤

  1. 定义模块参数:使用 module_param 或 module_param_array,并声明权限。加载时通过 insmod xxx.ko irq=32 debug=1 传入,运行时可从 /sys/module/xxx/parameters/ 查看或修改。
  2. 导出符号:在提供方用 EXPORT_SYMBOL 或 EXPORT_SYMBOL_GPL 导出函数,使用方直接调用,但需保证加载顺序,否则出现 Unknown symbol。
  3. 创建内核线程:用 kthread_run 启动,线程函数内循环检查 kthread_should_stop(),卸载时调用 kthread_stop 回收。
  4. 使用工作队列:定义 work_struct,在中断或线程中调用 schedule_work 或 queue_delayed_work,把可睡眠的工作放到进程上下文执行。
  5. 注册中断:用 request_irq 申请中断号,处理函数中只做快速操作,耗时部分通过工作队列或 tasklet 下推。
  6. 加锁保护:进程上下文用 mutex,中断上下文用 spin_lock_irqsave,并确保锁的获取顺序一致,避免死锁。

三、案例场景:一个带中断的字符设备模块

假设模块需要响应外部按键中断,并把按键事件写入环形缓冲区,用户读取时返回数据。模块参数用于配置中断号和缓冲区大小;中断处理函数只记录时间戳并调度工作队列;工作队列把事件写入缓冲区;内核线程负责周期性检查缓冲区水位;缓冲区读写用自旋锁保护。

static int irq_num = 32;
static int buf_size = 64;
module_param(irq_num, int, 0644);
module_param(buf_size, int, 0644);

static irqreturn_t key_isr(int irq, void *dev)
{
    schedule_work(&key_work);
    return IRQ_HANDLED;
}

static void key_work_fn(struct work_struct *work)
{
    unsigned long flags;
    spin_lock_irqsave(&buf_lock, flags);
    /* 写入环形缓冲区 */
    spin_unlock_irqrestore(&buf_lock, flags);
}

static int __init demo_init(void)
{
    int ret = request_irq(irq_num, key_isr, IRQF_SHARED,
                          "demo_key", &demo_dev);
    if (ret)
        return ret;
    return 0;
}
中断处理函数运行在原子上下文,不能睡眠,因此不能直接调用 mutex_lock、kmalloc(GFP_KERNEL) 或 copy_to_user。需要睡眠的操作必须交给工作队列或内核线程。

四、误区与复盘

常见误区后果正确做法
在中断中调用可能睡眠的函数内核崩溃或死锁下推到工作队列或 tasklet
忘记释放中断和线程卸载失败、资源泄漏在 exit 中 free_irq、kthread_stop
符号导出后加载顺序错误Unknown symbol先加载提供方,或用 modprobe 依赖
用普通变量保护共享数据多核竞争、数据错乱按上下文选择 mutex 或 spinlock
模块参数权限过大运行时被误改导致异常只读参数用 0444,调试参数用 0644
并发控制不是“加一把锁就完事”。要明确锁保护的数据范围、锁的上下文限制以及锁的嵌套顺序。中断上下文只能使用自旋锁,且必须使用 irqsave 变体防止本地中断重入。

复盘时建议按“参数是否可配、符号是否可复用、异步是否可回收、中断是否够短、并发是否安全”五个问题逐项检查。只有这五点都通过,模块才具备进入真实驱动项目的基础条件。