【精通内核】Linux内核seq锁实现原理与源码解析

367 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第11天,点击查看活动详情

前言

📫作者简介小明java问道之路,专注于研究 Java/ Liunx内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。

📫热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长

🏆InfoQ签约作者、CSDN专家博主/后端领域优质创作者/内容合伙人、阿里云专家/签约博主、51CTO专家🏆

🔥如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~


本文导读

前面的都是比较容易理解的普通互斥锁、自旋锁、信号量、读写锁,本问seq锁是在之前的锁实现上做了一些不一样的操作。

一、Linux内核seq锁实现原理

这里的 seq 叫作顺序锁,为什么叫顺序锁?就是对一个 unsigned sequence 变量执行操作。它的特点在哪呢?读和写不会产生阻塞。我们知道在读写锁中,当读锁持有时,写者被阻塞;而这里持有读锁时还能写,也就是读写互不影响。它是如何做到的呢?来看源码实现。

二、Linux内核seq锁源码解析

1、怎么用 seq 锁

do {
    seq=read_seqbegin(&foo); 
    
    // do Something

}while(read_seqretry(&foo, seq));

2、seq锁源码解析

如果理解了自旋锁、信号量、读写锁后,理解seq 锁就非常简单,其实就是让读者自己去做判断是否更改了数据,如果更改了那么自己重试去吧。

思考一下,写者之间互斥吗?

答案是互斥的,上述读锁、上锁,未看到自旋锁,所以写者互斥。

对于 seq 锁有什么场景限制吗?

答案是有场景限制。想象一下,如果seq锁被用于指针结构,那么会发生什么?A读者读取了M指针并正在操作,然而B写者将M指针结束了。这对吗?所以seq锁不能用于指针类型

seq 锁结构:声明一个无符号整型大小的变量sequence,自旋锁用于保护上面的sequence,不妨大胆猜测写写互斥。

这两个宏相当简单,和之前的自旋锁一样,初始化一个 seq 锁结构 #define SEQLOCK_UNLOCKED {0, SPIN_LOCK_UNLOCKED }、#define seqlock_init(x)

// seq 锁结构 
typedef struct{
    unsigned sequence;	// 声明一个无符号整型大小的变量sequence	
    spinlock_t lock;	// 自旋锁用于保护上面的sequence,不妨大胆猜测写写互斥	
}seqlock_t;

// 这两个宏相当简单,和之前的自旋锁一样,初始化一个 seq 锁结构
#define SEQLOCK_UNLOCKED {0, SPIN_LOCK_UNLOCKED }
#define seqlock_init(x) 
do { 
    *(x) =(seqlock_t) SEQLOCK_UNLOCKED; 
} while (0)

// 持有写锁
static inline void write_seqlock(seqlock_t*sl) {
    // 上自旋锁,这在之前已讲过,这里就不多说了 
    spin lock(&sl->lock);

    // 针对 sequence++,由于初始值为 0,不难看出,当 seq 为奇数时,为写者持有状态,为偶数时为无写状态
    ++sl->sequence;

    // 全屏障,保证指令有序性 
    smp_wmb();
}
    
// 写顺序释放锁
static inline void write_sequnlock(seqlock_t *sl) {
    smp_wmb();
    // 这里会变为偶数 
    sl->sequence++;
    spin_unlock(&sl->lock);
}

// 无阻塞的尝试获取写锁,其实就是调用spin_trylock
static inline int write_tryseqlock(seqlock_t *sl) {
    int ret =spin_trylock(&sl->lock); 
    if(ret) {
        ++sl->sequence; 
        smp_wmb();
    }
    return ret;
}

// 这里并没上锁,而是使用了rmb读屏障。
// 这是为何?其实读屏障是为了保证对sequence的读不会被重排序到后面的读操作之后。
// 读sequence后返回是为何?很简单,用来比较,想想为何在读者读数据时,
// 允许写操作进入执行修改操作?就是在读完并操作完后,读者通过这里读的变量与当前的sequence 做比较,
// 如果不相等,是不是就说明了操作完成时有写者更改了状态,那么怎么办?直接重试执行即可
static inline unsigned read seqbegin(const seqlockt*sl) {
    unsigned ret=sl->sequence; 
    smp_rmb(); 
    return ret;
}

// 用于判断在readseqbegin期间有没有写者更改了变量,如果之前的iv也就是读者开始读操作时保存的seq值为奇数
// 直接重试。如果当前的sequence异或之前保存的seq值不为0的话,即不相等的话,也须重试
static inline int read_seqretry(const seqlock_t*sl,unsigned iv) {
    smp_rmb();
    return (iv &1)|(sl->sequence ^iv);
}

持有写锁,上自旋锁,这在之前已讲过,这里就不多说了,针对 sequence++,由于初始值为 0,不难看出,当 seq 为奇数时,为写者持有状态,为偶数时为无写状态。

无阻塞的尝试获取写锁,其实就是调用spin_trylock。

这里并没上锁,而是使用了rmb读屏障。这是为何?其实读屏障是为了保证对sequence的读不会被重排序到后面的读操作之后。

用于判断在readseqbegin期间有没有写者更改了变量,如果之前的iv也就是读者开始读操作时保存的seq值为奇数直接重试。如果当前的sequence异或之前保存的seq值不为0的话,即不相等的话,也须重试。

总结

前面的都是比较容易理解的普通互斥锁、自旋锁、信号量、读写锁,本问seq锁是在之前的锁实现上做了一些不一样的操作。全屏障,保证指令有序性 。

读sequence后返回是为何?很简单,用来比较,想想为何在读者读数据时,

允许写操作进入执行修改操作?就是在读完并操作完后,读者通过这里读的变量与当前的sequence 做比较。

如果不相等,是不是就说明了操作完成时有写者更改了状态,那么怎么办?直接重试执行即可。