4. 内核模块编程进阶
基础模块只能完成加载与卸载,而真实驱动往往需要“可配置、可复用、可异步、可响应硬件、可安全并发”。本章围绕这五个进阶能力展开,它们共同构成 Linux 内核模块从“能跑”走向“可交付”的关键台阶。
一、核心原理:五个进阶能力的分工
模块参数让模块在加载时接受外部配置,避免把硬件地址、调试开关写死在代码里;符号导出让模块之间形成依赖,把通用能力封装成可被其他模块调用的接口;内核线程与工作队列解决“不能长时间占用调用上下文”的问题,把耗时或可能睡眠的工作推迟执行;中断处理负责对硬件事件做出快速响应;并发控制则保证多核、中断、软中断同时访问共享数据时的一致性。
这五者并非孤立:中断处理中常需要把下半部交给工作队列,工作队列又常由内核线程承载,而共享数据访问必须加锁。理解它们的协作关系,比单独记忆 API 更重要。
二、方法流程:从参数到并发的落地步骤
- 定义模块参数:使用
module_param或module_param_array,并声明权限。加载时通过insmod xxx.ko irq=32 debug=1传入,运行时可从/sys/module/xxx/parameters/查看或修改。 - 导出符号:在提供方用
EXPORT_SYMBOL或EXPORT_SYMBOL_GPL导出函数,使用方直接调用,但需保证加载顺序,否则出现Unknown symbol。 - 创建内核线程:用
kthread_run启动,线程函数内循环检查kthread_should_stop(),卸载时调用kthread_stop回收。 - 使用工作队列:定义
work_struct,在中断或线程中调用schedule_work或queue_delayed_work,把可睡眠的工作放到进程上下文执行。 - 注册中断:用
request_irq申请中断号,处理函数中只做快速操作,耗时部分通过工作队列或 tasklet 下推。 - 加锁保护:进程上下文用
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 变体防止本地中断重入。复盘时建议按“参数是否可配、符号是否可复用、异步是否可回收、中断是否够短、并发是否安全”五个问题逐项检查。只有这五点都通过,模块才具备进入真实驱动项目的基础条件。