前言
📫作者简介:小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫
🏆 InfoQ 签约博主、CSDN 专家博主/Java 领域优质创作者/CSDN 内容合伙人、阿里云专家/签约博主、华为云专家、51CTO 专家/TOP 红人 🏆
🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~
本文导读
Linux 内核读锁实现原理,描述自旋锁时,已经顺带描述了读写自旋锁,所以本节将不再描述自旋锁的读写锁实现。读者是否能想到,既然自旋锁有相关的读写锁实现,信号量也应该有呢?答案是一定的。所以可以到,读写锁实际上是在原有锁上进行优化读写的操作。下面讨论源码实现。
一、Linux 内核读写锁核心结构源码解读
定义一个结构体 rw_semaphore 代表读写信号量,然后义一宏定义表明读写信号量的偏移值。具体源码如下。
struct 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 内核读锁实现原理读写信号量初始化源码解读
读写信号量初始化时,将 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 函数。
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;
}
复制代码
四、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 内核读锁实现原理,描述自旋锁时,已经顺带描述了读写自旋锁,所以本节将不再描述自旋锁的读写锁实现。读者是否能想到,既然自旋锁有相关的读写锁实现,信号量也应该有呢?答案是一定的。所以可以到,读写锁实际上是在原有锁上进行优化读写的操作。
评论