顺序锁
当使用读/写锁时,读者必须等待写者完成时才能读,写者必须等待读者完成时才能写,两者的优先权是平等的。顺序锁是对读/写锁的优化,它允许读写同时进行,提高了并发性,读写操作同时进行的概率较小时,其性能很好。顺序锁对读/写锁进行了下面的改进:
- 写者不会阻塞读者,即写操作时,读者仍可以进行读操作。
- 写者不需要等待所有读者完成读操作后才进行写操作。
- 写者与写者之间互斥,即如果有写者在写操作时,其他写者必须自旋等待。
- 如果在读者进行读操作期间,有写者进行写操作,那么读者必须重新读取数据,确保读取正确的数据。
- 要求临界区的共享资源不含指针,因为如果写者使指针失效,读者访问该指针,将导致崩溃。
顺序锁实际上由一个自旋锁和一个顺序计数器组成,有的应用已包括自旋锁,只需要一个顺序计数器配合就可以实现顺序锁。针对这两种情况,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不一致 }