《Linux6.5源码分析:进程管理与调度系列文章》
本系列文章将对进程管理与调度进行知识梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中进程调度机制进行数据实时拿取与分析。
在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:
-
《深入理解Linux内核》
-
《Linux操作系统原理与应用》
-
《奔跑吧Linux内核》
-
《深入理解Linux进程与内存》
-
《基于龙芯的Linux内核探索解析》
Linux进程调度与管理:(四)进程的调度之schedule进程切换
在之前的文章中,我们介绍了进程是如何被创建出来的(我称之为进程肉体重塑)、进程是如何加载并启动的(我称之为进程灵魂注入)、进程的调度时机(进程何时加入就绪队列、何时被调度上CPU),具体详细信息请参考以下文章:
本篇文章将详细介绍进程是如何切换的,真正的进程切换是通过调用schedule() -> __schedule()函数去选择下一个要执行的进程, 并进行上下文切换。我们用一张图回忆一下,上一篇文章涉及到的调度时机(也就是何时调用schedule函数):
1.主动调度函数 schedule()
schedule是真正调度的核心函数,该函数会在关闭内核抢占的环境下执行_schedule()
, 将选择当前CPU上就绪队列中的一个待运行任务, 再执行上下文切换。
schedule()
函数执行流程为:首先提交调度请求给内核,然后禁用内核抢占以确保调度操作完整执行;随后调用核心调度函数__schedule()
进行实际的进程切换;调度完成后再重新启用抢占功能,循环上述步骤直到不再需要重新调度。
asmlinkage __visible void __sched schedule(void)
{
/*1. 获取当前任务的指针*/
struct task_struct *tsk = current;
/*2. 提交调度工作给内核*/
sched_submit_work(tsk);
/*3. 进入调度循环,直到不再需要调度 */
do {
/*3.1 禁用抢占 */
preempt_disable();
/*3.2 执行调度 */
__schedule(false);
/*3.3 启用抢占但不重新调度*/
sched_preempt_enable_no_resched();
} while (need_resched()); // 如果需要重新调度,则继续循环
/*4. 更新工作线程*/
sched_update_worker(tsk);
}
schedule()函数的主体是一个while循环,他一直循环直到不需要重新调度;
- preempt_disable()关闭内核抢占;
- __schedule()函数为核心函数,其具体实现的功能为:①选取下一个要执行的进程;②进行上下文切换;
- sched_preempt_enable_no_resched() 打开内核抢占;
1.1__schedule()
__schedule()
函数的主要步骤为:首先获取当前CPU及其运行队列信息;随后检查当前进程是否位于原子上下文,避免在不合适的上下文环境中调度;然后关闭CPU本地中断并记录此次上下文切换;对运行队列加锁后,根据当前进程状态判断是否为主动调度,若为主动调度则将当前进程移出运行队列;随后调用pick_next_task()
函数确定下一个执行的进程;最后调用context_switch()
函数完成进程间的上下文切换。
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq_flags rf;
struct rq *rq;
int cpu;
/*获取所在的cpu*/
cpu = smp_processor_id();
/*获取当前cpu所对应的运行队列*/
rq = cpu_rq(cpu);
/*获取运行队列中当前进程,prev指向当前进程,
调度完成之后,prev就指向了前一个进程了*/
prev = rq->curr;
/*用于判断当前进程是否处于 atomic 上下文中
*所谓 atomic 上下文包含硬件中断上下文、软中断上下文等
*若此时处于 atomic 上下文中,这是一个 bug,那么内核会发出警告并且输出内核函数调用栈(发出的警告是 *BUG:scheduling while atomic
*/
schedule_debug(prev);
.......
/*关闭本地CPU中断*/
local_irq_disable();
/*记录上下文切换*/
rcu_note_context_switch(preempt);
/*申请一个自旋锁*/
rq_lock(rq, &rf);
smp_mb__after_spinlock();
.......
/*用于判断当前进程是否主动请求调度
*preempt=0表示不抢占,=1抢占;
* 若发生了抢占,即处于中断返回前夕或者系统调用返回用户空间时
* 则直接跳过if语句,执行pick_next_task();
*
*prev->state=0表示运行状态,≠0表示其他状态
* 若当前进程处于运行态(0),则表示发生抢占,
* 则直接跳过if语句,执行pick_next_task();
* 若当前进程处于非运行状态,则表示主动请求调度,
* 主动请求调度则续提前将进程状态设置为阻塞态(TASK_UNINTERRUPTIBLE、TASK_INTERRUPTIBLE)
*/
if (!preempt && prev->state) {//处于主动调度状态
/*如果当前进程仍有信号量未处理*/
if (signal_pending_state(prev->state, prev)) {
prev->state = TASK_RUNNING;//将当前进程状态重设为运行态;
} else {
/*把当前进程移除运行队列*/
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
prev->on_rq = 0;
if (prev->in_iowait) {//当前进程处于io等待状态,即等待io资源
atomic_inc(&rq->nr_iowait);//增加运行队列的 io等待任务数量
delayacct_blkio_start();//开始计算io等待时间
}
if (prev->flags & PF_WQ_WORKER) {
struct task_struct *to_wakeup;
// 当一个工作线程要被调度器换出时,
//调用 wq_worker_sleeping() 看看是否需要唤醒同一个工作线程池中的其他工作线程
to_wakeup = wq_worker_sleeping(prev);
if (to_wakeup)/*尝试唤醒工作池中的某个工作线程*/
try_to_wake_up_local(to_wakeup, &rf);
}
}
switch_count = &prev->nvcsw;//切换计数器
}
/*核心函数,从就绪队列中选择一个最合适的进程*/
next = pick_next_task(rq, prev, &rf);
/*清除当前进程的 TIF_NEED_RESCHED 标识位,表示它接下来不会被调度*/
clear_tsk_need_resched(prev);
/*清楚当前任务的抢占标志
*在已经选择了下一个要执行的任务,并准备切换上下文时取消抢占标志位
*/
clear_preempt_need_resched();
/*如果要进行切换的两个进程不是同一个进程,则进行进程的切换*/
if (likely(prev != next)) {
........
/*跟踪调度切换,可以用tracepoint*/
trace_sched_switch(preempt, prev, next);
/* Also unlocks the rq也会对运行队列进行解锁操作: */
/*核心函数,切换上下文*/
rq = context_switch(rq, prev, next, &rf);
} else {
........
}
balance_callback(rq);/*平衡回调操作,负载均衡部分*/
}
该函数的核心函数分别为:①pick_next_task()、②context_switch();其主要步骤为:
- 获取获取所在cpu、该cpu的运行队列、当前进程等一些列信息;
- 运行队列上锁;
- 判断是否主动调度;
- 是主动调度:将当前进程移除运行队列 & 如果被移除的进程是工作池中的进程,试图唤醒该工作池中的一个进程;
- 是被动调度(即要抢占):直接运行
pick_next_task()
;
pick_next_task()
函数去选取下一个进程;context_switch()
进行上下文切换操作;
2. 选择下一个进程 pick_next_task():
pick_next_task()
函数首先判断当前CPU运行队列中的进程类型,若只有普通进程则直接通过CFS或idle调度类的快速路径选取下一个进程;否则,该函数会依次遍历所有的调度类(stop、deadline、实时、完全公平、空闲),直到成功选出一个适合运行的进程。
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
const struct sched_class *class;
struct task_struct *p;
if (likely((prev->sched_class == &idle_sched_class ||
prev->sched_class == &fair_sched_class) &&
rq->nr_running == rq->cfs.h_nr_running)) {
/*如果前一个进程的调度类是CFS完全公平调度类,
*并且该cpu整个运行队列中的进程数量 = cfs就绪队列中的进程数量
*则说明该cpu运行队列只有普通进程,无其他调度类进程;
*/
p = fair_sched_class.pick_next_task(rq, prev, rf);//通过cfs调度类的操作集进行pick_next_task
if (unlikely(p == RETRY_TASK))
goto again;//切换失败,重新遍历整个调度类
/* Assumes fair_sched_class->next == idle_sched_class */
if (unlikely(!p))
p = idle_sched_class.pick_next_task(rq, prev, rf);//通过idle调度类的操作集进行pick_next_task
return p;
}
again:
for_each_class(class) {//遍历所有调度类,
p = class->pick_next_task(rq, prev, rf);//依次用各个调度类中的pick_next_task
if (p) {
if (unlikely(p == RETRY_TASK))
goto again;
return p;//选到了任务,则返回该任务,没选到则继续遍历
}
}
/*所有调度类中都没有选到任务,则说明出错了,
*因为即使是空闲类别(idle class)也应该有一个可运行的任务
*/
BUG();
}
pick_next_task()
函数其实由两部分组成
- ①:优化部分,若当前cpu就绪队列全是普通进程,则直接调用cfs或idle调度类中的pick_next_task()函数,从而省去繁琐的遍历部分;
- ②:遍历部分,若不满足优化条件,则依次遍历所有调度类(stop_sched_class、dl_sched_class、rt_sched_class、fair_sched _class、idle_sched_class)知道找到一个要切换的进程;
- 如果连idle进程都没有找到,则说明出现bug。
对于cfs完全公平调度器,其会调用pick_next_task_fair()
函数,选择一个合适的进程用于调度
struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
...
// 简单路径:若没有使用组调度,或者 prev 不满足条件,则直接释放 prev 的调度实体
if (prev)
put_prev_task(rq, prev);
// 循环调用 pick_next_entity(),从最底层的 CFS 队列中选择实体,并依次向上找出最终的实体
do {
se = pick_next_entity(cfs_rq, NULL);
set_next_entity(cfs_rq, se);
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
// 从最终选出的 sched_entity 获取对应的任务
p = task_of(se);
...
}
我们可以看出,核心是使用pick_next_entity()
函数来选择一个调度实体;
3. 上下文切换工作 context_switch():
context_switch()函数的执行步骤为:首先调用prepare_task_switch()函数标记即将运行的进程;接着根据进程类型(用户进程或内核线程)调用switch_mm()函数切换其内存地址空间;最后调用switch_to()函数实现具体的CPU寄存器和栈的上下文切换。
上下文切换详细步骤:
在进入上下文切换流程时,首先在准备阶段调用prepare_task_switch
保存当前任务状态和更新调度器数据,同时通过arch_start_context_switch
启动体系结构相关的初始化;接下来,对于内核线程(即目标任务mm为空),系统通过enter_lazy_tlb
让它借用前一个任务的active_mm
进入懒惰TLB模式,并在必要时调用mmgrab_lazy_tlb
增加引用计数以确保active_mm
的有效性;而对于用户线程,调用switch_mm_irqs_off
完成内存上下文的实际切换,并更新页表的LRU状态,同时处理从内核线程切换过来的情况(保存prev的active_mm
);最后,调用switch_to
切换寄存器状态和栈,最终由finish_task_switch
完成收尾工作。
/*
* context_switch - 切换到新任务的内存管理(MM)以及新线程的寄存器状态。
*
* 该函数主要完成以下工作:
* 1. 做好上下文切换前的准备工作
* 2. 根据目标任务是否为内核线程(无 mm)或用户线程(有 mm),进行不同的内存管理切换操作
* 3. 更新内存管理相关的屏障和标识,保证切换后的内存一致性
* 4. 最后调用 switch_to() 切换寄存器状态和栈,并完成任务切换后收尾工作
*/
static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next, struct rq_flags *rf)
{
/* 1. 上下文切换前的准备工作:
* 函数 prepare_task_switch() 用于保存当前任务状态、更新调度器相关数据等,
* 为后续的上下文切换做好必要准备。
*/
prepare_task_switch(rq, prev, next);
/*2. 判断是否是内核线程
*2.1内核线程: 使用prev 进程的活跃内存描述,并进入懒惰 TLB 模式
* 懒惰TLB的意思是,让内核线程在不立即刷新 TLB 的情况下,
* 能够正确使用前一个任务的内存映射。
* 这种方式能够避免每次切换时都进行昂贵的 TLB 刷新操作。
*2.2 用户线程: 首先通过 membarrier_switch_mm() 进行内存屏障切换,
* swicth_mm_irq_off 进行进程地址空间的切换
*/
if (!next->mm) {
/*2.1.1 内核线程:进入懒惰 TLB 模式,利用当前任务(prev)的 active_mm*/ // to kernel
enter_lazy_tlb(prev->active_mm, next);
/*2.1.2 如果下一个要执行的是内核线程,需要借用 prev 进程的活跃内存描述符 active_mm*/
next->active_mm = prev->active_mm;
/*2.1.3 对于用户线程切换到内核线程的情况,
* 调用 mmgrab_lazy_tlb() 增加 active_mm 的引用计数*/
if (prev->mm)
mmgrab_lazy_tlb(prev->active_mm);
else
/*2.1.4 对于内核线程切换到内核线程的情况,清空其 active_mm */
prev->active_mm = NULL;
} else {
/*2.2 用户线程切换:
* 首先通过 membarrier_switch_mm() 进行内存屏障切换,
* 再调用 switch_mm_irqs_off() 进行进程地址空间的实际切换。
*/
membarrier_switch_mm(rq, prev->active_mm, next->mm);
switch_mm_irqs_off(prev->active_mm, next->mm, next);
lru_gen_use_mm(next->mm);
if (!prev->mm) { /* 如果 prev 为内核线程(即从内核切换到用户线程),
则保存 prev 的 active_mm 到 rq->prev_mm,并清空 prev->active_mm */
rq->prev_mm = prev->active_mm;
prev->active_mm = NULL;
}
}
/*3. 新旧进程的切换点,所有进程在调度时的切换都在switch_to函数
* 切换到 next 进程的内核态栈 和 硬件上下文
*/
switch_to(prev, next, prev);
barrier();
/*4. 此处由next进程来执行finish_task_switch函数;
* 会递减mm_count,
* 将prev进程的on_cpu置为0,即prev进程完全下cpu,退出执行状态;
*/
return finish_task_switch(prev);
}
context_switch函数主要执行以下几个步骤:
- 保存当前进程(prev 进程)的上下文;
- 恢复某个先前被调度出去的进程(next进程)的上下文;
- 运行下一个进程 next进程;
context_switch函数的核心实现:
- 进程地址空间的切换,
switch_mm()
函数进行,主要切换next进程的页表到硬件页表中; - 进程上下文切换(包括硬件和内核态栈栈部分),switch_to()函数进行,
3.1 TLB刷新
- 1.lazy_tlb是什么?
enter_lazy_tlb(prev->active_mm, next):
当即将切换到一个内核线程时(即 next->mm 为 NULL),内核线程本身没有自己的内存描述符,它需要借用前一个任务(通常是用户线程)的 active_mm。enter_lazy_tlb() 的作用就是“进入懒惰 TLB 模式”,让内核线程在不立即刷新 TLB 的情况下,能够正确使用前一个任务的内存映射。这种方式能够避免每次切换时都进行昂贵的 TLB 刷新操作。
- 2.mmgrab_lazy_tlb是什么?
mmgrab_lazy_tlb(prev->active_mm):
如果前一个任务是用户线程(即 prev->mm 非空),那么在切换到内核线程后,为了确保借用的 active_mm 在内核线程运行期间不被过早释放,需要增加该 mm 的引用计数。mmgrab_lazy_tlb() 就是用来完成这一操作的。它“抓取”(grab)了 active_mm 的引用,确保内存描述符保持有效,同时依然采用懒惰方式处理 TLB。
3.2 进程地址空间的切换 switch_mm()函数
switch_mm()
函数负责进程的地址空间切换,具体步骤为:如果是内核线程,则直接使用空地址空间或启动懒惰TLB模式;如果是用户进程,则调用check_and_switch_context()
函数判断ASID是否发生溢出,若未发生则直接更新TTBR0寄存器,若发生ASID溢出则重新分配ASID并刷新TLB后再更新TTBR0寄存器,从而完成地址空间切换。
-
进程地址空间切换 switch_mm 实质上就是完成TTBR0寄存器的改写, TTBR0修改为当前进程的PGD
-
用于切换next进程的页表到硬件页表中,通过核心函数__switch_mm(next)完成核心功能,再更新TTBR0_EL1 寄存器的值;
static inline void
switch_mm(struct mm_struct *prev, struct mm_struct *next,
struct task_struct *tsk)
{
/*要切换的两个进程 不是同一个进程*/
if (prev != next)
/*核心执行函数,将next进程的虚拟页表转换到具体的物理页表*/
__switch_mm(next);
update_saved_ttbr0(tsk, next);//更新当前被调度的任务的 TTBR0_EL1 寄存器的值
}
3.2.1 _switch_mm()函数
- 该函数先判断下一个进程的内存管理地址是否是内核虚拟地址空间,
- 如果是内核级虚拟地址空间,则直接将ttbr0寄存器指向0页面 , 因为内核地址空间不需要切换(内核空间用的是全局TLB,所以不用切换);
- 如果不是,则调用
check_and_switch_context(next, cpu)
函数 进行用户地址空间的切换工作,也是对ttbr0寄存器进行工作;
static inline void __switch_mm(struct mm_struct *next)
{
unsigned int cpu = smp_processor_id();//获取所在的cpu
/*1.内核线程的地址空间不需要切换, 及那个TTBR0指向零页*/
if (next == &init_mm) {
cpu_set_reserved_ttbr0();
return;
}
/*2. 用户线程 页表切换
* ASID硬件溢出: 刷新TLB
* ASID硬件未溢出: 直接进行 页表基址寄存器TTBR0 切换
*/
check_and_switch_context(next, cpu);
}
提问:这里init_mm是什么?
- init_mm是一个全局变量,表示系统启动时的内存管理结构体;
- init_mm包含了系统启动时的内核虚拟内存空间的映射信息
- init_mm负责管理内核虚拟地址空间;
3.2.2check_and_switch_context() 函数
void check_and_switch_context(struct mm_struct *mm, unsigned int cpu)
{
unsigned long flags;
u64 asid, old_active_asid;
if (system_supports_cnp())
cpu_set_reserved_ttbr0();//将禁用 TTBR0 进行内存访问翻译的目的。
/*通过原子操作,读取软件的 ASID*/
asid = atomic64_read(&mm->context.id);
/*读取 Per-CPU 变量的 active_asids*/
old_active_asid = atomic64_read(&per_cpu(active_asids, cpu));
/*判断全局原子变量 asid_generation 存储的软件 generation 计数
* 和进程内存描述符存储的软件 generation 计数是否相同;
* 如果相同,则说明ASID硬件未溢出,不需要进行TLB刷新操作,直接进行switch_mm_fastpath地址切换
*另外还需要通过 atomic64_cmpxchg() 原子交换指令
* 来设置新的 asid 到 Per-CPU 变量 active_asids 中
*/
if (old_active_asid &&
!((asid ^ atomic64_read(&asid_generation)) >> asid_bits) &&
atomic64_cmpxchg_relaxed(&per_cpu(active_asids, cpu),
old_active_asid, asid))
goto switch_mm_fastpath;//跳转到快速路径切换内存管理上下文;
//获取CPU的ASID锁
raw_spin_lock_irqsave(&cpu_asid_lock, flags);
/*重新做一次软件 generation 计数的比较,
* 如果还不相同,说明至少发生了一次 ASID 硬件溢出,
* 需要分配一个新的软件 ASID 计数;
*并设置到 mm->context.id 中
*/
asid = atomic64_read(&mm->context.id);
if ((asid ^ atomic64_read(&asid_generation)) >> asid_bits) {
/*发生ASID溢出*/
asid = new_context(mm);//重新分配asid
atomic64_set(&mm->context.id, asid);
}
/*硬件ASID发生溢出,刷新本地TLB*/
if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending))
local_flush_tlb_all();//刷新TLB;
atomic64_set(&per_cpu(active_asids, cpu), asid);//设置当前CPU的活跃ASID为当前ASID
//释放CPU的ASID锁
raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);
/*说明没有发生ASID硬件溢出*/
switch_mm_fastpath:
arm64_apply_bp_hardening();
/*
* Defer TTBR0_EL1 setting for user threads to uaccess_enable() when
* emulating PAN.
*/
if (!system_uses_ttbr0_pan())
cpu_switch_mm(mm->pgd, mm);//页表基址寄存器TTBR0切换
}
check_and_switch_context()
函数主要目的是用户地址空间的切换,而切换用户地址空间的主要动作就是页表基址寄存器TTBR0切换
- 判断ASID是否溢出(通过判断全局原子变量 asid_generation 存储的软件 generation 计数 和 进程内存描述符存储的软件 generation 计数是否相同)
- 溢出:local_flush_tlb_all() 刷新TLB;
- 未溢出:直接页表基址寄存器TTBR0切换;
- cpu_switch_mm(mm->pgd, mm) 页表基址寄存器TTBR0切换;
【cpu_switch_mm()函数】
static inline void cpu_switch_mm(pgd_t *pgd, struct mm_struct *mm)
{
//swapper_pg_dir是 内核用于启动时的页全局目录的指针
//这里如果pgd页全局目录指针 指向 swapper_pg_dir (用于启动时的页全局目录的指针)
//则说明处理的是切换的是一个内核线程, 而在context_switch函数中,如要切换的是内核级线程,则不执行switch_mm,更不会执行此函数;
BUG_ON(pgd == swapper_pg_dir);
//设置TTBR0_EL1指向 0页emty_zero_page
cpu_set_reserved_ttbr0();
//核心函数,进行真正地页表切换;
cpu_do_switch_mm(virt_to_phys(pgd),mm);
}
- 这里先判断页全局目录pdg是否指向内核启动时的页全局目录指针swapper_pg_dir,如果是的话,则说明要切换的进程是内核级线程,出问题了;
- 通过
cpu_set_reserved_ttbr0()
函数将TTBR0_EL1指向0页emty_zero_page ,用来禁用TTBR0进行内存访问翻译; - 核心函数:
cpu_do_switch_mm
通过汇编进行进程地址切换
【cpu_do_switch_mm()函数】
该函数是一个汇编指令,主要对两个寄存器TTRBR0_EL1 以及TTBR1_EL1 进行赋值 用来切换新进程next的地址空间, 其中两个寄存器分别存储:
- TTBR0_EL1: 放入next进程的页目录基地址;
- TTBR1_EL1: 放入next进程的硬件ASID;
3.3 栈和寄存器的切换 switch_to()函数
该函数定义在<include/asm-generic/switch to.h>
中,具体是将相关重要寄存器中的值进行保存和切换,这里引入了task_struct
结构体中的thread_struct
,该结构体中的cpu_context
结构体存储着涉及到进程上下文的寄存器中的值;
switch_to()
函数的步骤包括:首先将当前进程的寄存器状态保存到对应进程的cpu_context
结构中;随后恢复新进程的寄存器状态并切换至其对应的内核态栈,以此完成进程之间的上下文切换。
#define switch_to(prev, next, last) \
do { \
((last) = __switch_to((prev), (next))); \
} while (0)
#endif /* __ASM_GENERIC_SWITCH_TO_H */
显然, switch_to() 函数通过调用__switch_to()函数进行寄存器以及栈的切换;
__notrace_funcgraph struct task_struct *__switch_to(struct task_struct *prev,
struct task_struct *next)
{
struct task_struct *last; // 声明一个指向 task_struct 结构的指针变量 last,用于存储上一个任务的指针
// 执行一系列与特定硬件相关的线程切换操作
fpsimd_thread_switch(next); // 切换浮点寄存器上下文
tls_thread_switch(next); // 切换 TLS(线程局部存储)上下文
hw_breakpoint_thread_switch(next); // 切换硬件断点上下文
contextidr_thread_switch(next); // 切换上下文标识寄存器上下文
entry_task_switch(next); // 切换入口任务上下文
uao_thread_switch(next); // 切换 UAO(用户空间访问权)上下文
ptrauth_thread_switch(next); // 切换指针认证上下文
/*
* Complete any pending TLB or cache maintenance on this CPU in case
* the thread migrates to a different CPU.
* This full barrier is also required by the membarrier system
* call.
*/
dsb(ish); // 执行数据同步栅栏指令,用于确保在任务切换之前的所有内存访问已经完成
/* the actual thread switch */
last = cpu_switch_to(prev, next); // 调用实际的线程切换函数,将上一个任务切换到下一个任务,并返回上一个任务的指针
return last; // 返回上一个任务的指针
}
- __switch_to()函数会执行一些列的硬件相关线程操作函数, 用来切换硬件相关上下文,;
- 此处调用
cpu_switch_to()
函数进行具体寄存器的切换;- 将当前寄存器中的值放入prev进程 task_struct -> thread_struct -> cpu_context结构体中;
- 将next进程task_struct -> thread_struct -> cpu_context结构体中的相关寄存器的值 放入 寄存器中;
- 将next 进程描述符指针放入 SP_EL0寄存器中, 便于current宏拿到当前进程的task_struct;