【精通内核】Linux内核初始化与上读锁流程源码解读

340 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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内核读锁实现原理,描述自旋锁时,已经顺带描述了读写自旋锁,所以本节将不再描述自旋锁的读写锁实现。读者是否能想到,既然自旋锁有相关的读写锁实现,信号量也应该有呢?答案是一定的。所以可以到,读写锁实际上是在原有锁上进行优化读写的操作。