顺序锁替换读写锁

在内核中,顺序锁和读写锁比较相似,都是针对多读少写且快速处理的临界区的锁机制。

  对于 rwlock 而言,rwlock的全称是"reader-writer spin lock",和普通的spinlock不同,它对"read"和"write"的操作进行了区分。如果当前没有writer,那么多个reader可以同时获取这个rwlock。如果当前没有任何的reader,那么一个writer可以获取这个rwlock。rwlock限定了reader与writer之间,以及writer与writer之间的互斥,但它没有限定reader与reader之间的互斥。

  看起来rwlock比原生的spinlock控制更加精细,应用起来应该更加高效对不对;但rwlock存在一个问题,如果现在有多个reader在使用某个rwlock,那么writer需要等到所有的reader都释放了这个rwlock,才可以获取到,这容易造成writer执行的延迟,俗称饥饿(starve)。

  而且,在writer等待期间,reader还可以不断地加入进来执行,这对writer来说实在是太不公平了。即便writer的优先级更高,也不能先于优先级更低的reader执行,身份(是reader还是writer)决定一切。

【seqlock】

  seqlock是由Stephen Hemminger负责开发,自Linux 2.6版本引入的,其全称是"sequential lock"。相比起rwlock,它进一步解除了reader与writer之间的互斥,只保留了writer与writer之间的互斥。只要没有其他的writer持有这个seqlock(即便当前存在reader持有该seqlock),那么第一个试图获取该seqlock的writer就可以成功地持有。

  • 那如果在reader读取共享变量期间,writer对变量进行了修改,岂不是会造成读取数据的不一致?
typedef struct {
    struct seqcount seqcount;
    spinlock_t lock;
} seqlock_t;

数据结构存在两个成员,一个是锁变量 sequence,另一个是 spinlock 成员,该 spinlock 是用作写者之间的互斥的。 

开始写入

每当有writter持有seqlock之后,sequence number的值就会加1

/*
 * Lock out other writers and update the count.
 * Acts like a normal spin_lock/unlock.
 * Don't need preempt_disable() because that is in the spin_lock already.
 */
static inline void write_seqlock(seqlock_t *sl)
{
    spin_lock(&sl->lock);
    s->sequence++;
    smp_wmb();
}

最后设置一个内存屏障以保证加锁操作对所有 CPU 可见(数据刷新到memory;barrier() 的作用就是告诉编译器,内存中变量的值已经改变了,之前保存与寄存器或cache中的变量副本无效,如果访问该变量需要直接去内存中读取)

 结束写入

 当writer释放seqlock之前,sequence number的值会再次加1:

 

static inline void write_seqcount_end(seqcount_t *s)
{
    smp_wmb();
    s->sequence++;
        spin_unlock(&s->lock);      
}

sequence number的初始值是一个偶数(even),因而当writer持有spinlock时,sequence number的值将是一个奇数(odd),释放后则又变成偶数。

 

开始读取

reader在读取一个共享变量之前,需要先读取一下sequence number的值,如果为奇数,说明现在有writer正在修改这个变量,需要等待,直到sequence number变为偶数,才可以开始读取变

/**
 * __read_seqcount_begin - begin a seq-read critical section (without barrier)
 * @s: pointer to seqcount_t
 * Returns: count to be passed to read_seqcount_retry
 *
 * __read_seqcount_begin is like read_seqcount_begin, but has no smp_rmb()
 * barrier. Callers should ensure that smp_rmb() or equivalent ordering is
 * provided before actually loading any of the variables that are to be
 * protected in this critical section.
 *
 * Use carefully, only in critical code, and comment how the barrier is
 * provided.
 */
static inline unsigned __read_seqcount_begin(const seqcount_t *s)
{
    unsigned ret;

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

 

结束读取

读取变量之后,reader需要再次读取一下sequence number的值,并和读取之前的sequence number的值进行比较,看是否相等,相等则说明在此期间没有writer的操作

static inline int __read_seqcount_retry(const seqcount_t *s, unsigned start)
{
    return unlikely(s->sequence != start);
}

 

适用场景

理论上,reader可以随时读(相当于在读取一侧没有加锁)。在这一过程中,writer不会受到什么影响,但reader可能就需要多读几次。一个writer只会被其他writer造成starve,而不再会被reader造成starve。

显然,其设计策略是倾向于writer的。它适用于reader数量较多,而writer数量较少的场景。其在Linux中的一个重要应用就是表示时间的jiffies(jiffies记录了系统启动后的时钟节拍的数目)。

do {
    seq = read_seqbegin(&jiffies_lock);
    ret = jiffies_64;
} while (read_seqretry(&jiffies_lock, seq));
    do {
        __skb_pull(skb, skb_network_offset(skb));
        seq = read_seqbegin(&neigh->ha_lock);
        err = dev_hard_header(skb, dev, ntohs(skb->protocol),
                      neigh->ha, NULL, skb->len);
    } while (read_seqretry(&neigh->ha_lock, seq));
  • spinlock的“一读或一写”
  • rwlock的“多读或一写”
  • seqlock的“多读和一写”
  • RCU“多读和多写”
 

 

posted @ 2022-03-09 10:59  codestacklinuxer  阅读(7)  评论(0编辑  收藏  举报