《Android深度探索 卷1 HAL与驱动开发》笔记之Linux驱动程序中的并发操作(4)——顺序锁

顺序锁和读写自旋锁的区别

顺序锁 同样是自旋锁的一种衍生,顺序锁和读写自旋锁类似,但是顺序锁赋予了 写自旋锁 更高的权限。

读写自旋锁 中,读自旋锁和写自旋锁的优先级是相同的,当有任务线程或进程获取了读自旋锁后,写自旋锁必须等待读自旋锁被释放后,才能被获取,反过来也一样。

顺序锁 中,当任务线程或进程获取了读自旋锁后,不必等到读自旋锁被释放也可以获取写自旋锁,并在此过程中继续执行修改共享数据(临界区)的操作。总的来说,就是写自旋锁永远不会被读自旋锁阻塞,只会被写自旋锁阻塞。

顺序锁的实现

  • 定义顺序锁的结构体

顺序锁定义了一个seqlock_t变量。seqlock_t的定义如下:

typedef struct {
	unsigned sequence; /* 顺序锁的顺序计数器 */
	spinlock_t lock; /* 自旋锁变量 */
} seqlock_t;

当获取顺序锁(write_seqlock()函数)时,seqlock_t.sequence变量会被加1.释放写顺序锁(write_sequnlock函数)时,seqlock_t.sequence变量仍然会别加1。因此,在获取顺序锁但是并没有释放顺序锁的情况下,即正在执行写临界区的代码时,seqlock_t.sequence变量的值是奇数。释放写顺序锁后,seqlock_t.sequence变量的值是偶数。

  • 读顺序锁的使用

读取共享资源可以通过read_seqbegin()read_seqretry() 函数来配合实现。实现代码如下:

unsigned seq;
do {
	seq = read_seqbegin(&seqlock);
	... /* 临界区代码 */
} while(read_seqretry(&seqlock, seq);

read_seqbegin() 函数用来判断在执行读临界区代码的过程中顺序号是否发生了变化。从read_seqretry() 函数定义的源代码里也可以看出这点。

static _always_inline int read_reqretry(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)
{
	/* 比较通过read_seqbegin()函数获取的顺序号是否和当前的顺序号一致 */
	return unlikely(s->sequence != start);
}

如果在执行临界区代码的过程中顺序号并没有发生改变,那么read_seqretr() 函数返回0,否则返回非0。从这一点可以看出,在执行临界区代码时,如果发生了写操作,read_seqretry() 函数会返回非0值,do … while循环将继续执行。当再次执行到 read_seqbegin() 函数时,会检查顺序锁是否执行完。如果顺序锁没有执行我那,read_seqbegin() 函数将被阻塞,知道顺序锁释放。也就是说,read_seqbegin() 函数返回的顺序号一定是一个偶数。这一点从read_seqbegin() 函数定义的源代码可以看出来。

static _always_inline unsigned read_seqbegin(cont 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);
	/*************************************************************
	 * 如果顺序号是奇数,则跳到repeat标签,从而形成一个循环,unlikely和
	 * likely宏用于编译器优化。如果使用likely宏,说明发生这种情况的概率很
	 * 小,在编译成机器码时该语句就会紧跟着前面的语句后面,这样就可以被
	 * Cache语读进去,增加程序执行速度。unlikely则相反。
	 *************************************************************/
	if (unlikely(ret & 1)) {
		cpu_relax();
		goto repeat;
	}
	return ret;
}

从上述的描述就可以得出顺序锁之所以能够在获取读顺序锁时仍然可以获取写顺序锁,是因为如果在执行读临界区时发生了写操作,系统会重新执行读临界区的代码(再次读取共享数据),直到写顺序锁释放后,并且在执行读临界区代码期间没有写操作,系统才会认为读到的数据是正确的,并退出do…while循环。

使用顺序锁写临界区代码段参考程序

seqlock_t lock; /* 定义顺序锁变量 */
seqlock_init(&lock); /* 初始化顺序锁 */
write_seqlock(&lock);/* 获取顺序锁 */
... /* 写临界区 */
write_sequnlock(*lock); /* 释放顺序锁 */

顺序锁的一些衍生操作

与顺序自旋锁相关的宏与函数

顺序锁操作 类型 描述
DEFINE_SEQLOCK(lock) 定义并初始化顺序锁变量
void write_tryseqlock(seqlock_t *lock) 函数 获取顺序锁。如果成功获取顺序锁,函数立刻返回非0值,否则返回0。
void write_seqlock_irqsave(lock, flags) 获取顺序锁并保存中断。
void write_sequnlock_irqrestore(lock, flags) 释放顺序锁并恢复中断现场。
void write_seqlock_irq(lock) 获取顺序锁并禁止中断。
void write_sequnlock_irq(lock) 释放顺序锁并允许中断。
void write_seqlock_bh(lock) 获取顺序锁并禁止底半部。
void write_sequnlock_bh(lock) 释放顺序锁并允许底半部。
unsigned read_seqbegin(const seqlock_t *lock) 函数 获取顺序锁的顺序号。如果lock指定的顺序锁没有被释放,该函数会等待顺序锁释放后才获取顺序锁的顺序号。
int read_seqretry(const seqlock_t *lock, unsigned iv) 函数 检测read_seqbegin()函数获取的顺序号是否与执行完读临界区的顺序号一致。如果不一致,返回非0值,否则返回0。
read_seqbegin_irqsave(lock, flags) 获取顺序锁的顺序号并保存中断
read_seqbegin_irqstore(lock, iv, flags) 检查顺序锁的顺序号并恢复中断现场。

使用顺序锁需要注意的问题

顺序锁 的读临界区操作根本就没有获取任何锁来保护临界区不被修改,只是简单地读取共享数据。这也是为什么在读取共享数据时可以修改共享数据的原因。从顺序锁的特点可以看出顺序锁的优点是读共享数据期间可以写共享数据,当缺点也很明显,就是如果在读共享数据的过程中发生了写操作,会使得系统不断地循环等待(read_seqbegin() 函数会造成阻塞)和执行读临界区的代码。由此,其实不太建议在驱动程序中使用顺序锁来避免竞态的情况。而且,实际上如果你认真查阅过 Linux 内核源代码可以发现,顺序锁的使用并不多,更常用是是基本的自旋锁结合开关中断、底半部来使用。

发布了23 篇原创文章 · 获赞 3 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/karaskass/article/details/102458479
今日推荐