• Linux内核设计第八周学习总结 理解进程调度时机跟踪分析进程调度与进程切换的过程


    陈巧然 原创作品转载请注明出处

    《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

    一、视频内容

    Linux系统的一般执行过程

    最一般的情况:正在运行的用户态进程X切换到运行用户态进程Y的过程

    1. 正在运行的用户态进程X

    2. 发生中断——save cs:eip/esp/eflags(current) to kernel stack, then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

    3. SAVE_ALL //保存现场,这里是已经进入内核中断处里过程

    4. 中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

    5. 标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

    6. restore_all //恢复现场

    7. iret - pop cs:eip/ss:esp/eflags from kernel stack

    8. 继续运行用户态进程Y

    几种特殊情况

    • 通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;
    • 内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;
    • 创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;
    • 加载一个新的可执行程序后返回到用户态的情况,如execve;

    进程的调度时机与进程的切换

    操作系统原理中介绍了大量进程调度算法,这些算法从实现的角度看仅仅是从运行队列中选择一个新进程,选择的过程中运用了不同的策略而已。

    对于理解操作系统的工作机制,反而是进程的调度时机与进程的切换机制更为关键。

    进程调度的时机

    • 中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();比如sleep,就可能直接调用了schedule
    • 内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;
    • 用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。用户态进程只能被动调度。

    进程的切换

    • 为了控制进程的执行,内核必须有能力挂起正在CPU上执行的进程,并恢复以前挂起的某个进程的执行,这叫做进程切换、任务切换、上下文切换;即进程上下文切换!
    • 挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行;
    • 进程上下文包含了进程执行需要的所有信息
      • 用户地址空间:包括程序代码,数据,用户堆栈等
      • 控制信息:进程描述符,内核堆栈等
      • 硬件上下文(注意中断也要保存硬件上下文只是保存的方法不同)
    • schedule()函数选择一个新的进程来运行,并调用context_switch进行上下文的切换,这个宏调用switch_to来进行关键上下文切换

    31#define switch_to(prev, next, last)                    

    32do {                                   

    33  /*                              

    34   * Context-switching clobbers all registers, so we clobber  

    35   * them explicitly, via unused output variables.     

    36   * (EAX and EBP is not listed because EBP is saved/restored  

    37   * explicitly for wchan access and EAX is the return value of   

    38   * __switch_to())                     

    39   */                                

    40  unsigned long ebx, ecx, edx, esi, edi;                

    41                                  

    42  asm volatile("pushfl "      /* save    flags */   

    43           "pushl %%ebp "        /* save    EBP   */ 

    44           "movl %%esp,%[prev_sp] "  /* save    ESP   */ 

    45           "movl %[next_sp],%%esp "  /* restore ESP   */ 

    46           "movl $1f,%[prev_ip] "    /* save    EIP   */ 

    47           "pushl %[next_ip] "   /* restore EIP   */    

    48           __switch_canary                   

    49           "jmp __switch_to "  /* regparm call  */ 

    50           "1: " /*下一个进程开始执行的地方!*/                       

    51           "popl %%ebp "     /* restore EBP   */    

    52           "popfl "         /* restore flags */  

    53                                  

    54           /* output parameters */                

    55           : [prev_sp] "=m" (prev->thread.sp),     

    56             [prev_ip] "=m" (prev->thread.ip),        

    57             "=a" (last),                 

    58                                  

    59             /* clobbered output registers: */     

    60             "=b" (ebx), "=c" (ecx), "=d" (edx),      

    61             "=S" (esi), "=D" (edi)             

    62                                       

    63             __switch_canary_oparam                

    64                                  

    65             /* input parameters: */                

    66           : [next_sp]  "m" (next->thread.sp),        

    67             [next_ip]  "m" (next->thread.ip),       

    68                                       

    69             /* regparm parameters for __switch_to(): */  

    70             [prev]     "a" (prev),              

    71             [next]     "d" (next)               

    72                                  

    73             __switch_canary_iparam                

    74                                  

    75           : /* reloaded segment registers */           

    76          "memory");                  

    77} while (0)

    二、实验过程

    实验内容

    1.理解Linux系统中进程调度的时机,可以在内核代码中搜索schedule()函数,看都是哪里调用了schedule(),判断我们课程内容中的总结是否准确;
    2.使用gdb跟踪分析一个schedule()函数 ,验证您对Linux系统进程调度与进程切换过程的理解;推荐在实验楼Linux虚拟机环境下完成实验。
    3.特别关注并仔细分析switch_to中的汇编代码,理解进程上下文的切换机制,以及与中断上下文切换的关系;

    实验步骤

    1. 打开实验楼虚拟机 https://www.shiyanlou.com/courses/running/890

    2. 在shell中依次运行以下命令,获取本次实验的代码,并编译运行

    cd LinuxKernel

    rm menu -rf

    git clone https://github.com/mengning/menu.git

    cd menu

    mv test_exec.c test.c

    make rootfs 

    效果如下:



    3. 关闭QEMU窗口,在shell窗口中,cd LinuxKernel回退到LinuxKernel目录,使用下面的命令启动内核并在CPU运行代码前停下以便调试:

    qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S 

    接下来,我们就可以水平分割一个新的shell窗口出来,依次使用下面的命令启动gdb调试

    gdb

    (gdb) file linux-3.18.6/vmlinux

    (gdb) target remote:1234

    并在系统调用

    关闭QEMU窗口,在shell窗口中,cd LinuxKernel回退到LinuxKernel目录,使用下面的命令启动内核并在CPU运行代码前停下以便调试:

    qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S 

    接下来,我们就可以水平分割一个新的shell窗口出来,依次使用下面的命令启动gdb调试

    gdb

    (gdb) file linux-3.18.6/vmlinux

    (gdb) target remote:1234

    并在内核函数schedule的入口处设置断点,接下来输入c继续执行,则系统即可停在该函数处,接下来我们就可以使用命令n或者s逐步跟踪,可以详细浏览pick_next_task,switch_to等函数的执行过程,有图为证:

     

    三、总结

    进程调度时机有三:

    1、中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule();

    2、内核线程可以直接调用schedule()进行进程切换,也可以在中断处理过程中进行调度,也就是说内核线程作为一类的特殊的进程可以主动调度,也可以被动调度;

    3、用户态进程无法实现主动调度,仅能通过陷入内核态后的某个时机点进行调度,即在中断处理过程中进行调度。

    进程切换:为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换(process switch)、任务切换(task switch)或上下文切换(context switch)。

    挂起正在CPU上执行的进程,与中断时保存现场是不同的,中断前后是在同一个进程上下文中,只是由用户态转向内核态执行。

    进程上下文包含了进程执行需要的所有信息,包括:

    1、用户地址空间:包括程序代码,数据,用户堆栈等

    2、控制信息:进程描述符,内核堆栈等

    3、硬件上下文(与中断保存硬件上下文的方法不同)

    Linux系统的一般执行过程

    最一般的情况:

    正在运行的用户态进程X切换到运行用户态进程Y的过程

    1、正在运行的用户态进程X

    2、发生中断——save cs:eip/esp/eflags(current) to kernel stack,then load cs:eip(entry of a specific ISR) and ss:esp(point to kernel stack).

    3、SAVE_ALL //保存现场

    4、中断处理过程中或中断返回前调用了schedule(),其中的switch_to做了关键的进程上下文切换

    5、标号1之后开始运行用户态进程Y(这里Y曾经通过以上步骤被切换出去过因此可以从标号1继续执行)

    6、restore_all //恢复现场

    7、iret - pop cs:eip/ss:esp/eflags from kernel stack

    8、继续运行用户态进程Y

    几种特殊情况:

    1、通过中断处理过程中的调度时机,用户态进程与内核线程之间互相切换和内核线程之间互相切换,与最一般的情况非常类似,只是内核线程运行过程中发生中断没有进程用户态和内核态的转换;

    2、内核线程主动调用schedule(),只有进程上下文的切换,没有发生中断上下文的切换,与最一般的情况略简略;

    3、创建子进程的系统调用在子进程中的执行起点及返回用户态,如fork;

    4、加载一个新的可执行程序后返回到用户态的情况,如execve;

  • 相关阅读:
    servlet.txt笔记
    用数组实现集合的功能
    用父类声明的变量和用接口声明的变量的区别
    DHTML_____document对象的方法
    DHTML_____window对象的事件
    DHTML_____window对象属性
    DHTML_____window对象方法
    DHTML_____如何编写事件处理程序
    常用点击事件(鼠标、光标、键盘、body)
    鼠标滑动显示不同页面的效果——————获取鼠标相对于整个页面的坐标
  • 原文地址:https://www.cnblogs.com/20135310cqr/p/5380570.html
Copyright © 2020-2023  润新知