- linux 是抢占式的 (依赖时钟中断)
- 时间片 timeslice
- 【动态时间片计算】
- 【可配置的策略 policy】
- linux 的公平调度,没有采用时间片实现公平??
- o(1)调度程序
- 静态时间算法
- 每个CPU的调度队列
- 交互进程
- rotating staircase ??
- rotating staircase deadline scheduler 公平队列调度
- 进程的类型: IO密集 计算密集
- 调度算法需要在 IO响应时间t 与 吞吐量 p 之前寻找权衡点。
- QPS = 并发量 / 平均响应时间
- 对于给定的机器, QPS 往往是固定的,那么 并发量越大,响应时间也越大。
- 或者说,IO响应时间短,那么花费在切换上下文的时间片就会比较多,整体的吞吐量就比较差。(就好比,餐厅员工摸鱼的人多了,同一时间能服务的客人数目就少了)
- Unix / Linux 都倾向于 IO 密集型服务。
- nice 值,系统提供给用户影响调度的配置项。
- linux 采用两种优先级配置项,除了 nice ,还有实时优先级 prio
- 实时优先级 prio
- 时间片消耗计数器
- 抢占时机 与 CFS
- 【调度器模块】
- 进程可以选【调度算法类】
- 调度器类也有优先级。
- unix 中 不通 nice 值的 时间片大小不一样
- o(1) 调度算法中, 假如 nice = 0 为 100ms, nice = 1 为 95 ms, nice = 18 为 10 ms, 19 为 5 ms 。
- o(1) 调度算法跟我们工资一样,穷人涨薪都是翻倍涨的,所以穷人薪资百分比差值大,富人涨幅就比较小。
- 优先级 可能是一种后门,可以命令 CPU 做一些无用功
- CFS 算法提供了上下文切换频率,避免cpu独占好几毫秒,导致延迟高达好几ms。
- CFS 算法提高了切换频率,为了避免“切换时间 等于 做功的时间”,约定做功时间最小值为 1ms 。(如果分配的时间片小于 1 ms 就没必要给他分配CPU了)
- CFS的对时间片的分配,不再基于 优先级 0 , 而是基于进程间的相对差值。(比如 优先级 10, 15 对于 20ms, 分别获得 15ms 、5 ms)
- 影响进程吞吐量的两个因素: 1.进程本身优先级, 2. CPU的上下文切换频率。(内因外因)
- 动态时间片,【指标】虚拟运行时间 vruntime 计数器, 【行为】CFS 的 平均 vruntime
- 静态时间片,【指标】固定运行时间,【行为】FIFO 派发固定时间片。
- NICE 值 可以影响 CFS 类
- 优先级 PRIO 可以影响 FIFO 类 RR 类
- 调度有关的系统调用有 sched_setscheduler(), nice(), sched_setparam(), sched_setaffinity()
- sched_setaffinity 设置亲和度。
- sched_setparam 选择实时调度器时,设置优先级。
- nice 选择CFS调度器时,设置 nice 时间片权重。
- O(1)与 CFS 的区别, 时间片的比例,上下文切换的频率(响应性)
- CFS 的响应性更合理,时间片大小也更合理。
- 控制好实时调度类的时间片大小,避免把CPU资源独吞了。
- 【硬实时进程任务】,任务必须在指定的时限内完成。(时效性,即任务最好不被中断,准时交付执行结果)
- 【软实时进程任务】,具有优先执行权的普通进程。
- 【普通进程】通过nice 调配的普通进程(CFS)
-
- 避免无事可做的进程占用CPU,区分【等待队列】、【就绪队列】
-
- linux 有多种调度类,实时的调度类无任务时,才会调度普通任务。
-
- 当进程变为就绪可选择是否抢占当前CPU。
- 进程列表是n个元素的数组, O(1) 调度器算法的时间复杂度做到了跟 n 无关,所以叫 O(1)
- CFS改进 O(1), 更合理的CPU切换频率,更合理的几何加权时间片计算,新增【调度实体】的概念。
- 中断可以抢夺系统调用时间(通过中断程序进行上下文切换)
- 系统调用无法被其他进程抢夺(包括其他进程的系统调用)
- 中断程序无法被其他中断抢夺。
- 内核抢占(kernel preemption)允许内核态被其他进程抢夺(不开启该功能则不允许打断内核态)。
- unsigned long nvcsw, nivcsw; /* 上下文切换计数 */
- 内核线程是直接由内核本身启动的进程。
- 内核进程无法访问用户态内存空间
- 红黑树是内核的 标准数据结构
- 一个进程只能设置一个调度器
- normal_priority表示基于进程的静态优先级和调度策略计算出的动态优先级
- 静态优先级是进程启动时分配的优先级。它可以用nice 和sched_setscheduler系统调用修改。
- sched_entity 可以实现组调度的功能,组调度是调度实体的自我分配(类似 线程或者协程的概念)
- 如果不 打算用nice降低进程的静态优先级,可以考虑设置 policy 为 SCHED_BATCH和SCHED_IDLE
- policy 是比 nice 更加柔和的干预调度算法的手段。
- 在【编译时】已经建立调度类:没有在运行时动态增加新调度器类的机制。
- 【用户层应用程序】无法直接与【调度类】交互。
- 它们只知道上文定义的常量SCHED_xyz。在这些常量和 可用的调度类之间提供适当的映射,这是内核的工作。
- 各个CPU都有自身的就绪队列
- 各个活动进程只出现在一个就绪队列中。(在多个CPU上同时运行一个进程是不可能的)
- 每个【cpu】 通过 【全局调度器】访问 【调度类】的 【子就绪队列】。
- 【调度类】 给每个cpu都安排了一个【子就绪队列】。
- 为idle线程,在无其他可运行进程时执行
- 系统的所有就绪队列都在runqueues数组中,该数组的每个元素分别对应于系统中的一个CPU。 在单处理器系统中
- 虚拟时钟上流逝的时间数量由vruntime统计
- 更高的rt_priority值 表示更高的实时优先级。
- 优先级值 与 优先级
- 动态优先级 (task_struct->prio)、普通优先级(task_struct->normal_prio)和静态优先级(task_struct-> static_prio),则是 值越低, 权越大
- rt_policy 为 RR 和 FIFO 为实时进程
- static const int prio_to_weight[40] 权重值表,CFS的精华之一
- 进程每降低一个nice值,则多获得10%的CPU时间,每升高一个nice值,则放 弃10%的CPU时间。为执行该策略,内核将优先级转换为权重值。
- 周期性调度器在scheduler_tick, 有2个任务: 1. 统计进程的时间片。2.执行调度类的周期性任务。
- CFS 不是基于时间片调度,而是基于 vruntime 调度。
- 如果当前进程应该被重新调度,那么调度器类方法会在task_struct中设置TIF_NEED_RESCHED 标志,以表示该请求,而内核会在接下来的适当时机完成该请求。
- TIF_NEED_RESCHED
- 惰性TLB (延迟切换用户态)和 惰性FPU技术 (延迟切换浮点寄存器) 都用到了懒汉模式,减少CPU上下文切换的成本,但是代码复杂度变高了,而且有泄漏数据风险 。
- 内核抢占是低延迟应用的福音,也是并发问题的潘多拉魔盒。
- 亲和性和调度域的问题,跟调度作业就近机房调度问题与 就近主机调度问题一类问题。
- kernel/sched_fair.c
- static const struct sched_class fair_sched_class {}
- kernel/sched.c struct cfs_rq {}
- rb_leftmost 总是设置为指向树最左边的结点,即最需要被调度的进程。
- 通常只对红黑树最左边的结点感兴趣,因为这可以减少搜索树花费的平均 时间。
- 完全公平调度算法依赖于虚拟时钟,
- 虚拟时钟
- 数据结构中任何地方都没找到虚拟时钟
- 根据现 存的实际时钟
- 进程的负荷权重
- __uodate_curr() 更新进程的物理运行时间 sum_exec_runtime 和虚拟运行时间 vruntime
- __uodate_curr()
- rq_of是一个辅助函数,用于确定与CFS就绪队列相关的struct rq实例
- 辅助函数
- CFS就绪队列
- struct rq实例
- delta_exec = (unsigned long)(now -curr->exec_start); 计算工作的时间差,即计算进程使用CPU的时长。
- 忽略舍入和溢出检查 unlikely()
- Curr->load.weight 越大,vruntime 增幅越小。
- min_vruntime是单调递增的
- 进程进入睡眠,则其vruntime保持不变。
- 睡眠进程醒来后,在红黑树中的位置会更靠左。
- 内核有一个固有的概念,称之为良好的调度延迟
- 良好的调度延迟
- 即保证每个可运行的进程都应该至少运行一次 的某个时间间隔。
- 良好的调度延迟,默认值为20 000 000纳秒或20毫秒
- /proc/sys/kernel/sched_latency_ ns
- sysctl_sched_latency
- 控制参数sched_nr_latency,控制在一个延迟周 期中处理的最大活动进程数目。
- 控制参数 sched_nr_latency
- 进程的数目超出该上限,则延迟周期也成比例地线性扩展。(进程越多,能够得到保证越差)
- 进程越多,能够得到保证越差
- 调度延迟 ,避免了CFS的调度饥饿。
- /proc/sys/kernel/sched_min_granularity_ns 间接控制延迟周期
- 延迟时间间隔 sched_vslice 也是按照 vruntime 一样的权重比例算出来的。
- sched_vslice 是每个进程保底能享用CPU的时间(类似低保)
- static const struct sched_class fair_sched_class = {
- .next = &idle_sched_class,
- .enqueue_task = enqueue_task_fair,
- .dequeue_task = dequeue_task_fair,
- .yield_task = yield_task_fair,
- .check_preempt_curr = check_preempt_wakeup,
- .pick_next_task = pick_next_task_fair,
- .put_prev_task = put_prev_task_fair,
- .set_curr_task = set_curr_task_fair,
- .task_tick = task_tick_fair,
- .task_new = task_new_fair,
- };
- 全局调度器
- 全局调度器,其实是一个接类型
- 全局调度器包括:任务队列,出让CPU方法,挑选下一个任务,周期性调度,新建任务。
- check_preempt_curr
- 调用likely()或unlikely()告诉编译器这个条件很有可能或者不太有可能发生,好让编译器对这个条件判断进行正确地优化。
- 分支预测优化
- __builtin_expect是GCC提供的內建函数,用于给GCC提供分支预测优化信息。
- CPU无一例外的都引入了流水线技术,用于加快指令的执行,提高CPU的性能
- 换句话说,就是CPU在处理当前指令的同时,会先取出后面的多条指令进行预处理。
- I486拥有五级流水线。分别是:取指(Fetch),译码(D1, main decode),转址(D2, translate),执行(EX, execute),写回(WB)。
- 对于进程来说,虚拟时钟 就跟 虚拟地址一样 ,在读写的时候需要根据权重换算程物理时钟。
- 从而实现每个进程的虚拟时钟周期是一样 的,但物理时钟不一样。
- 比如:计算每个进程的最小保证允许时间 (进程的CPU低保)
- kernel/sched_fair.c
- gran = sysctl_sched_wakeup_granularity;
- if (unlikely(se->load.weight != NICE_0_LOAD))
- gran = calc_delta_fair(gran, &se->load);
- 当在try_to_wake_up和wake_up_new_task中唤醒进程时 (中断唤醒,或者当前进程唤醒),会判断被唤醒的进程是否是实时进程,如果是的话,就会 resched_task(curr); 抢占当前进程。
- 实时调度器类的实现比完全公平调度器简单。大约只需要250行代码,而CFS则需要1100行!
- 核心调度器的就绪队列也包含了用于实时进程的子就绪队列,是一个嵌入的struct rt_rq实例。
- 每个sched_class 只包含了对 全局调度器接口的实现方法,而针对每个CPU核心的就绪队列,则放在核心调度器 struct rq {} 中。 (kernel/sched.c)
- 核心调度器 struct rq {}
- RT scheduler / CFS调度器 / 全局调度器 / 核心调度器
- 全局调度器的实时调度器接口 const struct sched_class rt_sched_class {}
- 核心调度器 struct rq {}
- 周期性调度器在 scheduler_tick 中实现。如果系统正在活动中,内核会按照频率HZ自动调用该 函数。如果没有进程在等待调度,那么在计算机电力供应不足的情况下,也可以关闭该调度器以减少 电能消耗。
- 周期性调度器 scheduler_tick
- scheduler_tick 位于 当前进程的调度类中。
- 调度类 如:实时调度器 / CFS调度器
- TIF_NEED_RESCHED + schedule 是主调度器的入口。
- 核心调度器(core scheduler)
- 通用调度器(generic scheduler)
- 核心调度器 与 通用调度器 是一回事。
- 核心调度器 是一个分配器,与其他两个组件交互。
- 核心调度器 = 周期性调度器 + 主调度器(由特定调度时机触发,比如中断)
- 调度类,包括了RT / CFS / IDLE , 他们实现了 调度类 sched_class 接口,里面包含了 周期性调度器函数接口 + 主调度器函数接口
- 核心调度器 struct rq {},包含了多个调度类的 就绪队列
- 核心调度器 可以理解为 是 全局调度器。
《内核源代码情景分析》-CPU调度阅读笔记
于 2022-11-16 09:56:40 首次发布