文章插图
顺序锁为写者赋予了较高的优先级,即使在读者正在读的时候,也允许写着继续运行 。这种策略的好处是,写者永远不会等待,缺点是有时候读者不得不反复多次读相同的数据,直到它获得有效的副本 。
在linux内核代码中,顺序锁被定义成seqlock_t结构体(代码位于include/linux/seqlock.h中):
typedef struct { struct seqcount seqcount; spinlock_t lock;} seqlock_t;
所以,包含一个自旋锁lock和一个表示当前锁的顺序数的seqcount 。seqcount结构体被定义为:typedef struct seqcount { unsigned sequence;......} seqcount_t;
就是包含了一个无符号整型的sequence变量 。注:需要C/C++ Linux高级服务器架构师学习资料私信“资料”(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
文章插图
初始化顺序锁在使用之前需要先初始化,一般有两种方法:
DEFINE_SEQLOCK(lock1);seqlock_t lock2;seqlock_init(&lock2);
可以看到,第一种方法是用宏直接定义并且初始化一个顺序锁变量:#define SEQCNT_ZERO(lockname) { .sequence = 0, ......}......#define __SEQLOCK_UNLOCKED(lockname){.seqcount = SEQCNT_ZERO(lockname),.lock = __SPIN_LOCK_UNLOCKED(lockname)}......#define DEFINE_SEQLOCK(x)seqlock_t x = __SEQLOCK_UNLOCKED(x)
所以,直接通过宏定义初始化就是定义了一个seqlock_t结构体变量,将其内部的自旋锁lock初始化为未加锁,并将表示顺序数的seqcount变量初始化为0 。第二种方法是自己定义一个seqlock_t结构体变量,然后调用seqlock_init函数将其初始化:
static inline void __seqcount_init(seqcount_t *s, const char *name,struct lock_class_key *key){ ...... s->sequence = 0;}......# define seqcount_init(s) __seqcount_init(s, NULL, NULL)......#define seqlock_init(x)do {seqcount_init(&(x)->seqcount);spin_lock_init(&(x)->lock);} while (0)
也是初始化内部的自旋锁变量lock,并将表示顺序数的seqcount变量初始化为0 。写操作顺序锁区分写者与读者,对于写者来说,一般使用下面的用法:
write_seqlock(&seq_lock);/* 修改数据 */......write_sequnlock(&seq_lock);
将对数据的修改代码夹在write_seqlock和write_sequnlock函数之间就行了 。获得写顺序锁的write_seqlock函数定义如下:
static inline void write_seqlock(seqlock_t *sl){/* 获得自旋锁 */ spin_lock(&sl->lock); write_seqcount_begin(&sl->seqcount);}
首先要获得顺序锁内部的自旋锁,然后调用write_seqcount_begin函数:static inline void write_seqcount_begin(seqcount_t *s){ write_seqcount_begin_nested(s, 0);}
接着调用write_seqcount_begin_nested函数:static inline void write_seqcount_begin_nested(seqcount_t *s, int subclass){ raw_write_seqcount_begin(s); ......}
最终调用了raw_write_seqcount_begin函数:static inline void raw_write_seqcount_begin(seqcount_t *s){/* 累加顺序数 */ s->sequence++;/* 写内存屏障 */ smp_wmb();}
累加了顺序锁内的顺序数,这之后添加了一个写内存屏障 。这是为了保证在还没有正式执行write_seqlock函数之后的修改数据代码之前,保证系统中的其它模块能感知到顺序数已经被累加了 。也就是保证累加顺序数的指令不会被重排序到后面的修改数据代码中,否则,有可能修改数据代码的代码已经执行了一点了,别的CPU还没感知到顺序数被更改了,会造成读取数据不一致的情况 。当然,应该也要在读取的时候对应的添加上读内存屏障 。释放写顺序锁的write_sequnlock函数的功能基本上就是把获得锁的过程倒过来,定义如下:
static inline void write_sequnlock(seqlock_t *sl){ write_seqcount_end(&sl->seqcount);/* 释放自旋锁 */ spin_unlock(&sl->lock);}
先调用write_seqcount_end函数,然后释放自旋锁:static inline void write_seqcount_end(seqcount_t *s){ ...... raw_write_seqcount_end(s);}
接着调用raw_write_seqcount_end函数:static inline void raw_write_seqcount_end(seqcount_t *s){/* 写内存屏障 */ smp_wmb();/* 累加顺序数 */ s->sequence++;}
先加写内存屏障,再累加顺序数 。这是为了保证修改数据的代码都执行完毕后才能将顺序数累加 。所以,在前面write_seqlock的时候使用的写内存屏障和这里write_sequnlock使用的写内存屏障是成对的,组成了一个临界区,用来执行修改数据的操作 。
推荐阅读
- Linux系统扩展oracle数据库所在的分区
- Linux常用监视和故障排查命令详解
- 我的 Linux 故事:用开源打破语言壁垒
- Linux 网络编程之如何使用函数库libnet详解
- 掌握Linux文件权限,看这篇就够了
- Linux 文件查找与编辑命令集合
- 有比 ReadWriteLock更快的锁?
- 从LINUX 系统层次看PostgreSQL 内存消耗
- Linux 原来是这么管理内存的
- Linux必备知识之文件系统