顺序锁(seqlock)也是一种特殊的锁机制,读取时不加锁,写入时加锁。 适用于读多写少的场景,并提供了一种高效的读取操作,而写入操作需要保证数据的一致性。
顺序锁在实现上,为了保证读取操作的值没有被更新,顺序锁引入了一个额外的变量,叫顺序号(顺序值/序号值/seq号)。 基本思想是在读取操作之前获取该顺序号,并在读取完成后检查顺序号是否发生变化。如果序列号没有发生变化,则读取操作是有效的,否则需要重新进行读取。 同时,对于写入者,在其开始写入时需要更新该顺序号。
数据结构定义
顺序锁由struct seqlock
结构表示,内核定义如下,位于内核源码/include/linux/seqlock.h
中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* Sequential locks (seqlock_t)
*
* Sequence counters with an embedded spinlock for writer serialization
* and non-preemptibility.
*
* For more info, see:
* - Comments on top of seqcount_t
* - Documentation/locking/seqlock.rst
*/
typedef struct {
/*
* Make sure that readers don't starve writers on PREEMPT_RT: use
* seqcount_spinlock_t instead of seqcount_t. Check __SEQ_LOCK().
*/
seqcount_spinlock_t seqcount;
spinlock_t lock;
} seqlock_t;
相关接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//初始化顺序锁
void seqlock_init(seqlock_t *sl); //本身为宏,语义上是这样的
//读取者侧,读取前获取顺序锁的顺号号操作
unsigned read_seqbegin(const seqlock_t *sl);
//读取者侧,读取完毕后,进行检查,就是再次读取顺序号并与之前读取的顺序号比较,如果顺序号发生改变,需要用户自己重新进行读取过程
int read_seqretry(const seqlock_t *sl, unsigned start);
//写入着侧,获取顺序锁的写入锁,以确保写入操作的原子性。
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_bh(seqlock_t *sl); //中断下半部相关
void write_sequnlock_bh(seqlock_t *sl);
说明:seqlock_t
结构中是有自旋锁spinlock的,主要就是给写入者用的,用来解决写入者与写入者之间的并发写问题,使用的是自旋锁spinlock,所以在写入者加解锁上面, 也一些扩展,线程上下文使用的,中断上下文使用的等几个,这个和spinlock的使用场景是一样的。读取者方面,就没有,因为读取者是用顺序号的。
补充:seqlock还有更多的底层API接口,可以实现更细节的操作,但需要用户自己管理更多的东西,且需要对其实现有深入了解。
参考代码片段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//顺序锁
seqlock_t data_seqlock;
int shared_data;
//读取者
unsigned seq;
while ( (user_condition) ) {
seq = read_seqbegin(&data_seqlock); // 开始读取序列
data_value = shared_data; // 读取共享数据,就是普通的读取
if (read_seqretry(&data_seqlock, seq)) {
// 读取序列验证失败,用户需要重新读取,注意应当是连顺序号一起重新来过
continue;
}
...
}
//写入者
write_seqlock(&data_seqlock); // 获取写入锁,这里面是一个自旋锁
shared_data++; // 写入操作,正常写入
write_sequnlock(&data_seqlock); // 释放写入锁
小结
顺序锁适用于读取操作频繁而写入操作相对较少的场景 ,可以实现高并发读取。
顺序锁并不适用于所有并发读写的场景,特别是当写入操作非常频繁或写入操作的耗时较长时,顺序锁可能导致读取操作长时间阻塞。
seqlock和rwlock还有区别,rwlcok在写入时,和读取者或其他写入者都互斥。seqlock在写的时候只与其他写入者互斥。