本文调研的是 Completely Fair Scheduler 算法, 它当前是 Linux 中 SCHED_NORMAL(非实时任务) 一类 task 的默认调度器.
实际上, 运行在 Guest OS 中的应用程序线程还受到 Guest OS 的调度, 分时运行在 vCPU 上, 但这不在本文调研范围内. 本文仅调研 vCPU 被如何调度到 pCPU 上.
从软件架构看
从 vCPU - QEMU 线程 - 物理 CPU 对应关系看
通常, 一个 QEMU 进程创建并服务于一个 VM. QEMU 为 VM 分配一个或多个 vCPU, 每个 vCPU 对应于 QEMU 进程内的一个线程.
所有 QEMU 的线程在 Linux 线程调度上与寻常线程无异, 由 CFS 调度器调度.
从 CPU 状态迁移看
KVM 将自己注册为一个 misc 类的设备, 位于 /dev/kvm 上. 当一个 VM 被创建后, 运行在用户态的 QEMU 通过 ioctl 操作代表 VM, vCPU 的相关文件, 实现与 KVM 通信.
当 QEMU 启动/恢复运行一个 VM 时, pCPU 因 QEMU 发起的系统调用, 由用户态陷入到内核态, 再经由 KVM 调用 VM Entry, 进入到 non-root 模式, 恢复 vCPU 的运行.
CFS 算法
(本文参考的 Linux 内核版本为 5.17 )
vruntime 的具体计算
调度周期: 当运行的线程数 ≤ 8 时, 默认的调度时间周期是 48ms. 当多于 8 个线程时, 时间周期 = 6ms * 线程数. CFS 尝试在一个调度周期内运行所有的 task, 防止饥饿.
进程创建时: 若没有设置子进程先运行, 则一般子进程的 vruntime = 父进程的 vruntime + 等价于一个周期的 vruntime 值.
进程唤醒时: vruntime 值会被更新为 ≥ 所有待调度线程中最小的 vruntime. 使用最小 vruntime 还可以保证频繁睡眠的线程优先被调度, 这对于桌面操作系统非常适合, 可减少交互应用的响应延迟.
调度进程时: 将当前进程加入红黑树, 从红黑树摘下最左的节点, 作为下个进程来运行. 过程中有许多规则和特例.
时钟周期中断: 更新当前进程的 vruntime 和实际运行时间. 若超过了调度周期分配的实际运行时间, 会执行 resched_task(*rq). 在单核 CPU 中, 即为设置 CPU need_resched flag; 在 SMP 上需要触发目标 CPU 上的调度器.
虚拟时钟的时间流速:
static inline u64 calc_delta_fair(u64 delta /* 线程真实运行的时间片长 */, struct sched_entity *se)
{
if (unlikely(se->load.weight != NICE_0_LOAD))
// NICE_0_LOAD = 1024
// 大致上 = delta_exec * NICE_0_LOAD / cur->load.weight;
// 也就是说, 对于 nice = 0 的进程, vruntime 与真实时间流速一致
delta = __calc_delta(delta, NICE_0_LOAD, &se->load);
return delta;
}
参考自
- 计算所云计算课程 PPT - Ch 2.2
- 韦师兄 PPT 《KVM 虚拟化》
- Linux 5.17
- https://www.kernel.org/doc/html/latest/scheduler/sched-design-CFS.html
- https://developer.ibm.com/tutorials/l-completely-fair-scheduler/
- https://z.itpub.net/article/detail/4D5A28D92B6F801DF6E4C7F793969996
- https://blog.csdn.net/XD_hebuters/article/details/79623130
- https://www.cnblogs.com/tianguiyu/articles/6091378.html
- https://www.daimajiaoliu.com/daima/60fed6172434c00
- https://blog.csdn.net/liuxiaowu19911121/article/details/47070111
(免费订阅,永久学习)学习地址: Dpdk/网络协议栈/vpp/OvS/DDos/NFV/虚拟化/高性能专家-学习视频教程-腾讯课堂
更多DPDK相关学习资料有需要的可以自行报名学习,免费订阅,永久学习,或点击这里加qun免费
领取,关注我持续更新哦! !
Xen Credit 算法
Xen 基本架构
Xen:
- 一个 VM 被称作 Domain
- 隔离不同的 Domain, 为它们提供虚拟化的硬件资源
Domain 0:
- 特殊, 有特权
- 管理其他 Domain
vCPU:
- 每个 VM 拥有一个或多个
- 状态:
- 运行中
- 可运行
- 阻塞 (VM 自己阻塞的)
- 阻塞 (VCPU 睡眠 或 被 VMM 暂停)
Credit 算法
(注: Credit 2 算法是 2009 年 George Dunlap 对原算法的一个改进, 包含在 Xen 中, 但默认未启用. 本文调研的是原算法. )
Credit 算法代码实现于 xen/common/sched/credit.c.
(本文参考的 Xen 版本为 4.16, 此时最新的 commit 为 0e03ff97def12b121b5313094a76e5db7bb5c93c)
核心数据结构
// 优先级, 枚举值
#define CSCHED_PRI_TS_BOOST 0 /* time-share waking up */
#define CSCHED_PRI_TS_UNDER -1 /* time-share w/ credits */
#define CSCHED_PRI_TS_OVER -2 /* time-share w/o credits */
#define CSCHED_PRI_IDLE -64 /* idle */
struct csched_dom { // 意为 C scheduled domain
struct domain *dom; // 原 Domain 指针
// dom->sched_priv 指回本 struct
// 它们互相指向
// 附加的数据字段
// ...
uint16_t weight; // Weight
uint16_t cap; // CAP
};
struct csched_unit { // vCPU 结构
// ...
int pri; // 优先级
atomic_t credit; // 当前的 credit
};
struct csched_private { // 系统范围内所有 vCPU
// ...
struct list_head active_sdom; // 所有活跃的 csched_dom, 是循环链表的表头
uint32_t weight;
uint32_t credit;
int credit_balance;
};
vCPU 调度的核心过程
初始化时, 设置两个计时器:
- `csched_acct`, 每隔 30ms 执行一次 (全局唯一一个, 所在的物理 CPU 称作 the accounting master)
- `csched_tick`, 每隔 10ms 执行一次 (每个物理 CPU 分别设置了一个, 可能并行执行)
// 对每个物理 CPU 初始化
static void init_pdata(struct csched_private *prv, struct csched_pcpu *spc, int cpu)
{
// ...
prv->ncpus++;
// ...
if ( prv->ncpus == 1 ) // 第一个 CPU 作为 the accounting master
{
prv->master = cpu; // 标记为 master
// 设置计时器, 执行 csched_acct
init_timer(&prv->master_ticker, csched_acct, prv, cpu);
set_timer(&prv->master_ticker, NOW() + prv->tslice);
}
// ...
// 设置计时器, 执行 csched_tick
init_timer(&spc->ticker, csched_tick, (void *)(unsigned long)cpu, cpu);
set_timer(&spc->ticker, NOW() + MICROSECS(prv->tick_period_us) );
// ...
}
// 各个物理 CPU 每 10ms 运行一次
static void cf_check csched_tick(void *_cpu)
{
// ...
if ( !is_idle_unit(current->sched_unit) ) // 若当前 vCPU 不是 "空转 vCPU"
csched_unit_acct(prv, cpu); // 执行 "vCPU 审计"
csched_runq_sort(prv, cpu); // 对 run queue(就绪队列) 排序
// ...
}
// 所谓的 vCPU 审计
static void csched_unit_acct(struct csched_private *prv, unsigned int cpu)
{
// ...
if ( svc->pri == CSCHED_PRI_TS_BOOST ) // 若上次醒来特权级是 BOOST
{
svc->pri = CSCHED_PRI_TS_UNDER; // 降级为 UNDER
// ...
}
// 该 vCPU 的 credit 被消耗了 100
burn_credits(svc, NOW());
if ( list_empty(&svc->active_unit_elem) )
{
// 若当前 active vCPU 链表为空, 说明所有 vCPU 的 credit 都为负, 则重新计算credit值
__csched_unit_acct_start(prv, svc);
}
else
{
// ...
// 一些迷惑操作, 不明白作用
if ( new_cpu != cpu )
{
// ...
// 总之发起了一个 CPU 软中断, 将执行 schedule()
// 软中断在 xen/common/sched/core.c: scheduler_enable() 注册
// 本物理 CPU 可能将切换到其他 vCPU
cpu_raise_softirq(cpu, SCHEDULE_SOFTIRQ);
}
}
}
// 在 the accounting master 上, 每 30ms 执行一次
static void csched_acct(void* dummy)
{
struct csched_private *prv = dummy;
// 声明变量...
// 遍历当前活跃的调度中的 Domain 循环链表
list_for_each_safe( iter_sdom, next_sdom, &prv->active_sdom )
{
sdom = list_entry(iter_sdom, struct csched_dom, active_sdom_elem);
// sdom 是刚刚被调度器抢占的那个 Domain 的 csched_dom 结构
// ...
// ...
// ...
// 计算了 weight_left, credit_peak, credit_cap, credit_fair
// credit_peak: 若 Domain 的所有 vCPU 各自独占一个物理 CPU 时, 等价的 credit 值
// credit_cap : 根据 CAP 计算, Domain 能获得的 credit 上限
// credit_fair: 根据 weight 计算, Domain 能获得的 credit
if ( credit_fair < credit_peak )
{
credit_xtra = 1;
}
else
{
// 出大问题, 调整一些东西...
}
// 最后使用 credit_fair 作为 Domain 当前的 credit
// 现在把 credit 除以 Domain 拥有的 vCPU 数量
credit_fair = ( credit_fair + ( sdom->active_unit_count - 1 )
) / sdom->active_unit_count;
// 遍历 Domain 的 vCPU 循环链表
list_for_each_safe( iter_unit, next_unit, &sdom->active_unit )
{
svc = list_entry(iter_unit, struct csched_unit, active_unit_elem);
// svc 表示一个 vCPU
// 原子地 += 和 read vCPU 的 credit
atomic_add(credit_fair, &svc->credit);
credit = atomic_read(&svc->credit);
if ( credit < 0 )
{
// 超出 credit 配额了, 优先级降到 OVER
svc->pri = CSCHED_PRI_TS_OVER;
// 若超出 CAP, 直接靠边停车 (park) ...
// 若 credit 小于了下界, 置为下界 ...
}
else
{
// 超出 credit 配额了, 优先级降到 OVER
svc->pri = CSCHED_PRI_TS_UNDER;
// vCPU 可能此时因为曾超过 cap 而暂停, 现在 credit > 0, 再次启动...
// 若 credit 超过了上界, 直接除以 2, 而且停止继续获得 credit...
}
//...
}
}
// ...
}
Xen Credit 与 QEMU/KVM 调度算法对比分析
(纸上谈兵一下, 欢迎批评指正)
综合来看, CFS 调度器的成熟程度比 Xen Credit 算法高很多, 兼顾了更多的设计目标 (也多设置了一大堆编译开关 ). 这可能与 Linux 在服务器、嵌入式设备、大型机和超级计算机不同领域多样化的生态有一定关联.
数据结构
Credit 算法的调度主要基于 Weight 和 CAP 实现的三级 vCPU 队列. CFS 算法则基于以 vruntime 为 key 的进程红黑树实现.
公平性, 无饥饿
一般认为, CFS 能提供很好的公平性和可交互性. Xen 不清楚...
虚拟机支持
W. Jiang et al., "CFS Optimizations to KVM Threads on Multi-Core Environment," 2009 15th International Conference on Parallel and Distributed Systems, 2009, pp. 348-354, doi: 10.1109/ICPADS.2009.83.
这篇 2009 年的文章指出, 若在 VM 中运行多线程程序时 (多线程运行在多个 vCPU 上), 当年的 CFS 调度器性能会变得较差. 这是由于 KVM 线程优先级和 vCPU 线程在 pCPU 上的分配机制不够智能导致的, 以及 KVM 缺乏对 VM 中的自旋锁信息的了解.
不过这些问题都已经被改进了...
它们均对 SMP 有较好的支持.
实时性, IO/CPU 密集应用
Credit 算法的 trick 是引入 boost 级别. 但一个 vCPU 进入 BOOST 级别后, pCPU 在 10ms 内不会再被抢占, 这可能会使某些 UI 桌面程序或高实时性程序的响应变得迟缓. 而且, boost 级的 vCPU 是随机分布在系统中, 在不同 CPU 上的分布不一定均匀.
另外, 据官网文档承认, Xen Credit 对于 "重的" 工作负载, 性能很高; 但对于需要低延迟的应用 (e.g. 互联网应用, 音频 ...), 性能不好, 可通过减小时间片大小改善.
CFS 取消了多级队列的概念, 且启用了更细粒度的调度时间片, 相对实时性较好一些.
原文链接:https://www.zhihu.com/question/19844004/answer/2419797473