顺序锁(seqlock)

2020-03-24 11:03:47 浏览数 (2)

顺序锁的引入

前面学习了spin_lock可以知道,spin_lock对于临界区是不做区分的。而读写锁是对临界区做读写区分,并且度进程进入临界区的几率比较大,因为写进程进入时需要等待读进程退出临界区。而有没有一种方法,可以保护写进程的优先权,使得写进程可以更快的获得锁? 答案是有的,就是顺序锁。

顺序锁的原理

顺序锁的设计思想是:对某一个共享数据读取的时候不加锁,写的时候加锁。同时为了保证读取的过程中因为写进程修改了共享区的数据,导致读进程读取数据错误。在读取者和写入者之间引入了一个整形变量sequence,读取者在读取之前读取sequence, 读取之后再次读取此值,如果不相同,则说明本次读取操作过程中数据发生了更新,需要重新读取。而对于写进程在写入数据的时候就需要更新sequence的值。

也就是说临界区只允许一个write进程进入到临界区,在没有write进程的话,read进程来多少都可以进入到临界区。但是当临界区没有write进程的时候,write进程就可以立刻执行,不需要等待,

顺序锁的定义

Linux内核中使用seqlock_t表示顺序锁

代码语言:javascript复制
typedef struct {
	struct seqcount seqcount;
	spinlock_t lock;
} seqlock_t;

typedef struct seqcount {
	unsigned sequence;
} seqcount_t;

lock: spinlock变量,用来保证sequence操作的原子性。

seqcount: 无符号整形变量,用来在read和write进程之间做协调,更新使用。

如果想静态定义一个顺序锁的话,使用如下方法:

代码语言:javascript复制
#define __SEQLOCK_UNLOCKED(lockname)			
	{						
		.seqcount = SEQCNT_ZERO(lockname),	
		.lock =	__SPIN_LOCK_UNLOCKED(lockname)	
	}

#define DEFINE_SEQLOCK(x) 
		seqlock_t x = __SEQLOCK_UNLOCKED(x)

如果想动态定义一个顺序锁的话,使用如下方法:

代码语言:javascript复制
#define seqlock_init(x)					
	do {						
		seqcount_init(&(x)->seqcount);		
		spin_lock_init(&(x)->lock);		
	} while (0)

顺序锁API

写操作:

  • write_seqlock / write_sequnlock #获得锁 / 释放锁
  • write_seqlock_irq / write_sequnlock_irq #获得锁的同时disable中断 / 释放锁,enable中断
  • write_seqlock_bh / write_sequnlock_bh #获得锁同时disable中断下半部 / 释放锁,enable中断下半部
  • write_seqlock_irqsave / write_sequnlock_irqrestore #获得锁,保存中断标志位,disable中断 / 恢复中断标志,enable中断,释放锁

读操作:

  • read_seqlock_excl/ read_sequnlock_excl #获得锁和释放锁,导致read和write进程都无法获得锁,但是不更新sequence的值
  • read_seqbegin_or_lock #判断临界区是否有write进程,如果有的话就获取锁

顺序锁的实现

写入者的上锁操作:

代码语言:javascript复制
/*
 * 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);
	write_seqcount_begin(&sl->seqcount);
}

锁住临界区,同时更新sequence的值,但是不需要preempt_disbale,因为在spin_lock中已经关闭了抢占。

代码语言:javascript复制
static inline void write_seqcount_begin(seqcount_t *s)
{
	write_seqcount_begin_nested(s, 0);
}

static inline void write_seqcount_begin_nested(seqcount_t *s, int subclass)
{
	raw_write_seqcount_begin(s);
	seqcount_acquire(&s->dep_map, subclass, 0, _RET_IP_);
}

static inline void raw_write_seqcount_begin(seqcount_t *s)
{
	s->sequence  ;
	smp_wmb();
}

其中write进程的操作就是对sequence执行加1的操作。 smp_wmb是用在smp系统下写内存屏障,它确保了编译器以及CPU都不会打乱sequence counter内存访问以及临界区内存访问的顺序。

写操作解锁操作:

代码语言:javascript复制
static inline void write_sequnlock(seqlock_t *sl)
{
	write_seqcount_end(&sl->seqcount);
	spin_unlock(&sl->lock);
}

static inline void write_seqcount_end(seqcount_t *s)
{
	seqcount_release(&s->dep_map, 1, _RET_IP_);
	raw_write_seqcount_end(s);
}

static inline void raw_write_seqcount_end(seqcount_t *s)
{
	smp_wmb();
	s->sequence  ;
}

可以看到写操作的解锁操作,依然是对sequence的值执行了加1的操作。而不是减1。这样就可以保证只要sequence是偶数就说明临界区没有写进程,而奇数说明临界区存在写进程。这是因为只有写进程会改变sequence的值,读进程不会去改变此值的。

读操作:

代码语言:javascript复制
/*
 * Read side functions for starting and finalizing a read side section.
 */
static inline unsigned read_seqbegin(const seqlock_t *sl)
{
	return read_seqcount_begin(&sl->seqcount);
}

开始和结束读取侧的函数。

代码语言:javascript复制
static inline unsigned read_seqcount_begin(const seqcount_t *s)
{
	seqcount_lockdep_reader_access(s);
	return raw_read_seqcount_begin(s);
}

static inline unsigned raw_read_seqcount_begin(const seqcount_t *s)
{
	unsigned ret = __read_seqcount_begin(s);
	smp_rmb();
	return ret;
}

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

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

可以看到,最终调用到__read_seqcount_begin函数中。

代码语言:javascript复制
if (unlikely(ret & 1)) {
	cpu_relax();
	goto repeat;
}

前面说过,当sequence的值是偶数的时候,临界区不存在write进程,为奇数的时候临界区是存在write进程的。正如上面代码,如果是奇数的话,就放弃cpu,然后不停的查询,直到sequence的值变为偶数,也就是临界区write进程退出。

read_seqretry函数:

代码语言:javascript复制
static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start)
{
	return read_seqcount_retry(&sl->seqcount, start);
}

static inline int read_seqcount_retry(const seqcount_t *s, unsigned start)
{
	smp_rmb();
	return __read_seqcount_retry(s, start);
}

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

可以最后看到read_seqretry函数最终判断传入的参数start与sequence的值是否相同。一般read_seqretry和read_seqbegin配套使用。

例子:

代码语言:javascript复制
u64 get_jiffies_64(void)
{
	unsigned long seq;
	u64 ret;

	do {
		seq = read_seqbegin(&jiffies_lock);
		ret = jiffies_64;
	} while (read_seqretry(&jiffies_lock, seq));
	return ret;
}

以上代码是获得jiffies的值,如果读取过程中数据发生变化,就需要重新读取。直到前后读取的sequence的值相同,就认为读取正确。

小节

顺序锁是不是感觉和前面的读写锁很相似,但是不同之处是,读写锁对read和write进程都互斥,而顺序锁只对write进程互斥。所以顺序锁也适用与那种写操作很少,读操作很频繁的系统中,可以大大的提升系统性能。

0 人点赞