• 2018-2019-1 20189203《Linux内核原理与分析》第九周作业


    第一部分 课本学习

    进程的切换和系统的一般执行过程

    • 进程调度的时机
      Linux内核系统通过schedule函数实现进程调度,进程调度的时机就是内核调用schedule函数的时机。当内核即将返回用户空间时,内核会检查need_resched标志是否设置。如果设置,则调用schedule函数,此时是从中断(异常/系统调用)处理程序返回用户空间的时间点作为一个固定的调度时机点。
      简单总结进程调度时机如下:
      用户进程通过特定的系统调用主动让出CPU
      中断处理程序在内核返回用户态时进行调度。
      内核线程主动调用schedule函数让出CPU。
      中断处理程序主动调用schedule函数让出CPU,涵盖以上第一种和第二种情况。

    • 调度策略与算法
      调度策略:Linux系统中常用的几种调度策略为SCHED_NORMAL、SCHED_FIFO、SCHED_RR。其中SCHED_NORMAL是用于普通进程的调度类,而SCHED_FIFO和SCHED_RR是用于实时进程的调度类,优先级高于SCHED_NORMAL。内核中根据进程的优先级来区分普通进程和实时进程,Linux内核进程优先级为0139,数值越高,优先级越低,0为最高有限级。实时进程的优先级取值为099;而普通进程只具有nice值,nice值映射到优先级为100~139。子进程会继承父进程的优先级。
      CFS调度算法:CFS即为完全公平调度算法,其基本原理是基于权重的动态优先级调度算法。每个进程使用CPU的顺序由进程已使用的CPU虚拟时间(vruntime)决定,已使用的虚拟时间越少,进程排序就越靠前,进程再次被调度执行的概率就越高。每个进程每次占用CPU后能够执行的时间(ideal_runtime)由进程的权重决定,并且保证在某个时间周期(__sched_period)内运行队列里的所有进程都能够至少被调度执行一次。

    • 进程上下文切换
      在实际代码中,每个进程切换基本由两个步骤组成。
      切换页全局目录(CR3)以安装一个新的地址空间,这样不同进程的虚拟地址如0x8048400就会经过不同的页表转换为不同的物理地址。
      切换内核态堆栈和硬件上下文,因为硬件上下文提供了内核执行新进程所需要的所有信息,包含CPU寄存器状态

    • Linux系统的运行过程
      最基本和一般场景是:正在运行的用户态进程X切换到用户态进程Y的过程。具体过程如下。
      1、 正在运行的用户态进程X。
      2、发生中断(包括异常、系统调用等)。
      3、SAVE_ALL,保存现场,此时完成了中断上下文切换,即从进程X的用户态到进程X的内核态。
      4、中断处理过程中或中断返回前调用了schedule函数,其中的switch_to做了关键的进程上下文切换。
      5、标号1,之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)。
      6、restore_all,恢复现场,与3中保存现场相对应。
      7、iret - pop cs:eip/ss:esp/eflags,从Y进程的内核堆栈中弹出2中硬件完成的压栈内容。此时完成了中断上下文的切换,即从进程Y的内核态返回到进程Y的用户态。
      8、继续运行用户态进程Y。

    • Linux系统架构与执行过程概览
      Linux系统的整体架构如图所示:

      Ls命令执行过程示意图如下:

    第二部分 代码分析

    • context_switch代码
    static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
    {
        struct mm_struct *mm, *oldmm;
    
        prepare_task_switch(rq, prev, next);
    
        mm = next->mm;
        oldmm = prev->active_mm;
        /*
         * For paravirt, this is coupled with an exit in switch_to to
         * combine the page table reload and the switch backend into
         * one hypercall.
         */
        arch_start_context_switch(prev);
    
        if (!mm) {    //如果被切换进来的进程的mm为空切换,内核线程mm为空
            next->active_mm = oldmm;  //将共享切换出去的进程的active_mm
            atomic_inc(&oldmm->mm_count);  //有一个进程共享,所有引用计数加一
            enter_lazy_tlb(oldmm, next);  //普通mm不为空,则调用switch_mm切换地址空间
        } else
            switch_mm(oldmm, mm, next);
    
        if (!prev->mm) {
            prev->active_mm = NULL;
            rq->prev_mm = oldmm;
        }
        /*
         * Since the runqueue lock will be released by the next
         * task (which is an invalid locking op but in the case
         * of the scheduler it's an obvious special-case), so we
         * do an early lockdep release here:
         */
        spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
    
        context_tracking_task_switch(prev, next);
        // 这里切换寄存器状态和栈 
        switch_to(prev, next, prev);
    
        barrier();
        /*
         * this_rq must be evaluated again because prev may have moved
         * CPUs since it called schedule(), thus the 'rq' on its stack
         * frame will be invalid.
         */
        finish_task_switch(this_rq(), prev);
    }
    
    • switch_to代码
    #define switch_to(prev, next, last) //prev指向当前进程,next指向被调度的进程                                   
    do {                                                                              
                                                            
             unsigned long ebx, ecx, edx, esi, edi;
                                      
             asm volatile("pushfl
    	"  //把prev进程的flag保存到prev进程的内核堆栈中
                          "pushl %%ebp
    	" //把prev进程的基址ebp保存到prev进程的内核堆栈中
               
                          "movl %%esp,%[prev_sp]
    	"//保存ESP
                          "movl %[next_sp],%%esp
    	"//更新ESP,将下一栈顶保存到ESP中 
                          
                          "movl $1f,%[prev_ip]
    	"//保存当前进程EIP*  
                          "pushl %[next_ip]
    	"//把next进程起点压入next进程的内核堆栈栈顶 
                          __switch_canary                                        
                          "jmp __switch_to
    "//prev进程中设置next进程堆栈
                                             //jmp不同于call,是通过寄存器传递参数,而不是通过堆栈传递参数,所以ret时弹出的是之前压入栈顶的next进程起点
                                             //wancheng EIP的切换
                          "1:	"                                                    
                          "popl %%ebp
    	"   
                          "popfl
    "                         
                                                                                    
                          /* output parameters */                                   
                          : [prev_sp] "=m"(prev->thread.sp),     //保存prev进程的esp
                            [prev_ip] "=m"(prev->thread.ip),     //保存prev进程的eip
                            "=a" (last),                                                
                                                                                       
                          /* clobbered output registers: */              
                            "=b" (ebx), "=c"(ecx), "=d" (edx),              
                            "=S" (esi), "=D"(edi)                            
                                                                                        
                           __switch_canary_oparam                                     
                                                                                        
                              /* input parameters: */                                  
                          : [next_sp]  "m" (next->thread.sp),      //next进程内核堆栈栈顶地址,即esp
                            [next_ip]  "m" (next->thread.ip),      //next进程的原eip
                                                                                       
                          /* regparm parameters for __switch_to():*/  
                          //jmp通过eax寄存器和edx寄存器传递参数
                            [prev]     "a" (prev),                                   
                            [next]     "d" (next)                                    
                                                                                         
                            __switch_canary_iparam                             
                                                                                        
                          : /* 重新加载段寄存器            
                         "memory");                                           
    } while (0)  
    

    第三部分 实验楼实验

    在实验楼中配置运行MenuOS系统:


    配置gdb断点:

    开始执行,分别停在各断点处:


    Schedule函数的作用非常重要,是进程调度的主体函数。其中pick_next_task函数是schedule函数中重要的函数,负责根据调度策略和调度算法选择下一个进程,context_switch函数是schedule函数中实现进程切换的函数,switch_to是context_switch函数中进行进程关键上下文切换的函数。由于switch_to内部是内嵌汇编代码,无法跟踪调试。

  • 相关阅读:
    数字重排
    环游世界
    Hibernate笔记(一)增删改查CRUD
    Sliding Window Algorithm 滑动窗口算法
    纯HTML/CSS/JS实现淘宝、京东两种轮播图
    COA计算机组织与结构笔记
    数据结构与算法笔记:最小生成树Kruskal、Prim算法与JAVA实现
    数据结构与算法笔记:图的基础与JAVA实现
    数据结构与算法:并查集
    JDBC学习笔记
  • 原文地址:https://www.cnblogs.com/23du/p/10090930.html
Copyright © 2020-2023  润新知