Home 内核-seqlock(顺序锁)
Post
Cancel

内核-seqlock(顺序锁)

顺序锁(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在写的时候只与其他写入者互斥。

This post is licensed under CC BY 4.0 by the author.

内核-mutex(互斥锁)

内核-RCU