文章目录
3.进程调度
进程调度是操作系统中非常重要的一部分,其主要目的是实现对计算机资源的合理分配和利用。当同一时刻会有多个进程需要同时运行的时候,进程调度可以合理分配计算机资源,以实现多任务处理;当多个进程在竞争计算机资源的时候,进程调度可以保证资源公平竞争,平衡资源的分配和利用;当有些进程需要在限定时间内完成任务,进程调度可以让这些进程获得更多的CPU时间,以保证任务可以在规定时间内完成。从整个系统的角度来看,有些进程需要的计算机资源很多,而有些进程只需要很少的资源,进程调度可以根据进程的属性、资源需求等,通过不同的调度策略,使整个系统资源利用率最大化。
3.1进程和调度类
最常见的进程分类方法是根据优先级分类,Linux进程调度策略更是基于优先级的调度,每个进程根据它重要程度的不同被赋予不同的优先级,调度器在每次调度时,总选择优先级最高的进程开始执行。不同优先级的进程有这不同的调度策略。
目前Linux内核中默认实现了5种调度类,优先级从高到低分别是stop、deadline、realtime、CFS和idle,它们分别使用sched_class来定义,并且通过next指针串联在一起。最重要的stop调度类的进程的优先级是最高,工作也是最重要的,比如进程迁移、softlockup检测、CPU热插拔等进程;deadline调度用于调度有严格时间要求的实时进程;realtime调度类用于普通的实时进程,如IRQ线程化;CFS调度类则用于由CFS来调度的普通进程;idle调度类的优先级是最低的,当就绪队列中没有其他进程时才会进入idle调度类,idle调度类会让CPU进入低功耗模式。这些调度类都是struct sched_class的数据结构,他们分别定义在下面的文件中:
说到调度类,肯定离不开谈到调度算法,其中stop和idle是没有调度算法的,因为这两种调度类是没有办法进行调度的,cpu进入stop调度类是不被被调度的,只有stop进程执行完毕cpu才会进行调度。Idle调度类是cpu没有其他任务的情况下才会进入的,当有其他任务存在的时候,cpu就会离开idle调度类,选择该任务调度类型。stop和idle跟其他调度类还有一个地方不一样,就是他们没有优先级的。其他3中调度类都有有优先级的,而且每种调度类的优先级范围都不一样。
调度类deadline只有一种调度算法,SCHED_DEADLINE,优先级为-1。deadline调度类采用Earliest Deadline First (EDF)调度算法,即优先调度截止时间最早的任务。具体实现上,系统内部维护一个任务集合,在任何时刻都能够确定n个任务中哪一个是最紧急的,并分配足够的CPU时间片给它。
调度类realtime有两种调度类SCHED_FIFO、SCHED_RR,优先级为0-99;FIFO指First-In-First-Out,该调度策略将进程按照进程的优先级进行排序,优先级较高的进程优先执行。如果有新的高优先级进程出现,则内核会立即将该进程置于队头,等待执行。该策略不考虑进程时间片的问题,进程一旦开始执行,直到进程完成或者被另一个更高优先级的进程替代为止。RR指Round-Robin。该调度策略与SCHED_FIFO类似,采用优先级排序,并将时间片分配给队列中的首个进程。不同的是,SCHED_RR会为每个进程分配固定的时间片,一旦时间片耗尽,当前进程便被置于队列的尾部,等待下一轮调度。而如果队列中有更高优先级的进程,当前进程将被挂起,更高优先级的进程得以执行。
调度类CFS有三种调度类,SCHED_NORMAL、SCHED_BATCH和SCHED_IDLE,优先级为100-139;SCHED_NORMAL是标准调度类的默认调度策略,采用动态时间片调度策略。该调度策略的优先级取决于进程的nice值和已使用的CPU时间片,可以保证进程的公平分享CPU时间片,从而维持系统的吞吐量和相应能力的平衡。相比之下,SCHED_BATCH更注重在系统负载较低的情况下最大化系统的吞吐量。SCHED_BATCH调度策略会定期分配长时间的静态时间片,这样可以避免进程频繁地发生上下文切换,从而提高系统的吞吐量和性能。SCHED_IDLE不会抢占已经在运行的进程,而是等待CPU完全空闲后才会尝试运行该进程,主要是调节系统的能耗,它让CPU在空闲时进入较低功耗状态,从而降低系统的能耗。
3.2调度队列
每一个cpu会维护一个调度队列,在内核表现为struct rq结构体,是Linux内核中实现进程调度和负载均衡的关键数据结构,通过对它的成员进行操作,内核实现了进程的调度、负载均衡、运行时间的计算和统计。
3.2.1进程调度相关
每一个调度队列都记录了这5种调度器的调度队列,其中stop和idle由于其特殊性,没有调度队列,只有一个指向相关任务的指针,相关成员:
1.struct rq {
2. struct cfs_rq cfs;//指向CFS的就绪队列
3. struct rt_rq rt; //指向rt进程的就绪队列
4. struct dl_rq dl; //指向dl进程的就绪队列
5. struct task_struct __rcu *curr;//指向正在运行的进程
6. struct task_struct *idle;//指向idle进程
7. struct task_struct *stop;//指向系统的stop进程
8.};
其中,stop指向系统的stop进程,stop调度器的优先级是最高的,永远不会被动调度出去,所以只需要一个stop指针指向stop进程即可,如果存在stop进程,队列会最优先运行此任务,如果没有,stop则为空指针。成员dl、rt、cfs是依次几个调度器的就绪队列,当dl就绪队列存在可运行状态的任务则优先使用dl调度器,只有dl和rt就绪队列中没有可运行状态的任务的情况下,才会使用cfs调度器。如果前面几种调度器都没有可运行状态的任务,系统会运行idle指向的进程。struct rq中记录了多个就绪队列,包括各个进程的优先级、就绪时间等信息,内核通过对这些信息的修改和排序,来实现进程的调度。
3.2.2 状态和统计信息
struct rq结构体也有以一些成员表示队列的状态,统计调度相关信息:
1.struct rq {
2. unsigned int nr_running;//就绪队列中可运行的进程数量
3. u64 nr_switches;//记录进程切换的次数
4. unsigned int clock_update_flags;//用于更新就绪队列时钟的标志位
5. //每次时钟节拍到来时会更新这个时钟,计算进程vruntime时使用该时钟
6. u64 clock;//每次时钟节拍到来时会更新这个时钟
7. int cpu;//用于表示就绪队列运行在哪个CPU上
8. int online;//用于表示CPU处于active状态
9. struct list_head cfs_tasks;//可运行状态的调度实体会添加到这个链表头里
10. struct sched_avg avg_rt;//统计正在运行的rt进程的平均负载情况
11. struct sched_avg avg_dl;//统计正在运行的dl进程的平均负载情况
12. struct sched_avg avg_irq;//统计中断服务进程的平均负载情况
13. struct sched_avg avg_thermal;//统计温控进程的负载情况
};
我们可以队列记录了整个队列就绪队列中可运行的进程数量,通过这个成员可以进行最简单的负载均衡了,通过对比这个数量和cfs队列的可运行程序数量,即可快速知道其他调度器是否有任务要运行。结构体还记录了进程切换次数、就绪队列时钟标志位、系统时钟、队列运行所在的cpu和各种情况的负载,他们都有着各自的作用。这些信息可以用来统计系统的性能和调度质量,并用于优化系统调度策略和负载均衡算法。
3.2.3 负载均衡相关
1.struct rq {
2. unsigned long next_balance;//下一次做负载均衡的时间
3. struct mm_struct *prev_mm;//进程切换时用于指向前任进程的内存描述符mm
4. struct root_domain *rd;//调度域的根
5. struct sched_domain __rcu *sd;//指向CPU对应的最低等级的调度域
6. unsigned long cpu_capacity;//对CPU的量化计算能力
7. unsigned long cpu_capacity_orig;//最强CPU的量化计算能力
8. struct callback_head *balance_callback;//指向系统中的一个平衡回调函数
9. unsigned char nohz_idle_balance;//表示系统是否启用 CPU 无挂起时负载均衡
10. unsigned char idle_balance;//用于记录最后一次在idle进程队列和其他队列之间的平衡时间戳
11. unsigned long misfit_task_load;//记录不适合进程(实际算力大于CPU额定算力的80%)的量化负载
12. int active_balance;//表示系统是否主动进行负载均衡
13. int push_cpu;//用于负载均衡,表示迁移的目标CPU
14. struct cpu_stop_work active_balance_work;//CPU空闲时的负载平衡ops
15.};
对于多CPU系统,内核需要通过负载均衡来平衡各个CPU的负载。这些成员记录了cpu的调度域,这个调度域记录了整个soc中各个cpu的关系,主要是cache共享关系。结构体也记录了各个cpu的量化计算能力,通过获取到各个cpu的负载情况,可以进一步的进行负载均衡。
3.3进程调度的时机
Linux操作系统什么情况会发生调度呢?比较常见的有一下几种:
1.进程主动调用函数 schedule把进程调度出去;
2.进程进行资源等待(锁、IO)的时候退出就绪队列;
3.创建新进程的时候,新进程可能抢占当前进程;
4.唤醒进程的时候,被唤醒的进程可能抢占当前进程;
5.进程的时间片耗尽,被动调度出去;
6.进程死亡前一刻,主动调度出去。
3.3.1 主动调度
我们前面已经追过创建新进程的代码了,我们知道vfork会让子进程运行在父进程前面,他的具体做法是设置完成量等待子进程执行完成,然后调用函数wake_up_new_task唤醒子进程。wake_up_new_task函数会调用check_preempt_curr函数检查是否需要抢占,check_preempt_curr函数会调用函数resched_curr设置_TIF_NEED_RESCHED标志位表示该进程需要进行重新调度。我们前面还看过进程死亡的流程,
在进程死亡之前也是通过主动调度的方式来调度的,还记得在do_task_dead函数中的最后调用__schedule调度出去。如果进程需要等待某个资源,例如互斥锁或信号量,那么把进程的状态设置为睡眠状态,然后调用 schedule()函数以调度进程。进程也可以通过系统调用 sched_yield()让出处理器,这种情况下进程是不会睡眠。
这就是主动调度的几种方法了。
3.3.2 被动调度
现在很多进程都不会主动让出处理器,内核依靠周期性的时钟中断夺回处理器的控制权,时钟中断是调度器的脉搏。时钟中断处理程序检查当前进程的执行时间有没有超过限额,如果超过限额,设置需要重新调度的标志。当时钟中断处理程序准备把处理器还给被打断的进程时,如果被打断的进程在用户模式下运行,就检查有没有设置需要重新调
度的标志,如果设置了,调用 schedule()函数以调度进程。
接下来我们在看看时间片耗尽是怎么处理的,先看时间片中断的初始化:
1.static void hrtick_rq_init(struct rq *rq)
2.{
3.#ifdef CONFIG_SMP
4. rq_csd_init(rq, &rq->hrtick_csd, __hrtick_start);
5.#endif
6. hrtimer_init(&rq->hrtick_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_HARD);
7. rq->hrtick_timer.function = hrtick;
8.}
初始化函数初始化定时器rq->hrtick_timer,主要是定时间中断的处理函数是hrtick:
1.static enum hrtimer_restart hrtick(struct hrtimer *timer)
2.{
3. struct rq *rq = container_of(timer, struct rq, hrtick_timer);
4. struct rq_flags rf;
5.
6. WARN_ON_ONCE(cpu_of(rq) != smp_processor_id());
7.
8. rq_lock(rq, &rf);
9. update_rq_clock(rq);
10. rq->curr->sched_class->task_tick(rq, rq->curr, 1);
11. rq_unlock(rq, &rf);
12.
13. return HRTIMER_NORESTART;
14.}
hrtick函数唯一作用就是找到当前cpu的rq队列,找到当前队列的调度类,调用调度类的task_tick函数。Linux操作系统使用最多的cfs调度类,我们从它的定义找到这个函数是task_tick_fair,也就是说内核会周期性的运行task_tick_fair函数:
1.static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
2.{
3. struct cfs_rq *cfs_rq;
4. struct sched_entity *se = &curr->se;
5.
6. //从当前进程到根任务组的每级公平调度实体的遍历
7. for_each_sched_entity(se) {
8. cfs_rq = cfs_rq_of(se);//根据se找到cfs_rq
9. entity_tick(cfs_rq, se, queued);//更新进程的调度实体的状态
10. }
11.
12. //如果系统开启了 NUMA 内存区域均衡功能
13. if (static_branch_unlikely(&sched_numa_balancing))
14. task_tick_numa(rq, curr);//在NUMA架构下内存均衡的调度
15.
16. update_misfit_status(curr, rq);//跟新rq的misfit_task_load成员,表示不适应度
17. update_overutilized_status(task_rq(curr));//更新cpu有没有过度利用的状态
18.}
task_tick_fair函数主要是遍历当前进程到当前进程到根任务组的每级公平调度实体,正常我们没有使用调度分组的话,只有一个调度实体,如果我们使用cgroup控制cpu的使用率,才会有多个调度实体,这些调度实体随着cgroup形成树状,我们只会往上遍历parent节点,针对每一个调度实体,我们会找到对应的cfs_rq后调用函数entity_tick更新进程的调度实体的状态;后面其他函数对调度影响不大,自己看注释即可。我们主要关注entity_tick函数:
1.static void
2.entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
3.{
4. update_curr(cfs_rq);//更新当前进程的vruntime和就绪队列的min_vruntime
5.
6. //更新该进程调度实体的负载和就绪队列的负载
7. update_load_avg(cfs_rq, curr, UPDATE_TG);
8. update_cfs_group(curr);//根据组运行队列的当前状态重新计算se。
9.
10. if (queued) {//如果是时间片到了的调度,不需要其他验证
11. resched_curr(rq_of(cfs_rq));//重新调度当前进程后返回
12. return;
13. }
14.
15. if (cfs_rq->nr_running > 1)
16. //检查当前进程是否需要调度
17. check_preempt_tick(cfs_rq, curr);
18.}
entity_tick函数主要做了以下几件事:
- 调用函数update_load_avg更新当前进程的vruntime和就绪队列的min_vruntime;
- 调用函数update_cfs_group根据组运行队列的当前状态重新计算调度实体se;
- queued表示是否时间片耗尽,如果时间片到了,调用函数resched_curr重新调度当前进程后返回;
- 如果时间片没有耗尽,那一般是唤醒的进程了。如果cfs就绪任务数量不止一个,则调用函数check_preempt_tick检查当前进程是否需要调度;
我们比较关注resched_curr和check_preempt_tick函数,其实check_preempt_tick函数会调用到resched_curr,所以我们先看看check_preempt_tick函数:
1.static void
2.check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
3.{
4. unsigned long ideal_runtime, delta_exec;
5. struct sched_entity *se;
6. s64 delta;
7.
8. ideal_runtime = sched_slice(cfs_rq, curr);//计算理想的运行时间
9. //当前调度实体的实际运行时间
10. delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
11. //如果超过了理论运行时间,说明该进程要被调度出去
12. if (delta_exec > ideal_runtime) {
13. resched_curr(rq_of(cfs_rq));//重新进行任务调度
14. clear_buddies(cfs_rq, curr);//设置cfs_rq->last为NULL
15. return;
16. }
17.
18. //如果运行时间少于750us
19. if (delta_exec < sysctl_sched_min_granularity)
20. return;//不用调度,返回
21.
22. //从红黑树中找到最左边的调度实体
23. se = __pick_first_entity(cfs_rq);
24. //对比下一个调度实体和当前的虚拟运行时间
25. delta = curr->vruntime - se->vruntime;
26.
27. if (delta < 0)//当前进程的虚拟运行时间是最少的
28. return;//不用调度,返回
29.
30. //当前进程的虚拟运行时间不是最少的,
31. if (delta > ideal_runtime)//当前进程的运行时间已经多运行了一个理论时间了
32. resched_curr(rq_of(cfs_rq));//重新进行任务调度
33.}
check_preempt_tick函数主要做了以下几件事:
- 调用函数sched_slice计算理想的运行时间;
- 如果当前调度实体实际运行时间超过了理论运行时间的2倍,运行时间明显太多,调用函数resched_curr重新进行任务调度,并且清空cfs_rq的last后返回;
- 如果运行时间少于调度体最少运行时间,也就是750us,直接返回,不进行调度了;
- 到了这里说明调度体运行时间没有太多,也没有太少,那么调用函数__pick_first_entity从cfs_rq的红黑树中找到最左边的调度实体;
- 对比当前调度实体和就绪队列最左边的调度实体的虚拟运行时间,如果当前调度实体的虚拟运行时间更少,则不用调度直接返回;
- 如果当前调度实体的虚拟运行时间比就绪队列最左边的调度实体的虚拟运行时间还多,多运行了一个理论时间了,则调用函数resched_curr重新进行任务调度。
我们现在可以查看resched_curr函数了:
1.void resched_curr(struct rq *rq)
2.{
3. struct task_struct *curr = rq->curr;
4. int cpu;
5.
6. //防止死锁的WARN_ON,
7. lockdep_assert_held(&rq->lock);
8.
9. //通过判断TIF_NEED_RESCHED标志位查看进程是否需要调度
10. if (test_tsk_need_resched(curr))
11. return;
12.
13. cpu = cpu_of(rq);//获取rq执行的cpu号
14.
15. //如果进程在当前cpu上执行
16. if (cpu == smp_processor_id()) {
17. set_tsk_need_resched(curr);//设置TIF_NEED_RESCHED标志
18. set_preempt_need_resched();//设置thread_info.preempt.need_resched置位
19. return;
20. }
21. //如果进程在其他cpu上执行
22.
23. if (set_nr_and_not_polling(curr))//设置TIF_NEED_RESCHED标志
24. smp_send_reschedule(cpu);//使用IPI_RESCHEDULE通知其他cpu
25. else
26. trace_sched_wake_idle_without_ipi(cpu);
27.}
resched_curr函数主要做了这几件事:
- 调用函数test_tsk_need_resched判断TIF_NEED_RESCHED标志位是否已经置位,如果已经置位,则不需要重复一遍,可以返回;
- 如果进程在当前cpu上执行,调用函数set_tsk_need_resched设置TIF_NEED_RESCHED标志,然后调用函数set_preempt_need_resched给thread_info.preempt.need_resched置位,最后返回;
- 到了这里说明进程在其他cpu上执行,需要调用函数set_nr_and_not_polling设置TIF_NEED_RESCHED标志,然后调用函数smp_send_reschedule使用IPI_RESCHEDULE通知其他cpu 。
到这里,中断处理函数已经执行完毕了。中断返回时调度,如果进程正在用户模式下运行,那么中断抢占时,ARM64 架构的中断处理程序的入口是 e10_irq。中断处理程序执行完以后,跳转到标号 ret_to_user 以返回用户模式。我们看看et_to_user :
1.SYM_CODE_START_LOCAL(ret_to_user)
2. disable_daif
3. gic_prio_kentry_setup tmp=x3
4.#ifdef CONFIG_TRACE_IRQFLAGS
5. bl trace_hardirqs_off
6.#endif
7. ldr x19, [tsk, #TSK_TI_FLAGS]
8. and x2, x19, #_TIF_WORK_MASK
9. cbnz x2, work_pending
10.finish_ret_to_user:
11. user_enter_irqoff
12. /* Ignore asynchronous tag check faults in the uaccess routines */
13. clear_mte_async_tcf
14. enable_step_tsk x19, x2
15.#ifdef CONFIG_GCC_PLUGIN_STACKLEAK
16. bl stackleak_erase
17.#endif
18. kernel_exit 0
19.
20.
21.work_pending:
22. mov x0, sp // 'regs'
23. mov x1, x19
24. bl do_notify_resume
25. ldr x19, [tsk, #TSK_TI_FLAGS] // re-check for single-step
26. b finish_ret_to_user
27.SYM_CODE_END(ret_to_user)
函数ret_to_user是汇编代码,在第9行判断当前进程的进程描述符的成员 thread_info.flags 的一些标志位有没有被置位(包括_TIF_NEED_RESCHED),如果设置了其中一个标志位,那么跳转到标号 work_pending,标号 work_pending 在24行调用函数 do_notify_resume。我们看看do_notify_resume函数:
1.asmlinkage void do_notify_resume(struct pt_regs *regs,
2. unsigned long thread_flags)
3.{
4. do {
5. /* Check valid user FS if needed */
6. addr_limit_user_check();
7.
8. if (thread_flags & _TIF_NEED_RESCHED) {
9. /* Unmask Debug and SError for the next task */
10. local_daif_restore(DAIF_PROCCTX_NOIRQ);
11.
12. schedule();
13. } else {
14....
15. local_daif_mask();
16. thread_flags = READ_ONCE(current_thread_info()->flags);
17. } while (thread_flags & _TIF_WORK_MASK);
18.}
我们看到do_notify_resume函数对thread_flags的各置位的标志位进行处理,其中_TIF_NEED_RESCHED也就是需要重新调度的标志位,需要调用函数schedule进行主动调度。也就是说被动调度的尽头是主动调度。
3.4进程切换过程分析
我们知道无论是主动调度还是被动调度,其实都是通过调用函数__schedule或者schedule来进行进程切换的。我们知道schedule函数也是调用__schedule函数的,所以我们先看看schedule函数吧,代码在kernel/sched/core.c文件中
1.asmlinkage __visible void __sched schedule(void)
2.{
3. struct task_struct *tsk = current;
4.
5. sched_submit_work(tsk);//将进程提交到就绪队列中
6. do {
7. preempt_disable();//关闭抢占
8. __schedule(false);//进行非抢占的调度
9. sched_preempt_enable_no_resched();//开启抢占
10. } while (need_resched());//抢占标志位TIF_NEED_RESCHED置位了
11. sched_update_worker(tsk);//更新调度信息,用于将工作线程迁移到cpu上之后
12.}
13.EXPORT_SYMBOL(schedule);
我们看到schedule函数主要做了以下几件事:
- 调用函数sched_submit_work将进程提交到就绪队列中;
- 调用函数preempt_disable关闭抢占
- 调用函数__schedule进行非抢占的调度
- 调用函数sched_preempt_enable_no_resched开启抢占
- 调用函数sched_update_worker更新调度信息,这函数一般用于将工作线程迁移到cpu上之后
我们看看__schedule,这是进程调度的核心函数,各情况的调度都是通过调用它的:
1.static void __sched notrace __schedule(bool preempt)
2.{
3. struct task_struct *prev, *next;
4. unsigned long *switch_count;
5. unsigned long prev_state;
6. struct rq_flags rf;
7. struct rq *rq;
8. int cpu;
9.
10. cpu = smp_processor_id();//获取当前CPU
11. rq = cpu_rq(cpu);//由当前CPU获取数据结构rq
12. prev = rq->curr;
13.
14. local_irq_disable();//关闭本地CPU中断
15. //通知 RCU 子系统当前线程发生了上下文切换
16. rcu_note_context_switch(preempt);
17.
18. rq_lock(rq, &rf);//申请一个自旋锁
19. smp_mb__after_spinlock();//内存屏障
20.
21. /* Promote REQ to ACT */
22. rq->clock_update_flags <<= 1;
23. update_rq_clock(rq);//更新rq->clock
24.
25. switch_count = &prev->nivcsw;
26.
27. prev_state = prev->state;
28. //如果本次调度不是抢占调度并且当前进程不是TASK_RUNNING
29. if (!preempt && prev_state) {
30. //如果当前进程有可处理的信号
31. if (signal_pending_state(prev_state, prev)) {
32. prev->state = TASK_RUNNING;//设置为可运行
33. } else {//否则进程现在还不能运行
34. //如果任务在睡眠中则设置sched_contributes_to_load为1
35. prev->sched_contributes_to_load =
36. (prev_state & TASK_UNINTERRUPTIBLE) &&
37. !(prev_state & TASK_NOLOAD) &&
38. !(prev->flags & PF_FROZEN);
39.
40. if (prev->sched_contributes_to_load)
41. rq->nr_uninterruptible++;//nr_uninterruptible状态数量加一
42.
43. //把当前进程移出就绪队列
44. deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
45.
46. if (prev->in_iowait) {//如果当前进程是在等待IO
47. atomic_inc(&rq->nr_iowait);//设置nr_iowait+1
48. delayacct_blkio_start();//设置current->delays->blkio_start
49. }
50. }
51. switch_count = &prev->nvcsw;//主动调度次数加一
52. }
53.
54. //从就绪队列中选择下一个最合适的进程
55. next = pick_next_task(rq, prev, &rf);
56. //清除当前进程的TIF_NEED_RESCHED标志位
57. clear_tsk_need_resched(prev);
58. //清除thread_info的preempt.need_resched
59. clear_preempt_need_resched();
60.
61. if (likely(prev != next)) {//下一个运行进程不是当前进程
62. rq->nr_switches++;//队列的切换次数加一
63. //修改运行队列的rq->curr为next
64. RCU_INIT_POINTER(rq->curr, next);
65. ++*switch_count;//进程的切换次数加一
66.
67. rq = context_switch(rq, prev, next, &rf);//进程切换重要函数
68. } else {//下一个运行进程就是当前进程
69. rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
70. rq_unlock_irq(rq, &rf);
71. }
72.
73. balance_callback(rq);//主动调用队列的balance_callback进行负载均衡
74.}
函数__schedule做了以下几件事:
- 获取当前cpu号,获取cpu的调度队列rq、获取将要调度出去的进程;
- 调用函数local_irq_disable关闭本地CPU中断;
- 调用函数rcu_note_context_switch通知 RCU 子系统当前线程发生了上下文切换;
- 如果本次调度不是抢占调度并且当前进程不是TASK_RUNNING,如果当前进程有可处理的信号,设置进程状态为TASK_RUNNING;如果前进程没有可处理的信号,调用函数deactivate_task把当前进程移出就绪队列;
- 调用函数pick_next_task从就绪队列中选择下一个最合适的进程;
- 调用函数clear_tsk_need_resched清除当前进程的TIF_NEED_RESCHED标志位;
- 调用函数clear_preempt_need_resched清除thread_info的preempt.need_resched标志位
- 如果下一个运行进程不是当前进程,调用函数context_switch进行上下文切换;
- 调用函数balance_callback主动调用队列的balance_callback进行负载均衡。
我们继续分析context_switch函数,他是进程切换重要函数:
1.static __always_inline struct rq *
2.context_switch(struct rq *rq, struct task_struct *prev,
3. struct task_struct *next, struct rq_flags *rf)
4.{
5. //为next进程做好切换的准备
6. prepare_task_switch(rq, prev, next);
7.
8. if (!next->mm) {//如果切换到内核进程 // to kernel
9. next->active_mm = prev->active_mm;//使用旧的mm_struct
10. if (prev->mm)//如果切换出去的是用户进程 // from user
11. mmgrab(prev->active_mm);//用户进程的mm_struct使用数量+1
12. else//切换出去的是内核进程
13. prev->active_mm = NULL;//切换出去的内核进程的active_mm置空
14. } else { //如果切换到用户进程 // to user
15. //保证进程切换使用不同mm_struct时有个内存屏障
16. membarrier_switch_mm(rq, prev->active_mm, next->mm);
17. //宏,就是switch_mm函数,切换mm_struct,包括更新ttbr0,ASID
18. switch_mm_irqs_off(prev->active_mm, next->mm, next);
19.
20. if (!prev->mm) {//切换出去的是内核进程// from kernel
21. /* will mmdrop() in finish_task_switch(). */
22. rq->prev_mm = prev->active_mm;//保存前任进程的内存描述符mm
23. prev->active_mm = NULL;//切换出去的内核进程的active_mm置空
24. }
25. }
26.
27. rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
28.
29. prepare_lock_switch(rq, next, rf);
30.
31. //切换进程,从prev进程切换到next进程来运行
32. switch_to(prev, next, prev);
33. barrier();//内存屏障
34.
35. //对旧进程进行收尾工作
36. return finish_task_switch(prev);
37.}
- 调用函数prepare_task_switch为next进程做好切换的准备;
- 如果是用户进程切换到内核进程,使用旧的mm_struct,然后用户进程的mm_struct使用数量+1;
- 如果是内核进程切换到内核进程,使用旧的mm_struct,切换出去的内核进程的active_mm置空;
- 如果是要切换到用户进程,调用函数switch_mm_irqs_off切换mm_struct,包括更新ttbr0,ASID;
- 如果是内核进程切换到用户进程,还要保存前任进程的内存描述符mm到rq队列中,置空切换出去的内核进程的active_mm;
- 调用函数switch_to切换进程,从prev进程切换到next进程来运行;
- 调用函数finish_task_switch对旧进程进行收尾工作。
3.4.1prepare_task_switch函数
1.static inline void
2.prepare_task_switch(struct rq *rq, struct task_struct *prev,
3. struct task_struct *next)
4.{
5. kcov_prepare_switch(prev);
6. sched_info_switch(rq, prev, next);//更新调度器统计信息的
7. //向perf发送事件通知:PERF_COUNT_SW_CONTEXT_SWITCHES
8. perf_event_task_sched_out(prev, next);
9. rseq_preempt(prev);//关闭抢占
10. //调用preempt_notifierr->ops->sched_out函数
11. fire_sched_out_preempt_notifiers(prev, next);
12. prepare_task(next);//设置next进程描述符中的on_cpu成员为1
13. prepare_arch_switch(next);//空
14.}
我们看switch_mm_irqs_off函数其实就是switch_mm,switch_mm函数首先判断切换的两个mm_struct是否同一个,如果相同就没有必要切换了,不是同一个就调用函数__switch_mm进行切换mm_struct。__switch_mm首先判断切换进来的是否为init_mm,如果是的话只要设置ttbr0_el1寄存器为reserved_pg_dir这个内核地址,因为init_mm是所有进程共享的内存映射信息其页表基地址为reserved_pg_dir,然后返回;如果切换进来的不是init_mm,则需要调用函数check_and_switch_context检查和切换内存context。
我们来看看check_and_switch_context函数:
1.void check_and_switch_context(struct mm_struct *mm)
2.{
3. unsigned long flags;
4. unsigned int cpu;
5. u64 asid, old_active_asid;
6.
7. if (system_supports_cnp())
8. cpu_set_reserved_ttbr0();
9.
10. asid = atomic64_read(&mm->context.id);//读取进程中mm的ASID
11.
12. //获取cpu的ASID
13. old_active_asid = atomic64_read(this_cpu_ptr(&active_asids));
14. //如果进程和cpu的ASID都有效,进程的ASID版本号和cpu的ASID版本号相同
15. //再次读取的ASID和刚刚读取的比较,如果相同则赋值给asid
16. if (old_active_asid && asid_gen_match(asid) &&
17. atomic64_cmpxchg_relaxed(this_cpu_ptr(&active_asids),
18. old_active_asid, asid))
19. goto switch_mm_fastpath;//去下面走快速路径
20.
21. //ASID版本不一致
22. raw_spin_lock_irqsave(&cpu_asid_lock, flags);//禁止硬中断并且申请锁
23.
24. asid = atomic64_read(&mm->context.id);//读取进程中mm的ASID
25. if (!asid_gen_match(asid)) {//进程的ASID版本号和cpu的ASID版本号不同
26. asid = new_context(mm);//给进程重新分配 ASID
27. atomic64_set(&mm->context.id, asid);//ASID写入进程的mm
28. }
29.
30. cpu = smp_processor_id();//获取cpu id
31. //如果位图 tlb_flush_pending 中当前处理器对应的位被设置
32. if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending))
33. local_flush_tlb_all();//把当前处理器的页表缓存清空
34.
35. atomic64_set(this_cpu_ptr(&active_asids), asid);//ASID写入cpu
36. raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);//释放锁并且开启硬中断
37.
38.switch_mm_fastpath:
39.
40. arm64_apply_bp_hardening();
41.
42. /*
43. * Defer TTBR0_EL1 setting for user threads to uaccess_enable() when
44. * emulating PAN.
45. */
46. if (!system_uses_ttbr0_pan())//如果没有开启PAN功能
47. cpu_switch_mm(mm->pgd, mm);//更新ttbr0和ttbr1寄存器
48.}
check_and_switch_context函数主要做了以下几件事:
- 读取进程中mm的ASID和获取cpu的ASID;
- 如果进程的ASID有效并且进程的ASID版本号和cpu的ASID版本号相同,那么调用函数 atomic64_xchg_relaxed 把当前cpu的 active_asids 设置成进程的 ASID,这说明ASID版本一致,去下面第7步骤走快速路径;
- 到了这里说明cpu的ASID和进程ASID版本不一致,需要重新读取进程的ASID,
- 如果进程的ASID版本号和cpu的ASID版本号不同,调用函数new_context给进程重新分配 ASID,然后把这个ASID写入进程的mm;
- 调用函数local_flush_tlb_all把当前处理器的页表缓存清空;
- 使用原子操作把刚刚分配的ASID写入cpu;
- 调用函数system_uses_ttbr0_pan检查是否有支持,我们一般不支持,需要调用函数cpu_switch_mm更新ttbr0和ttbr1寄存器。
我们看看new_context函数:
1.static u64 new_context(struct mm_struct *mm)
2.{
3. static u32 cur_idx = 1;
4. u64 asid = atomic64_read(&mm->context.id);
5. u64 generation = atomic64_read(&asid_generation);//读取cpu的ASID版本
6.
7. if (asid != 0) {//如果进程已经有 ASID ,
8. u64 newasid = generation | (asid & ~ASID_MASK);
9.
10. //进程的 ASID 是保留 ASID
11. if (check_update_reserved_asid(asid, newasid))
12. return newasid;//继续使用原来的 ASID ,只需更新 ASID 版本号
13.
14. if (refcount_read(&mm->context.pinned))//如果被固定了
15. return newasid;//继续使用原来的 ASID ,只需更新 ASID 版本号
16.
17. if (!__test_and_set_bit(asid2idx(asid), asid_map))// ASID 在位图中是空闲的
18. return newasid;//继续使用原来的 ASID ,只需更新 ASID 版本号
19. }
20.
21. //从上一次分配的 ASID 开始分配 ASID,
22. asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, cur_idx);
23. if (asid != NUM_USER_ASIDS)//如果ASID没有达到上限
24. goto set_asid;
25.
26. //那么把全局 ASID 版本号加 1
27. generation = atomic64_add_return_relaxed(ASID_FIRST_VERSION,
28. &asid_generation);
29. flush_context();//重新初始化 ASID 分配状态
30.
31. //从1开始分配 ASID,
32. asid = find_next_zero_bit(asid_map, NUM_USER_ASIDS, 1);
33.
34.set_asid:
35. __set_bit(asid, asid_map);//为刚分配的 ASID 在位图中设置已分配的标志
36. cur_idx = asid;//记录下次分配 ASID
37. return idx2asid(asid) | generation;
38.}
函数new_context主要做了以下几件事:
- 读取进程的ASID和cpu的ASID版本;
- 如果进程已经有ASID了,asid更新版本号组成newasid;
- 调用函数check_update_reserved_asid遍历全部cpu,如果asid等于保留的asid中的一个,返回newasid;
- 如果进程的ASID是固定的,返回newasid;
- 如果ASID 在位图中是空闲的,返回newasid;
- 到这里说明进程没有ASID或者之前的ASID已经被使用了,调用函数find_next_zero_bit 从ASID位图中找一个空闲的ASID;
- 如果找到的ASID有效,去到第11步;
- 到这里说明ASID已经被用光了,ASID版本需要加一;
- 调用函数flush_context重新初始化 ASID 分配状态,包括清空保留的ASID和ASID位图,每个cpu的active_asids参数置为0,清空所有cpu的TLB;
- 调用函数find_next_zero_bit从1开始分配 ASID;
- 调用函数__set_bit为刚分配的 ASID 在位图中设置已分配的标志;
- 记录下次分配 ASID,然后返回ASID。
3.4.3switch_to函数
1.#define switch_to(prev, next, last) \
2. do { \
3. ((last) = __switch_to((prev), (next))); \
4. } while (0)
5.
6.__notrace_funcgraph struct task_struct *__switch_to(struct task_struct *prev,
7. struct task_struct *next)
8.{
9. struct task_struct *last;
10.
11. fpsimd_thread_switch(next);//保存和恢复浮点寄存器状态的函数
12. tls_thread_switch(next);//保存和恢复线程本地存储(TLS)
13. hw_breakpoint_thread_switch(next);//保存和恢复硬件断点信息
14. contextidr_thread_switch(next);//恢复进程的用户空间上下文标识符CONTEXTIDR_EL1
15. entry_task_switch(next);//保存和恢复进程的 CPU 上下文信息
16. uao_thread_switch(next);//恢复进程的 UAO 寄存器的状态
17. ssbs_thread_switch(next);//恢复SSBS 寄存器,用于向量操作的特殊寄存器
18. erratum_1418040_thread_switch(next);//恢复cntkctl_el1寄存器
19.
20. dsb(ish);//cpu内部的数据同步指令
21.
22. mte_thread_switch(next);//更新sctlr_el1寄存器的MTE相关
23.
24. last = cpu_switch_to(prev, next);//更新cpu_context记录的硬件上下文
25.
26. return last;
27.}
函数switch_to实际是通过__switch_to函数实现的:
- 调用函数fpsimd_thread_switch保存和恢复浮点寄存器状态的函数,浮点寄存器是 32 个 128 位的寄存器;
- 调用函数tls_thread_switch保存和恢复线程本地存储(TLS);
- 调用函数hw_breakpoint_thread_switch保存和恢复硬件断点信息;
- 调用函数contextidr_thread_switch恢复进程的用户空间上下文标识符CONTEXTIDR_EL1;
- 调用函数entry_task_switch保存和恢复__entry_task,其实就是把新的进程描述符的地址写入每cpu的__entry_task变量中;
- 调用函数uao_thread_switch恢复进程的 UAO 寄存器的状态;
- 调用函数ssbs_thread_switch恢复SSBS 寄存器,用于向量操作的特殊寄存器;
- 调用函数erratum_1418040_thread_switch复cntkctl_el1寄存器;
- 调用函数dsb同步cpu内部的数据;
- 调用函数mte_thread_switch更新sctlr_el1寄存器的MTE相关;
- 调用函数cpu_switch_to更新cpu_context记录的硬件上下文,主要是切换通用寄存器;
我们看看cpu_switch_to函数:
1.SYM_FUNC_START(cpu_switch_to)
2. mov x10, #THREAD_CPU_CONTEXT
3. add x8, x0, x10
4. mov x9, sp
5. stp x19, x20, [x8], #16 // store callee-saved registers
6. stp x21, x22, [x8], #16
7. stp x23, x24, [x8], #16
8. stp x25, x26, [x8], #16
9. stp x27, x28, [x8], #16
10. stp x29, x9, [x8], #16
11. str lr, [x8]
12. add x8, x1, x10
13. ldp x19, x20, [x8], #16 // restore callee-saved registers
14. ldp x21, x22, [x8], #16
15. ldp x23, x24, [x8], #16
16. ldp x25, x26, [x8], #16
17. ldp x27, x28, [x8], #16
18. ldp x29, x9, [x8], #16
19. ldr lr, [x8]
20. mov sp, x9
21. msr sp_el0, x1
22. ptrauth_keys_install_kernel x1, x8, x9, x10
23. scs_save x0, x8
24. scs_load x1, x8
25. ret
26.SYM_FUNC_END(cpu_switch_to)
27.NOKPROBE(cpu_switch_to)
函数cpu_switch_to主要做了:
- 寄存器 x10 存放进程描述符的成员 thread.cpu_context 的偏移;
- 寄存器x8 存放上一个进程的进程描述符的成员 thread.cpu_context 的地址;
- 寄存器 x9 保存栈指针;
- 第5-1行代码,把上一个进程的寄存器 x19~x28、x29、SP 和 LR 保存到上一个进程的进程描述符的成员 thread.cpu_context 中;
- 寄存器 x8 存放下一个进程的进程描述符的成员 thread.cpu_context 的地址;
- 第13-20行代码,使用下一个进程的进程描述符的成员 thread.cpu_context 保存的值恢复下一个进程的寄存器 x19~x28、x29、SP 和 LR;
- 把用户栈指针寄存器 SP_EL0 设置为下一个进程的进程描述符的第一个成员 thread_info 的地址;
- 安装了四组密钥值,分别存储在寄存器 x1、x8、x9 和 x10 中
- 保存特权级上下文,也就是寄存器x8;
- 加载特权级上下文,也就是寄存器x8;
- 函数返回,返回值是寄存器 x0 的值:上一个进程的进程描述符的地址。
3.4.4finish_task_switch函数
1.static struct rq *finish_task_switch(struct task_struct *prev)
2. __releases(rq->lock)
3.{
4. struct rq *rq = this_rq();
5. struct mm_struct *mm = rq->prev_mm;
6. long prev_state;
7.
8. rq->prev_mm = NULL;//把 rq->prev_mm 设置为空指针
9.
10. prev_state = prev->state;
11. vtime_task_switch(prev);//计算进程 prev 的时间统计
12. perf_event_task_sched_in(prev, current);
13. //把prev->on_cpu设置为0,表示进程prev没有在处理器上运行
14. finish_task(prev);
15. finish_lock_switch(rq);
16. finish_arch_post_lock_switch();//执行处理器架构特定的清理工作,这里为空
17. kcov_finish_switch(current);
18.
19. if (mm) {//如果进程 prev 是内核线程
20. membarrier_mm_sync_core_before_usermode(mm);//提供内存屏障
21. //内存描述符的引用计数减1,如果引用计数减到0,那么释放内存描述符
22. mmdrop(mm);
23. }
24. //如果进程prev的状态是TASK_DEAD,即进程主动退出或者被终止
25. if (unlikely(prev_state == TASK_DEAD)) {
26. //如果存在进程prev所属调度类的task_dead方法
27. if (prev->sched_class->task_dead)
28. prev->sched_class->task_dead(prev);//调用它
29.
30. //删除与此任务关联的函数返回探测实例,并将它们放回空闲列表中。
31. kprobe_flush_task(prev);
32.
33. //进程的栈内存使用计数减一,如果减到0,释放它
34. put_task_stack(prev);
35. //释放进程描述符的引用计数,然后将其标记为用户级别延迟释放
36. put_task_struct_rcu_user(prev);
37. }
38.
39. tick_nohz_task_switch();//检查现在是否在非活动状态,如果是则启用nohz
40. return rq;
41.}
函数finish_task_switch主要做了几件事:
- 把 rq->prev_mm 设置为空指针
- 调用函数vtime_task_switch计算进程 prev 的时间统计
- 调用函数finish_task把prev->on_cpu设置为0,表示进程prev没有在处理器上运行
- 调用函数finish_arch_post_lock_switch执行处理器架构特定的清理工作,这里为空
- 如果进程 prev 是内核线程,调用函数mmdrop把内存描述符的引用计数减1,如果引用计数减到0,那么释放内存描述符
- 如果进程prev的状态是TASK_DEAD,即进程主动退出或者被终止,调用进程prev所属调度类的task_dead方法,调用函数kprobe_flush_task删除与此任务关联的函数返回探测实例,并将它们放回空闲列表中;调用函数put_task_stack进程的栈内存使用计数减一,如果减到0,释放它;调用函数put_task_struct_rcu_user释放进程描述符的引用计数,然后将其标记为用户级别延迟释放;
- 调用函数tick_nohz_task_switch检查现在是否在非活动状态,如果是则启用nohz;