Linux驱动-内核锁的介绍与使用
2024年7月 · 预计阅读时间: 4 分钟
#
锁的类型Linux 内核提供了很多类型的锁,它们可以分为两类:
- 自旋锁(spinning lock);
- 睡眠锁(sleeping lock)
#
自旋锁简单地说就是无法获得锁时,不会休眠,会一直循环等待。有这些自旋锁:
自旋锁 | 描述 |
---|---|
raw_spinlock_t | 原始自旋锁(后面讲解) |
bit spinlocks | 位自旋锁(似乎没什么意义) |
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock
。上锁默认情况只是禁止别进程来抢占,但还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
后缀 | 描述 |
---|---|
_bh() | 加锁时禁止下半部(软中断),解锁时使能下半部(软中断) |
_irq() | 加锁时禁止中断,解锁时使能中断 |
_irqsave/restore() | 加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态 |
#
睡眠锁简单地说就是无法获得锁时,当前线程就会休眠。有这些休眠锁:
休眠锁 | 描述 |
---|---|
mutex | mutual exclusion,彼此排斥,即互斥锁(后面讲解) |
rt_mutex | |
semaphore | 信号量、旗语(后面讲解) |
rw_semaphore | 读写信号量,读写互斥,但是可以多人同时读 |
ww_mutex | |
percpu_rw_semaphore | 对 rw_semaphore 的改进,性能更优 |
#
内核关于锁的函数#
自旋锁spinlock 函数在内核文件include\linux\spinlock.h
中声明,如下表:
函数名 | 作用 |
---|---|
spin_lock_init(_lock) | 初始化自旋锁为 unlock 状态 |
void spin_lock(spinlock_t *lock) | 获取自旋锁(加锁),返回后肯定获得了锁 |
int spin_trylock(spinlock_t *lock) | 尝试获得自旋锁,成功获得锁则返回 1,否则返回 0 |
void spin_unlock(spinlock_t *lock) | 释放自旋锁,或称解锁 |
int spin_is_locked(spinlock_t *lock) | 返回自旋锁的状态,已加锁返回 1,否则返回 0 |
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock
,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
后缀 | 描述 |
---|---|
_bh() | 加锁时禁止下半部(软中断),解锁时使能下半部(软中断) |
_irq() | 加锁时禁止中断,解锁时使能中断 |
_irqsave/restore() | 加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态 |
#
信号量 semaphoresemaphore 函数在内核文件include\linux\semaphore.h
中声明,如下表:
函数名 | 作用 |
---|---|
DEFINE_SEMAPHORE(name) | 定义一个 struct semaphore name 结构体, count 值设置为 1 |
void sema_init(struct semaphore *sem, int val) | 初始化 semaphore |
void down(struct semaphore *sem) | 获得信号量,如果暂时无法获得就会休眠 返回之后就表示肯定获得了信号量 在休眠过程中无法被唤醒, 即使有信号发给这个进程也不处理 |
int down_interruptible(struct semaphore *sem) | 获得信号量,如果暂时无法获得就会休眠, 休眠过程有可能收到信号而被唤醒, 要判断返回值: 0:获得了信号量 -EINTR:被信号打断 |
int down_killable(struct semaphore *sem) | 跟 down_interruptible 类似, down_interruptible 可以被任意信号唤醒, 但 down_killable 只能被“fatal signal”唤醒, 返回值: 0:获得了信号量 -EINTR:被信号打断 |
int down_trylock(struct semaphore *sem) | 尝试获得信号量,不会休眠, 返回值: 0:获得了信号量 1:没能获得信号量 |
int down_timeout(struct semaphore *sem, long jiffies) | 获得信号量,如果不成功,休眠一段时间 返回值: 0:获得了信号量 -ETIME:这段时间内没能获取信号量,超时返回 down_timeout 休眠过程中,它不会被信号唤醒 |
void up(struct semaphore *sem) | 释放信号量,唤醒其他等待信号量的进程 |
#
互斥量 mutexmutex 函数在内核文件 include\linux\mutex.h 中声明,如下表:
函数名 | 作用 |
---|---|
mutex_init(mutex) | 初始化一个 struct mutex 指针 |
DEFINE_MUTEX(mutexname) | 初始化 struct mutex mutexname |
int mutex_is_locked(struct mutex *lock) | 判断 mutex 的状态 1:被锁了(locked) 0:没有被锁 |
void mutex_lock(struct mutex *lock) | 获得 mutex,如果暂时无法获得,休眠 返回之时必定是已经获得了 mutex |
int mutex_lock_interruptible(struct mutex *lock) | 获得 mutex,如果暂时无法获得,休眠; 休眠过程中可以被信号唤醒, 返回值: 0:成功获得了 mutex -EINTR:被信号唤醒了 |
int mutex_lock_killable(struct mutex *lock) | 跟 mutex_lock_interruptible 类似, mutex_lock_interruptible 可以被任意信号唤醒, 但 mutex_lock_killable 只能被“fatal signal”唤醒, 返回值: 0:获得了 mutex -EINTR:被信号打断 |
int mutex_trylock(struct mutex *lock) | 尝试获取 mutex,如果无法获得,不会休眠, 返回值: 1:获得了 mutex, 0:没有获得 注意,这个返回值含义跟一般的 mutex 函数相反, |
void mutex_unlock(struct mutex *lock) | 释放 mutex,会唤醒其他等待同一个 mutex 的线程 |
int atomic_dec_and_mutex_lock(atomic_t cnt, struct mutex lock) | 让原子变量的值减 1, 如果减 1 后等于 0,则获取 mutex, 返回值: 1:原子变量等于 0 并且获得了 mutex 0:原子变量减 1 后并不等于 0,没有获得 mutex |
#
semaphore 和 mutex 的区别semaphore 中可以指定 count 为任意值,而 mutex 的值只能设置为 1 或 0。
是不是把 semaphore 的值设置为 1 后,它就跟 mutex 一样了呢?不是的。
看一下 mutex 的结构体定义,如下:
semaphore 并没有这些限制,它可以用来解决“读者-写者”问题:程序 A 在等待数据 ── 想获得锁,程序 B 产生数据后释放锁,这会唤醒 A 来读取数据。semaphore 的锁定与释放,并不限定为同一个进程。
semaphore | mutex | |
---|---|---|
几把锁 | 任意,可设置 | 1 |
谁能解锁 | 别的程序、中断等都可以 | 谁加锁,就得由谁解锁 |
多次解锁 | 可以 | 不可以,因为只有 1 把锁 |
循环加锁 | 可以 | 不可以,因为只有 1 把锁 |
任务在持有锁的期间可否退出 | 可以 | 不建议,容易导致死锁 |
硬件中断、软件中断上下文中使用 | 可以 | 不可以 |
#
何时用何种锁本节参考:https://wenku.baidu.com/view/26adb3f5f61fb7360b4c656e.html
英文原文:https://mirrors.edge.kernel.org/pub/linux/kernel/people/rusty/kernel-locking/
举例简单介绍一下,上表中第一行“IRQ Handler A”和第一列“Softirq A”的交叉点是“spin_lock_irq()”,意思就是说如果“IRQ Handler A”和“Softirq A”要竞争临界资源,那么需要使用“spin_lock_irq()”函数。为什么不能用 spin_lock 而要用 spin_lock_irq?也就是为什么要把中断给关掉?假设在 Softirq A 中获得了临界资源,这时发生了 IRQ A 中断,IRQ Handler A 去尝试获得自旋锁,这就会导致死锁:所以需要关中断。
#
内核抢占的补充早期的的 Linux 内核是“不可抢占”的,假设有 A、B 两个程序在运行,当前是程序 A 在运行,什么时候轮到程序 B 运行呢?
① 程序 A 主动放弃 CPU:
比如它调用某个系统调用、调用某个驱动,进入内核态后执行了 schedule()主动启动一次调度。
② 程序 A 调用系统函数进入内核态,从内核态返回用户态的前夕:
这时内核会判断是否应该切换程序。
③ 程序 A 正在用户态运行,发生了中断:
内核处理完中断,继续执行程序 A 的用户态指令的前夕,它会判断是否应该切换程序。
从这个过程可知,对于“不可抢占”的内核,当程序 A 运行内核态代码时进程是无法切换的(除非程序 A 主动放弃),比如执行某个系统调用、执行某个驱动时,进程无法切换。
这会导致 2 个问题:
① 优先级反转:
一个低优先级的程序,因为它正在内核态执行某些很耗时的操作,在这一段时间内更高优先级的程序也无法运行。
② 在内核态发生的中断不会导致进程切换
为了让系统的实时性更佳,Linux 内核引入了“抢占”(preempt)的功能:进程运行于内核态时,进程调度也是可以发生的。
回到上面的例子,程序 A 调用某个驱动执行耗时的操作,在这一段时间内系统是可以切换去执行更高优先级的程序。
对于可抢占的内核,编写驱动程序时要时刻注意:你的驱动程序随时可能被打断、随时是可以被另一个进程来重新执行。对于可抢占的内核,在驱动程序中要考虑对临界资源加锁。