【精通内核】Linux内核写锁实现原理与源码解析

173 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情

前言

📫作者简介小明java问道之路,专注于研究 Java/ Liunx内核/ C++及汇编/计算机底层原理/源码,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计与演进、系统优化与稳定性建设。

📫热衷分享,喜欢原创~ 关注我会给你带来一些不一样的认知和成长

🏆InfoQ签约作者、CSDN专家博主/后端领域优质创作者/内容合伙人、阿里云专家/签约博主、51CTO专家🏆

🔥如果此文还不错的话,还请👍关注 、点赞 、收藏三连支持👍一下博主~


本文导读

实际上,针对读写信号量,如果我们用C语言代码高级语言来描述的话,则十分简单,即一个公平的读写锁。也就是说,当有读锁持有时,如果有读任务,则可以直接获得读锁;但如果此时有写仕务在等待的情况下,那么将会导致读锁获取失败,转而进入等待状态。当读锁释放后返回看看有没写者在等待,如果有写者在等待且传入了唤醒写者的标识1,那么看看等待列表的下一个等待任务是1是写节点,如果不是,那么遍历等待列表,唤醒所有读者,直到遇到一个写节点。然而,如果在持有写锁的情况下,那么读锁肯定获取失败,然后进入等待队列中,写锁被释放后,如果有锁等待,那会唤醒等待任务。

一、Linux内核获取写锁源码解读

首先原子性减0xffff0001,然后判断原来的状态是否为 0,如果是,则表明获取写锁成功;

否则需要调用 rwsem_down_write_failed 函数进行阻塞排队操作。

static inline void_down_write(struct rw_semaphore *sem) {
    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 唤醒等待的任务

static inline void_up_write(struct rw_semaphore *sem) {
    _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 函数唤醒等待的任务。

struct rw_semaphore *rwsem wake(struct rw semaphore*sem) {
    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是写节点,如果不是,那么遍历等待列表,唤醒所有读者,直到遇到一个写节点。然而,如果在持有写锁的情况下,那么读锁肯定获取失败,然后进入等待队列中,写锁被释放后,如果有锁等待,那会唤醒等待任务。