【原创】(三)Linux进程调度器-进程切换

背景

  • Read the fucking source code! --By 鲁迅
  • A picture is worth a thousand words. --By 高尔基

说明:架构

  1. Kernel版本:4.14
  2. ARM64处理器,Contex-A53,双核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

进程切换:内核将CPU上正在运行的进程挂起,选择下一个进程来运行。
ARM架构中,CPU上一次只能运行一个任务,内核须要为任务分配运行时间来进行调度,以便同时能处理多个任务请求。
以下图所示:函数

当进行任务切换的时候,思考下两个问题:工具

  1. 怎样经过抢占来实现进程的切换?
  2. 当进程切换的时候,到底切换的什么,是怎么实现的?

这两个问题,也是本文探讨的主题了。this

2. 抢占

2.1 用户抢占

2.1.1 抢占触发点

  • 能够触发抢占的状况不少,好比进程的时间片耗尽、进程等待在某些资源上被唤醒时、进程优先级改变等。Linux内核是经过设置TIF_NEED_RESCHED标志来对进程进行标记的,设置该位则代表须要进行调度切换,而实际的切换将在抢占执行点来完成。

不看代码来说结论,那都是耍流氓。先看一下两个关键结构体:struct task_structstruct thread_info。咱们在前边的文章中也讲过struct task_struct用于描述任务,该结构体的首个字段放置的正是struct thread_infostruct thread_info结构体中flag字段就可用于设置TIF_NEED_RESCHED,此外该结构体中的preempt_count也与抢占相关。线程

struct task_struct {
#ifdef CONFIG_THREAD_INFO_IN_TASK
    /*
     * For reasons of header soup (see current_thread_info()), this
     * must be the first element of task_struct.
     */
    struct thread_info      thread_info;
#endif
        ...
}

/*
 * low level task data that entry.S needs immediate access to.
 */
struct thread_info {
    unsigned long       flags;      /* low level flags */
    mm_segment_t        addr_limit; /* address limit */
#ifdef CONFIG_ARM64_SW_TTBR0_PAN
    u64         ttbr0;      /* saved TTBR0_EL1 */
#endif
    int         preempt_count;  /* 0 => preemptable, <0 => bug */
};

#include <asm/current.h>
#define current_thread_info() ((struct thread_info *)current)   //经过该宏能够直接获取thread_info的信息
#endif

看看具体哪些函数过程当中,设置了TIF_NEED_RESCHED标志吧:3d

  • 内核提供了set_tsk_need_resched函数来将thread_infoflag字段设置成TIF_NEED_RESCHED
  • 设置了TIF_NEED_RESCHED标志,代表须要发生抢占调度;

2.1.2 抢占执行点

用户抢占:抢占执行发生在进程处于用户态。
抢占的执行,最明显的标志就是调用了schedule()函数,来完成任务的切换。
具体来讲,在用户态执行抢占在如下几种状况:code

  • 异常处理后返回到用户态;
  • 中断处理后返回到用户态;
  • 系统调用后返回到用户态;

以下图:blog

  • ARMv8有4个Exception Level,其中用户程序运行在EL0,OS运行在EL1,Hypervisor运行在EL2,Secure monitor运行在EL3;
  • 用户程序在执行过程当中,遇到异常或中断后,将会跳到ENTRY(vectors)向量表处开始执行;
  • 返回用户空间时进行标志位判断,设置了TIF_NEED_RESCHED则须要进行调度切换,没有设置该标志,则检查是否有收到信号,有信号未处理的话,还须要进行信号的处理操做;

2.2 内核抢占

Linux内核有三种内核抢占模型,先上图:接口

  • CONFIG_PREEMPT_NONE:不支持抢占,中断退出后,须要等到低优先级任务主动让出CPU才发生抢占切换;
  • CONFIG_PREEMPT_VOLUNTARY:自愿抢占,代码中增长抢占点,在中断退出后遇到抢占点时进行抢占切换;
  • CONFIG_PREEMPT:抢占,当中断退出后,若是遇到了更高优先级的任务,当即进行任务抢占;

2.2.1 抢占触发点

  • 在内核中抢占触发点,也是设置struct thread_infoflag字段,设置TIF_NEED_RESCHED代表须要请求从新调度。
  • 抢占触发点的几种状况,在用户抢占中已经分析过,无论是用户抢占仍是内核抢占,触发点都是一致的;

2.2.2 抢占执行点

内核抢占:抢占执行发生在进程处于内核态。队列

整体而言,内核抢占执行点能够归属于两大类:

  • 中断执行完毕后进行抢占调度;
  • 主动调用preemp_enableschedule等接口的地方进行抢占调度;

2.3 preempt_count

  • Linux内核中使用struct thread_info中的preempt_count字段来控制抢占。
  • preempt_count的低8位用于控制抢占,当大于0时表示不可抢占,等于0表示可抢占。
  • preempt_enable()会将preempt_count值减1,并判断是否须要进行调度,在条件知足时进行切换;
  • preempt_disable()会将preempt_count值加1;

此外,preemt_count字段还用于判断进程处于各种上下文以及开关控制等,如图:

3. 上下文切换

  • 进程上下文:包含CPU的全部寄存器值、进程的运行状态、堆栈中的内容等,至关于进程某一时刻的快照,包含了全部的软硬件信息;
  • 进程切换时,完成的就是上下文的切换,进程上下文的信息会保存在每一个struct task_struct结构体中,以便在切换时能完成恢复工做;

进程上下文切换的入口就是__schedule(),分析也围绕这函数展开。

3.1 __schedule()

__schedule()函数调用分析以下:

主要的逻辑:

  • 根据CPU获取运行队列,进而获得运行队列当前的task,也就是切换前的prev;
  • 根据prev的状态进行处理,好比pending信号的处理等,若是该任务是一个worker线程还须要将其睡眠,并唤醒同CPU上的另外一个worker线程;
  • 根据调度类来选择须要切换过去的下一个task,也就是next
  • context_switch完成进程的切换;

3.2 context_switch()

context_switch()的调用分析以下:

核心的逻辑有两部分:

  • 进程的地址空间切换:切换的时候要判断切入的进程是否为内核线程,1)全部的用户进程都共用一个内核地址空间,而拥有不一样的用户地址空间;2)内核线程自己没有用户地址空间。在进程在切换的过程当中就须要对这些因素来考虑,涉及到页表的切换,以及cache/tlb的刷新等操做。
  • 寄存器的切换:包括CPU的通用寄存器切换、浮点寄存器切换,以及ARM处理器相关的其余一些寄存器的切换;

进程的切换,带来的开销不只是页表切换和硬件上下文的切换,还包含了Cache/TLB刷新后带来的miss的开销。在实际的开发中,也须要去评估新增进程带来的调度开销。

相关文章
相关标签/搜索