顺序锁

from:顺序锁

当使用读/写自旋锁时,内核控制路径发出的执行read_lockwrite_lock操作的请求具有相同的优先级,读者必须等待,直到写操作完成,同样,写者也必须等待,直到读操作的完成。


Linux2.6中引入了顺序锁(seqlock),它与读/写自旋锁非常相似,只是它为写者赋予了较高的优先级,事实上,即使在读者正在读的时候也可以允许写者继续运行。这种策略的好处时写者永远不会等待,除非另一个写者也尝试写入同样的一个数据结构。但同样有缺点,缺点时有些时候读者不得不反复多次读取相同的数据结构直到获得它有效的副本。


每个顺序所都是包括两个字段的seqlock_t结构,代码如下:


<include/linux/seqlock.h>

typedef struct {
    unsigned sequence;
    spinlock_t lock;
} seqlock_t;


seqlock_t是一个包含两个字段的结构,其中一个类型为spinlock_tlock锁字段,用于实现锁,而sequence是一个顺序计数器。每个读者都必须在读取数据前后两次读顺序计数器,并检查两次读到的值是否相同,如果不相同,说明新的写者已经开始写并增加了顺序计数器,所以刚才读到的数据结构是无效的。


初始化一个顺序锁的代码如下:


<include/linux/seqlock.h>

#define seqlock_init(x)    do {
        (x)->sequence = 0;
        spin_lock_init(&(x)->lock);
    } while (0)


通过把SEQLOCK_UNLOCKED赋值给变量seqlock_t或执行seqlock_init宏可以将锁初始化为未锁的状态。从上面的代码可以看出,除了初始化锁,也将读取顺序计数器初始化为0。


写者通过调用write_seqlock()write_sequnlock()获取和释放顺序锁,第一个函数获取seqlock_t数据结构中的自旋锁,然后使顺序计数器加1,第二个函数再次增加顺序计数器,然后释放自旋锁,这样可以保证写者在写的过程中,计数器会是技术,并且当没有写者改变数据的时候,计数器的值是偶数。


<include/linux/seqlock.h>

static inline void write_seqlock(seqlock_t *sl)
{
    spin_lock(&sl->lock);
    ++sl->sequence;
    smp_wmb();
}

static inline void write_sequnlock(seqlock_t *sl)
{
    smp_wmb();
    sl->sequence++;
    spin_unlock(&sl->lock);
}


read_seqbegin()返回顺序锁当前的顺序号,如果局部变量seq的值是负数,或者seq的值与顺序所的顺序计数器的当前值不匹配,那么该函数就返回1。


<include/linux/seqlock.h>

static inline unsigned read_seqcount_begin(
    const seqcount_t *s)
{
    unsigned ret;

repeat:
    ret = s->sequence;
    smp_rmb();
    if (unlikely(ret & 1)) {
        cpu_relax();
        goto repeat;
    }
    return ret;
}


当读者进入临界区时,无需禁用内核抢占,另一方面,由于写者获取自旋锁,所以它进入临界区时自动禁用内核抢占。当然,并不是每一种锁都可以使用顺序锁来保护,如果要使用顺序锁,则必须满足以下条件:

  1. 被保护的数据结构不包括被写者和被读者间接引用的指针。

  2. 读者的临界区代码没有副作用1
此外,读者临界区代码应该简短,而且写者应该不常获取顺序所,否则,反复的读访问会引起严重的开销,在Linux中,使用顺序锁的典型例子包括保护一些与系统时间处理相关的数据结构。
  1. 否则多个读者的操作会与单独的读操作有不同的结果。