顺序锁(seqlock)

顺序锁的引入

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

顺序锁的原理

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

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

顺序锁的定义

Linux内核中使用seqlock_t表示顺序锁
typedef struct {
	struct seqcount seqcount;
	spinlock_t lock;
} seqlock_t;

typedef struct seqcount {
	unsigned sequence;
} seqcount_t;
lock:           spinlock变量,用来保证sequence操作的原子性。
seqcount:   无符号整形变量,用来在read和write进程之间做协调,更新使用。

如果想静态定义一个顺序锁的话,使用如下方法:
#define __SEQLOCK_UNLOCKED(lockname)			\
	{						\
		.seqcount = SEQCNT_ZERO(lockname),	\
		.lock =	__SPIN_LOCK_UNLOCKED(lockname)	\
	}

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

如果想动态定义一个顺序锁的话,使用如下方法:
#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进程,如果有的话就获取锁

顺序锁的实现

写入者的上锁操作:

/*
 * 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中已经关闭了抢占。
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内存访问以及临界区内存访问的顺序。

写操作解锁操作:

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的值,读进程不会去改变此值的。

读操作:

/*
 * 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);
}
开始和结束读取侧的函数。
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函数中。
if (unlikely(ret & 1)) {
	cpu_relax();
	goto repeat;
}
前面说过,当sequence的值是偶数的时候,临界区不存在write进程,为奇数的时候临界区是存在write进程的。正如上面代码,如果是奇数的话,就放弃cpu,然后不停的查询,直到sequence的值变为偶数,也就是临界区write进程退出。

read_seqretry函数:

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配套使用。

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






  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Linux内核中,有多种机制可用于实现同步和互斥操作。以下是一些常见的机制: 1. 自旋(Spinlock):自旋是一种基于忙等待的机制。当一个进程或线程尝试获取自旋时,如果已被占用,它会一直自旋等待,直到被释放。 2. 互斥(Mutex):互斥是一种基于阻塞的机制。当一个进程或线程尝试获取互斥时,如果已被占用,它会被阻塞,直到被释放。 3. 读写(ReadWrite Lock):读写允许多个读操作同时进行,但只有一个写操作可以进行。读操作之间不会互斥,而写操作会独占资源。 4. 原子操作(Atomic Operations):原子操作是一种不可中断的操作,可以确保在多线程环境下对共享变量的原子性访问。原子操作可以用于实现简单的同步和互斥。 5. 信号量(Semaphore):信号量是一种计数器,用于控制对共享资源的访问。它可以用于限制同时访问资源的进程或线程数目。 6. 屏障(Barrier):屏障是一种同步机制,它可以使一组进程或线程在某个点上等待,直到所有进程或线程都到达该点,然后再继续执行。 除了上述常见的机制,Linux内核中还提供了其他更高级的机制,如读写自旋(Read-Write Spinlock)、顺序Seqlock)等,用于满足不同场景下的同步需求。 这些机制在Linux内核中被广泛应用于实现同步和互斥操作,确保共享资源的正确访问和保护。选择适当的机制取决于具体的需求和性能要求。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值