-10 +

Process scheduler

Process lifetime

进程的生命周期如下图所示:

在调度器中,上下文切换分为以下两个步骤:

在 SMP 系统中,操作系统至少会为每个 CPU 创建一个 run-queue。 当一些 CPU 空闲时,这个会检查其他 CPU 的 run-queues 并且 steal task, 因此 task 会在 CPU 间迁移(5)。这个能平衡不同 CPU 间的 task, 但是其他因素也是需要考量的,如 NUMA 进程内存的局部性,缓存热度等。

如前所述,调度器的主要目标是分配 task 的执行时间。为了做到这个, 为了 task 的优先,所以会创建多个 queues 选择一个 task 来执行(每一个优先级一个队列)。 而不是遍历所有的队列选择高优先级的 task 执行。这种方式被叫做 cyclic planning。 Fair planning,会考虑不同的线程,进程,用户,服务,尝试平衡处理器上的运行时间。

通常 task 会阻塞在各种同步对象上等待可用数据,例如 accept() 会一直阻塞直到 client 端 connect, recv() 将一直阻塞直到 client 端发送新的数据。当前没有可用数据时是不需要 CPU 的, 所以 task 简单的放弃 CPU,并且放置到相关对象特定的 sleep queue 中。 也就是说 accept() 调用,linux kernel socket 对象的 sk_wq 等待队列上。

Scheduling in Linux

让我们来看看 CFS 调度器的实现细节。 首先,调度器不会直接处理 task,调度器以 struct sched_entity 为调度单元。 调度单元可以代表一个 task,或者一组以队列存在的 task,数据结构为 struct cfs_rq(通过属性 my_q 引用), 所有允许构建有层级的调度单元,分配资源给一组 task 这种技术叫做 CGroupsstruct rq 中的属性 cfs 这个是 struct cfs_rq 的一个实例,包含了所有高优先级的调度单元,这个是处理器的运行队列。 每一个调度单元有一个 cfs_rq 指针,这个指向 CFS 的运行队列:

我们举例来说明,运行队列有两个调度单元: 一个是 CFS 队列包含单个 task(这个通过父指针引用 cfs_rq),一个是 top-level 基本的 task。

CFS 不会像 Solaris 系统中的 TS 调度器一样分配时间片。 使用 task 在 CPU 上的总运行时间来代替,这个时间保存在 sum_exec_runtime 属性上。 当调度 task 调度到 CPU 上时,sum_exec_runtime 会保存到 prev_sum_exec_runtime 上, 所以计算这个两个属性的差值,就能直到这个 task 在 CPU 上的运行时间。 sum_exec_runtime 的计时单位是纳秒,但是这个不会直接用来度量 task 的运行时间。 为了实现抢占式,CFS 使用运行时长除以 task 权重(属性 load.weight), 所以更高权重的 task 将会有更低的度量(保存在 vruntime 属性中)。 根据 task 的 vruntime 排序在红黑树中,这个红黑树被叫做 tasks_timeline, 所有最左边的 task 有最低的 vruntime,这个保存在红黑树的 rb_leftmost

CFS 对于已经被换醒的 task 存在特殊的例子。 因为这个 task 已经 sleep 太久,所以它们的 vruntime 可能会太低,它们会存在不公平的 CPU 时间。 为了防止这种情况,CFS 会保留一个最小的 vruntime 在 task 属性 min_vruntime 中, 所有唤醒的 task 将使用 min_vruntime 减去预计的 timeslice 值。 CFS 有个伙伴系统,帮助调度程序指向:

- 下一个最近唤醒的 task
- 最后一个被驱逐的并且跳过通过调用 sched_yield() 的 task

参考

关于我

85 后程序员, 比较熟悉 Java,JVM,Golang 相关技术栈, 关注 Liunx kernel,目前痴迷于分布式系统的设计和实践。 研究包括但不限于 Docker Kubernetes eBPF 等相关技术。

Blog

Code

Life

Archive