【精通内核】Linux内核V操作原理解析

302 阅读4分钟

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

前言

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

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

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

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


本文导读

本文深入Linux内核源码,从核心源码入口讲起,详细对信号量、互斥量的内核代码讲解。

其中对P-V操作实现逐行剖析,Linux内核并发控制原理的锁实现和原理在后续文章中一一讲解,本文深入浅出Linux中断控制的实现原理。

一、释放信号量 V 操作原理解析

唤醒信号量通过对 semaphore 中的 counter 进行加1,同样通过 LOCK 前缀保证指令的原子性,根据返回值是否小于或等于0来判断是否有线程在等待,如果有线程等待,那么调用_up_wakeup 函数唤醒等待信号量的线程。

    static inline void up(struct semaphore*sem) {
        _asm___volatile_(
            LOCK "incl %O" // 原子性实现++sem->count
            "jle 2f"       // 如果小于或等于0,则跳到2标志处,调用__up_wakeup函数唤醒等待任务
            "1:"
            LOCK SECTION_START ("") 
            "2:call__up_wakeup" 
            "jmp 1b"
            LOCK_SECTION_END 
            ".subsection 0"
            :"=m” (sem->count)"
            :"c" (sem)"
            :" memory ");
    }

    // 汇编代码保存影响的寄存器,然后调用_up函数唤醒任务 
    asm(
    ".text"
    ".align 4"
    ".globl___up_wakeup" 
    "__up_wakeup: "
            "pushl %eax" 
            "pushl %edx" 
            "pushl %ecx"
            "call_ up"    // 调用__up 函数
            "popl %ecx" 
            "popl %edx" 
            "popl %eax" 
            "ret");

我们可以看到up()函数,原子性实现++sem->count,如果小于或等于0,则跳到2标志处,调用__up_wakeup函数唤醒等待任务,汇编代码保存影响的寄存器,然后调用_up函数唤醒任务,最终调用_up唤醒。

下面看下__up唤醒的源码实现。

二、_up唤醒函数源码实现

__up直接调用wake_up,通过唤醒宏定义,调用唤醒函数实现__wake_up,获取自旋锁,调用__wake_up_common函数唤醒任务,注意这里传入的是sync为0,释放自旋锁的过程

    void __up(struct semaphore *sem) {
        wake_up( & sem -> wait); // 直接调用wake_up
    }

    //唤醒宏定义
    # define wake_up(x) __wake_up((x),TASK_UNINTERRUPTIBLE |TASK_INTERRUPTIBLE,1)

    // 唤醒函数实现
    void __wake_up(wait_queue_head_t *q, unsigned int mode, int nr_exclusive) {
        unsigned long flags;
        spin_lock_irqsave(&q->lock, flags);     // 获取自旋锁
        // 调用__wake_up_common函数唤醒任务,注意这里传入的是sync为0
        _wake_up_common(q, mode, nr_exclusive, 0);
        spin_unlock_irqrestore(&q->lock, flags); // 释放自旋锁
    }
    
    // 唤醒操作_wake_up_common 函数的实现原理。
    static void _wake_up_common(wait_queue_head_*qunsigned int mode, int r_exclusive, int sync) {

        struct list_head *tmp,*next;
        list_for_each_safe(tmp, next, & q -> task_list){ // 遍历等待列表
            wait_queue_t * curr;
            unsigned flags;
            //获取当前任务节点wait_queue_I
            curr = list_entry(tmp, wait_queue_, task_list);
            flags = curr -> flags//获取当前等待标志位

            // 调用唤醒函数
            // 如果成功唤醒任务、 当前任务标志位 WQ_FLAG_EXCLUSIVE、
            // nr_exclusive 互斥数量,自减为0,那么退出循环
            if (curr -> func(curr, mode, sync) && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
                break;
        }
    }

    // 默认唤醒函数 default wake function。其实现过程如下
    int default_wake_function(walt_queue_curr, unsigned mode, int sync) {
        task_t * p = curr -> task;            // 获取当前任务	
        return try_to_wake_up(p, mode, sync); // 调用try_to_wake_up函数唤醒
    }

我们可以看到是通过调用try_to_wake_up函数唤醒,其中涉及Linux调度器,且该方法涉及大量实现,这里我们给出源码的相关原理

首先获取到当前CPU的执行队列 runqueue,并且关闭中断,保存当前状态信息,如果当前状态信息和传入状态不为0并且如果当前任务不属于任何优先级队列,执行

任务重调度实现:如果是对称多处理器结构,那么需要通过跨CPU 调用,触发目标CPU调度器的调度工作。

信号量代码逻辑较为简单,对于 P-V 操作,我们通过原子性对 counter 变量操作,然后其放入信号量的等待队列中,或者将其从等待队列中取出。

由于是多线程操作阻塞队列,因此需要把自旋锁来保护阻塞队列。接着判断任务是否处于任务就绪队列 runqueue 中,如果在队列中,则设置标志为 TASKRUNNING 状态,否将入 runqueue 中。这里 runqueue 是每个 CPU 都拥有的任务就绪调度队列。