前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >顺序锁(seqlock)

顺序锁(seqlock)

作者头像
DragonKingZhu
发布2020-03-24 11:03:47
1.7K0
发布2020-03-24 11:03:47
举报
文章被收录于专栏:Linux内核深入分析

顺序锁的引入

前面学习了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进程互斥。所以顺序锁也适用与那种写操作很少,读操作很频繁的系统中,可以大大的提升系统性能。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 顺序锁的引入
  • 顺序锁的原理
  • 顺序锁的定义
  • 顺序锁API
    • 写操作:
      • 读操作:
      • 顺序锁的实现
        • 写入者的上锁操作:
          • 写操作解锁操作:
            • 读操作:
              • read_seqretry函数:
              • 小节
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档