概述
相关资料
《linux 调度子系统》这个博客讲的非常好
《linux调度器源码分析 - 概述(一)》讲的也挺好,有源码分析。
SCHED_FIFO等调度策略的定义和设置可以参考https://man7.org/linux/man-pages/man7/sched.7.html
每个CPU核都有自己的RunQueue(以x86 SMP为背景画的?)
图来自这里
CPU核上进程调度的标识时期和真正调度时期
硬件定时器触发时间中断(如针对使用时间片的SCHED_RR场景)、进程唤醒、进程创建、进程迁移时会设置进程调度标志位(TIF_NEED_RESCHED),此时相当于检查哪些进程需要被调度,但并没有实际进行调度操作。,检查是否需要调度和调度具体时机分离,因为中断中不能直接进行调度。
在中断上下文返回时或系统调用返回等时候(会调用schedule函数的地方等,schedule函数包含了切出A进程及切回A进程)
被动调度
系统调用返回到用户空间
中断返回到用户空间
中断返回到内核空间,需要内核支持内核抢占,不过内核抢占基本上已经是目前 linux 的默认配置
重新使能内核抢占时
主动调度
主动调用schedule()
会根据策略(pick_next_task函数所依据的优先级策略,stop_sched_class > dl_sched_class > rt_sched_class > fair_sched_class > idle_sched_class)并检查调度标志切换CPU核上具体执行的进程(context_switch函数,切换内存结构体、通用寄存器、段寄存器、内核栈)。此时,真正执行调度操作。
schedule()
->__schedule()
->pick_next_task()
返回__schedule()->context_switch()//切换进程上下文
->switch_to()//具体切换寄存器状态和栈
Idle进程
每个CPU核上的RunQueue为空时,会让该CPU核(负载均衡下会检查其他CPU核上的负载是否需要被迁移到此CPU核执行)调度Idle进程,标志着该CPU空闲。Idle进程前身是进程0(通过汇编代码构建没有经过Fork的进程,PID为0)。Idle进程会通过hlt指令让CPU处于暂停状态,等待硬件中断发生的时候恢复,从而达到节能的目的(将处理器C0态变到C1态,见ACPI标准)。参考这里。
硬件定时器时钟中断设置度标志位
通过一个硬件器件来触发周期性的中断,帮助调度器每隔一段时间进行一次调度标志位设置。
时钟中断中,复杂耗时的操作应放到中断下半部分执行,避免中断占据进程的大量运行时间。中断下半部的介绍可参考这个,中断下半部旨在处理不紧急的事情,实现机制包括tasklet、工作队列机制(有一个专门的内核线程处理,类似于有个代理性质的内核线程完成中断下半部)。当中断返回时,会触发schedule函数来真正执行调度操作。
调度器类和核心调度部分分离
调度器类是调度策略的落实,最终体现在pick_next_task函数中确定下一个调度的任务是哪个,也就是说起到了判断任务优先情况,并确定下一个调度任务的功能。通过在构建自己的调度器类(如fair_sched_class,它是个struct sched_class类型的变量)及其中的成员函数(会在必要位置被回调),就能应用自己的调度策略。
核心调度部分基于各个调度器类的决策选择下一个任务,但它不关心如何选出这个任务。核心调度部分拿到任务后,将原来的上下文切换成新的任务的上下文(context_switch函数)。
下图源自这里
调度组
调度组task group概念下,可以将各个用户的进程被合并到用户对应的调度组中,避免进程多的用户获得更多的调度时间。
CFS调度带宽控制
在某些应用场景,比如虚拟化或者用户付费使用服务器中,需要对用户的使用带宽或者时长进行限制,就需要用到 cfs 调度的带宽控制,其实现原理就是在一个周期内,通过某些算法设置用户组应该运行多长时间、同时计算已经运行了多长时间,如果超时,就将该用户组 throttle,夺取其执行权直到下一个周期。(点这里)
进程调度优先级
实时进程优先级(sched_priority,可以由sys_setpriority系统调用设置,同时也称为nice值,最终在内核set_one_prio函数中赋值到静态优先级static_prio)范围为 0~99 ,数字越大优先级越高。非实时进程优先级范围为 100~139,数字越大优先级越低。normal_prio旨在以统一方式表达进程优先级,实时优先级需要转换,非实时优先级不再需要转换,使得总体表现为数字越大优先级越低,load系数,vruntime增长速率越大,能被分配的时间越少。dl进程的normal_prio为-1,rt进程normal_prio为0~99(和sched_priority的表示反向),非实时进程normal_prio为100~X。
非实时进程在用户空间的优先级由nice值(历史遗留物)表示,是linux最早使用的方案,范围为 -20~19(与sched_priority取值范围100~139对应)。(点这里)
进程入/出队RunQueue时机
在进程状态间切换时(如正在运行/可运行状态、休眠状态、不可中断休眠状态、停止或Trace状态、僵尸进程和即将结束的进程)进行相应的出队(dequeue_task)入队(enqueue_task)。
与之不同的是,schedule调度进程是只针对就绪态(运行/可运行状态,还包括进程被唤醒后)的进程而言的。
硬件计时器的细节(arm和x86)
1. arm硬件定时器
从硬件中断处理句柄到具体设置调度标志位的代码流程(local timer of percpu)
PPI硬件中断
->arch_timer_handler_phys()//针对硬件中断所注册的回调函数
->timer_handler()
(调用注册函数)->tick_handle_periodic()//针对时钟中断事件注册的回调函数
->tick_periodic()//其中通过TICK_DO_TIMER_BOOT指定一个CPU更新全局jiffies(对jiffies所在地址进行操作),更新墙上时间(更新底层数据结构clock_source和timerkeeper,供上层date命令使用)。
->update_process_times()
->scheduler_tick()
->sched_clock_tick()//从sched_clock获取时间更新到struct sched_clock_data *scd
->sched_clock()//获取硬件时间System Counter
返回scheduler_tick()->update_rq_clock()//更新run queue时钟
返回scheduler_tick()->task_tick()//CFS下task_tick()是task_tick_fair()
->entity_tick()
->check_preempt_tick()
注册硬件中断句柄和时钟时间事件处理句柄tick_handle_periodic(local timer of percpu)
TIMER_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);
->arch_timer_of_init()
->arch_timer_register()//设置percpu的clock_event_device结构体(描述定时器),给PPI中断注册回调函数arch_timer_handler_phys
->arch_timer_starting_cpu()//CPU热插拔插入过程中会调用这个函数
->__arch_timer_setup()
->clockevents_config_and_register()
->clockevents_register_device()
->tick_check_new_device()
->tick_setup_device()
->tick_setup_periodic()
->tick_set_periodic_handler()//设置struct clock_event_device.event_handler(如指向tick_handle_periodic),即注册时钟事件处理句柄,会在硬件中断处理句柄中回调
返回tick_setup_device()->tick_setup_oneshot()//也设置event_handler
System Counter读操作更新到系统
TIMER_OF_DECLARE(armv8_arch_timer, "arm,armv8-timer", arch_timer_of_init);
->arch_timer_of_init()
->arch_timer_common_init()
->arch_counter_register()//获取System Counter的读取函数
->sched_clock_register()//System Counter的读取函数放到struct clock_read_data.read_sched_clock,后者会被sched_clock调用用于获取时间
->update_clock_read_data()//向系统注册System Counter的读取函数
sched_clock函数获取System Counter
sched_clock()
->[struct clock_read_data.read_sched_clock()]//update_clock_read_data函数中向系统注册System Counter的读取函数
ARMv7-A MP Core,下图源自这里
2. x86定时器(可参考这里)
注册硬件中断句柄和时钟时间事件处理句柄
start_kernel()
->time_init()
->x86_late_time_init()
->hpet_time_init()//High Precision Event Timer
->hpet_enable()//尝试启动HPET
->hpet_counting()//检测HPET计数器正常工作
返回hpet_enable()->clocksource_register_hz()//注册HPET时钟源
//static struct clocksource clocksource_hpet = {
// .name = "hpet",
// .rating = 250,
// .read = read_hpet,
// .mask = HPET_MASK,
// .flags = CLOCK_SOURCE_IS_CONTINUOUS,
// .resume = hpet_resume_counter,
//};
返回hpet_enable()->hpet_legacy_clockevent_register()
->clockevents_config_and_register()//设置struct clock_event_device.event_handler(如指向tick_handle_periodic),即注册时钟事件处理句柄,会在硬件中断处理句柄中回调
返回hpet_legacy_clockevent_register()//将上一行设置好的clock_event_device赋值给global_clock_event
返回hpet_time_init()->setup_default_timer_irq()
->setup_irq(0,irq0)//设置irq0中断
//static struct irqaction irq0 = {
// .handler = timer_interrupt,
// .flags = IRQF_NOBALANCING | IRQF_IRQPOLL | IRQF_TIMER,
// .name = "timer"
//};
硬件中断irq0的响应
irq0硬件中断
->timer_interrupt()
->[global_clock_event->event_handler]
(调用注册函数)->tick_handle_periodic()
注册System Counter读函数(?)
start_kernel()
->sched_clock_init()
->generic_sched_clock_init()//更新如jiffy_sched_clock_read的调度时钟读函数,供sched_clock使用
sched_clock读函数(?)
sched_clock()
->[struct clock_read_data.read_sched_clock()]//update_clock_read_data函数中向系统注册System Counter的读取函数
疑似x86定时器示意图,下图源自这里。
备注
我看的linux kernel版本是v5.4
最后
以上就是痴情绿茶为你收集整理的Linux调度器笔记相关资料每个CPU核都有自己的RunQueue(以x86 SMP为背景画的?)CPU核上进程调度的标识时期和真正调度时期Idle进程硬件定时器时钟中断设置度标志位调度器类和核心调度部分分离调度组CFS调度带宽控制进程调度优先级进程入/出队RunQueue时机硬件计时器的细节(arm和x86)备注的全部内容,希望文章能够帮你解决Linux调度器笔记相关资料每个CPU核都有自己的RunQueue(以x86 SMP为背景画的?)CPU核上进程调度的标识时期和真正调度时期Idle进程硬件定时器时钟中断设置度标志位调度器类和核心调度部分分离调度组CFS调度带宽控制进程调度优先级进程入/出队RunQueue时机硬件计时器的细节(arm和x86)备注所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复