目录
- 1. 前言
- 2. __schedule
- |- -deactivate_task
- |- -pick_next_task
- |- -context_switch
- |- - -switch_to
- 参考文档
1. 前言
本专题我们开始学习进程管理部分。本文主要参考了《奔跑吧, Linux内核》、ULA、ULK的相关内容。本文只是作为学习笔记以用于构建知识框架,可能存在一些理解不恰当或不到位的地方,后续会随着学习的深入,逐步进行迭代。
在进程管理概述部分介绍过,进程的抢占分为触发抢占(设置TIF_NEED_RESCHED标记)和执行抢占,执行抢占的过程就是调用了schedule,其核心函数为__schedule。
kernel版本:5.10
平台:arm64
2. __schedule
这段注释翻译成中文如下:
__schedule()是调度器的主函数
让调度器执行调度,并进入到__schedule()函数的方法主要有:
1.调用block的函数,如: mutex, semaphore, waitqueue等等。
2.在中断时返回用户空间时会检查TIF_NEED_RESCHED标志。例如:可以参考arch/x86/entry_64.S:
为了触发抢占,在定时器中断处理函数scheduler_tick()中,调度器会设置TIF_NEED_RESCHED 抢占标志;
3.唤醒一个进程并不真正会引发执行schedule()。唤醒只是添加一个进程到run-queue,仅此而已;
假设,如果一个新的进程被添加到run-queue, 如果抢占当前运行的进程,唤醒操作会设置当前进程的TIF_NEED_RESCHED 标志,
在第一次发生如下情形时,schedule()会被调用:
(1)如果kernel允许抢占(CONFIG_PREEMPT=y)
- 在系统调用或异常上下文,preempt_enable()中会执行schedule()抢占调度(这种情形一般是在wake_up()后执行spin_unlock()时);
- 在中断上下文,从中断处理函数返回到被中断的上下文
(2)如果kernel抢占被禁用 (CONFIG_PREEMPT is not set),那么在下一次遇到如下情形时会执行schedule()抢占调度
- cond_resched()调用
- schedule()调用
- 从系统调用或异常返回到用户空间时
- 从中断处理函数返回到用户空间时
对如上注释进行简单总结为:
- 如果内核允许抢占,如下时机可以执行抢占:
(1)preempt_enable
(2)异常上下文返回到用户空间
(3)异常上下文返回到内核空间 - 如果内核禁用抢占,如下时机可以执行抢占:
(1)显式调用schedule()函数(mutex也属于此情况)
(2)异常返回用户空间
asmlinkage __visible void __sched schedule(void)|--struct task_struct *tsk = current;|--sched_submit_work(tsk);//进程的plug队列泄洪操作|--do {| preempt_disable();//关闭内核抢占| __schedule(false); //调度核心实现| sched_preempt_enable_no_resched();//打开内核抢占| } while (need_resched());//判断当前进程的TIF_NEED_RESCHED是否置位\--sched_update_worker(tsk);
static void __sched notrace __schedule(bool preempt)|--cpu = smp_processor_id();| rq = cpu_rq(cpu);| prev = rq->curr;//prev指向当前进程|--schedule_debug(prev, preempt)//判断当前进程是否处于原子上下文|--if (!preempt && prev->state)//发生了非抢占调度,如主动调用schedule| deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);//当前进程移出就绪队列|--next = pick_next_task(rq, prev, &rf);//选择下个合适的进程开始运行|--clear_tsk_need_resched(prev);//清除进程的TIF_NEED_RESCHED| clear_preempt_need_resched();|--if (likely(prev != next))| rq->nr_switches++;| rq->curr = next;| ++*switch_count;| rq = context_switch(rq, prev, next, &rf);//上下文切换,从prev进程切换到next进程\--balance_callback(rq);
-
!preempt && prev->state:preempt 用于表示是否是抢占调度,如果preempt为true表示是抢占调度,抢占调度指的是在中断或异常返回前夕发生的调度,prev->state为非0,表示进程处于非running状态,一般是主动调用schedule前会设置此状态为非running,因此满足如上两个条件才为非抢占调度,对于非抢占调度,会通过deactivate_task将进程移出就绪队列。此处的判断条件是为了保证抢占调度被切换出的进程不要移出就绪队列。
-
pick_next_task:选择下个合适的进程开始运行,CFS调度类pick_next_task方法是pick_next_task_fair()函数
-
context_switch:上下文切换,从prev进程切换到next进程
|- -deactivate_task
deactivate_task(struct rq *rq, struct task_struct *p, int flags)|--if (task_contributes_to_load(p))| rq->nr_uninterruptible++;|--dequeue_task(rq, p, flags);|--dequeue_task_fair(rq, p, flags)//该函数是enqueue_task_fair()函数反操作|--for_each_sched_entity(se)//针对组调度操作,没有使能组调度情况下,循环仅一次| cfs_rq = cfs_rq_of(se);| dequeue_entity(cfs_rq, se, flags);//将调度实体se从对应的就绪队列cfs_rq上删除|--for_each_sched_entity(se)cfs_rq = cfs_rq_of(se);cfs_rq->h_nr_running--;if (cfs_rq_throttled(cfs_rq))break;update_load_avg(cfs_rq, se, UPDATE_TG);update_cfs_group(se);
在__schedule()函数中,如果prev进程主动睡眠。那么会调用deactivate_task()函数。deactivate_task()函数最终会调用调度类dequeue_task方法,对于cfq就是dequeue_task_fair
dequeue_entity(cfs_rq, se, flags);|--update_curr(cfs_rq);//借机更新当前正在运行进程的虚拟时间信息|--update_load_avg(cfs_rq, se, UPDATE_TG);|--dequeue_runnable_load_avg(cfs_rq, se);//更新调度器队列平均负载|--update_stats_dequeue(cfs_rq, se, flags);|--if (se != cfs_rq->curr)| __dequeue_entity(cfs_rq, se);|--se->on_rq = 0 //调度实体已经从就绪队列的红黑树上删除,因此更新on_rq成员|--account_entity_dequeue(cfs_rq, se) //更新就绪队列相关信息,例如权重信息|--if (!(flags & DEQUEUE_SLEEP))//如果进程不是睡眠(例如从一个CPU迁移到另一个CPU)| //进程虚拟时间需要减去当前就绪队列对应的最小虚拟时间.| //迁移之后会在enqueue的时候加上对应的CFS就绪队列最小拟时间| se->vruntime -= cfs_rq->min_vruntime;|--return_cfs_rq_runtime(cfs_rq);|--update_cfs_group(se);\--if ((flags & (DEQUEUE_SAVE | DEQUEUE_MOVE)) != DEQUEUE_SAVE)update_min_vruntime(cfs_rq);
dequeue_task将调度实体从对应的就绪队列删除
|- -pick_next_task
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
pick_next_task用于选择下一个合适的进程开始运行,此处首先判断如果一个cpu就绪队列上的进程全是普通进程,则直接调用cfs的fair_sched_class.pick_next_task来选取下一个进程,否则将依次遍历stop_sched_class->dl_sched_class->rt_sched_class->fair_sched_class->idle_sched_class,以cfq为例,pick_next_task_fair将选取vruntime最小的进程运行。
|- -context_switch
context_switch(struct rq *rq, struct task_struct *prev,struct task_struct *next, struct rq_flags *rf)|--prepare_task_switch(rq, prev, next);|--mm = next->mm;| oldmm = prev->active_mm;|--arch_start_context_switch(prev);|--if (!mm)//内核线程| next->active_mm = oldmm;//借用前一个进程的空间| mmgrab(oldmm);| enter_lazy_tlb(oldmm, next);|--else//用户进程| switch_mm_irqs_off(oldmm, mm, next);//设置新进程的页表基址到页表基址寄存器|--if (!prev->mm) //前一个进程为内核线程| prev->active_mm = NULL;| rq->prev_mm = oldmm;|--prepare_lock_switch(rq, next, rf);|--switch_to(prev, next, prev);//切换到next, Here we just switch the register state and the stack|--barrier();\--return finish_task_switch(prev);
context_switch上下文切换,从prev进程切换到next进程
-
switch_mm_irqs_off(oldmm, mm, next)
设置新进程的页表基址到页表基址寄存器,对于内核空间由于所有进程共享,因此不需要切换,对于进程空间,ARM64实现了ASID机制,不同进程的TLB项通过ASID硬件标识,这样切换不同进程时,原有进程的TLB得以保留,不会被invalidate,这样提升了性能 -
switch_to(prev, next, prev)
保存旧进程的寄存器(包含旧进程的进程描述符指针)到它自己的栈里;将新进程栈中的保存的寄存器值恢复到物理寄存器中
|- - -switch_to
switch_to(prev, next, last)|--((last) = __switch_to((prev), (next)));|--fpsimd_thread_switch(next);| tls_thread_switch(next);| hw_breakpoint_thread_switch(next);| contextidr_thread_switch(next);| entry_task_switch(next);| uao_thread_switch(next);| ssbs_thread_switch(next);| erratum_1418040_thread_switch(prev, next);\--last = cpu_switch_to(prev, next);//the actual thread switch
switch_to函数的精妙之处在于,switch_to执行的前半部分在旧进程上下文(记为进程A)执行,switch_to的后半部分在新选出进程的上下文(记为进程B)执行。准确的说,切换点位于cpu_switch_to。cpu_switch_to中mov sp, x9将进程栈切换到新进程B,通过ldr lr, [x8] 设置链接地址,在ret时会切换到进程B的代码中运行,而不会继续switch_to的后半部分运行。
/** Register switch for AArch64. The callee-saved registers need to be saved* and restored. On entry:* x0 = previous task_struct (must be preserved across the switch)* x1 = next task_struct* Previous and next are guaranteed not to be the same.**/
SYM_FUNC_START(cpu_switch_to)//>>>>>> 如下用于保存旧进程的CPU上下文到旧进程的task_struct.thread.cpu_contextmov x10, #THREAD_CPU_CONTEXTadd x8, x0, x10mov x9, spstp x19, x20, [x8], #16 // store callee-saved registersstp x21, x22, [x8], #16stp x23, x24, [x8], #16stp x25, x26, [x8], #16stp x27, x28, [x8], #16stp x29, x9, [x8], #16str lr, [x8]//>>>>>> 如下用于将新进程的task_struct.thread.cpu_context恢复到到新进程的CPU上下文add x8, x1, x10ldp x19, x20, [x8], #16 // restore callee-saved registersldp x21, x22, [x8], #16ldp x23, x24, [x8], #16ldp x25, x26, [x8], #16ldp x27, x28, [x8], #16ldp x29, x9, [x8], #16ldr lr, [x8] //恢复链接地址到链接寄存器mov sp, x9 //切换为新进程的栈msr sp_el0, x1 //为了方便current获取当前进程,参考aarch64的current实现ptrauth_keys_install_kernel x1, x8, x9, x10scs_save x0, x8scs_load x1, x8ret //从函数返回时会将lr赋值给pc
SYM_FUNC_END(cpu_switch_to)
NOKPROBE(cpu_switch_to)
cpu_switch_to会将旧进程的进程描述符作为第一个参数传递给cpu_switch_to,这样旧进程A的进程描述符地址就保存到了X0中,根据调用规范,返回值也为X0即为旧进程A的进程描述符地址,保存在last中,这样switch_to宏的last就为旧进程A的进程描述符地址,依此,当某个时刻进程X重新切换到进程A时,此时last保存了进程X的进程描述符,而进程A将继续switch_to的后半部分执行,根据对cpu_switch_to的分析,执行地址为调用cpu_switch_to的返回地址lr,准确的说是context_switch->finish_task_switch,它将会对X进程做一些清理工作。
注:为何不直接使用prev来对旧进程A进行处理?因为在进程A切换前,prev指向进程A,当进程栈切换到B进程后,此时的prev指向进程B,因此通过此时的栈是无法获取到A进程描述符,只能通过预先将A进程描述符保存到X0,然后再通过返回值从X0读取的方式。
参考文档
- 奔跑吧,Linux内核
- ULK
- ULA
- 进程切换分析(1):基本框架