在Linux内核驱动开发中,并发控制和中断处理是两个最核心也最容易出问题的技术点。我经历过无数次半夜被叫起来处理驱动程序导致的系统崩溃,90%的问题都源于这两个机制的误用。本文将结合我15年内核开发经验,带你彻底掌握这些机制的原理和正确使用方法。
并发控制要解决的核心问题是:当多个执行路径(进程、中断、内核线程等)同时访问共享数据时,如何保证数据一致性。而中断处理则面临一个基本矛盾:中断响应必须快速完成,但实际工作往往耗时较长。Linux用"中断上下半部"的机制来化解这个矛盾。
理解这些机制的重要性怎么强调都不为过。一个典型的案例是某厂家的网卡驱动曾因为并发控制不当,导致每百万个数据包就会丢失1-2个,这种隐蔽bug在实验室根本测不出来,上线后造成了巨大损失。
竞态条件(race condition)是指多个执行路径以不可预测的顺序访问共享数据,导致结果依赖于执行时序的情况。在内核中,竞态可能来自:
临界区(critical section)是必须被保护的代码段,这些代码访问共享资源且不能被打断。识别临界区是并发编程的第一步,也是最容易出错的地方。我常用的方法是:
原子操作是最轻量级的保护手段,适用于简单的计数器等场景。常用API包括:
c复制atomic_t v = ATOMIC_INIT(0);
atomic_inc(&v);
atomic_dec_and_test(&v);
注意:原子操作只能保护变量本身,如果要保护变量之间的关系(如a==b),必须使用锁。
自旋锁的特点是获取不到锁时会忙等待,适用于:
基本用法:
c复制DEFINE_SPINLOCK(my_lock);
spin_lock(&my_lock);
/* 临界区 */
spin_unlock(&my_lock);
实际开发中容易犯的错误:
信号量是睡眠锁,适用于:
典型用法:
c复制DECLARE_MUTEX(my_mutex);
down(&my_mutex);
/* 临界区 */
up(&my_mutex);
当数据结构读多写少时,读写锁可以提升并发性能:
c复制rwlock_t my_rwlock = RW_LOCK_UNLOCKED;
read_lock(&my_rwlock);
/* 只读临界区 */
read_unlock(&my_rwlock);
write_lock(&my_rwlock);
/* 写临界区 */
write_unlock(&my_rwlock);
选择锁类型时考虑以下因素:
| 因素 | 自旋锁 | 信号量 |
|---|---|---|
| 临界区长度 | 短(<100指令) | 长 |
| 可睡眠 | 不行 | 可以 |
| 持有锁时调度 | 禁止 | 允许 |
| 使用场景 | 中断上下文 | 进程上下文 |
经验法则:
中断处理程序(ISR)有两个硬性约束:
这就导致了一个矛盾:很多硬件事件需要大量后续处理(如网络包处理)。Linux的解决方案是将中断处理分为上半部(top half)和下半部(bottom half)。
上半部是在中断上下文中执行的部分,典型工作包括:
注册中断处理程序的正确方式:
c复制irqreturn_t my_isr(int irq, void *dev_id) {
/* 处理硬件 */
return IRQ_WAKE_THREAD; // 唤醒下半部
}
/* 驱动初始化时 */
request_threaded_irq(irq, my_isr, my_thread_fn,
IRQF_SHARED, "mydev", dev);
常见错误:
Linux提供了三种下半部机制:
软中断是性能最高的机制,但有以下限制:
内核网络子系统就大量使用软中断。
tasklet基于软中断实现,但简化了接口:
典型用法:
c复制void my_tasklet_fn(unsigned long data) {
/* 下半部处理 */
}
DECLARE_TASKLET(my_tasklet, my_tasklet_fn, 0);
/* 在上半部调度 */
tasklet_schedule(&my_tasklet);
工作队列是最通用的机制:
现代驱动推荐使用:
c复制struct work_struct my_work;
void my_work_fn(struct work_struct *work) {
/* 可以睡眠的处理 */
}
INIT_WORK(&my_work, my_work_fn);
/* 在上半部调度 */
schedule_work(&my_work);
| 特性 | 软中断 | tasklet | 工作队列 |
|---|---|---|---|
| 执行上下文 | 中断 | 中断 | 进程 |
| 可睡眠 | 否 | 否 | 是 |
| 并发性 | 可能并行 | 串行化 | 可配置 |
| 延迟 | 最低 | 低 | 较高 |
| 适用场景 | 极高性能需求 | 一般中断处理 | 耗时任务 |
实际项目中的经验选择:
我们以一个虚拟的字符设备为例,展示如何正确实现并发控制。设备维护一个内部缓冲区,支持多进程并发读写。
c复制struct my_device {
char buffer[BUFFER_SIZE];
int read_idx, write_idx;
spinlock_t lock; // 保护缓冲区索引
wait_queue_head_t readq; // 读等待队列
struct fasync_struct *async_queue; // 异步通知
};
读实现示例:
c复制ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *ppos) {
struct my_device *dev = filp->private_data;
DEFINE_WAIT(wait);
int ret = 0;
spin_lock_irq(&dev->lock);
while (dev->read_idx == dev->write_idx) {
prepare_to_wait(&dev->readq, &wait, TASK_INTERRUPTIBLE);
spin_unlock_irq(&dev->lock);
schedule();
spin_lock_irq(&dev->lock);
finish_wait(&dev->readq, &wait);
if (signal_pending(current)) {
ret = -ERESTARTSYS;
goto out;
}
}
/* 实际拷贝数据 */
ret = copy_to_user(buf, dev->buffer + dev->read_idx,
min(count, dev->write_idx - dev->read_idx));
if (!ret) {
dev->read_idx += count;
ret = count;
}
out:
spin_unlock_irq(&dev->lock);
return ret;
}
设备中断处理示例:
c复制static irqreturn_t my_interrupt(int irq, void *dev_id) {
struct my_device *dev = dev_id;
unsigned long flags;
int data;
data = read_hardware();
spin_lock_irqsave(&dev->lock, flags);
if (dev->write_idx < BUFFER_SIZE) {
dev->buffer[dev->write_idx++] = data;
wake_up_interruptible(&dev->readq);
kill_fasync(&dev->async_queue, SIGIO, POLL_IN);
}
spin_unlock_irqrestore(&dev->lock, flags);
return IRQ_HANDLED;
}
内核并发编程中最头疼的就是死锁。常见死锁模式:
调试技巧:
| 工具 | 用途 | 示例 |
|---|---|---|
| lockdep | 锁顺序验证 | CONFIG_PROVE_LOCKING=y |
| ftrace | 跟踪锁事件 | echo 1 > /proc/sys/kernel/ftrace_enabled |
| perf | 锁争用分析 | perf lock record -a -- sleep 10 |
| printk | 简单调试 | pr_info("lock %p acquired\n", &lock) |
某存储控制器驱动曾出现随机崩溃问题,最终发现是:
解决方案:
这个案例告诉我们:内核编程必须时刻清楚当前执行上下文,以及每个API的约束条件。