2019-2020-1 20199304《Linux内核原理与分析》第九周做业

第八章 进程的切换和系统的通常执行过程

知识点

1.进程调度的时机

1.1硬中断和软中断
中断是指在计算机执行期间,系统内发生任何非寻常的或非预期的急需处理事件,使得CPU暂时中断当前正在执行的程序而转去执行相应的时间处理程序。待处理完毕后又返回原来被中断处继续执行或调度新的进程执行的过程。
引发中断的事件称为中断源。中断源向CPU提出处理的请求称为中断请求。发生中断时被打断程序的暂停点称为断点。CPU暂停现行程序而转为响应中断请求的过程称为中断响应。处理中断源的程序称为中断处理程序。CPU执行有关的中断处理程序称为中断处理。而返回断点的过程称为中断返回。中断的实现由软件和硬件综合完成,硬件部分叫作硬件装置,软件部分称为软件处理程序。linux

  • 硬中断:由与系统相连的外设(好比网卡、硬盘)自动产生的。主要是用来通知操做系统系统外设状态的变化。好比当网卡收到数据包的时候,就会发出一个中断。咱们一般所说的中断指的是硬中断(hardirq)。
  • 软中断:为了知足实时系统的要求,中断处理应该是越快越好。linux为了实现这个特色,当中断发生的时候,硬中断处理那些短期就能够完成的工做,而将那些处理事件比较长的工做,放到中断以后来完成,也就是软中断(softirq)来完成。
  • 异常:
    • 故障(Fault):出现问题,能够恢复到当前指令。
    • 退出(Abort):不可恢复的严重故障,致使程序没法继续运行,只能退出。
    • 陷阱(Trap):程序主动产生的异常。
      1.2进程调度时机
  • schedule函数
    • Linux内核经过schedule函数实现进程调度,schedule函数在运行队列中找到一个进程,把CPU分配给它。因此调用schedule函数的时候就是进程调度的时机。
  • 上下文
    通常来讲,CPU在任什么时候刻都处于如下3种状况之一。算法

    • 运行于用户空间,执行用户进程上下文。
    • 运行于内核空间,处于进程(通常是内核线程)上下文。
    • 运行于内核空间,处于中断上下文。
      运行在进程上下文的内核代码是能够被抢占的(Linux2.6支持抢占)。可是一个中断上下文,一般都会始终占有CPU(固然中断能够嵌套,但咱们通常不这样作),不能够被打断。正由于如此,运行在中断上下文的代码就要受一些限制,不能作下面的事情:
    • 睡眠或者放弃CPU:这样作的后果是灾难性的,由于内核在进入中断以前会关闭进程调度,一旦睡眠或者放弃CPU,这时内核没法调度别的进程来执行,系统就会死掉。
    • 尝试得到信号量:若是得到不到信号量,代码就会睡眠,会产生和上面相同的状况。
    • 执行耗时的任务:中断处理应该尽量快,由于内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。
    • 访问用户空间的虚拟地址:由于中断上下文是和特定进程无关的,它是内核表明硬件运行在内核空间,因此在终端上下文没法访问用户空间的虚拟地址。
  • 进程调度
    简单总结进程调度时机以下:
    • 用户进程经过特定的系统调用主动让出CPU。
    • 中断处理程序在内核返回用户态时进行调度。
    • 内核线程主动调用schedule函数让出CPU。
    • 中断处理程序主动调用schedule函数让出CPU,涵盖第一和第二种状况。架构

      2.调度策略和算法

      调度算法就是从就绪队列中选一个进程。调度策略是寻找知足需求的方法,而调度算法是如何实现这个调度策略。
      2.1 进程的分类
      按CPU占用率分类:ide

  • I/O消耗型进程。这种进程的特色是CPU负载不高,大量时间都在等待读写数据
  • 处理器消耗型进程。这种进程的特色是CPU占用率为100%,但没有太多的硬件进行读写操做
  • 按对系统响应时间要求分类
  • 交互式进程。此类进程有大量人机交互,对系统响应时间要求比较高,不然用户会感受系统反应迟缓。
  • 批处理进程。此类进程不须要人机交互,在后台运行,须要占用大量的系统资源,可是可以忍受响应延迟。
  • 实时进程。实时进程对调度延迟的要求最高,这些进程执行很是重要的操做,要求当即执行响应并执行。
    当前Linux系统的解决方案是,对于实时进程,linux采用FIFO(先进先出)或者Round Robin(时间片轮转)的调度策略。对其它进程,则采用CFS调度器,核心是“彻底公平”。函数

2.2 调度策略
Linux系统中经常使用的几种调度策略为SCHED_NORMAL、SCHED_FIFO、SCHED_RR。
其中SCHED_NORMAL是用于普通进程的调度类,而SCHED_FIFO和SCHED_RR是用于实时进程的调度类,优先级高于SCHED_NORMAL。内核根据进程的优先级来区分普通进程与实时进程,Linux内核进程优先级为0~139,数值越高,优先级越低,0为最高优先级。实时进程的优先级取值为0~99,普通进程只具备nice值,nice值映射到优先级为100~139。学习

2.3 CFS调度算法
CFS即为彻底公平调度算法,其基本原理是基于权重的动态优先级调度算法。每一个进程使用CPU的顺序进程由已使用的CPU虚拟时间(vruntime)决定,已使用的虚拟时间越少,进程排序就越靠前,进程再次被调度执行的几率也就越高。每一个进程每次占用CPU后可以执行的时间(ideal_runtime)由进程的权重决定,而且保证在某个时间周期(_sched_period)内运行队列的因此进程都可以至少被调度执行一次。atom

3. 进程上下文切换

3.1 进程执行环境的切换
为了控制进程的执行,内核必须有能力挂起正在CPU中运行的进程,并恢复执行之前挂起的某个进程。这种行为被称为进程切换,任务切换或进程上下文切换。进程上下文包含了进程执行须要的全部信息。idea

  • 用户空间地址:包括程序代码、数据、用户堆栈等。
  • 控制信息:进程描述符、内核堆栈等。
  • 硬件上下文,相关寄存器的值。
    在实际的代码中,每一个进程切换基本由两个步骤组成:操作系统

  • 切换页全局目录(CR3)以安装一个新的地址空间,这样不一样进程的虚拟地址就会通过不一样的页表转换为不一样的物理地址。
  • 切换内核态堆栈和硬件上下文,由于硬件上下文提供了内核执行新进程所须要的全部信息。
    线程

4.Linux系统的运行过程

  • Linux系统的通常执行过程:正在运行的用户态进程X切换到用户态进程Y的过程:
    • 1.正在运行的用户态进程X;
    • 2.发生中断(包括异常,系统调用等),硬件完成如下动做:
      • save cs:eip/esp/eflags:当前CPU上下文压入用户态进程X的内核堆栈;
      • 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,从Y进程的内核堆栈中弹出(2)中硬件完成的压栈内容;
    • 8.继续运行用户态进程Y。
      Linux操做系统的总体架构以下图所示

实验:进程调度相关源代码跟综和分析

实验要求

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

实验过程

1.从新克隆一个menu,而后从新编译内核。



2.打开调试模式,另打开一个窗口进行gdb远程调试,配置gdb远程调试并设置断点。




关键代码分析

context_switch关键代码部分

static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next)
{
    ...
    arch_start_context_switch(prev);

    if (unlikely(!mm)) {    //若是被切换进来的进程的mm为空切换,内核线程mm为空
        next->active_mm = oldmm;  //将共享切换出去的进程的active_mm
        atomic_inc(&oldmm->mm_count);  //有一个进程共享,全部引用计数加一
        enter_lazy_tlb(oldmm, next);  //将per cpu变量cpu_tlbstate状态设为LAZY
    } else   //普通mm不为空,则调用switch_mm切换地址空间
        switch_mm(oldmm, mm, next);
...
    //这里切换寄存器状态和栈 
    switch_to(prev, next, prev);

switch_to关键代码部分

#define switch_to(prev, next, last)
do {
    /*
     * Context-switching clobbers all registers, so we clobber
     * them explicitly, via unused output variables.
     * (EAX and EBP is not listed because EBP is saved/restored
     * explicitly for wchan access and EAX is the return value of
     * __switch_to())
     */
    unsigned long ebx, ecx, edx, esi, edi;

    asm volatile(
             "pushfl\n\t"  //保存当前进程flags
             "pushl %%ebp\n\t"  //当前进程堆栈基址压栈
             "movl %%esp,%[prev_sp]\n\t"  //保存ESP,将当前堆栈栈顶保存起来
             "movl %[next_sp],%%esp\n\t"  //更新ESP,将下一栈顶保存到ESP中
                     // 完成内核堆栈的切换
             "movl $1f,%[prev_ip]\n\t"    //保存当前进程的EIP
             "pushl %[next_ip]\n\t"       //将next进程起点压入堆栈,即next进程的栈顶为起点
             __switch_canary              //next_ip通常为$1f,对于新建立的子进程是ret_from_fork      
             "jmp __switch_to\n"    //prve进程中,设置next进程堆栈,jmp与call不一样,是经过寄存器传递参数(call经过堆栈),因此ret时弹出的是以前压入栈顶的next进程起点
             //完成EIP的切换
             "1:\t"            //next进程开始执行       
             "popl %%ebp\n\t"  //restore EBP
             "popfl\n"         //restore flags

             //输出量
             : [prev_sp] "=m" (prev->thread.sp),   //保存当前进程的esp
               [prev_ip] "=m" (prev->thread.ip),     //保存当前进仓的eip
               "=a" (last),

               //要破坏的寄存器
               "=b" (ebx), "=c" (ecx), "=d" (edx),
               "=S" (esi), "=D" (edi)

               __switch_canary_oparam

              //输入量
             : [next_sp]  "m" (next->thread.sp),   //next进程的内核堆栈栈顶地址,即esp
               [next_ip]  "m" (next->thread.ip),     //next进程的eip

               // regparm parameters for __switch_to(): 
               [prev]     "a" (prev),
               [next]     "d" (next)

               __switch_canary_iparam

             : //从新加载段寄存器
            "memory");
} while (0)

总结

经过本次实验,我学习到了进程调度的时机和进程切换进行,分析进程的调度时机,调度策略和算法,并跟踪schedule,pick_next_task和context_switch等函数。 我了解到进程调度是为了合理分配计算机资源,并让每一个进程都得到适当的执行机会。 而在中断处理过程(包括时钟中断、I/O中断、系统调用和异常)中,系统直接调用schedule(),或者返回用户态时根据need_resched标记调用schedule()。 用户态进程没法实现主动调度,仅能经过陷入内核态后的某个时机点进行调度,即在中断处理过程当中进行调度。

相关文章
相关标签/搜索