前面学习了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)
/*
* 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进程退出。
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进程互斥。所以顺序锁也适用与那种写操作很少,读操作很频繁的系统中,可以大大的提升系统性能。