Linux 网络协议栈之内核锁—— 读写自旋锁

Posted zqixiao_09

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 网络协议栈之内核锁—— 读写自旋锁相关的知识,希望对你有一定的参考价值。

读写自旋锁(rwlock)是一种比自旋锁粒度更小的自旋锁机制,它保留了“自旋”的概念。 但是在写操作方面,只能最多有一个写进程,在读方面,同时可拥有多个执行单元,当然读和写也不能同时进行。 一句话: 防写不防读

读写自旋锁的实现原理,它其实是自旋锁的升级版。同样,我们先看看它能实现的功能:针对写:最多只有一个写进程;针对读:可以同时有多个读写单元。但读和写不能同时进行。它于include\\linux\\rwlock.h下定义,而它提供的函数形式和自旋锁类似,仅将“spin_”替换成“read_”“write_”。包括read_lock(lock)read_unlock(lock)write_lock(lock)write_unlock(lock)等。接下来我们来看下它该是如何使用的。而关于它的使用其实很简单,如图4.1所示,很好理解,即是在临界区前后分别加上加解锁函数即可,这里便不在细说。

 图4.1      读写自旋锁的使用示例

然后我们来讨论它的实现核心。事实上,它的实现讨论起来也挺简单,就是有点绕。实现流程与spin_lock几乎完全一致,唯一不同的是最后调用体系结构相关的函数是arch_read_lock而不是arch_spin_lock(自旋锁的arch_spin_lock最终调用__ticket_spin_lock())等。

下面分析它的源码实现。假如现在有进程P1向操作系统申请了读写自旋锁,设读写锁变量A已经被定义,它的初值为RW_LOCK_UNLOCKED(0x01000000)。配合图4.2,图4.3所示的源码内容,若此时进程P1对它申请读锁操作,则read_lock()对读写锁变量A1,如果相减后A的值结果为负,则说明此时系统中已经有某个进程(设为P2)对这个读写锁变量使用写锁函数write_lock()上锁,这时候系统便会持续等待P2对写锁的释放,持续等待过程的源码实现如图4.5所示。假如进程P1读锁申请成功,若P1在使用读锁过程中,存在另一个进程P3申请写锁操作。此时write_lock()对读写锁变量A0x01000000并判断,如果结果非零,则说明此时锁变量A已被write_lock()函数上锁或用read_lock()函数上锁,进而便跳转到图4.6所示的写锁失败的汇编函数,持续等待锁变量A被进程P1释放。

4.2  读锁函数的内核源码                                          4.3  写锁函数的内核源码

下面给出对应的解锁函数。如图4.4所示。

图4.4      解锁函数的内核源码

可以看出解锁函数中的实现其实简单的加1和减1操作。下面重点关注加锁函数中的两个申请锁失败时的两个的链接函数,如图4.5,图4.6所示。采用纯汇编实现,由于图4.5所示的源码和图4.6所示的源码内容实现机理实际上均是相同,故这里仅重点分析图4.5源码。图4.5的源码搞懂了,图4.6的源码自然也不是问题。

在分析源码前,可能读者对于源码中的rep指令等有些许疑问。事实上,rep重复串操作直到cx寄存器的内容为0为止。结合的指令有限,只有MOVSSTOSLODSINSOUTS,如rep stosb;等。而rep; nop是一个混合指令,被翻译成pause指令。分号在此非注释,AT&T中表示指令的有效定界符。pause指令会向处理器提供一种提示:告诉处理器所执行的代码序列是一个自旋等待状态,处理器会根据这个提示而避开内存序列冲突(Intel手册说明)

read_lock()函数申请读锁失败后,首先将rw变量加1(注:rdi实际上代表的是rw,它们之间是通过寄存器来传递的,未加1rw的值为-1)。紧接着,在执行cmpl指令前rdi的值为0,于是同立即数1进行比较。如果不相等,则继续跳转到“1”标志位处继续循环比较,直到相等,因为相等的时候,说明系统执行了读锁的释放函数,将rw变量加1了,具体可参看read_unlock()函数实现(图4.4所示)。此时在退出__read_lock_failed函数之前还需将rdi变量即rw变量减1,表示另一进程申请读锁成功,从而保证后续申请读写锁的进程的正确性。至此,已将__read_lock_failed函数源码分析完毕,至于__writed_lock_failed也是类似的思想,读者可自己类比推理即可理解。

            4.5   读锁函数中的跳转函数                                            4.6   写锁函数中的跳转函数


操作:
定义于#include<linux/rwlock.h>
或#include<linux/spinlock.h>

rwlock_t x;               
rwlock_init(&x);    //动态初始化
rwlock_t x=RW_LOCK_UNLOCKED;    //静态初始化 

void read_lock(rwlock_t *lock);    //如果不能获得,它将自旋,直到获得该读写锁
void read_unlock(rwlock_t *lock);
在对共享资源进行读取之前,应该先调用读锁定函数锁定共享资源,完成之后再调用读解锁函数释放共享资源

void write_lock(rwlock_t *lock);    //如果不能获得,它将自旋,直到获得该读写锁
void write_unlock(rwlock_t *lock);
在对共享资源进行写操作之前,应该先调用写锁定函数锁定共享资源,完成之后再调用写解锁函数释放共享资源

read_trylock(lock);
write_trylock(lock);

read_lock_irq(lock);    //读者获取读写锁,并禁止本地中断
read_unlock_irq(lock);    //读者释放读写锁,并使能本地中断

write_lock_irq(lock);//写者获取读写锁,并禁止本地中断
write_unlock_irq(lock);

read_lock_irqsave(lock, flags);//读者获取读写锁,同时保存中断标志,并禁止本地中断
read_unlock_irqrestores(lock,flags);//读者释放读写锁,同时恢复中断标志,并使能本地中断

write_lock_irqsave(lock,flags);//写者获取读写锁,同时保存中断标志,并禁止本地中断
write_unlock_irqstore(lock,flags);

read_lock_bh(lock);//读者获取读写锁,并禁止本地软中断
read_unlock_bh(lock);

write_lock_bh(lock);//写者获取读写锁,并禁止本地软中断
write_unlock_bh(lock);

用例:
rwlock_t lock;     //定义rwlock
rwlock_init(&lock);    //初始化rwlock

//读时获取锁
read_lock(&lock);
...临界区...
read_unlock(&lock);

 //写时获取锁
write_lock_irqsave(&lock, flags);
…临界区...
write_unlock_irqrestore(&lock, flags);


以上是关于Linux 网络协议栈之内核锁—— 读写自旋锁的主要内容,如果未能解决你的问题,请参考以下文章

Linux 网络协议栈之内核锁—— 自旋锁在抢占(非抢占)单核和多核中的作用

Linux 网络协议栈之内核锁—— RCU锁机制

Linux 网络协议栈之内核锁—— RCU锁机制

Linux 网络协议栈之内核锁—— RCU锁

Linux 网络协议栈之内核锁—— 进程调度

Linux 网络协议栈之内核锁—— 内核抢占