一、自旋锁简介
原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区(所谓临界区就是共享数据段)。比如,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,这些工作上面的原子操作都不能胜任,需要本节要讲的锁机制,在 Linux内核中就是自旋锁。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。
例如:
自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环-旋转-等待状态,线程 B 不会进入休眠状态或者说去做其他的处理,而是会一直傻傻的在那里“转圈圈”的等待锁可用。
自旋锁的“自旋”也就是“原地打转”的意思。从这里我们可以看到自旋锁的一个缺点:等待自旋锁的线程会一直处于自旋状态,这样会浪费处理器时间,降低系统性能,所以自旋的持有时间不能太长,即自旋锁适用于短时期的轻量级加锁。
Linux 内核使用结构体 spinlock_t 表示自旋锁,先定义一个自旋锁变量,定义好自旋锁变量以后就可以使用相应的 API 函数来操作自旋锁:
spinlock_t lock; //定义自旋锁
二、自旋锁 API 函数
最基本的自旋锁 API 函数:
函数 | 描述 |
DEFINE_SPINLOCK(spinlock_t lock) | 定义并初始化一个自旋变量。 |
int spin_lock_init(spinlock_t *lock) | 初始化自旋锁。 |
void spin_lock(spinlock_t *lock) | 获取指定的自旋锁,也叫做加锁。 |
void spin_unlock(spinlock_t *lock) | 释放指定的自旋锁。 |
int spin_trylock(spinlock_t *lock) | 尝试获取指定的自旋锁,如果没有获取到就返回 0 |
int spin_is_locked(spinlock_t *lock) | 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。 |
自旋锁API函数适用于 SMP(多处理器) 或支持抢占的单CPU 下线程之间的并发访问,也就是用于线程与线程之间。(自旋锁本来就是为了解决并发访问共享资源的问题)
注意事项一:被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API 函数,否则的话会可能会导致死锁现象的发生。(不允许别人访问,自己还休眠不使用)
注意事项二:自旋锁可以在中断中使用,因此在使用自旋锁之前要禁止中断,不然会产生死锁。
线程 A 先运行(可能是中断外,也可能是中断中),并且获取到了 lock 这个锁,当线程 A 运行 functionA 函数的时候中断(如果线程A本身就是中断线程,那么此处中断是更高级的中断)发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁,但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完之前,线程 A 是不可能执行的,线程 A 说“你先放手”,中断说“你先放手”,场面就这么僵持着,死锁发生!
最好的解决方法就是获取锁之前关闭本地中断,Linux 内核提供了相应的 API 函数:
函数 | 描述 |
void spin_lock_irq(spinlock_t *lock) | 禁止本地中断,并获取自旋锁。 |
void spin_unlock_irq(spinlock_t *lock) | 激活本地中断,并释放自旋锁。 |
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | 保存中断状态,禁止本地中断,并获取自旋锁。 |
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。 |
Linux内核十分庞大,难确定某个时刻的中断状态,而spin_lock_irq / spin_unlock_irq 无法保存中断状态,因此不推荐使用。建议使用 spin_lock_irqsave/ spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。
// 使用示例
DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */
/* 线程 A */
void functionA (){
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags) /* 获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}
/* 中断服务函数 */
void irq() {
spin_lock(&lock) /* 获取锁 */
/* 临界区 */
spin_unlock(&lock) /* 释放锁 */
}
下半部(BH)也会竞争共享资源,有些资料也会将下半部叫做底半部,如果要在下半部里面使用自旋锁,可以使用 API 函数:
函数 | 描述 |
void spin_lock_bh(spinlock_t *lock) | 关闭下半部,并获取自旋锁。 |
void spin_unlock_bh(spinlock_t *lock) | 打开下半部,并释放自旋锁。 |
三、其他类型的锁
在自旋锁的基础上还衍生出了其他特定场合使用的锁,这些锁在驱动中其实用的不多,更多的是在 Linux 内核中使用。
1、读写自旋锁
前面讲的自旋锁,每次只能一个读操作或者写操作,但是,实际上是可以并发读取的。只需要保证在修改的时候没人读取,或者在其他人读取的时候没有人修改即可。(也就是此表的读和写不能同时进行,但是可以多人并发的读取此表)。
读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。
Linux 内核使用 rwlock_t 结构体表示读写锁:
typedef struct {
arch_rwlock_t raw_lock;
} rwlock_t;
相关 API 函数如下:
函数 | 描述 |
DEFINE_RWLOCK(rwlock_t lock) | 定义并初始化读写锁 |
void rwlock_init(rwlock_t *lock) | 初始化读写锁。 |
读锁 API 函数:
函数 | 描述 |
void read_lock(rwlock_t *lock) | 获取读锁 |
void read_unlock(rwlock_t *lock) | 释放读锁。 |
void read_lock_irq(rwlock_t *lock) | 禁止本地中断,并且获取读锁。 |
void read_unlock_irq(rwlock_t *lock) | 打开本地中断,并且释放读锁。 |
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) | 保存中断状态,禁止本地中断,并获取读锁。 |
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。 |
void read_lock_bh(rwlock_t *lock) | 关闭下半部,并获取读锁。 |
void read_unlock_bh(rwlock_t *lock) | 打开下半部,并释放读锁。 |
写锁 API 函数:
函数 | 描述 |
void write_lock(rwlock_t *lock) | 获取写锁。 |
void write_unlock(rwlock_t *lock) | 释放写锁。 |
void write_lock_irq(rwlock_t *lock) | 禁止本地中断,并且获取写锁。 |
void write_unlock_irq(rwlock_t *lock) | 打开本地中断,并且释放写锁。 |
void write_lock_irqsave(rwlock_t *lock, unsigned long flags) | 保存中断状态,禁止本地中断,并获取写锁。 |
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。 |
void write_lock_bh(rwlock_t *lock) | 关闭下半部,并获取读锁。 |
void write_unlock_bh(rwlock_t *lock) | 打开下半部,并释放读锁。 |
2、顺序锁
使用读写锁的时候读操作和写操作不能同时进行,使用顺序锁可以允许在写的时候进行读操作,但是不允许同时进行并发的写操作。
Linux 内核使用 seqlock_t 结构体表示顺序锁:
typedef struct {
struct seqcount seqcount;
spinlock_t lock;
} seqlock_t;
注意事项:
- 如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。
- 顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生。
相关 API 函数如下:
函数 | 描述 |
DEFINE_SEQLOCK(seqlock_t sl) | 定义并初始化顺序锁 |
void seqlock_ini seqlock_t *sl) | 初始化顺序锁。 |
顺序锁写操作 API 函数如下:
函数 | 描述 |
void write_seqlock(seqlock_t *sl) | 获取写顺序锁。 |
void write_sequnlock(seqlock_t *sl) | 释放写顺序锁。 |
void write_seqlock_irq(seqlock_t *sl) | 禁止本地中断,并且获取写顺序锁 。 |
void write_sequnlock_irq(seqlock_t *sl) | 打开本地中断,并且释放写顺序锁。 |
void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags) | 保存中断状态,禁止本地中断,并获取写顺序锁。 |
void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁。 |
void write_seqlock_bh(seqlock_t *sl) | 关闭下半部,并获取写读锁。 |
void write_sequnlock_bh(seqlock_t *sl) | 打开下半部,并释放写读锁。 |
顺序锁读操作 API 函数如下:
函数 | 描述 |
unsigned read_seqbegin(const seqlock_t *sl) | 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号。 |
unsigned read_seqretry(const seqlock_t *sl, unsigned start) | 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 |
四、自旋锁使用注意事项
- 因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。
- 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。
- 不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!