当使用读/写自旋锁时,内核控制路径发出的执行read_lock或write_lock操作的请求具有相同的优先权:读者必须等待,直到写操作完成。同样地,写者也必须等待,直到读操作完成。
Linux 2.6中引入了顺序锁(seqlock),它与读/写自旋锁非常相似,只是它为写者赋予了较高的优先级:事实上,即使在读者正在读的时候也允许写者继续运行。这种策略的好处是写者永远不会等待读(除非另外一个写者正在写),缺点是有些时候读者不得不反复读多次相同的数据直到它获得有效的结果。
每个顺序锁都是包括两个字段的seqlock_t结构:
typedef struct {
unsigned sequence;
spinlock_t lock;
} seqlock_t;
一个类型为spinlock_t的lock字段和一个整型的sequence字段,第二个字段sequence是一个顺序计数器。
每个读者都必须在读数据前后两次读顺序计数器,并检查两次读到的值是否相同,如果不相同,说明新的写者已经开始写并增加了顺序计数器,因此暗示读者刚读到的数据是无效的。
通过把SEQLOCK_UNLOCKED赋给变量seqlock_t或执行seqlock_init宏,把seqlock_t变量初始化为“未上锁”,并把sequence设为0:
#define __SEQLOCK_UNLOCKED(lockname) /
{ 0, __SPIN_LOCK_UNLOCKED(lockname) }
#define SEQLOCK_UNLOCKED /
__SEQLOCK_UNLOCKED(old_style_seqlock_init)
# define __SPIN_LOCK_UNLOCKED(lockname) /
(spinlock_t) { .raw_lock = __RAW_SPIN_LOCK_UNLOCKED, /
SPIN_DEP_MAP_INIT(lockname) }
#define __RAW_SPIN_LOCK_UNLOCKED { 1 }
写者通过调用write_seqlock()和write_sequnlock()获取和释放顺序锁。write_seqlock()函数获取seqlock_t数据结构中的自旋锁,然后使顺序计数器sequence加1;write_sequnlock()函数再次增加顺序计数器sequence,然后释放自旋锁。这样可以保证写者在整个写的过程中,计数器sequence的值是奇数,并且当没有写者在改变数据的时候,计数器的值是偶数。读者进程执行下面的临界区代码:
unsigned int seq;
do {
seq = read_seqbegin(&seqlock);
/* ... CRITICAL REGION ... */
} while (read_seqretry(&seqlock, seq));
read_seqbegin()返回顺序锁的当前顺序号;如果局部变量seq的值是奇数(写者在read_seqbegin()函数被调用后,正更新数据结构),或seq的值与顺序锁的顺序计数器的当前值不匹配(当读者正执行临界区代码时,写者开始工作),read_seqretry()就返回1:
static __always_inline int read_seqretry(const seqlock_t *sl, unsigned iv)
{
smp_rmb();
return (iv & 1) | (sl->sequence ^ iv);
}
注意在顺序锁机制里,读者可能反复读多次相同的数据直到它获得有效的结果(read_seqretry返回0)。另外,当读者进入临界区时,不必禁用内核抢占;另一方面,由写者获取自旋锁,所以它进入临界区时自动禁用内核抢占。
并不是每一种资源都可以使用顺序锁来保护。一般来说,必须在满足下述条件时才能使用顺序锁:
(1)被保护的数据结构不包括被写者修改和被读者间接引用 的指针(否则,写者可能在读者的眼皮子底下就修改指针)。
(2)读者的临界区代码没有副作用(否则,多个读者的操作会与单独的读操作有不同的结果)。
此外,读者的临界区代码应该简短,而且写者应该不常获取顺序锁,否则,反复的读访问会引起严重的开销。在Linux 2.6中,使用顺序锁主要是保护一些与系统时间处理相关的数据结构。