顺序锁的特点
- 允许任意多个读操作同时进入临界区
- 只允许1个写操作同时进入临界区 ,其他写操作会忙等待
- 如果当前只有读操作,则写操作可以随时进入临界区,不用理会读操作
seqlock_init(lock)//初始化锁
read_seqbegin(lock)//读者进入临界区
read_deqretry(lock) //读者退出临界区
write_seqlock(lock)//写者加锁
write_sequnlock(lock) //写者解锁
一般使用方法:
seqlock_t lock;//定义顺序锁
seqlock_init(lock);//初始化顺序锁
read_fun
{
write_seqlock(&lock)//写者加锁
/* 写数据 */
write_sequnlock(&lock);//写者解锁
}
write_fun
{
do {
seq = read_seqbegin(&lock);//读者进入临界区
/* 需要完成的工作 */
} while read_deqretry(&lock, seq);//读者退出临界区
}
使用方法也比较简单,只有read_deqretry可能有点疑问,就是读者退出临界区的时候会检测在读操作的过程中有没有写操作,如果有写操作则返回1,说明需要重新读,再跑一次循环吧;如果没有写操作,则返回0,说明不需要重新读,可以退出循环。
三、顺序锁
1.锁的结构体
typedef struct {
struct seqcount seqcount;
spinlock_t lock;
} seqlock_t;
typedef struct seqcount {
unsigned sequence;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} seqcount_t;
顺序锁的结构体很简单,只有两个成员,一个成员是struct seqcount结构体,这个结构体其实就是一个unsigned 的数据类型而已,另一个成员就是一个自旋锁。
2.锁的初始化
我们是使用seqlock_init函数来初始化一个顺序锁的:
#define seqlock_init(x) \
do { \
seqcount_init(&(x)->seqcount); \
spin_lock_init(&(x)->lock); \
} while (0)
看这个宏,我们很清楚的知道,他就是初始化seqcount结构体和自旋锁。spin_lock_init前面已经讲过了,不过还是可以看看怎么初始化seqcount结构体的:
# define seqcount_init(s) \
do { \
static struct lock_class_key __key; \
__seqcount_init((s), #s, &__key); \
} while (0)
static inline void __seqcount_init(seqcount_t *s, const char *name,
struct lock_class_key *key)
{
/*
* Make sure we are not reinitializing a held lock:
*/
lockdep_init_map(&s->dep_map, name, key, 0);
s->sequence = 0;
}
看了也很简单就是把seqcount结构的sequence 成员初始化为0。
3.读者进入临界区
我们是使用read_seqbegin函数来进行读者进入临界区:
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 = READ_ONCE(s->sequence);
if (unlikely(ret & 1)) {
cpu_relax();
goto repeat;
}
return ret;
}
#define READ_ONCE(x) __READ_ONCE(x, 1)
#define __READ_ONCE(x, check) \
({ \
union { typeof(x) __val; char __c[1]; } __u; \
if (check) \
__read_once_size(&(x), __u.__c, sizeof(x)); \
else \
__read_once_size_nocheck(&(x), __u.__c, sizeof(x)); \
smp_read_barrier_depends(); /* Enforce dependency ordering from x */ \
__u.__val; \
})
static __always_inline
void __read_once_size(const volatile void *p, void *res, int size)
{
__READ_ONCE_SIZE;
}
#define __READ_ONCE_SIZE \
({ \
switch (size) { \
case 1: *(__u8 *)res = *(volatile __u8 *)p; break; \
case 2: *(__u16 *)res = *(volatile __u16 *)p; break; \
case 4: *(__u32 *)res = *(volatile __u32 *)p; break; \
case 8: *(__u64 *)res = *(volatile __u64 *)p; break; \
default: \
barrier(); \
__builtin_memcpy((void *)res, (const void *)p, size); \
barrier(); \
} \
})
一步步追,我们最后才发现,read_seqbegin啥也没做,只是把seqlock_t 结构体的seqcount 结构体的sequence成员的值返回而已,好像真的没有做啥。
4.读者退出临界区
我们是使用read_seqretry函数来进行读者退出临界区:
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);
}
读者退出临界区的函数更简单,就是比较seqlock_t 结构体的seqcount 结构体的sequence成员的值和start而已,start就是读者进入临界区的的时候返回的值。如果读者在进入临界区和退出临界区的时候,他们的sequence值是一样的,说明顺序锁没有写入过东西。如果他们的sequence值是不一样的,说明在读者进入和退出临界区的期间,有写者获取过锁。
5.写者加锁
我们是使用write_seqlock函数来进行写者加锁操作的:
static inline void write_seqlock(seqlock_t *sl)
{
spin_lock(&sl->lock);//自旋锁加锁
write_seqcount_begin(&sl->seqcount);
}
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_seqlock也很简单,首先给自旋锁加锁,然后修改sequence的值,让sequence加一,最后使用写内存屏障保证代码的执行顺序。
6.写者解锁
我们是使用write_sequnlock函数来进行写者解锁操作的:
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++;
}
write_sequnlock也很简单,首先使用写内存屏障保证代码的执行顺序,然后修改sequence的值,让sequence加一,最后给自旋锁解锁。
顺序锁就是这样子实现的,主要是通过自旋锁保护写者的临界区,然后通过sequence的值表示是否锁保护的临界区有没有遭遇到写者的修改。如果读者在进入临界区和退出临界区的时候,他们的sequence值是一样的,说明顺序锁没有写入过东西。如果他们的sequence值是不一样的,说明在读者进入和退出临界区的期间,有写者获取过锁。
不过,我这里有个疑问,就是当我们在写者加锁和写者解锁期间,进行了读者进入临界区和退出临界区的操作,读者是不会意识到目前的是在写者写入过程中的,所以我们应该尽量快速的完成写入操作。