Linux进程调度与管理:(四)进程的调度之schedule进程切换

《Linux6.5源码分析:进程管理与调度系列文章》

本系列文章将对进程管理与调度进行知识梳理与源码分析,重点放在linux源码分析上,并结合eBPF程序对内核中进程调度机制进行数据实时拿取与分析。

在进行正式介绍之前,有必要对文章引用进行提前说明。本系列文章参考了大量的博客、文章以及书籍:

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;

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值