同时,由于顺序锁的顺序数被初始化为1,当上写锁的时候会将顺序数加1,且开写锁的时候仍然会加1,所以当读到顺序数是奇数的时候就一定表示有一个写者获得了写顺序锁,而当读到顺序数是偶数的时候就一定表示当前没有任何写者获得了写顺序锁 。
而且,对于不同写入者来说,序列锁是有自旋锁保护的,所以同一时间只能有一个写入者 。
最后,非常关键的,写顺序锁不会造成当前进程休眠 。
读操作接下来,我们分析一下顺序锁的读者 。对于读者来说,一般使用下面的用法:
unsigned int seq;do {seq = read_seqbegin(&seq_lock); /* 读取数据 */......} while read_seqretry(&seq_lock, seq);
一般都是先使用read_seqbegin函数读取出顺序锁的顺序数,接着执行实际的读取数据操作,最后调用read_seqretry函数,看看当前顺序锁的顺序数是否和前面读到的顺序锁一致 。如果一致,那证明读取的过程中没有写者在写入,可以直接退出了;如果不一致,说明在读取的过程中已经至少有一个写者修改了数据,那就循环重新执行上面的步骤,直到前后读到的顺序数一致为止 。
读取当前顺序锁顺序数的read_seqbegin函数定义如下:
static inline unsigned read_seqbegin(const seqlock_t *sl){ return read_seqcount_begin(&sl->seqcount);}
调用了raw_read_seqcount_begin函数:
static inline unsigned read_seqcount_begin(const seqcount_t *s){ ...... return raw_read_seqcount_begin(s);}
接着调用了raw_read_seqcount_begin函数:
static inline unsigned raw_read_seqcount_begin(const seqcount_t *s){/* 读取顺序数 */ unsigned ret = __read_seqcount_begin(s);/* 读内存屏障 */ smp_rmb(); return ret;}
先调用了__read_seqcount_begin函数读取了当前顺序锁的顺序数,然后加上了一个读内存屏障 。
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;}
先读取顺序锁的顺序数,加上READ_ONCE是为了防止编译器将其和后面的条件判断一起优化,打乱了执行次序 。然后,判断顺序数是否是奇数,前面提过,如果是奇数的话说明有一个写者正在持有写顺序锁,这时候就调用cpu_relax函数,让出CPU的控制权,并且再次从头循环读取顺序数,直到其为偶数为止 。
cpu_relax函数由各个架构自己实现,Arm64架构的实现如下(代码位于arch/arm64/include/asm/processor.h中):
static inline void cpu_relax(void){ asm volatile("yield" ::: "memory");}
在大多数的Arm64的实现中,yield指令等同于nop空指令 。它只是告诉当前CPU核,目前执行的线程无事可做,当前CPU核可以去做点别的 。通常,这种指令只对支持超线程的CPU核有用,但是目前所有Arm64的实现都不支持超线程技术,所以只是作为空指令来处理 。
接着我们来看看判断当前顺序锁的顺序数是否和前面读到的顺序锁一致的read_seqretry函数的实现:
static inline unsigned read_seqretry(const seqlock_t *sl, unsigned start){ return read_seqcount_retry(&sl->seqcount, start);}
调用了read_seqcount_retry函数:
static inline int read_seqcount_retry(const seqcount_t *s, unsigned start){/* 读内存屏障 */ smp_rmb(); return __read_seqcount_retry(s, start);}
先加上了一个读内存屏障,和在前面read_seqbegin的时候使用的读内存屏障是成对的,组成了一个临界区,用来执行读取数据的操作 。接着,调用了__read_seqcount_retry函数:
static inline int __read_seqcount_retry(const seqcount_t *s, unsigned start){ return unlikely(s->sequence != start);}
就是简单判断了一下当前顺序锁的顺序数是否和传入的start参数的值一致 。
读者是没有自旋锁保护的,所以可以多个读者同时读取数据,并且读顺序锁也不会造成当前进程休眠 。
使用场景顺序锁并不是万能的,适合它的使用场景要满足下面的条件:
- 比较适合读多写少的场景 。前面分析代码的时候看到了,写者是有自旋锁保护的,因此一次只能有一个写者写入数据,而读者没有任何其它锁保护,是并发读取的 。所以,本来写的性能就不高,而读者要保证在读数据的整个期间不会有写者写入,如果写者有很多的话,就会不停的重新尝试读取,也会严重影响性能 。
- 被保护的数据一般不会太大太多,否则也会影响性能 。
推荐阅读
- Linux系统扩展oracle数据库所在的分区
- Linux常用监视和故障排查命令详解
- 我的 Linux 故事:用开源打破语言壁垒
- Linux 网络编程之如何使用函数库libnet详解
- 掌握Linux文件权限,看这篇就够了
- Linux 文件查找与编辑命令集合
- 有比 ReadWriteLock更快的锁?
- 从LINUX 系统层次看PostgreSQL 内存消耗
- Linux 原来是这么管理内存的
- Linux必备知识之文件系统