Linux内核同步:自旋锁与中断处理深度解析
1. 内核测试与自旋锁规则
在标准“发行版”系统和内核(如Ubuntu 22.04 LTS VM)上进行测试时,即使未将内核配置为“调试”内核(即未设置
CONFIG_DEBUG_ATOMIC_SLEEP
内核配置选项),较新的发行版内核仍能捕获原子调度时的错误。例如,在Ubuntu 22.04(内核版本5.19.0 - 45 - generic)和Fedora 38(内核版本6.5.6 - 200.fc38.x86_64)上测试时就有此现象,而在早期的Ubuntu 20.04 VM标准内核(5.4)上则未捕获该错误。
LDV项目有关于自旋锁的规则:
- 不允许两次获取
spin_lock
。
- 不允许释放未获取的
spin_lock
。
- 所有
spin_lock
最后都应被释放。
- 不允许通过
spin_unlock
/
spin_unlock_irqrestore
函数重复释放锁。
违反这些规则可能导致系统不稳定,如驱动程序中尝试两次释放自旋锁的实际错误示例。
2. 锁与中断的场景分析
在驱动程序开发中,当实现读取方法时,可能会遇到非阻塞的关键部分,通常使用自旋锁来保护。但如果在读取方法的关键部分期间,设备的硬件中断触发,就可能出现问题。下面分析几种场景:
| 场景 | 描述 | 是否有数据竞争 |
| ---- | ---- | ---- |
| 场景1 | 驱动的中断处理程序仅使用局部变量,即使读取方法处于关键部分,中断处理也会很快完成,控制权会交还给被中断的部分,无数据竞争。 | 否 |
| 场景2 | 驱动的中断处理程序处理(全局)共享可写数据,但与读取方法使用的共享数据项不同,无冲突和数据竞争,但中断处理程序的关键部分需用自旋锁保护。 | 否 |
| 场景3 | 驱动的中断处理程序和读取方法处理相同(或部分相同)的共享可写数据,存在数据竞争,需要锁保护。 | 是 |
3. 自旋锁在不同场景中的使用问题
3.1 错误的读取方法示例
/* Driver read method ; WRONG ! */
driver_read(...) << time t0 >>
{
[ ... ]
spin_lock(&slock);
<<--- time t1 : start of read method critical section >>
... << critical section: operating on global data object gctx >> ...
spin_unlock(&slock);
<<--- time t2 : end of read method critical section >>
[ ... ]
} << time t3 >>
3.2 不同场景下的详细分析
- 场景1:驱动方法和硬件中断处理程序顺序执行
handle_interrupt(...) << time t4; hardware interrupt fires! >>
{
[ ... ]
spin_lock(&slock);
<<--- time t5: start of interrupt critical section >>
... << critical section: operating on global data object gctx >> ...
spin_unlock(&slock);
<<--- time t6 : end of interrupt critical section >>
[ ... ]
} << time t7 >>
此场景中硬件中断在读取方法关键部分完成后触发,没有问题,但不能依赖这种运气来保证系统安全。
-
场景2:驱动方法和硬件中断处理程序交错执行
- 单核心(UP)系统 :若硬件中断在读取方法关键部分期间触发,中断处理程序会尝试获取已被读取方法锁定的自旋锁,导致死锁,因为读取方法被中断抢占无法解锁。
- 多核心(SMP)系统 :自旋锁在SMP系统中更直观。读取方法在核心1上运行,中断在核心2上触发,中断处理程序会等待读取方法完成关键部分解锁后继续执行。
4. 解决交错执行问题的方法
4.1 使用
spin_[un]lock_irq()
API变体
#include <linux/spinlock.h>
void spin_lock_irq(spinlock_t *lock);
spin_lock_irq()
API会屏蔽本地CPU核心上的硬件中断(除不可屏蔽中断外),在驱动的读取方法中使用此API可避免硬件中断抢占关键部分。其对应的解锁API是
spin_unlock_irq()
。
/* Driver read method ; CORRECT ! */
driver_read(...) << time t0 >>
{
[ ... ]
spin_lock_irq(&slock); // Note: we're using the _irq version of the spinlock!
<<--- time t1 : start of critical section >>
[now all interrupts + kernel preemption on local CPU core are masked (disabled)]
... << critical section: operating on global data object gctx >> ...
spin_unlock_irq(&slock);
<<--- time t2 : end of critical section >>
[now all interrupts + kernel preemption on local CPU core are unmasked (enabled)]
[ ... ]
} << time t3 >>
4.2 场景3:部分中断被屏蔽,驱动方法和硬件中断处理程序交错执行
在复杂项目中,可能有开发者提前设置了硬件中断掩码,而另一位开发者使用
spin_lock_irq()
API保护关键部分时,会将本地CPU核心上的所有中断屏蔽,恢复时可能错误地将中断掩码设置为全1,破坏项目设置。
解决方法是使用
spin_lock_irqsave()
和
spin_unlock_irqrestore()
宏:
#include <linux/spinlock.h>
unsigned long spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);
spinlock_t slock;
spin_lock_init(&slock);
[ ... ]
driver_read(...)
{
[ ... ]
spin_lock_irqsave(&slock, flags);
<<--- time t1 : start of critical section >>
[now the CPU state is saved; as a side effect, all interrupts + kernel
preemption on the local CPU core are masked (disabled) ]
<< ... critical section ... >>
spin_unlock_irqrestore(&slock, flags);
<<--- time t2 : end of critical section >>
[now the CPU state is restored; as a side effect, only the previously masked
interrupts + kernel preemption on the local CPU core are unmasked (enabled) ]
[ ... ]
}
5. 总结
在处理内核同步问题时,尤其是涉及自旋锁和中断时,需要仔细考虑各种场景,选择合适的API来避免数据竞争和死锁。在不同系统(单核心和多核心)中,自旋锁的行为有所不同,要根据具体情况进行处理。同时,在复杂项目中,要注意中断掩码的设置和恢复,避免因不当操作破坏项目的原有设置。
下面是一个简单的mermaid流程图,展示了在驱动读取方法中使用自旋锁处理中断的基本流程:
graph TD;
A[开始读取方法] --> B[获取自旋锁];
B --> C{是否有硬件中断触发};
C -- 否 --> D[执行关键部分];
C -- 是 --> E{使用何种自旋锁API};
E -- spin_lock --> F[死锁风险];
E -- spin_lock_irq --> G[屏蔽中断,继续执行关键部分];
D --> H[释放自旋锁];
G --> H;
F --> F;
H --> I[结束读取方法];
通过以上分析和示例代码,希望能帮助开发者更好地理解和使用自旋锁来处理内核中的同步问题。
Linux内核同步:自旋锁与中断处理深度解析
6. 自旋锁API的详细分析
在前面的内容中,我们介绍了几种自旋锁相关的API,下面详细分析它们的特点和使用场景。
| API名称 | 功能描述 | 使用场景 |
|---|---|---|
spin_lock
| 普通的自旋锁加锁操作,不处理中断相关问题 | 当确定不会有中断干扰关键部分时使用 |
spin_lock_irq
| 加锁的同时屏蔽本地CPU核心上的硬件中断(除不可屏蔽中断) | 可能有硬件中断在关键部分触发,需要避免中断抢占的情况 |
spin_lock_irqsave
| 加锁并保存当前CPU状态,屏蔽本地CPU核心上的硬件中断和内核抢占 | 在复杂项目中,已有部分中断被屏蔽,需要保护关键部分且不破坏原有中断掩码设置的情况 |
spin_unlock
| 普通的自旋锁解锁操作 |
与
spin_lock
配对使用
|
spin_unlock_irq
| 解锁并恢复本地CPU核心上的硬件中断和内核抢占 |
与
spin_lock_irq
配对使用
|
spin_unlock_irqrestore
| 解锁并恢复之前保存的CPU状态,恢复相应的中断和内核抢占 |
与
spin_lock_irqsave
配对使用
|
7. 中断下半部与自旋锁的使用
接下来我们讨论如何在中断下半部使用自旋锁。中断下半部通常用于处理一些耗时的操作,避免中断处理程序执行时间过长影响系统性能。
在中断下半部使用自旋锁时,同样需要考虑数据竞争和死锁的问题。一般来说,中断下半部的处理函数可以使用与中断上半部相同的自旋锁来保护共享数据。
以下是一个简单的示例代码,展示了如何在中断下半部使用自旋锁:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>
spinlock_t slock;
irqreturn_t interrupt_handler(int irq, void *dev_id)
{
// 上半部处理,快速响应中断
spin_lock(&slock);
// 处理共享数据
spin_unlock(&slock);
// 触发下半部处理
tasklet_schedule(&my_tasklet);
return IRQ_HANDLED;
}
void my_tasklet_function(unsigned long data)
{
spin_lock(&slock);
// 下半部处理,处理耗时操作
spin_unlock(&slock);
}
DECLARE_TASKLET(my_tasklet, my_tasklet_function, 0);
static int __init my_module_init(void)
{
spin_lock_init(&slock);
// 注册中断处理程序
request_irq(irq_number, interrupt_handler, IRQF_SHARED, "my_interrupt", &my_dev_id);
return 0;
}
static void __exit my_module_exit(void)
{
// 释放中断处理程序
free_irq(irq_number, &my_dev_id);
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
8. 总结与最佳实践
通过前面的内容,我们对内核中自旋锁和中断处理有了更深入的理解。下面总结一些最佳实践:
- 仔细设计锁的使用 :在使用锁之前,仔细分析代码的关键部分和可能的并发情况,选择合适的锁类型和API。
-
避免死锁
:在设计锁的使用时,要避免出现死锁的情况。例如,在单核心系统中,使用
spin_lock时要确保不会出现中断抢占导致的死锁。 -
注意中断掩码的设置
:在复杂项目中,要注意已有中断掩码的设置,使用
spin_lock_irqsave和spin_unlock_irqrestore来保存和恢复CPU状态,避免破坏原有设置。 - 合理使用中断下半部 :对于耗时的操作,尽量放在中断下半部处理,避免中断处理程序执行时间过长影响系统性能。
下面是一个mermaid流程图,展示了在设计驱动程序时选择自旋锁API的基本流程:
graph TD;
A[开始设计驱动程序] --> B{是否可能有中断干扰关键部分};
B -- 否 --> C[使用spin_lock和spin_unlock];
B -- 是 --> D{是否已有部分中断被屏蔽};
D -- 否 --> E[使用spin_lock_irq和spin_unlock_irq];
D -- 是 --> F[使用spin_lock_irqsave和spin_unlock_irqrestore];
C --> G[完成设计];
E --> G;
F --> G;
通过遵循这些最佳实践,开发者可以更好地处理内核中的同步问题,提高系统的稳定性和性能。希望本文能为大家在处理内核同步问题时提供一些帮助。
超级会员免费看
851

被折叠的 条评论
为什么被折叠?



