Linux调度

150 阅读7分钟

任务

在内核层面并没有对进程与线程做显示的区分,二者统一都叫做任务(Task),区别体现在对资源的共享程度上:进程本质上是对资源的一种抽象与隔离,包括内存地址空间、文件、信号量等,Linux在通过系统调用 clone 创建一个新的 task 时,如果所有的资源都不共享,那么对用户而言就是创建了一个新的进程,如果除了函数调用栈(Stack)不共享、其他所有资源都共享,则创建的便是一个线程。 Linux系统通过可运行队列来管理可运行的任务,不同的调度有不同的可运行队列。

进程组和进程调度

进程调度

操作系统运行进程的时候,是按 时间片 来运行的。时间片 是指一段很短的时间段(如20毫秒),操作系统会为每个进程分配一些时间片。当进程的时间片用完后,操作系统将会把当前运行的进程切换出去,然后从进程队列中选择一个合适的进程运行,这就是所谓的 进程调度

进程组

进程组是一组相关的进程的集合,它们可以被统一地控制和管理。每个进程组都有一个唯一的进程组ID,同时也有一个领头进程(也称为组长进程)。进程组ID(PGID)即为组长进程的进程ID(PID)。在 Linux 系统启动时,会创建一个根进程组 init_task_group。可以用cgroup的CPU子系统来创建新的进程组。创建子进程默认是父进程的进程组。进程调用setpgid系统调用也可将自己划分到新的进程组。进程组内可以是进程,也可以同时存在进程组。

进程分组调度(CFS调度算法)

  1. 获取当前CPU的可运行队列
  2. 把当前运行的进程放回到运行队列
  3. 从根进程组开始从可运行队列中获取最优的可运行实体,如果是进程组,继续迭代该进程组从可运行队列中获取,直到取到进程。

可运行队列

每个CPU都有一个可运行队列,其数据结构裁剪如下:

struct rq {
    struct cfs_rq cfs; /* CFS runqueue 的实现 */
    struct rt_rq rt;   /* RT runqueue 的实现 */
    struct dl_rq dl;   /* Deadline runqueue 的实现 */

    struct task_struct __rcu *curr; /* 当前 runqueue 中正在运行的任务 */
    struct task_struct *idle;       /* Idle 调度类的任务 */
    struct task_struct *stop;       /* Stop 调度类的任务 */
    ...
};

调度类和调度

linux设计了几种不同的调度类,用户定义不同的调度逻辑。调度类又有不同的调度策略。

  1. stop_sched_class Stop 是特殊的调度类,内核使用该调度类来停止 CPU. 该调度类用来强行停止CPU 上的其他任务,由于该调度类的优先级最高,因此一旦生效就将抢占任何当前正在运行的任务,并且在运行过程中自己不会被抢占。该调度类只有在SMP架构的系统中存在,内核使用该调度类来完成负载均衡与CPU热插拔等工作。
  2. dl_sched_class 有些任务必须在指定时间窗口内完成。例如视频的编码与解码,CPU 必须以特定频率完成对应的数据处理;这类任务是优先级最高的用户任务,CPU 应该首先满足。 Deadline 调度类用来调度这类任务,dl 便是单词 Deadline 的缩写,因此该调度类的优先级仅仅低于 Stop 调度类。
  3. rt_sched_class 在本节开头我们提到,实时任务(Real-time Task)对响应时间要求更高,例如编辑器软件,它可能由于等待用户输入长期处于睡眠之中,但一旦用户有输入动作,我们就期望编辑器能够立马响应,而不是等系统完成其它任务之后才开始反应,这一点对用户体验十分重要。RT 调度类用来调度这类任务,该调度类的优先级低于 DL.
  4. fair_sched_class Fair 调度类用来调度绝大多数用户任务,CFS 实现的就是这种调度类,其核心逻辑是根据任务的优先级公平地分配 CPU 时间。我们会在后续章节中详细讨论 CFS 的实现细节。
  5. idle_sched_class 与 Stop 类似,Idle 调度类也是仅供内核使用的特殊调度类,其优先级最低,只有在没有任何用户任务时才会用到。内核会为每个 CPU 绑定一个内核线程(kthread)来完成该任务,该线程会在队列无事可做的情况下启动该任务,并将 CPU 的功耗降到最低。

调度的过程就是调度器按照顺序对具体的调度类进行遍历,依次调用每个调度类的 pick_next_task 方法从指定 rq 中寻找下一个可执行任务,如果返回非空,则将该任务返回。

调度类又有如下的调度策略:

  • Stop调度类 中只有一个任务可供执行,不需要定义任何调度策略。

  • DL(Deadline)调度类 只实现了一种调度策略:SCHED_DEADLINE, 用来调度优先级最高的用户任务。

  • RT(Real-Time)调度类 提供了两种调度策略:SCHED_FIFOSCHED_RR,

    1. 对于使用 SCHED_FIFO 的任务,其会一直运行到主动放弃CPU;
    2. 而对于 SCHED_RR 的任务,如果多个任务的优先级相同,则大家会按照一定的时间配额来交替运行,即使一个任务一直处于可运行状态,在使用完自己的时间切片之后也会被抢占,然后被放入队列的尾巴等待下次机会。
  • CFS调度类 实现了三种调度策略:

    1. SCHED_NORMAL : 被用于绝大多数用户进程
    2. SCHED_BATCH : 适用于没有用户交互行为的后台进程,用户对该类进程的响应时间要求不高,但对吞吐量要求较高,因此调度器会在完成所有 SCHED_NORMAL 的任务之后让该类任务不受打扰地跑上一段时间,这样能够最大限度地利用缓存。
    3. SCHED_IDLE : 这类调度策略被用于系统中优先级最低的任务,只有在没有任何其他任务可运行时,调度器才会将运行该类任务。
  • Idle 调度类也没有实现调度策略(注意不要将这类调度类与 CFS 中的 SCHED_IDLE 混淆)。

CFS调度算法

CFS(Completely Fair Scheduler)完全公平调度器。CFS使用红黑树结构,来存储要调度的任务队列,每个节点代表了一个要调度的任务,节点的key即为虚拟时间(vruntime),虚拟时间由这个任务的运行时间计算而来,key越小,也就是vruntime越小的话,红黑树对应的节点就越靠左。CFS scheduler每次都挑选最左边的节点(vrutime最小的任务)作为下一个要运行的任务, 这个节点是“缓存的”——由一个特殊的指针指向;不需要进行O(logn)遍历来查找。也因此,CFS搜索的时间是O(1)。

vruntime

对于新任务,vruntime是 0 ,vruntime的增量计算方式 vruntime = (wall_time * ((NICE_0_LOAD * 2^32) / weight)) >> 32;其中wall_time是该任务已经运行的vruntime,NICE_0_LOAD=1024,weight是通过nice值查表得到的。当nice=0时weight恰好是1024。