Android深度探索:HAL与驱动开发学习笔记--并发控制之顺序锁

顺序锁

当使用读/写锁时,读者必须等待写者完成时才能读,写者必须等待读者完成时才能写,两者的优先权是平等的。顺序锁是对读/写锁的优化,它允许读写同时进行,提高了并发性,读写操作同时进行的概率较小时,其性能很好。顺序锁对读/写锁进行了下面的改进:

  • 写者不会阻塞读者,即写操作时,读者仍可以进行读操作。
  • 写者不需要等待所有读者完成读操作后才进行写操作。
  • 写者与写者之间互斥,即如果有写者在写操作时,其他写者必须自旋等待。
  • 如果在读者进行读操作期间,有写者进行写操作,那么读者必须重新读取数据,确保读取正确的数据。
  • 要求临界区的共享资源不含指针,因为如果写者使指针失效,读者访问该指针,将导致崩溃。

顺序锁实际上由一个自旋锁和一个顺序计数器组成,有的应用已包括自旋锁,只需要一个顺序计数器配合就可以实现顺序锁。针对这两种情况,Linux内核给顺序锁提供了两套API函数。一套API函数为*seq*,完整地实现了顺序锁;另一套API函数为*seqcount*,只包含了顺序计数器,需要与用户的自旋锁配套实现顺序锁。顺序锁API函数的功能说明如表5所示。

表5 顺序锁API函数功能说明
函数名功能说明
seqlock_init(x)初始化顺序锁,将顺序计数器置0。
write_seqlock(seqlock_t *sl)加顺序锁,将顺序号加1。写者获取顺序锁s1访问临界区,它使用了函数spin_lock。
write_sequnlock(seqlock_t *sl)解顺序锁,使用了函数spin_unlock,顺序号加1。
write_tryseqlock(seqlock_t *sl)功能上等同于spin_trylock,顺序号加1。
read_seqbegin(const seqlock_t *sl)返回顺序锁s1的当前顺序号,读者没有开锁和释放锁的开销。
read_seqretry(const seqlock_t *sl, unsigned start)检查读操作期间是否有写者访问了共享资源,如果是,读者就需要重新进行读操作,否则,读者成功完成了读操作。
seqcount_init(x)初始化顺序号。
read_seqcount_begin(const seqcount_t *s)读者在读操作前用此函数获取当前的顺序号。
read_seqcount_retry(const seqcount_t *s, unsigned start)读者在访问完后调用此函数检查在读期间是否有写者访问临界区。如果有,读者需要重新进行读操作,否则,完成读操作。
write_seqcount_begin(seqcount_t *s)写者在访问临界区前调用此函数将顺序号加1,以便读者检查是否在读期间有写者访问过。
write_seqcount_end(seqcount_t *s)写者写完成后调用此函数将顺序号加1,以便读者能检查出是否在读期间有写者访问过。

用户使用顺序锁时,写操作加锁方法与自旋锁一样,但读操作需要使用循环查询,使用顺序锁的读操作样例列出如下(在kernel/time.c中):

u64 get_jiffies_64(void)
{
	unsigned long seq;
	u64 ret;
 
	do {
		seq = read_seqbegin(&xtime_lock);   //获取当前的顺序号
		ret = jiffies_64;        //读取临界区数据
          /*检查seq值与当前顺序号是否相等,若不等,说明有写者开始工作,函数read_seqretry返回1,继续循环*/
	} while (read_seqretry(&xtime_lock, seq)); 
	return ret;
}

在非SMP系统上,自旋锁消失,但写者还必须递增顺序变量,因为中断例程可能改变数据的状态。

下面分析顺序锁的数据结构及API函数:

(1)顺序锁结构seqlock_t

顺序锁用结构seqlock_t描述,它包括顺序计数器sequence和自旋锁lock。结构seqlock_t列出如下(在include/linux/seqlock.h中):

typedef struct {
	unsigned sequence;
	spinlock_t lock;
} seqlock_t;

在结构seqlock_t中,顺序计数器sequence存放顺序号,每个读者在读数据前后两次读顺序计数器,并检查两次读到的顺序号是否相同。如果不相同,说明新的写者已经开始写并增加了顺序计数器,表明刚读到的数据无效。

写者通过调用函数write_seqlock获取顺序锁,将顺序号加1,调用函数write_sequnlock释放顺序锁,再将顺序号加1。这样,写者正在写操作时,顺序号为奇数,写完临界区数据后,顺序号为偶数。

读者应以循环查询方法读取临界区数据,读者执行的临界区代码的方法列出如下:

do {
	    seq = read_seqbegin(&foo);   //返回当前的顺序号
 	...    //临界区数据操作
    } while (read_seqretry(&foo, seq));

在上述代码中,读者在读临界区数据之前,先调用函数read_seqbegin获致当前的顺序号,如果顺序号seq为奇数,说明写者正写临界区数据,或者seq值与顺序号当前值不等,表明读者正读时,写者开始写,函数read_seqretry返回1,读者继续循环等待写者完成。

(2)顺序锁初始化函数seqlock_init

函数seqlock_init初始化顺序锁,顺序锁实际上由一个自旋锁和一个顺序计数器组成。其列出如下:

#define seqlock_init(x)					/
	do {						/
		(x)->sequence = 0;			/
		spin_lock_init(&(x)->lock);		/
	} while (0)

(3)写者加锁函数write_seqlock

函数write_seqlock加顺序锁。方法是:它先加自旋锁,然后将顺序号加1,此时,顺序号值为奇数。此函数不需要关闭内核抢占,因为自旋锁加锁时已关闭了内核抢占。其列出如下:

static inline void write_seqlock(seqlock_t *sl)
{
	spin_lock(&sl->lock);
	++sl->sequence;
	smp_wmb();
}

(4)写者解锁函数write_sequnlock

函数write_sequnlock表示写者解顺序锁,它将顺序号加1,然后解开自旋锁。此时,顺序号应为偶数。其列出如下(在include/linux/seqlock.h中):

static inline void write_sequnlock(seqlock_t *sl)
{
	smp_wmb();   //加上SMP写内存屏障
	sl->sequence++;   //顺序号加1
	spin_unlock(&sl->lock);  //解开自旋锁
}

(5)读操作开始时读顺序号函数read_seqbegin

函数read_seqbegin读取顺序号,如果顺序号为奇数,说明写者正在写操作,处理器执行空操作,进行循环等待,否则,函数返回读取的顺序号值。其列出如下:

static __always_inline unsigned read_seqbegin(const seqlock_t *sl)
{
	unsigned ret;
 
repeat:
	ret = sl->sequence;
	smp_rmb();      //加上SMP读内存屏障
	if (unlikely(ret & 1)) { //如果ret & 1为true,表示顺序号为奇数,写者正在写操作
		cpu_relax();       //空操作
		goto repeat; 
	}
 
	return ret;
}

(6)读操作完成时顺序号检查函数read_seqretry

函数read_seqretry用于读操作完成后检测读的数据是否有效。如果读操作完成后的顺序号与读操作开始前的顺序号不一致,函数返回1,说明有写者更改了临界区数据,因此,调用者必须重新读临界者数据。

函数read_seqretry列出如下:

static __always_inline int read_seqretry(const seqlock_t *sl, unsigned start)
{
	smp_rmb();//加上SMP读内存屏障
 
	return (sl->sequence != start);  //顺序锁的顺序号值与读操作开始时的顺序号值start不一致
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值