Linux内核与驱动:5.并发与竞争

张开发
2026/5/16 20:30:00 15 分钟阅读
Linux内核与驱动:5.并发与竞争
本节先介绍在Linux驱动程序中的并发与竞争的处理再介绍应用层与驱动层并发与竞争的区别。1.在Linux驱动中什么会产生竞争在 Linux 内核中竞争通常源于以下四个方面多核并发 (SMP)多个物理 CPU 核同时运行不同的进程或中断。内核抢占 (Preemption)高优先级的进程抢占当前正在执行的低优先级进程。硬件中断 (Interrupts)硬件触发中断强制 CPU 停止当前任务转而执行中断处理函数ISR。软中断与 Tasklet内核的异步延迟执行机制。2.并发处理机制本节将系统介绍 Linux 驱动中解决并发竞争的四种常用机制原子变量、自旋锁、信号量和互斥锁包括原理、适用场景及典型代码示例。2.1原子变量Atomic Variables原子变量是一种特殊的整型变量其读写、加减、测试等操作由硬件保证在单条指令内完成不会被中断或其它 CPU 核心打断。Linux 内核提供 atomic_t 类型以及一系列原子操作函数。适用场景计数器如统计设备打开的引用计数、丢包计数等。标志位仅需进行“读取-修改-写回”且不涉及复杂数据结构的场景。常用API#include linux/types.h atomic_t cnt ATOMIC_INIT(0); // 读原子变量 int val atomic_read(cnt); // 加/减 atomic_inc(cnt); // 自增 1 atomic_dec(cnt); // 自减 1 atomic_add(2, cnt); // 加 2 atomic_sub(1, cnt); // 减 1 // 测试并设置如果当前值为 0则设置为 1 并返回 true if (!atomic_add_unless(cnt, 1, 1)) { // 已经是 1无法设置 } // 设置新值 atomic_set(cnt, 10);优点不涉及线程切换不消耗多余 CPU效率极高。2.2自旋锁spinlock原理:自旋锁是一种忙等待锁。当线程尝试获取已被占用的自旋锁时它会在一个循环中不断检测锁的状态“自旋”直到锁被释放。自旋期间该线程不会睡眠也不会让出 CPU。在单核非抢占内核中自旋锁退化为空操作因为一次只能有一个线程运行只需关中断即可。在多核系统中自旋锁通常会在获取前禁用内核抢占防止其他 CPU 上的进程抢占当前线程导致死锁。核心特性持有自旋锁期间绝对不能睡眠不能调用schedule()、copy_from_user()、mutex_lock()等可能阻塞的函数否则其他等待锁的 CPU 会永远自旋系统死锁。由于自旋锁是忙等的所以自旋锁的临界区尽可能的短快进快出。适用场景临界区极短几个内存访问、几条指令锁持有时间远小于两次上下文切换开销。保护中断上下文与进程上下文共享的数据需结合spin_lock_irqsave()禁止本地中断。SMP 环境下对共享数据结构的快速更新如链表、哈希表节点插入删除。常用API:#include linux/spinlock.h DEFINE_SPINLOCK(my_lock); // 静态初始化 spinlock_t my_lock; spin_lock_init(my_lock); // 动态初始化 // 普通版本假定不会在中断中使用 spin_lock(my_lock); /* 临界区 */ spin_unlock(my_lock); // 禁用本地中断的版本防止中断处理程序争用 unsigned long flags; spin_lock_irqsave(my_lock, flags); /* 临界区 */ spin_unlock_irqrestore(my_lock, flags);自旋锁不允许持有者睡眠的原因典型的死锁场景 (The Deadlock Trap)假设你在 CPU A 上持有了自旋锁然后调用了 msleep() 睡着了CPU A进入睡眠调度器被迫切换到了进程 B。进程 B恰好也需要访问被这个自旋锁保护的资源于是它也调用了 spin_lock()。由于自旋锁是“忙等”锁进程 B会在CPU A上疯狂原地打转自旋等待锁释放。关键点持有锁的那个进程还在睡眠队列里躺着呢它需要CPU A重新调度它才能继续运行并释放锁。但此时CPU A正在被进程 B的自旋动作 100% 占用。结果进程 B 永远等不到锁持有锁的进程永远回不来。CPU A 彻底锁死。2.3信号量我们讲自旋锁不允许休眠是忙等的信号量是一种可以引起休眠的锁。它通常包含一个计数器允许 N 个执行单元同时进入临界区如果 N1则等同于互斥锁。信号量就相当于钥匙如果有5把钥匙就是可以同时有五个人可以访问。行为拿不到锁时进程会进入休眠状态释放 CPU 给其他任务。适用场景临界区执行时间较长涉及磁盘 IO 或大量内存拷贝。限制严禁在中断上下文使用。因为中断不能休眠。常用API:#include linux/semaphore.h struct semaphore sem; sema_init(sem, 1); // 初始值 1互斥信号量类似于互斥锁 sema_init(sem, 5); // 初始值 5允许 5 个并发 // 获取信号量可被信号中断返回 -EINTR if (down_interruptible(sem)) { return -ERESTARTSYS; } /* 临界区 */ up(sem); // 释放信号量 // 不可中断版本较少用因为难以终止进程 down(sem);2.4互斥锁Mutex互斥锁mutex是信号量的一种特例计数值为 1但它有更严格的约束和更优的性能。互斥锁也导致进程睡眠但比信号量更轻量且增加了调试检查例如不能在中段上下文使用、不能递归获取、持有者必须由同一线程释放适用场景标准的互斥访问一次只有一个线程进入临界区。临界区可能较长但不需要计数功能。进程上下文中保护共享资源。常用API:#include linux/mutex.h DEFINE_MUTEX(my_mutex); // 静态初始化 struct mutex my_mutex; mutex_init(my_mutex); // 动态初始化 mutex_lock(my_mutex); /* 临界区 */ mutex_unlock(my_mutex); // 可被信号中断的版本 if (mutex_lock_interruptible(my_mutex)) { return -ERESTARTSYS; }2.5对比需求场景推荐方案理由仅保护一个简单的整数原子变量效率最高无锁开销临界区极短且可能在中断中使用自旋锁中断不能休眠自旋锁是唯一选择临界区很长涉及 IO 操作互斥锁避免长时间占用 CPU允许系统调度允许多个读者同时访问仅写者互斥读写锁/RCU优化高频读取场景的性能3.驱动并发与应用并发的区别在 Linux 系统中应用层User Space与驱动层Kernel Space在处理并发Concurrency与竞争Race Condition时虽然目标一致都是保护共享资源但其实现机制、约束条件和底层逻辑有着天壤之别。以下从五个维度对两者的差异进行详细拆解1. 竞争对手的“身份”差异这是理解两者区别的根基。在应用层你面对的干扰相对单一而在驱动层干扰无处不在。应用层竞争对手进程/线程多线程同一个进程内的线程抢夺全局变量。多进程不同进程通过共享内存或文件进行竞争。特点所有的竞争者都受操作系统调度器Scheduler管辖行为可预测。驱动层竞争对手上下文/中断/硬件进程上下文不同应用进程通过系统调用进入内核同时操作同一个驱动。中断上下文正在执行驱动代码时硬件突然触发中断中断处理函数ISR强行插入并修改数据。多核抢占SMP在多核 CPU 上核 A 在运行驱动核 B 也在运行同样的驱动代码两者物理并行。软中断/Tasklet内核的异步延迟执行机制。2. 同步机制的“工具箱”对比A. 互斥锁Mutex与信号量Semaphore应用层调用pthread_mutex_lock。如果拿不到锁线程会被操作系统挂起休眠直到锁释放。驱动层也存在struct mutex。但关键区别在于驱动层互斥锁绝对不能在中断上下文中使用。因为中断上下文没有进程实体一旦进入休眠调度内核就无法再切回中断现场导致系统“挂死”。B. 自旋锁Spinlock——驱动层的“核心杀手锏”应用层极少使用虽然有pthread_spin_lock。因为应用层无法关抢占如果持锁线程被切走其他线程会白白空转 CPU。驱动层必选方案。逻辑拿不到锁时不睡觉而是原地打转自旋。进阶版spin_lock_irqsave。它在加锁的同时关闭本地 CPU 中断。这是为了防止进程拿到锁后被中断抢占而中断里也想拿这个锁导致“原地自旋死锁”。C. RCU (Read-Copy-Update) ——驱动层的“黑科技”应用层很难实现通常需要复杂的库支持。驱动层广泛使用。它的核心思想是“读者不加锁写者先拷贝”。在读取路由表、设备列表等读多写少的场景下性能几乎是无损的。3. “睡眠”的权限差异这是开发者最容易犯错的地方。应用层几乎任何时候都可以睡眠。不管是等 IO、等锁还是手动sleepOS 会帮你处理好上下文切换。驱动层严禁随意睡眠。持有自旋锁时严禁睡眠因为别人正在原地等你你一睡大家一起死。处于中断上下文时严禁睡眠没有进程实体无法被调度器唤醒。而在应用层你永远不需要关心自己是否处于“中断上下文”。4. 硬件层面的控制力应用层完全没有硬件控制权。你无法告诉 CPU“现在不要响应键盘中断”。驱动层拥有最高权限。关中断Local IRQ Disable驱动可以暂时关闭当前 CPU 的中断响应从而在物理上消灭“被中断打断”的可能性。关抢占Preemption Disable可以告诉内核“在我这段代码跑完前不要把我切走”。5. 内存顺序与屏障Memory Barriers在现代多核处理器中为了性能CPU 会对指令进行重排。应用层通常不需要担心。编译器和高级语言如 C11 的std::atomic已经帮你封装好了内存模型。驱动层必须手动处理。当你操作硬件寄存器时顺序至关重要。比如必须先给硬件写数据再发“启动”指令。驱动开发者必须显式使用wmb()写屏障、rmb()读屏障来强制 CPU 按照代码顺序执行否则硬件会因为指令重排而行为异常。

更多文章