大家好,我是王鸽,今天分享一篇文章关于Linux驱动的内核态的锁。Linux 中的锁可以按照作用范围和实现机制分为以下几类:
1.1 按作用范围分类
进程级锁
用于协调不同进程之间的资源访问。
线程级锁
用于协调同一进程内不同线程之间的资源访问。
1.2 按实现机制分类
内核态锁
由内核直接实现,用于内核代码中的同步。
用户态锁
由用户空间程序实现,依赖内核提供的系统调用。
如下图
这篇文章主要讲的内核态的锁,内核态锁主要用于内核代码中的同步,防止多个 CPU 核心或中断上下文同时访问共享资源。
对于内核而言,任何一种锁机制都是需要解决多核通信问题的。对于除了RCU锁以外的其他锁而言,它们最低的开销也至少是一对原子访存指令,核心数越多,其原子访存开销也会跟着以O(N2)的级别增长。
在Linux内核驱动中,有多种同步机制,用于保护共享资源,防止竞态条件。以下是常见的锁类型及其区别:一、自旋锁 (Spinlock)
工作原理:忙等待,循环检查锁状态,不会让出CPU
适用场景:
临界区很小(执行时间短)
中断上下文(不可睡眠)
多核系统
特点:
持有锁时不能睡眠
开销小,无上下文切换
可能导致CPU空转浪费
DEFINE_SPINLOCK(my_lock); spin_lock(&my_lock); // 临界区spin_unlock(&my_lock);
- 自旋锁只适用于临界区代码比较短的情况,因为自旋等待的过程会占用CPU资源。
- 自旋锁不可重入,也就是说,如果一个进程已经持有了自旋锁,那么它不能再次获取该自旋锁。
- 在持有自旋锁的情况下,应该尽量避免调用可能会导致调度的内核函数,比如睡眠函数,因为这可能会导致死锁的发生。
- 在使用自旋锁的时候,应该尽量避免嵌套使用不同类型的锁,比如自旋锁和读写锁,因为这可能会导致死锁的发生。
- 当临界区代码较长或者需要睡眠时,应该使用信号量或者读写锁来代替自旋锁。
二、互斥锁 (Mutex)
工作原理:阻塞等待,获取不到锁时任务睡眠
适用场景:
临界区可能较长
进程上下文(可睡眠)
特点:
可睡眠,会进行上下文切换
不可在中断上下文使用
支持优先级继承(防止优先级反转)
struct mutex { raw_spinlock_t wait_lock; struct list_head wait_list; struct task_struct *owner; int recursion;#ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_mapdep_map;#endif};
互斥锁的使用非常简单,通常只需要调用两个函数即可完成:
DEFINE_MUTEX(my_mutex); mutex_lock(&my_mutex); // 临界区 mutex_unlock(&my_mutex);
三、信号量 (Semaphore)
工作原理:计数信号量,允许多个任务同时访问
适用场景:
控制对多个资源的访问
生产者-消费者问题
特点:
计数机制(非0/1二元)
可睡眠,可用于进程间同步
有up()和down()操作
DECLARE_SEMAPHORE(my_sem, 5); // 初始值5down(&my_sem); // P操作,减1// 访问资源 up(&my_sem); // V操作,加1
四、读写信号量 (rwsem)
工作原理:读写锁,允许多个读者或单个写者
适用场景:
读多写少的共享数据
需要区分读写操作
特点:
读者可并发,写者互斥
读者优先或写者优先(可配置)
可睡眠
DECLARE_RWSEM(my_rwsem); down_read(&my_rwsem); // 读者 // 读操作up_read(&my_rwsem); down_write(&my_rwsem); // 写者 // 写操作 up_write(&my_rwsem);
五、完成变量 (Completion)
工作原理:用于一个任务等待另一个任务完成
适用场景:
等待异步操作完成
线程/模块初始化同步
特点:
简单的一次性同步机制
类似信号量但更轻量
DECLARE_COMPLETION(my_comp); // 等待方 wait_for_completion(&my_comp); // 完成方complete(&my_comp);
六、原子操作 (Atomic Operations)
工作原理:硬件级别的原子指令
适用场景:
简单的计数器操作
标志位设置/清除
不需要复杂锁的简单操作
特点:
无锁机制,性能高
只能用于整数或位操作
不可睡眠
atomic_t counter = ATOMIC_INIT(0); atomic_inc(&counter); // 原子加1 atomic_dec(&counter); // 原子减1atomic_read(&counter); // 原子读
七、对比总结
锁类型 | 睡眠能力 | 适用上下文 | 特点 | 性能开销 |
|---|
自旋锁 | 不可睡眠 | 中断/进程 | 忙等待,短临界区 | 低(无切换) |
互斥锁 | 可睡眠 | 进程上下文 | 阻塞等待,可处理长临界区 | 中等(有切换) |
信号量 | 可睡眠 | 进程上下文 | 计数机制,资源控制 | 中等 |
读写信号量 | 可睡眠 | 进程上下文 | 读写分离,读并发 | 中等 |
完成变量 | 可睡眠 | 进程上下文 | 一次性同步 | 低 |
原子操作 | 不可睡眠 | 任何上下文 | 无锁,硬件原子指令 | 最低 |
八、选择建议
中断上下文 → 自旋锁 或 原子操作
短临界区(<µs级) → 自旋锁 或 原子操作
长临界区或可能睡眠 → 互斥锁
读多写少 → 读写锁/读写信号量
资源计数 → 信号量
简单标志/计数器 → 原子操作
等待任务完成 → 完成变量
九、使用注意事项
避免死锁:注意锁的获取顺序
防止优先级反转:互斥锁支持优先级继承
性能考量:锁的粒度要适中,避免过度保护
可重入性:某些锁不可重入(如自旋锁)
调试支持:内核提供lockdep等工具检测锁问题
举个生活中的例子
当我们在买咖啡的时候,柜台前可能会有一个小桶,上面写着“请取走您需要的糖果,每人一颗”这样的字样。这个小桶就是一个信号量,它限制了每个人能够取走的糖果的数量,从而保证了公平性。如果我们把这个小桶换成互斥锁,那么就可以只允许一个人在柜台前取走糖果。如果使用读写锁,那么在非高峰期的时候,多个人可以同时取走糖果,但在高峰期时只允许一个人取走。而如果我们把这个小桶换成自旋锁,那么当有人在取走糖果时,其他人就需要一直在那里等待,直到糖果被取走为止。这样可能会造成浪费时间的情况,因为其他人可能有更紧急的事情需要处理。区别总结:
自旋锁和互斥锁都是互斥锁,但自旋锁忙等待,互斥锁睡眠等待。
信号量可以设置计数,实现多个任务同时访问,而互斥锁只能互斥。
读写信号量是信号量的变种,优化了读多写少的场景。
完成变量用于任务间的同步,而不是互斥。
原子操作是最轻量级的,但只能用于简单的变量操作。
在选择同步机制时,需要考虑临界区的大小、是否在中断上下文、是否需要睡眠、以及并发访问的模式(读多写少等)等因素。