十七、内核中的锁机制
参考链接:linux驱动并发与竞态
Linux内核中的同步机制:原子操作、信号量、读写信号量和自旋锁的API。
一、引言
在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。
在主流的Linux内核中包含了几乎所有现代的操作系统具有的同步机制,这些同步机制包括:原子操作、信号量(semaphore)、读写信号量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4内核中)、RCU(只包含在2.6内核中)和seqlock(只包含在2.6内核中)
二、原子操作
1、概述
所谓原子操作,就是该操作绝不会在执行完毕前被任何其他任务或事件打断,也就说,它的最小的执行单位,不可能有比它更小的执行单位,因此这里的原子实际是使用了物理学里的物质微粒的概念。
原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都定义在内核源码树的include/asm/atomic.h文件中,它们都使用汇编语言实现,因为C语言并不能实现这样的操作。
2、API函数介绍
(1)定义整型原子变量并设置初始值
1 2 3 4 5 6 7 | typedef struct { int counter; }atomic_t #define ATOMIC_INIT(i) {(i)} atomic_t data = ATOMIC_INIT( int i); |
参数:
- i:设置整型原子变量的初始值。
atomic_t类型:用它定义整型原子变量
(2)设置整型原子变量的值
1 | #define aotmic_set(v,i) WRITE_ONCE(((v)->counter),(i)) |
参数:
- v:要设置的整型原子变量的指针(地址)。
- 设置的值。
(3)获取原子变量的值
1 2 3 | #define atomic_read(v) ACCESS_ONCE((v)->counter) atomic_read(atomic_t *v) |
参数:
- v:要设置的整型原子变量的指针(地址)。
返回值:
获取到的整型原子变量的值。
(4)整型原子变量的加/减
1 2 | void atomic_add( int i, atomic_t *v); //整型原子变量加i void atomic_sub( int i, atomic_t *v); //整型原子变量减i |
参数:
-
v:要设置的整型原子变量的指针(地址)。
-
i: 设置的值。
(5)整型原子变量的自增/自减
1 2 | void atomic_inc(atomic_t *v); //整型原子变量自增1 void atomic_dec(atomic_t *v); //整型原子变量自减1 |
参数:
-
v:要设置的整型原子变量的指针(地址)。
(6)整型原子变量操作返回函数(宏定义)
1 2 | int atomic_add_return( int i, atomic_t *v); //返回增加后的结果 int atomic_sub_return( int i, atomic_t *v); //返回减少后的结果 |
参数:
-
v:要设置的整型原子变量的指针(地址)。
-
i: 要进行加/减操作的大小
返回值:
返回加/减计算后的结果。
(7)整型原子变量测试函数
1 2 3 4 5 6 | /*整型原子变量减i后是否为0*/ #define atomic_sub_and_test(i, v) (atomic_sub_return((i), (v)) == 0) /*整型原子变量自减1后是否为0*/ #define atomic_dec_and_test(v) (atomic_dec_return(v) == 0) /*整型原子变量自加1后是否为0*/ #define atomic_inc_and_test(v) (atomic_inc_return(v) == 0) |
参数:
-
v:要设置的整型原子变量的指针(地址)。
-
i:要进行加/减操作的大小
返回值:
进行运算后的结果如果为0,则返回true,否则返回false
三、信号量
1、概念
信号量是一种休眠锁,当一个进程获取不到某个信号量,进入休眠状态,放弃CPU,调度器调度其他进程执行。当先获取到信号量的进程释放信号量,因获取不到信号量而休眠的进程会被唤醒,继续执行。
2、使用方法
(1)定义信号量
1 | struct semaphore led_sem; |
(2)初始化
1 | static inline void sema_init( struct semaphore *sem, int val) |
(3)获取信号量
1 2 | void down( struct semaphore *sem);---获取不到,休眠,不能被信号唤醒 int __must_check down_interruptible( struct semaphore *sem);---获取不到,休眠,可以被信号唤醒 |
(4)释放信号量
1 | void up( struct semaphore *sem); |
3、特点
- 信号量适用于锁会被长时间持有的情况。
- 持有信号量锁的线程可以睡眠,而持有自旋锁的线程是不允许睡眠的。
- 信号锁不会禁止内核抢占。
- 最重要的信号锁同时允许任意数量的锁持有者,而自旋锁在同一时刻最多只允许一个任务持有他。
四、自旋锁
1、概念
顾名思义,自旋,表示原地打转。当进程A获得自旋锁,可以进入临界区,访问临界资源,此时进程B也想获得自旋锁去访问临界资源,但是自旋锁已经被进程A占用,进程B只能“自旋”。自旋不是休眠,它没有放弃CPU的使用权,是一个忙等的状态。
2、基本概念
(1)临界资源:共享的,但是不能同时访问的资源。
(2)临界区:访问临界资源的代码段。
3、自旋锁的使用
(1)定义自旋锁
1 | static DEFINE_SPINLOCK(wdt_lock); //自旋锁的静态初始化 |
1 | spin_lock_init(spinlock_t *lock) //动态初始化 |
(2)尝试获取锁
1 | static __always_inline int spin_trylock(spinlock_t *lock) |
尝试获取一次,获取成功返回"true",获取失败返回"false"。程序继续往下执行,不会忙等待。而spin_lock如果没有获取锁成功则忙等待
(3)获取自旋锁(上锁)
1 | static inline void spin_lock(spinlock_t *lock)------获取成功,继续往下执行,否则原地打转 |
(4)释放自旋锁
1 | static inline void spin_unlock(spinlock_t *lock) |
4、自旋锁使用的注意事项
- 在中断处理程序中,可以使用自旋锁,不会造成中断处理程序的休眠阻塞。
- 自旋锁保护的临界资源,访问时间不能太长,否则会造成其他想获取锁的进程 占着CPU空转,浪费CPU资源。
- 一个进程获取到自旋锁A后,不要尝试再次获取自旋锁A,否则会造成死锁。
- 在释放一个自旋锁之前,不要尝试获取另外一个自旋锁,否则也可能死锁。
五、互斥锁
1、概念
互斥锁也是休眠锁,可以认为是一个二值信号量,只有上锁和解锁两种情况
2、互斥锁的使用
(1)定义互斥锁
1 | struct mutex xxx_mutex; |
(2)初始化
1 | mutex_init(&xxx_mutex); |
(3)加锁
1 | void __sched mutex_lock( struct mutex *lock) |
(4)解锁
1 | void __sched mutex_unlock( struct mutex *lock) |
六、自旋锁和互斥锁的应用区别
- 自旋锁是忙等锁,不休眠;互斥锁是休眠锁
- 自旋锁可以用于中断处理程序;互斥锁或信号量不能
- 自旋锁持有时间要短,持有期间不能休眠;信号量或互斥锁可以长时间持有,并持有期间可以休眠
- 使用时都应注意避免死锁
- 自旋锁持有期间,内核不能抢占;信号量持有期间可以被抢占
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2019-01-03 嵌入式基础知识