一、读写自旋锁
现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被修改和读取。此表肯定是数据,那么必须要对其进行保护,如果现在使用自旋锁对其进行保护。每次只能一个读操作或者写操作,但是,实际上此表是可以并发读取的。只需要保证在修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表的读和写不能同时进行,但是可以多人并发的读取此表。像这样,当某个数据结构符合读/写或生产者/消费者模型的时候就可以使用读写自旋锁。读写自旋锁为读和写操作提供了不同的锁,一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。Linux内核使用rwlock_t结构体表示读写锁,结构体定义如下(删除了条件编译):
typedef struct {
arch_rwlock_t raw_lock;
}rwlock t;
读写锁操作API函数分为两部分,一个是给读使用的,一个是给写使用的,这些API函数如下表所示:
函数 | 描述 |
---|---|
DEFINE_RWLOCK(rwlock_t lock) | 定义并初始化读写锁。 |
void rwlock_init(rwlock_t *lock) | 初始化读写锁。 |
读锁
函数 | 描述 |
---|---|
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) | 打开下半部,并释放读锁。 |
写锁
函数 | 描述 |
---|---|
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) | 打开下半部,并释放读锁。 |
二、顺序锁
顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。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) | 初始化顺序锁。 |
顺序锁写操作
函数 | 描述 |
---|---|
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) | 打开下半部,并释放写读锁。 |
顺序锁读操作
函数 | 描述 |
---|---|
unsigned read_seqbegin(const seqlock_t *sl) | 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号。 |
unsigned read_seqretry(const seqlock_t *sl,unsigned start) | 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读。 |
三、自旋锁使用注意事项
- ①、因为在等待自旋锁的时候处于“自旋”状态,因此锁的持有时间不能太长,一定要短,否则的话会降低系统性能。如果临界区比较大,运行时间比较长的话要选择其他的并发处理方式,比如稍后要讲的信号量和互斥体。
- ②、自旋锁保护的临界区内不能调用任何可能导致线程休眠的API函数,否则的话可能导致死锁。
- ③、不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
- ④、在编写驱动程序的时候必须考虑到驱动的可移植性,因此不管用的是单核的还是多核的SOC,都将其当做多核SOC来编写驱动程序。