持续创造,加快生长!这是我参加「日新方案 10 月更文应战」的第2天,点击检查活动概况

前言

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

热心共享,喜爱原创~ 重视我会给你带来一些不一样的认知和生长

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

假如此文还不错的话,还请重视 、点赞 、收藏三连支撑一下博主~


本文导读

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

其间对P-V操作完成逐行剖析,Linux内核并发操控原理的锁完成和原理在后续文章中一一解说,本文深入浅出Linux中止操控的完成原理。

一、Linux内核P-V原语

获取信号量 P 操作,经过内联汇编原子性对 counte 操作。首先经过 decl 同时依据是否是多处理器加 lock 前缀,确保了单条指令的原子性,然后依据递减后的值是否为负数来判断获取信号量是否成功,假如失利,那么需要将线程进行睡眠,此时调用 _down_failed 函数完成此操作。

下面我们来看,如何 获取信号量 P 操作原理解析与# void__down终究完成函数。

二、获取信号量 P 操作原理解析

此时调用 _down_failed 函数完成此操作,具体完成原理如下。

经过 lock 前缀完成原子性的-sem->count操作,decl指令相当于对操作数自减

    static inline void down(struct semaphore*sem) {
        _asm__volatile_( // 经过 lock 前缀完成原子性的-sem->count操作,decl指令相当于对操作数自减 
            LOCK "decl %O" // 假如减完后发现sign标志位为1,则标明count值为负,往前跳到标号2处,调用 __down_failed处理,否则获取成功,直接退出
            "js 2f"
            "1: "
            LOCK_SECTION_START("") 
            "2:call__down_failed" 
            "jmp 1b"
            // 这儿采用了 LOCK SECTION START 和LOCK SECTION END 宏界说,将call
            // __down_failed 和 jmp 1b的汇编代码放到.textlock段中
            // 所以假如履行完 __down_failed 办法后调用jmp 1b
            // 会回到 LOCK SECTION START之前的段中,即退出down办法
            LOCK SECTION END:
            : "=m" (sem -> count)
            :"c" (sem)
            :"memory");
    }
    // 经过汇编声明晰 __down_failed的代码地址
    asm(
        ".text"
        ".align 4"         // 4字节对齐
        ".globl___down_failed"
        "__down failed:"
        #if defined(CONFIG_FRAME_POINTER)  // 假如界说了栈帧指针,那么拓荒新的办法帧
            "pushl %ebp"
            "movl %esp, %ebp"
        #endif
        // 保存影响的寄存器值,因为随后要调用_down 函数, 可能会影响 eax、edx、ecx 寄存器,
        // 所以这儿需要先对其进行保存,在办法回来后再复原
        "pushl %eax"
        "pushl %edx"
        "pushl %ecx"
        "call __down"     // 调用 __down来履行当counter为0时的操作
        "popl %ecx"    // 调用回来后康复保存的寄存器
        "popl %edx"
        "popl %eax"
        #if defined(CONFIG_FRAME_POINTER)    //复原办法帧
            "movl %ebp,%esp"
            "popl %ebp"
        #endif 
        "ret"
    );

我们终究是调用函数 __ down 来履行终究的 __down_failed 操作。

三、void__down终究完成函数

下面是 void__down 函数源码,经过current 宏获取当前使命结构体,获取到了使命PCB,初始化wait_queuet,也便是等候线程代表,宏界说为;wait_queue_t name = {.task = tsk, .func = defauft_wake_function, .tasklist = {NULL, NULL}}

设置使命状况为TASK_UNINTERRUPTIBLE,标明不行中止的堵塞,获取自旋锁,将等候使命节点插入等候链表的队尾处,添加等候计数,循环等候开释信号量,对等候线程减1后与当前信号量的counter值相加

1、使命状况TASK_RUNNING

假如成果等于0则完毕循环,这儿等于0的条件便是等候信号量满足包容更多的线程,所以不需要堵塞,设置等候使命数为1,开释自旋锁,唤醒调度器履行其他使命,当前使命就被堵塞在了等候行列里

当使命从头被唤醒时,将从头获取自旋锁,从头设置使命状,唤醒等候使命,开释自旋锁,设置当前使命状况为TASK_RUNNING。

2、Liunx内核完成原理

    void__down(struct semaphore *sem) {
        // 经过current 宏获取当前使命结构体,获取到了使命PCB
        struct task_struct *tsk = current;
        // 初始化wait_queue t,也便是等候线程代表
        // 宏界说为;wait_queue_t name = {.task = tsk, .func = defauft_wake_function, .tasklist = {NULL, NULL}}
        DECLARE_WAITQUEUE(wait, tsk);
        unsigned long flags;
        tsk->state = TASK UNINTERRUPTIBLE;           // 设置使命状况为TASK_UNINTERRUPTIBLE,标明不行中止的堵塞 
        spin_lock_irqsave(&sem -> waitlock, flags); // 获取自旋锁
        add_wait_queue_exclusive_locked(&sem -> wait, &wait); // 将等候使命节点插入等候链表的队尾处
        sem -> sleepers++;          // 添加等候计数
        for (; ; ) {                // 循环等候开释信号量
            int sleepers = sem -> sleepers;
            // 对等候线程减1后与当前信号量的counter值相加
            // 假如成果等于0则完毕循环,这儿等于0的条件便是等候信号量满足包容更多的线程,所以不需要堵塞
            if (!atomic_add_negative(sleepers - 1, & sem -> count)){
                sem -> sleepers = 0;
                break;
            }
            sem -> sleepers = 1;    // 设置等候使命数为1
            spin_unlock_irqrestore( & sem -> wait.lock, flags); // 开释自旋锁
            // 唤醒调度器履行其他使命,当前使命就被堵塞在了等候行列里
            schedule();
            spin_lock_irqsave( & sem -> wait.lock flags);  // 当使命从头被唤醒时,将从头获取自旋锁 
            tsk -> state = TASK UNINTERRUPTIBLE;           // 从头设置使命状况为不行中止状况,继续循环 
        }
        // 至此使命现已获取了信号量,等候线程从行列中移出来 
        remove_wait_queue_locked( & sem -> wait, &wait);
        wake_up_locked( & sem -> wait);                     // 唤醒等候使命
        spin_unlock_irqrestore( & sem -> wait.lock, flags); // 开释自旋锁
        tsk->state = TASK_RUNNING;                          // 设置当前使命状况为TASK_RUNNING 
    }

上面代码我们可以看到,使用了自旋锁、P-V操作,并添加了堵塞行列完成信号量。假如读者对Linux进程调度原理不清楚,这儿面办法 schedule ,其效果便是开释 CPU 的操控权,交给调度程序

然后由调度程序切换到其他进程履行,直到信号量开释后,再由其他进程将其状况设置为 RUNNABLE 后,交由调度进程从头调度履行。