跳到主要内容位置

Linux驱动-内核锁的介绍与使用

锁的类型#

Linux 内核提供了很多类型的锁,它们可以分为两类:

  • 自旋锁(spinning lock);
  • 睡眠锁(sleeping lock)

自旋锁#

简单地说就是无法获得锁时,不会休眠,会一直循环等待。有这些自旋锁:

自旋锁描述
raw_spinlock_t原始自旋锁(后面讲解)
bit spinlocks位自旋锁(似乎没什么意义)

自旋锁的加锁、解锁函数是:spin_lock、spin_unlock。上锁默认情况只是禁止别进程来抢占,但还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:

后缀描述
_bh()加锁时禁止下半部(软中断),解锁时使能下半部(软中断)
_irq()加锁时禁止中断,解锁时使能中断
_irqsave/restore()加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态

睡眠锁#

简单地说就是无法获得锁时,当前线程就会休眠。有这些休眠锁:

休眠锁描述
mutexmutual 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()加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态

信号量 semaphore#

semaphore 函数在内核文件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)释放信号量,唤醒其他等待信号量的进程

互斥量 mutex#

mutex 函数在内核文件 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 的结构体定义,如下:

struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_MUTEX_SPIN_ON_OWNER)
struct task_struct *owner;
#endif
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
struct mutex {
atomic_long_t owner;
spinlock_t wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
struct list_head wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};

semaphore 并没有这些限制,它可以用来解决“读者-写者”问题:程序 A 在等待数据 ── 想获得锁,程序 B 产生数据后释放锁,这会唤醒 A 来读取数据。semaphore 的锁定与释放,并不限定为同一个进程。

semaphoremutex
几把锁任意,可设置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 调用某个驱动执行耗时的操作,在这一段时间内系统是可以切换去执行更高优先级的程序。

对于可抢占的内核,编写驱动程序时要时刻注意:你的驱动程序随时可能被打断、随时是可以被另一个进程来重新执行。对于可抢占的内核,在驱动程序中要考虑对临界资源加锁。