kernel启动流程-start_kernel的执行_5.sched_init

1. 前言

本专题文章承接之前《kernel启动流程_head.S的执行》专题文章,我们知道在head.S执行过程中保存了bootloader传递的启动参数、启动模式以及FDT地址等,创建了内核空间的页表,最后为init进程初始化好了堆栈,并跳转到start_kernel执行。
本文重点介绍start_kernel的sched_init的主要流程.

kernel版本:5.10
平台:arm64

2. 调度类与调度策略

将调度器公共的部分抽象,使用struct sched_class结构体描述一个具体的调度类,系统核心调度代码会通过struct sched_class结构体的成员调用具体调度类的核心算法。

针对每一个调度器定义了一个调度类,有5个调度类,对于每一个调度类系统中有明确的优先级概念,每一个调度类利用next成员构建单项链表。优先级从高到低排列为:

stop_sched_class -> 
    dl_sched_class->
        rt_sched_class->
            fair_sched_class->
                idle_sched_class

即通过for循环先处理最高优先级的调度类,调用它的调度算法找到下个调度实体,再处理次高优先级的调度类,调用它的调度算法找到下个调度体,直到处理最低优先级的调度类。
注:调度实体是调度对象的基本单位

3. 关于组调度

如下引用自:http://www.wowotech.net/process_management/449.html
在这里插入图片描述
在每个CPU上都有一个全局的就绪队列struct rq,在4个CPU的系统上会有4个全局就绪队列,如图中紫色结构体。系统默认只有一个根task_group叫做root_task_group。rq->cfs_rq指向系统根CFS就绪队列。根CFS就绪队列维护一棵红黑树,红黑树上一共10个就绪态调度实体,其中9个是task se,1个group se(图上蓝色se)。group se的my_q成员指向自己的就绪队列。该就绪队列的红黑树上共9个task se。其中parent成员指向group se。每个group se对应一个group cfs_rq。4个CPU会对应4个group se和group cfs_rq,分别存储在task_group结构体se和cfs_rq成员。se->depth成员记录se嵌套深度。最顶层CFS就绪队列下的se的深度为0,group se往下一层层递增。cfs_rq->nr_runing成员记录CFS就绪队列所有调度实体个数,不包含子就绪队列。cfs_rq->h_nr_running成员记录就绪队列层级上所有调度实体的个数,包含group se对应group cfs_rq上的调度实体。例如,图中上半部,nr_runing和h_nr_running的值分别等于10和19,多出的9是group cfs_rq的h_nr_running。group cfs_rq由于没有group se,因此nr_runing和h_nr_running的值都等于9。

关于task_group的进一步说明可参考:

引自:https://www.cnblogs.com/LoyenWang/p/12459000.html
在这里插入图片描述•内核维护了一个全局链表task_groups,创建的task_group会添加到这个链表中;
•内核定义了root_task_group全局结构,充当task_group的根节点,以它为根构建树状结构;
•struct task_group的子节点,会加入到父节点的siblings链表中;
•每个struct task_group会分配运行队列数组和调度实体数组(以CFS为例,RT调度类似),其中数组的个数为系统CPU的个数,也就是为每个CPU都分配了运行队列和调度实体;

4. sched_init

以CFS为例

|- -wait_bit_init

wait_bit_init
    |--for (i = 0; i < WAIT_TABLE_SIZE; i++)
           init_waitqueue_head(bit_wait_table + i)

|- -初始化root_task_group

初始化root_task_group
    |--ptr += 2 * nr_cpu_ids * sizeof(void **)
    |--ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT)
    |--root_task_group.se = (struct sched_entity **)ptr
    |--root_task_group.cfs_rq = (struct cfs_rq **)ptr
    |--root_task_group.shares = ROOT_TASK_GROUP_LOAD
    \--init_cfs_bandwidth(&root_task_group.cfs_bandwidth)
  1. ptr += 2 * nr_cpu_ids * sizeof(void **)
    ptr = (unsigned long)kzalloc(ptr, GFP_NOWAIT)
    分配空间用于存放struct sched_entity **指针数组和struct cfs_rq **指针数组,其中每个cpu core分配一个调度实体和一个cfs_rq队列,分别存放在如上所述的两个数组。

  2. root_task_group.se = (struct sched_entity **)ptr
    root_task_group.cfs_rq = (struct cfs_rq **)ptr
    root_task_group.shares = ROOT_TASK_GROUP_LOAD
    内核定义了root_task_group全局结构,充当task_group的根节点,以它为根构建树状结构,root_task_group的se和cfs_rq分别存放了cpu core的调度实体和cfs_rq队列指针,每个cpu core一个。

  3. init_cfs_bandwidth(&root_task_group.cfs_bandwidth)
    带宽控制与task group密切相关,task group作为一个调度实体,需要分配配额和调度周期,task group中针对每个cpu core都维护一个cfs_rq,每个cfs_rq也有配额,所有cfs_rq的配额相加不能超过task group的配额。

from:http://www.wowotech.net/process_management/451.html
在多核系统中,一个用户组使用task_group描述,用户组中包含CPU数量的调度实体,以及调度实体对应的group cfs_rq。如何限制一个用户组中的进程呢?我们可以简单的将用户组管理的调度实体从对应的就绪队列上删除即可,然后标记调度实体对应的group cfs_rq的标志位。quota和period的值存储在task_group的cfs_bandwidth结构体中,该结构体嵌在tasak_group中,cfs_bandwidth结构体还包含runtime成员记录剩余限额时间。每当用户组中的进程运行一段时间时,对应的runtime时间也在减少。系统会启动一个高精度定时器,周期时间是period,在定时器时间到达后重置剩余限额时间runtime为quota,开始下一轮时间跟踪。所有的用户组进程运行的时间累加在一起,保证总的运行时间小于quota。每个用户组会管理CPU个数的就绪队列group cfs_rq。每个group cfs_rq中也有限额时间,该限额时间是从全局用户组quota中申请。例如,周期period值100ms,限额quota值50ms,2个CPU系统。CPU0上group cfs_rq首先从全局限额时间中申请5ms时间(此实runtime值为45),然后运行进程。当5ms时间消耗完时,继续从全局时间限额quota中申请5ms(此实runtime值为40)。CPU1上的情况也同样如此,先以就绪队列cfs_rq的身份从quota中申请一个时间片,然后供进程运行消耗。当全局quota剩余时间不足以满足CPU0或者CPU1申请时,就需要throttle对应的cfs_rq。在定时器时间到达后,unthrottle所有已经throttle的cfs_rq。
总结一下就是,cfs_bandwidth就像是一个全局时间池(时间池管理时间,类比内存池管理内存)。每个group cfs_rq如果想让其管理的红黑树上的调度实体调度,必须首先向全局时间池中申请固定的时间片,然后供其进程消耗。当时间片消耗完,继续从全局时间池中申请时间片。终有一刻,时间池中已经没有时间可供申请。此时就是throttle cfs_rq的大好时机。

init_cfs_bandwidth的主要工作如下:
(1)初始化root_task_group.cfs_bandwidth,包括runtime,quota,period
(2)初始化root_task_group.cfs_bandwidth的period_timer,回调函数为sched_cfs_period_timer,它会调用do_sched_cfs_period_timer,通过__refill_cfs_bandwidth_runtime来重新填充runtime
(3)初始化root_task_group.cfs_bandwidth的slack_timer,回调函数为sched_cfs_slack_timer,它会调用do_sched_cfs_slack_timer。
do_sched_cfs_period_timer函数与do_sched_cfs_slack_timer()函数都调用了distrbute_cfs_runtime(),该函数用于分发tg->cfs_b的全局运行时间runtime,用于在该task_group中平衡各个CPU上的cfs_rq的运行时间runtime

|- -init_defrootdomain

此处主要是初始化root schedule domain

from:https://blog.csdn.net/wh8_2011/article/details/52089419
每个 Scheduling Domain 其实就是具有相同属性的一组 cpu 的集合。并且跟据 Hyper-threading, Multi-core, SMP, NUMA architectures 这样的系统结构划分成不同的级别。不同级之间通过指针链接在一起,从而形成一种的树状的关系。负载平衡就是针对 Scheduling domain 的。从叶节点往上遍历。直到所有的 domain 中的负载都是平衡的。当然对不同的 domain 会有不同的策略识别是否负载不平衡,以及不同的调度策略。通过这样的方式,从而很好的发挥众多 cpu 的效率

|- -初始化每个cpu的rq

初始化每个cpu的rq
    |--for_each_possible_cpu(i)
            init_cfs_rq(&rq->cfs)
            init_rt_rq(&rq->rt)
            init_dl_rq(&rq->dl)
            rq_attach_root(rq, &def_root_domain);

每个cpu都有一个运行队列,通过for_each_possible_cpu(i)遍历每个cpu, 初始化每个cpu的rq,最后将每个cpu的rq挂接到默认root调度域def_root_domain
注:Scheduling Domain 其实就是具有相同属性的一组 cpu 的集合。

|- -set_load_weight(&init_task, false)

记录调度实体的权重信息,在调度实体sched_entity中有定义此结构体。内核通过set_load_weight函数来查询prio_to_weight和prio_to_wmult如下两个表来初始化此结构体
unsigned long weight;
调度实体权重,直接从prio_to_weight表查到的权重
u32 inv_weight;
是权重的一个中间结果,从prio_to_wmult表查到的值,主要是为了计算方便
如用于CFS调度器计算 vruntime

对于init_task的policy被初始化为SCHED_NORMAL,此处就是通过set_load_weight
设置init进程的load->weight和load->inv_weight

|- -mmgrab(&init_mm)

|- -enter_lazy_tlb(&init_mm, current)

标识该cpu进入lazy tlb mode。

from: http://www.wowotech.net/process_management/context-switch-arch.html
什么是lazy tlb mode?
如果要切入的进程实际上是内核线程,那么我们也暂时不需要flush TLB,因为内核线程不会访问usersapce,所以那些无效的TLB entry也不会影响内核线程的执行。在这种情况下,为了性能,我们会进入lazy tlb mode。

|- -init_idle(current, smp_processor_id())

init_idle(current, smp_processor_id())
    |--__sched_fork(0, idle)
    |--__set_task_cpu(idle, cpu)
    |--init_idle_preempt_count(idle, cpu)
    |--idle->sched_class = &idle_sched_class
    |--idle_thread_set_boot_cpu()
    |--init_sched_fair_class()
    |--psi_init()
    |--init_uclamp()
    \--scheduler_running = 1
  1. __sched_fork(0, idle):将idle进程的调度实体执行赋0操作;
  2. __set_task_cpu:设置idle进程所在的cpu为当前cpu
  3. init_idle_preempt_count:使能idle进程允许被抢占
  4. idle_thread_set_boot_cpu:将idle进程设置为当前处理器的idle进程
  5. init_sched_fair_class
    通过open_softirq(SCHED_SOFTIRQ, run_rebalance_domains)注册SCHED_SOFTIRQ软中断
  6. psi_init

from:https://www.kernel.org/doc/html/latest/accounting/psi.html#:~:text=When%20CPU%2C%20memory%20or%20IO,the%20risk%20of%20OOM%20kills.&text=This%20allows%20maximizing%20hardware%20utilization,disruptions%20such%20as%20OOM%20kills.
psi为pressure stall information的缩写。当CPU、内存或IO设备被占用时,工作负载会经历延迟高峰、吞吐量损失,并面临OOM杀死的风险。如果不能准确地衡量这种争用情况,用户就不得不经常遭受过度争用资源造成的中断。psi功能可以识别和量化这种资源紧缺造成的中断,以及它对复杂工作负载甚至整个系统的时间影响。准确衡量资源短缺造成的生产力损失,有助于用户根据硬件确定工作负载的大小,或根据工作负载需求配置硬件。由于psi实时汇总了这些信息,因此可以使用诸如减载、将作业迁移到其他系统或数据中心,或者战略性地暂停或杀死低优先级或可重启的批处理作业等技术对系统进行动态管理。这样可以最大限度地提高硬件利用率,而不会牺牲工作负载的健康性,也不会有OOM杀死等重大中断的风险。

  1. init_uclamp

Utilization翻译过来是利用率、使用率的意思,存在CPU Utilization和Task Utilization两个维度的跟踪信号。
—CPU Utilization用于指示CPU的繁忙程度,内核调度器使用此信号驱动CPU频率的调整(schedutil governor生效时);
—Task Utilization用于指示一个task对CPU的使用量,表明一个task是“大”还是“小”,此信号可以辅助内核调度器进行选核操作。
但是用PELT负载跟踪算法得到的task util与用户空间期望有时候会出现分裂,比如对于控制线程或UI线程,PELT计算出的util可能较小,认为是“小”task,而用户空间则希望调度器将控制线程或UI线程看作“大”task,以便被调度到高性能核运行在高频点上使任务更快更及时的完成处理。同样地,对于某些长时间运行的后台task,eg:日志记录,PELT计算出的task util可能很大,认为是“大”task,但是对于用户空间来说,此类task对于完成时间要求并不高,并不希望被当作“大”task,以利于节省系统功耗和缓解发热。
uclamp提供了一种用户空间对于task util进行限制的机制,通过该机制用户空间可以将task util钳制在[util_min, util_max]范围内,而cpu util则由处于其运行队列上的task的uclamp值决定。通过将util_min设置为一个较大值,使得一个task看起来像一个“大”任务,使CPU运行在高性能状态,加速任务的处理,提升系统的性能表现;对于一些后台任务,通过将util_max设置为较小值,使其看起来像一个“小”任务,使CPU运行在高能效状态,以节省系统的功耗。

5.总结

sched_init主要完成了如下的工作:

  1. 初始化了root task group,包含每个cpu core的调度实体,调度队列,以及配额时间和调度周期;
  2. 初始化root调度域,它是负载均衡的基本单位;
  3. 初始化每个cpu的运行队列
  4. 初始化init进程的负载权值,它与运行时间相关;
  5. 初始化idle进程

参考文档

  1. sched-domains.rst
  2. https://lwn.net/Articles/80911/
  3. https://www.cnblogs.com/LoyenWang/p/12459000.html
  4. http://www.wowotech.net/process_management/449.html
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值