持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情
前言
📫作者简介:小明java问道之路,专注于研究 Java/ Liunx内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。
📫热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长。
🏆InfoQ签约作者、CSDN专家博主/后端领域优质创作者/内容合伙人、阿里云专家/签约博主、51CTO专家🏆
🔥如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~
本文导读
Linux内核读锁实现原理,描述自旋锁时,已经顺带描述了读写自旋锁,所以本节将不再描述自旋锁的读写锁实现。读者是否能想到,既然自旋锁有相关的读写锁实现,信号量也应该有呢?答案是一定的。所以可以到,读写锁实际上是在原有锁上进行优化读写的操作。下面讨论源码实现。
一、Linux内核读锁实现原理读写信号量初始化源码解读
读写信号量初始化时,将count初始化为0,自旋锁也同时进行初始化,等待链表也相应地进行了初始化。
static inline void init_rwsem(struct rw_semaphore *sem) {
sem->count =RWSEM_UNLOCKED_VALUE; // 初始值为0
spin_lock_init(&sem->wait_lock); // 初始化自旋锁
INIT_LIST_HEAD(&sem->wait_list); //初始化等待链表
}
二、Linux内核上读锁流程源码解读
我们先对 count值原子性自增,如果成功则退出;否则应保存ecx、edx,然后调用rwsem_down_ read_failed 函数。
原子性自增,如果自增后的值小于0,即写锁小于0,则上锁失败,并跳到标号2处,保存ecx、edx,然后调用rwsem_down_read_failed函数,最后恢复ecx、edx的值结束方法"2:",调用rwsem_down_read_failed 函数。
执行上读锁失败的逻辑,创建等待节点。
static inline void_down_read(struct rw_semaphore*sem) {
_asm_volatile_(
LOCK_PREFIX"incl (%%eax)" // 原子性自增
" js 2f" // 如果自增后的值小于0,即写锁小于0,则上锁失败,并跳到标号2处
"1:"
LOCK_SECTION_START("")
// 保存ecx、edx,然后调用rwsem_down_read_failed函数,最后恢复ecx、edx的值结束方法"2:"
" pushl %%ecx"
" pushl %%edx"
" call rwsem_down_read failed" // 调用rwsem_down_read_failed 函数
" popl %%edx"
" popl %%ecx"
" jmp 1b"
LOCK_SECTION END
: "=m"(sem->count)
: "a"(sem),"m"(sem->count)
: "memory","cc");
}
// 执行上读锁失败的逻辑
struct rw semaphore *rwsem_down_read failed(struct rw_semaphore*sem) {
// 创建等待节点
struct rwsem_waiter waiter;
waiter.flags=RWSEM_WAITING FOR_READ;
//RWSEM WAITING BIAS-RWSEM ACTIVE BIAS=> 0xffff0000-0x0000 0001=0x fffefff
rwsem_down_failed_common(sem, &waiter, RWSEM WAITING BIAS-RWSEM ACTIVE BIAS);
return sem;
}
// 读写信号量共用逻辑,等待锁释放
static inline struct rw semaphore*rwsem_down_failed_common(struct rw_semaphore *sem,
struct rwsem waiter*waiter, signed long adjustment) {
truct task_struct *tsk= current; //获取当前任务PCB
signed long count;
set_task_state(tsk,TASK_UNINTERRUPTIBLE); //设置任务为不可中断阻塞状态
// 上自旋锁
spin_lock(&sem->wait_lock);
// 将PCB和等待节点关联
waiter->task=tsk;
list_add_tail(&waiter->list,&sem->wait_list); // 将等待节点插入等待队列末尾处
count = rwsem_atomic_update(adjustment, sem); // 原子性更新sem中的count值
// 如果不再有活动的锁,那唤醒之前等待的任务,因为这里可能有其他任务已经释放了锁
if (!(count &RWSEM_ACTIVE_MASK))
sem=__rwsem_do_wake(sem,1); // 传入1,表明可以唤醒写者
// 释放自旋锁
spin_unlock(&sem->wait_lock);
// 到这一步开始等待锁释放,如果等待者的等待标志位为0,则直接退出
for (;;){
if(!waiter->flags)
break; //否则调用调度器调度其他任务执行
schedule();
set_task_state(tsk, TASK_UNINTERRUPTIBLE);//设置任务状态为不可中断等待
}
//到这一步任务获得了锁,可直接修改任务状态为TASK_RUNNING
tsk->state=TASK RUNNING;
return sem;
}
读写信号量共用逻辑,等待锁释放,获取当前任务PCB,设置任务为不可中断阻塞状态,上自旋锁将P,CB和等待节点关联,将等待节点插入等待队列末尾处,原子性更新sem中的count值,如果不再有活动的锁,那唤醒之前等待的任务,因为这里可能有其他任务已经释放了锁,传入1,表明可以唤醒写者,释放自旋锁
到这一步开始等待锁释放,如果等待者的等待标志位为0,则直接退出,否则调用调度器调度其他任务执行,设置任务状态为不可中断等待
三、Linux内核释放读锁流程
这里先对count 进行原子性减1,如果有写任务正在等待锁释放,那么看看是否还有其他读线程执行操作,如果有,则退出;否则唤醒等待的写任务。
static inline void__up_read(struct rw_semaphore*sem){
s32 tmp =-RWSEM ACTIVE READ BIAS; asm volatile
// 原子性减1, 返回旧值
LOCK_PREFIX" xadd %%edx,(%%eax)"
// 如果小于0,那么有写任务在等待锁释放,将跳到标号为2处执行
" js 2f"
"1:"
LOCK_SECTION_START("")
"2:"
// 对edx也就是上一步替换的lock值的低16位即 dx自减
decw %%dx"
// 如果不为0,则表明有其他任务在操作,可什么都不做,退出即可
inz 1b"
// 否则保存ecx,调用rwsem_wake唤醒等待的任务
pushl %%eсx"
call rwsem_wake"
popl %%есx"
jmp 1b"
LOCK_SECTION_END
"=m"(sem->count), "=d"(tmp)
"a"(sem), "1"(tmp), "m"(sem->count)
"memory", "cc");
}
总结
Linux内核读锁实现原理,描述自旋锁时,已经顺带描述了读写自旋锁,所以本节将不再描述自旋锁的读写锁实现。读者是否能想到,既然自旋锁有相关的读写锁实现,信号量也应该有呢?答案是一定的。所以可以到,读写锁实际上是在原有锁上进行优化读写的操作。