

【精通内核】Linux 内核写锁实现原理与源码解析
source link: https://blog.51cto.com/u_15773567/5683054
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【精通内核】Linux 内核写锁实现原理与源码解析
精选 原创本文导读 Linux 内核读锁实现原理,描述自旋锁时,已经顺带描述了读写自旋锁,所以本节将不再描述自旋锁的读写锁实现。读者是否能想到,既然自旋锁有相关的读写锁实现,信号量也应该有呢?答案是一定的。所以可以到,读写锁实际上是在原有锁上进行优化读写的操作。下面讨论源码实现。
一、Linux 内核读写锁核心结构解读 定义一个结构体 rw_semaphore 代表读写信号量,然后义一宏定义表明读写信号量的偏移值。具体源码如下。
// 符号长整型,看到long类型,读者就知道,这又是将一个long类型长度大小切割成不同部分来使用的
// 由于使用i38632位来作为例子,因此这里long为32位,同样我们分割为高16位和低16位来使用
signed long count;
#define RWSEM UNLOCKED VALUE 0x0000 0000 // 无锁状态值为0
#define RWSEM_ACTIVE_BIAS 0x0000 0001 // 锁活动偏移值1
// 锁活动位数为4(4个16进制)*4(一个16进制等于4个二进制)=16,即2^16次方个锁位
#define RWSEM ACTIVE MASK Ox0000 ffff
#define RWSEM_WAITING BIAS (-0x00010000) // 锁等待偏移量,即 0xffff 0000
#define RWSEM ACTIVE READ_BIAS RWSEM_ACTIVE_BIAS // 读锁偏移量
// 写锁偏移量0xffff0001 为负数
#define RWSEM ACTIVE WRITE BIAS (RWSEM_WAITING_BIAS+RWSEM_ACTIVE_BIAS)
spinlock t wait_lock; // 保护等待链表的自旋锁
struct list_head wait_list; // 等待链表
};
//等待读写信号量的任务结构体
struct rwsem_waiter{
struct list_head list;
struct task_struct *task;
unsigned int flags; // 标志位声明为等待读锁还是写锁
#define RWSEM_WAITING_FOR_READ 0x00000001
#define RWSEM_WAITING_FOR_WRITE 0x00000002
};
二、Linux 内核获取写锁源码解读 首先原子性减 0xffff0001,然后判断原来的状态是否为 0,如果是,则表明获取写锁成功;
否则需要调用 rwsem_down_write_failed 函数进行阻塞排队操作。
int tmp=RWSEM_ACTIVE_WRITE BIAS;
_asm__volatile_(
//原子性减0xffff001即写锁偏移量,返回旧值被放到edx寄存器中
LOCK_PREFIX" xadd %%edx,(%%eax)"
//查看之前的count值是否为0,因为只有为0,才是无锁状态
" testl %%edx,%%edx"
//如果不为0,则获取锁失败跳到标号2处执行
" jnz 2f""1:"
LOCK_SECTION_START("")
//保存ecx,然后调用rwsem_down_write_failed进行阻塞排队操作
"2:"
" pushl %%есx"
" call rwsem_down_write failed"
" popl %%eсx"
" jmp 1b"
LOCK_SECTION_END
: "=m"(sem->count), "=d"(tmp)
: "a"(sem),"1"(tmp), "m"(sem->count)
: "memory", "cc");
}
// 处理写锁上锁失败逻辑
struct rw_semaphore *rwsem_down_write_failed(struct rw_semaphore *sem) {
// 创建等待节点
struct rwsem waiter waiter;
waiter.flags=RWSEM WAITING FOR WRITE;
// 调用公共处理逻辑执行等待操作。-RWSEM ACTIVE BIAS =Oxffff fff
rwsem_down_failed_common(sem,&waiter,-RWSEM_ACTIVE BIAS);
return sem;
}
三、Linux 内核释放写锁源码解读 首先将锁状态变为无锁状态,如果发现有任务正在等待唤醒,那么调用 rwsem_wake 唤醒等待的任务
_asm__volatile_(
" movl %2,%%edx" // 将写锁偏移量取负数后的值,即0x0000 ffff 放入edx中
// 尝试从Oxffff0001(持有写锁且无等待任务的状态,因为写写、读写互斥)变为 0x00000000
LOCK PREFIX" xaddl %%edx,(%%eax)"
" jnz 2f" //如果之前count值不为0,则有任务正在等待,跳到标号2处执行
" 1:"
LOCK_SECTION_START("")
"2:"
// 对dx也就是释前的lock值低16位自减,看看是否为0,即看看是否有活动的任务
" decw %%dx"
// 如果不为0,则表示写锁被释放后有任务获得了锁,退出;
// 否则,调用rwsem_wake唤醒等待任务
" jnz 1b"
" pushl %%ecx"
" call rwsem_wake"
" popl %%eсx"
" jmp 1b"
LOCK SECTION END
: "=m"(sem->count)
: "a"(sem), "i"(-RWSEM_ACTIVE_WRITE_BIAS),"m"(sem->count)
: "memory", "cc", "edx");
}
四、Linux 内核读写锁锁降级源码解读 有时候我们需要在获取到写锁后,进行降级为读锁,这可以通过 downgrade_write 方法进行锁降级有先原子性的降锁状态从写锁状态置为读锁状态,如果结果小于 0,则表明有任务正在等待被唤醒,此时可以调用 rwsem_downgrade_wake 函数唤醒等待读锁的任务,因为此时写锁已经被释放,可以让等待读锁的任务一起并行执行。
static inline void___downgrade_write(struct rw_semaphore*sem) {
_asm__volatile_(
LOCK PREFIX" addl %2,(%%eax)" //将状态从0xZZZZ0001变为0xYYYY0001
// 如果小于0,即锁正在等待被释放,则跳到标号2处执行rwsem_downgrade_wake函数,降级唤醒操作
" js 2f"
"1:"
LOCK_SECTION_START("")
"2:"
" pushl %%ecx"
" pushl %%edx"
" call rwsem_downgrade_wake" // 调用rwsem_downgrade_wake 函数
" popl %%edx"
" popl %%есx"
" jmp 1b"
LOCK_SECTION_END
: "=m"(sem->count)
: "a"(sem), "i"(-RWSEM_WAITING_BIAS), "m"(sem->count):
: "memory", "cc");
}
// 接下来查看rwsem_downgrade_wake 函数实现过程。
struct rw_semaphore*rsem_downgrade_wake(struct rw_semaphore*sem){
// 获取自旋锁
spin_lock(&sem->wait lock);
// 如果等待队列不为空,那么调用_rwsem_do_wake函数唤醒
// 注意,这里传入为0,表明只唤醒读任务 if(!list_empty(&sem->wait list))
sem=___rwsem_do_wake(sem,0); // 释放自旋锁
spin_unlock(&sem->wait lock);
return sem;
}
五、Linux 内核读写锁唤醒线程过程 首先获取保护等待队列的自旋锁,然后检测队列是否为空,如果不为空,那么调用 rwsem_do_wake 函数唤醒等待的任务。
spin lock(&sem->wait lock); // 获取自旋锁
// 如果等待链表为空,则什么也不做,否则调用rwsemdo wake函数唤醒任务
// 注:这里传入为0,表名只唤醒读任务
if(!listempty(&sem->wait list))
sem =_rwsem_do_wake(sem,1);// 1表明唤醒写任务
spin_unlock(&sem->wait_lock);
return sem;
}
// 真正唤醒流程
static inline struct rw_semaphore*__rwsem_do_wake(struct rw_semaphore *sem,int wakewrite) {
struct rwsem waiter *waiter;
struct list head *next;
signed long oldcount; int woken, loop;
// 如果不唤醒写任务,那么直接跳转到 dont_wake_writers执行
if(!wakewrite)
goto dont_wake_writers;
try again:
oldcount =rwsem_atomic_update(RWSEM_ACTIVE_BIAS,sem)-RWSEM_ACTIVE_BIAS;
// 如果之前count与上RWSEM_ACTIVE_MASK不为0,也就是还有活动的任务,则还原修改之前的值
if (oldcount & RWSEM_ACTIVE_MASK)
goto undo;
// 否则取出下一个等待任务,如果下一个等待的任务不是一个写任务,那么调用readers_only
//函数唤醒读任务
waiter =list_entry(sem->wait_list.next,struct rwsem_waiter,list);
if(!(waiter->flags&RWSEM_WAITING_FOR_WRITE))
goto readers_only;
//否则将写者从队列中移出,修改 flags 为0,调用wake_up_process函数唤醒任务,并且退出
list_del(&waiter->list);
waiter->flags =0;
wake_up_process(waiter->task);
goto out;
不唤醒写者操作流程,取出下一个等待者,如果等待者是写者,那么直接退出
dont wake writers:
waiter =list_entry(sem->wait_list.next,struct rwsem_waiter,list);
if(waiter->flags &RWSEM_WAITING_FOR_WRITE)
goto out;
// 只唤醒读者操作流程,遍历等待链表,直到等待者为写者时停下
readers_only:
woken =0;
do {
woken++;
if (waiter->list.next==&sem->wait_list)
break;
waiter =list_entry(waiter->listnext,struct rwsem_waiter,list);
} while (waiter->flags &RWSEM_WAITING_FOR_READ);
loop=woken;
woken *= RWSEM_ACTIVE_BIAS-RWSEM_WAITING_BIAS; woken-=RWSEM_ACTIVE_BIAS;
rwsem_atomic_add(woken,sem); // 更新counter 值
next = sem->wait_list.next; // 获取循环开始节点
for (; loop>0;loop--){ // 从当前节点一直遍历唤醒所有读等待任务
waiter =list_entry(next,struct rwsem_waiter,list);
next = waiter->list.next;
waiter->flags =0;
wake_up_process(waiter->task);
// 然后将唤醒了的一系列链表断开链接 sem->wait_list.next=next; next->prev = &sem->wait_list;
// 退出流程
out:
return sem;
//还原操作流程
undo:
// 再次判断,如果还有活动任务,则退出
if (rwsem_atomic_update(-RWSEM_ACTIVE_BIAS,sem)!=0)
goto out;
goto try_again;
}
总结 实际上,针对读写信号量,如果我们用 C 语言代码高级语言来描述的话,则十分简单,即一个公平的读写锁。也就是说,当有读锁持有时,如果有读任务,则可以直接获得读锁;但如果此时有写仕务在等待的情况下,那么将会导致读锁获取失败,转而进入等待状态。当读锁释放后返回看看有没写者在等待,如果有写者在等待且传入了唤醒写者的标识 1,那么看看等待列表的下一个等待任务是 1 是写节点,如果不是,那么遍历等待列表,唤醒所有读者,直到遇到一个写节点。然而,如果在持有写锁的情况下,那么读锁肯定获取失败,然后进入等待队列中,写锁被释放后,如果有锁等待,那会唤醒等待任务。
- 赞
- 收藏
- 评论
- 分享
- 举报
Recommend
-
98
摘要: 原创出处 http://www.iocoder.cn/Dubbo/good-collection/ 「芋道源码」欢迎转载,保留摘要,谢谢! 1.【芋艿】精尽 Dubbo 原理与源码专栏 2.【老徐】RPC 专栏 3.【肥朝】Dubbo 源码解析 4.
-
50
本文是基于MySQL5.7.22进行分析1.SQL总体执行流程图通过上面图,可以从全局上了解SQL语句执行流程以及与其他模块交互1.1SQL查询执行流程2.语法解析2.1编程语言知识回顾在介绍具体的MySQL数据库解析SQL之前,先来回归一下编程语言的知识点形式语言(Formallanguage)形...
-
45
MYSQL服务器接收SQL格式的查询,首先要对sql进行解析,内部将文本格式转换为二进制结构,这个转换就是解析器,解析的目的是为了让优化器更好的处理指令,以便以最优的路径,最少的耗时返回我们想要的结果。sql解析器的构成:1、词法分析(Lexicalscanner):作用是将...
-
32
kube-proxy介绍 为什么需要kube-proxy 我们知道容器的特点是快速创建、快速销毁,Kubernetes Pod和容器一样只具有临时的生命周期,一个Pod随时有可能被终止或者漂移,随着集群的状态变化而变化,一旦Pod变化,则该P...
-
7
Go中定时器实现原理及源码解析 Posted on 2021年3月7日2021年3月7日 by luozhiyun 转载请声明出处哦~,本篇文章发布于luozhiyun的博客:
-
6
鸿蒙内核源码分析(ELF解析篇) | 你要忘了她姐俩你就不是银 | 百篇博客分析HarmonyOS源码 | v53.02 - 鸿蒙内核源码分析的个人空间 - OSCHINA - 中文开源技术交流社区
-
3
深入源码,深度解析Java 线程池的实现原理-HollisChuang's BlogGitHub 19k Star 的Java工程师成神之路,不来了解一下吗! java 系统的运行归根到底是程序的运行...
-
6
Ribbon的核心作用就是进行请求的负载均衡,它的基本原理如下图所示。就是客户端集成Ribbon这个组件,Ribbon中会针对已经配置的服务提供者地址列表进行负载均衡的...
-
4
V2EX › Linux 有精通 Linux 内核内存管理的老哥能说说你是怎么学这一块的知识的吗? kgdb00
-
4
从Oracle日志解析学习数据库内核原理 原创 沃趣QFusion数据库私有云 2022...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK