【Linux驱动开发】016 自旋锁

一、自旋锁简介

原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形变量或位这么简单的临界区(所谓临界区就是共享数据段)。比如,设备结构体变量就不是整型变量,我们对于结构体中成员变量的操作也要保证原子性,这些工作上面的原子操作都不能胜任,需要本节要讲的锁机制,在 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; 

注意事项:

  1. 如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。
  2. 顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生。

相关 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) 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读 


四、自旋锁使用注意事项

  1. 因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。
  2. 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。 
  3. 不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了! 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kashine

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值