文章内容
1. 如何管理进程
1.1 TIF_* 标志
1.2 内核栈与用户栈
1.3 进程优先级
2. 进程切换的过程
2.1 context_switch
2.2 switch_mm 切换页表
2.3 switch_to 切换寄存器
2.4 ret_from_fork
3. 进程切换的时机
3.1 时间片耗尽
3.2 进程主动放弃 CPU
3.3 更高优先级进程可运行
Linux 并没有区分进程和线程,两者没有差别,统一是 task,使用 task_struct 描述

thread_info.flags 保存了线程信息标志,主要用于描述线程在运行时的各种状态或需求,直接影响当前线程的执行路径(如调度、异常处理、系统调用返回等)
need_resched() 函数返回当前进程是否需要切换
static __always_inline bool need_resched(void){ return unlikely(tif_need_resched());}static __always_inline bool tif_need_resched(void){ return tif_test_bit(TIF_NEED_RESCHED);}在 flags 中写入 TIF_NEED_RESCHED 来标记需要切换某个进程
static inline void set_tsk_need_resched(struct task_struct *tsk){ if (tracepoint_enabled(sched_set_need_resched_tp) && !test_tsk_thread_flag(tsk, TIF_NEED_RESCHED)) __trace_set_need_resched(tsk, TIF_NEED_RESCHED); set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);}static inline void clear_tsk_need_resched(struct task_struct *tsk){ atomic_long_andnot(_TIF_NEED_RESCHED | _TIF_NEED_RESCHED_LAZY, (atomic_long_t *)&task_thread_info(tsk)->flags);}task_stack_page() 函数返回内核栈内存,加上 THREAD_SIZE 就是内核栈栈顶
/* * When accessing the stack of a non-current task that might exit, use * try_get_task_stack() instead. task_stack_page will return a pointer * that could get freed out from under you. */static __always_inline void *task_stack_page(const struct task_struct *task){ return task->stack;}创建新的进程(包括 pthread_create() 创建线程)时,都会创建内核栈
static int alloc_thread_stack_node(struct task_struct *tsk, int node){struct vm_struct *vm_area; void *stack; int i; // 遍历当前 CPU 的缓存池(cached_stacks),尝试获取之前被释放但保留下来的栈内存 for (i = 0; i < NR_CACHED_STACKS; i++) { vm_area = this_cpu_xchg(cached_stacks[i], NULL); if (!vm_area) continue; if (memcg_charge_kernel_stack(vm_area)) { vfree(vm_area->addr); return -ENOMEM; } /* Reset stack metadata. */ kasan_unpoison_range(vm_area->addr, THREAD_SIZE); stack = kasan_reset_tag(vm_area->addr); /* Clear stale pointers from reused stack. */ memset(stack, 0, THREAD_SIZE); tsk->stack_vm_area = vm_area; tsk->stack = stack; // 设置内核栈内存 return 0; } // 调用 vmalloc 分配一块新的连续虚拟内存 stack = __vmalloc_node(THREAD_SIZE, THREAD_ALIGN, GFP_VMAP_STACK, node, __builtin_return_address(0)); if (!stack) return -ENOMEM; vm_area = find_vm_area(stack); if (memcg_charge_kernel_stack(vm_area)) { vfree(stack); return -ENOMEM; } /* * We can't call find_vm_area() in interrupt context, and * free_thread_stack() can be called in interrupt context, * so cache the vm_struct. */ tsk->stack_vm_area = vm_area; stack = kasan_reset_tag(stack); tsk->stack = stack; // 设置内核栈内存 return 0;}task_pt_regs 宏返回内核栈栈顶保存寄存器的内存地址
#define task_pt_regs(p) \ ((struct pt_regs *)(THREAD_SIZE + task_stack_page(p)) - 1)#define KSTK_EIP(tsk) ((unsigned long)task_pt_regs(tsk)->pc)#define KSTK_ESP(tsk) user_stack_pointer(task_pt_regs(tsk))在 copy_thread() 拷贝父进程资源时,在新进程的内核栈地址上,构建返回用户态时寄存器值
int copy_thread(struct task_struct *p, const struct kernel_clone_args *args){ u64 clone_flags = args->flags; unsigned long stack_start = args->stack; // user 栈空间 unsigned long tls = args->tls; // user TLS 内存空间空间struct pt_regs *childregs = task_pt_regs(p); // 子进程内核栈构建返回用户态寄存器值 int ret; memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context)); // ... if (likely(!args->fn)) { *childregs = *current_pt_regs(); // 继承父进程所有寄存器 childregs->regs[0] = 0; // 子进程返回 0 /* * Read the current TLS pointer from tpidr_el0 as it may be * out-of-sync with the saved value. */ *task_user_tls(p) = read_sysreg(tpidr_el0); // 继承父进程 TLS 地址空间 if (system_supports_poe()) p->thread.por_el0 = read_sysreg_s(SYS_POR_EL0); if (stack_start) { // 使用 user 传入的用户栈空间 if (is_compat_thread(task_thread_info(p))) childregs->compat_sp = stack_start; else childregs->sp = stack_start; } // ... if (clone_flags & CLONE_SETTLS) p->thread.uw.tp_value = tls; // 使用 user TLS 地址空间 ret = copy_thread_gcs(p, args); if (ret != 0) return ret; } else { // ... } p->thread.cpu_context.pc = (unsigned long)ret_from_fork; p->thread.cpu_context.sp = (unsigned long)childregs; /* * For the benefit of the unwinder, set up childregs->stackframe * as the final frame for the new task. */ p->thread.cpu_context.fp = (unsigned long)&childregs->stackframe; ptrace_hw_copy_thread(p); return 0;}当子进程被调度,进程切换时,将新任务的内核栈地址 tsk->stack 写入 SP_EL1 寄存器
Linux 进程优先级范围 [-1, 140),其中 [-1, 100) 表示实时进程优先级,[100, 140) 是普通进程优先级,-1 表示 deadline 进程(也是实时进程)
普通进程优先级通过 nice 值调整

task_struct 中有四个成员表示优先级
区分 prio 和 normal_prio,是因为实时进程优先级调整并不一定立即生效,和 rtmutex 实现有关系
sched_setscheduler() 函数根据优先级 prio 设置进程调度策略,把进程添加到对应的运行队列中
static void __setscheduler_prio(struct task_struct *p, int prio){ if (dl_prio(prio)) p->sched_class = &dl_sched_class; else if (rt_prio(prio)) p->sched_class = &rt_sched_class; else p->sched_class = &fair_sched_class; p->prio = prio;}用户空间可用操作的是 dl/rt/fair 三种调度器,支持设置不同的调度策略

每个 CPU 都有一个运行队列 runqueues,管理所有等待执行的 task
task 根据优先级添加相应的队列中,等待调度器调度,获取 CPU 后开始执行

dl_sched_class 就绪队列使用红黑树管理所有的 task,红黑树使用 deadline 当作 key,每次选择红黑树最左节点
static struct sched_dl_entity *pick_next_dl_entity(struct dl_rq *dl_rq){struct rb_node *left = rb_first_cached(&dl_rq->root); if (!left) return NULL; return __node_2_dle(left);}rt_sched_class 就绪队列按照优先级 prio 添加到 active.queue[prio] 队列中,同一优先级队列中并没有区分 SCHED_RR 和 SCHED_FIFO 调度策略,pick_next_task_rt() 总是选择队首元素
fair_sched_class 调度器 CFS 算法已经被 EEVDF 取代,EEVDF 算法的核心思想是:在所有 e 时刻到来的进程中,选择具有最小虚拟截止时间的进程,分配其 CPU 运行 q 个时间单位
在运行队列 runqueues 的进程属于 TASK_RUNNING 状态,但是不一定正在运行,只有被调度才正在运行

ARM64 架构下 Linux 内核的进程切换(上下文切换)是一个极其精密、软硬件高度协同的过程
整个过程可以清晰地划分四个核心阶段
context_switch() 进程上下文切换重要的工作
context_switch(rq, pref, next, rf) prepare_task_switch(rq, prev, next) prepare_task(next) WRITE_ONCE(next->on_cpu, 1) // 标记 next 占用 CPU if (!next->mm) next->active_mm = prev->active_mm // next 是 kernel 进程,复用 prev 进程空间 else switch_mm_irqs_off(prev->active_mm, next->mm, next) // switch_mm switch_to(prev, next, prev) finish_task_switch(prev) finish_task(prev) smp_store_release(&prev->on_cpu, 0) // 标记 prev 不占用 CPU fire_sched_in_preempt_notifiers(current)ARM64 架构拥有两个页表基址寄存器
ARM64 中 switch_mm_irqs_off 其实就是 switch_mm() 函数
#ifndef switch_mm_irqs_off# define switch_mm_irqs_off switch_mm#endif如果 next 是内核线程,不用切换地址空间,只是将用户态地址空间设置为 reserved_pg_dir:一个内容全是零的只读页表
否则使用 check_and_switch_context() 切换用户态地址空间
switch_mm(prev, next, tsk) if (prev != next) // 不同的地址空间才需要切换 __switch_mm(next) // next 是内核进程 if (next == &init_mm) cpu_set_reserved_ttbr0() cpu_set_reserved_ttbr0_nosync() ttbr = phys_to_ttbr(__pa_symbol(reserved_pg_dir)) write_sysreg(ttbr, ttbr0_el1) // 更新用户态地址空间 isb() // 刷新当前 CPU 的指令流水线 return // next 是用户态进程 check_and_switch_context(next)check_and_switch_context() 并不一定刷新 TLB:只有在分配 ASID 导致 Generation 递增的情况下,才会刷新 TLB
void check_and_switch_context(struct mm_struct *mm){ // ... asid = atomic64_read(&mm->context.id); /* * The memory ordering here is subtle. * If our active_asids is non-zero and the ASID matches the current * generation, then we update the active_asids entry with a relaxed * cmpxchg. Racing with a concurrent rollover means that either: * * - We get a zero back from the cmpxchg and end up waiting on the * lock. Taking the lock synchronises with the rollover and so * we are forced to see the updated generation. * * - We get a valid ASID back from the cmpxchg, which means the * relaxed xchg in flush_context will treat us as reserved * because atomic RmWs are totally ordered for a given location. */ old_active_asid = atomic64_read(this_cpu_ptr(&active_asids)); // 新进程 ASID 还是有效的,直接切换页表,不执行刷新 TLB if (old_active_asid && asid_gen_match(asid) && atomic64_cmpxchg_relaxed(this_cpu_ptr(&active_asids), old_active_asid, asid)) goto switch_mm_fastpath; raw_spin_lock_irqsave(&cpu_asid_lock, flags); /* Check that our ASID belongs to the current generation. */ asid = atomic64_read(&mm->context.id); if (!asid_gen_match(asid)) { asid = new_context(mm); // 尝试分配 ASID,可能导致 Rollover atomic64_set(&mm->context.id, asid); } cpu = smp_processor_id(); // 分配 ASID,导致递增 ASID generation,需要刷新 TLB if (cpumask_test_and_clear_cpu(cpu, &tlb_flush_pending)) local_flush_tlb_all(); atomic64_set(this_cpu_ptr(&active_asids), asid); raw_spin_unlock_irqrestore(&cpu_asid_lock, flags);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); // 更新页表}cpu_switch_mm() 封装 cpu_do_switch_mm
static inline void cpu_switch_mm(pgd_t *pgd, struct mm_struct *mm){ BUG_ON(pgd == swapper_pg_dir); cpu_do_switch_mm(virt_to_phys(pgd),mm);}cpu_do_switch_mm() 函数更新内核态和用户态页表寄存器
void cpu_do_switch_mm(phys_addr_t pgd_phys, struct mm_struct *mm){ unsigned long ttbr1 = read_sysreg(ttbr1_el1); // 内核态页表 unsigned long asid = ASID(mm); unsigned long ttbr0 = phys_to_ttbr(pgd_phys); // 用户态页表 /* Skip CNP for the reserved ASID */ if (system_supports_cnp() && asid) ttbr0 |= TTBR_CNP_BIT; /* SW PAN needs a copy of the ASID in TTBR0 for entry */ if (IS_ENABLED(CONFIG_ARM64_SW_TTBR0_PAN)) ttbr0 |= FIELD_PREP(TTBR_ASID_MASK, asid); /* Set ASID in TTBR1 since TCR.A1 is set */ ttbr1 &= ~TTBR_ASID_MASK; ttbr1 |= FIELD_PREP(TTBR_ASID_MASK, asid); cpu_set_reserved_ttbr0_nosync(); write_sysreg(ttbr1, ttbr1_el1); // 更新内核态页表寄存器 write_sysreg(ttbr0, ttbr0_el1); // 更新用户态页表寄存器 isb(); // 刷新处理器的指令流水线 post_ttbr_update_workaround();}local_flush_tlb_all() 刷新整个 TLB 缓存
static inline void local_flush_tlb_all(void){ dsb(nshst); __tlbi(vmalle1); dsb(nsh); isb();}switch_to 宏封装 __switch_to() 函数
#define switch_to(prev, next, last) \ do { \ ((last) = __switch_to((prev), (next))); \ } while (0)switch_to 主要职责是在真正切换硬件寄存器之前,处理所有与体系结构相关的软件状态、特殊寄存器和硬件特性的切换
struct task_struct *__switch_to(struct task_struct *prev, struct task_struct *next){struct task_struct *last; fpsimd_thread_switch(next); // 切换浮点寄存器(FPSIMD)状态 tls_thread_switch(next); // 切换 TLS hw_breakpoint_thread_switch(next); contextidr_thread_switch(next); entry_task_switch(next); ssbs_thread_switch(next); cntkctl_thread_switch(prev, next); ptrauth_thread_switch_user(next); permission_overlay_switch(next); gcs_thread_switch(next); /* * vendor hook is needed before the dsb(), * because MPAM is related to cache maintenance. */ trace_android_vh_mpam_set(prev, 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. Additionally it makes any * in-progress pgtable writes visible to the table walker; See * emit_pte_barriers(). */ dsb(ish); // 确保在当前 CPU 上所有未完成的 TLB 或缓存维护操作(如页表写入)全部完成 /* * MTE thread switching must happen after the DSB above to ensure that * any asynchronous tag check faults have been logged in the TFSR*_EL1 * registers. */ mte_thread_switch(next); /* avoid expensive SCTLR_EL1 accesses if no change */ if (prev->thread.sctlr_user != next->thread.sctlr_user) update_sctlr_el1(next->thread.sctlr_user); trace_android_vh_is_fpsimd_save(prev, next); /* the actual thread switch */ last = cpu_switch_to(prev, next); return last;}cpu_switch_to 负责保存旧进程(prev)的执行状态,并恢复新进程(next)的状态,最终通过 ret 指令完成程序计数器(PC)的跳转
执行到 cpu_switch_to 时,x0 保存的是 prev, x1 保存是 next
THREAD_CPU_CONTEXT 宏表示成员 thread.cpu_context 在 task_struct 中的偏移
DEFINE(THREAD_CPU_CONTEXT, offsetof(struct task_struct, thread.cpu_context))所以 x0 + THREAD_CPU_CONTEXT 就是 prev->thread.cpu_context
同理 x1 + THREAD_CPU_CONTEXT 就是 next->thread.cpu_context
SYM_FUNC_START(cpu_switch_to) save_and_disable_daif x11 // 中断屏蔽 mov x10, #THREAD_CPU_CONTEXT add x8, x0, x10 // x8 = prev->thread.cpu_context mov x9, sp stp x19, x20, [x8], #16 // store callee-saved registers stp x21, x22, [x8], #16 stp x23, x24, [x8], #16 stp x25, x26, [x8], #16 stp x27, x28, [x8], #16 stp x29, x9, [x8], #16 // 保存帧指针(x29)和栈指针(sp) str lr, [x8] // 保存链接寄存器(lr),即 prev 的返回地址(PC) add x8, x1, x10 // x8 = next->thread.cpu_context ldp x19, x20, [x8], #16 // restore callee-saved registers ldp x21, x22, [x8], #16 ldp x23, x24, [x8], #16 ldp x25, x26, [x8], #16 ldp x27, x28, [x8], #16 ldp x29, x9, [x8], #16 // 恢复 next 的 fp 和 sp 到 x9 ldr lr, [x8] // 恢复 next 的返回地址(PC)到 lr mov sp, x9 // 将内核栈指针切换为 next 的栈 msr sp_el0, x1 // 将 next 的 task_struct 指针存入用户态栈指针寄存器 ptrauth_keys_install_kernel x1, x8, x9, x10 scs_save x0 scs_load_current restore_irq x11 // 恢复之前保存的 DAIF 状态,重新允许中断 retSYM_FUNC_END(cpu_switch_to)当进程创建出来后第一次被调度执行,执行的是 ret_from_fork() 函数
回顾 copy_thread() 函数构建的 cpu_context 内容
int copy_thread(struct task_struct *p, const struct kernel_clone_args *args){ u64 clone_flags = args->flags; unsigned long stack_start = args->stack; // user 栈空间 unsigned long tls = args->tls; // user TLS 内存空间空间struct pt_regs *childregs = task_pt_regs(p); // 子进程内核栈构建返回用户态寄存器值 int ret; memset(&p->thread.cpu_context, 0, sizeof(struct cpu_context)); // ... p->thread.cpu_context.pc = (unsigned long)ret_from_fork; p->thread.cpu_context.sp = (unsigned long)childregs; /* * For the benefit of the unwinder, set up childregs->stackframe * as the final frame for the new task. */ p->thread.cpu_context.fp = (unsigned long)&childregs->stackframe; ptrace_hw_copy_thread(p); return 0;}完成调度收尾工作,并根据进程类型的不同,将执行流精准地分流到“内核线程执行路径”或“返回用户态路径”
SYM_CODE_START(ret_from_fork) bl schedule_tail // 解锁、重新允许抢占、向用户态回写线程 ID cbz x19, 1f // user 进程 x19 是 0,跳转到 1 mov x0, x20 blr x191: get_current_task tsk mov x0, sp // 将当前内核栈指针 (sp) 移动到 x0 bl asm_exit_to_user_mode // CPU 正式从内核态 (EL1) 切换回用户态 (EL0) b ret_to_user // 执行 ret_to_userSYM_CODE_END(ret_from_fork)ret_to_user 调用 kernel_exit 使用在内核栈上 pt_regs 保存的寄存器值恢复 user 线程上下文
S_SP 宏记录的是 sp 在 pt_regs {} 数据结构的偏移
DEFINE(S_SP, offsetof(struct pt_regs, sp));kernel_exit() 是返回用户态最后的处理函数
.macro kernel_exit, el .if \el != 0 disable_daif .endif#ifdef CONFIG_ARM64_PSEUDO_NMI// ...#endif ldp x21, x22, [sp, #S_PC] // load ELR, SPSR#ifdef CONFIG_ARM64_SW_TTBR0_PAN// ...#endif .if \el == 0 ldr x23, [sp, #S_SP] // 加载保存的用户态栈指针 msr sp_el0, x23 // 将用户态栈指针写入 sp_el0 tst x22, #PSR_MODE32_BIT // native task? b.eq 3f#ifdef CONFIG_ARM64_ERRATUM_845719alternative_if ARM64_WORKAROUND_845719#ifdef CONFIG_PID_IN_CONTEXTIDR mrs x29, contextidr_el1 msr contextidr_el1, x29#else msr contextidr_el1, xzr#endifalternative_else_nop_endif#endif3: scs_save tsk /* Ignore asynchronous tag check faults in the uaccess routines */ ldr x0, [tsk, THREAD_SCTLR_USER] clear_mte_async_tcf x0#ifdef CONFIG_ARM64_PTR_AUTHalternative_if ARM64_HAS_ADDRESS_AUTH /* * IA was enabled for in-kernel PAC. Disable it now if needed, or * alternatively install the user's IA. All other per-task keys and * SCTLR bits were updated on task switch. * * No kernel C function calls after this. */ tbz x0, SCTLR_ELx_ENIA_SHIFT, 1f __ptrauth_keys_install_user tsk, x0, x1, x2 b 2f1: mrs x0, sctlr_el1 bic x0, x0, SCTLR_ELx_ENIA msr sctlr_el1, x02:alternative_else_nop_endif#endif mte_set_user_gcr tsk, x0, x1 apply_ssbd 0, x0, x1 .endif msr elr_el1, x21 // set up the return data msr spsr_el1, x22 ldp x0, x1, [sp, #16 * 0] ldp x2, x3, [sp, #16 * 1] ldp x4, x5, [sp, #16 * 2] ldp x6, x7, [sp, #16 * 3] ldp x8, x9, [sp, #16 * 4] ldp x10, x11, [sp, #16 * 5] ldp x12, x13, [sp, #16 * 6] ldp x14, x15, [sp, #16 * 7] ldp x16, x17, [sp, #16 * 8] ldp x18, x19, [sp, #16 * 9] ldp x20, x21, [sp, #16 * 10] ldp x22, x23, [sp, #16 * 11] ldp x24, x25, [sp, #16 * 12] ldp x26, x27, [sp, #16 * 13] ldp x28, x29, [sp, #16 * 14] .if \el == 0#ifdef CONFIG_UNMAP_KERNEL_AT_EL0 alternative_insn "b .L_skip_tramp_exit_\@", nop, ARM64_UNMAP_KERNEL_AT_EL0 msr far_el1, x29 ldr_this_cpu x30, this_cpu_vector, x29 tramp_alias x29, tramp_exit msr vbar_el1, x30 // install vector table ldr lr, [sp, #S_LR] // 恢复 x30 (LR) add sp, sp, #PT_REGS_SIZE // 恢复内核栈指针,释放 pt_regs 内存 br x29.L_skip_tramp_exit_\@:#endif .endif ldr lr, [sp, #S_LR] add sp, sp, #PT_REGS_SIZE // restore sp .if \el == 0 /* This must be after the last explicit memory access */alternative_if ARM64_WORKAROUND_SPECULATIVE_UNPRIV_LOAD tlbi vale1, xzr dsb nshalternative_else_nop_endif .else /* Ensure any device/NC reads complete */ alternative_insn nop, "dmb sy", ARM64_WORKAROUND_1508412 .endif eret sb .endm进程切换可以在多个时刻发生,三类典型触发点及其实现路径:时间片耗尽、进程主动放弃 CPU、以及更高优先级进程可运行(抢占)
平台 clockevent 硬件中断触发后,clockevent 框架会调用设备的 event_handler
根据运行模式的不同,处理路径分为两种
ARM64 默认的编译配置,内核会优先把可用的 clockevent 设备切到 oneshot/high‑res 模式,并尽早(启动期间)调用 hrtimer_switch_to_hres() 函数,把 clockevent 设备的 IRQ 中断处理函数设置为 hrtimer_interrupt() 函数,随后内核用 per‑CPU hrtimer(tick_sched)来模拟周期 jiffy
## Timers subsystem#CONFIG_TICK_ONESHOT=yCONFIG_NO_HZ_COMMON=y# CONFIG_HZ_PERIODIC is not setCONFIG_NO_HZ_IDLE=y# CONFIG_NO_HZ_FULL is not set# CONFIG_NO_HZ is not setCONFIG_HIGH_RES_TIMERS=y# CONFIG_POSIX_AUX_CLOCKS is not set# end of Timers subsystem
两个处理函数都调用 update_process_times() 函数,从而触发 sched_tick() 做时间片更新
hrtimer_interrupt __hrtimer_run_queues __run_hrtimer tick_nohz_handler tick_sched_do_timer tick_do_update_jiffies64 tick_sched_handle update_process_times account_process_tick // CPU 时间记账 run_local_timers // 驱动定时器子系统 rcu_sched_clock_irq sched_tick // 调度器心跳 sched_clock_tick // 更新 sched_clock 的 tick update_rq_clock // 更新 runqueue 的时间 update_hw_load_avg // 更新 runqueue 的硬件负载 // 把 tick 事件交给调度类的 task_tick donor->sched_class->task_tick(rq, donor, 0) sched_core_tick // 调度域/核心层级的周期性工作DEADLINE 调度器 task_tick_dl() 函数更新当前进程剩余运行时间:时间消耗完就标记需要进程切换
task_tick_dl update_curr_dl delta_exec = update_curr_common(rq) // 计算进程运行执行时间 update_curr_dl_se(rq, dl_se, delta_exec) dl_se->runtime -= scaled_delta_exec // 更新剩余时间 if (dl_runtime_exceeded(dl_se) || dl_se->dl_yielded) { // 可能重新加入红黑树队列,所以需要判断 if (!is_leftmost(dl_se, &rq->dl)) resched_curr(rq); // 标记需要进程切换 } update_dl_rq_load_avgREALTIME 调度器 task_tick_rt() 函数只会更新 SCHED_RR 类型的进程:时间消耗完就标记需要进程切换
SCHED_RR 进程默认占用 CPU 100ms 时间
task_tick_rt update_curr_rt(rq) update_rt_rq_load_avg(rq_clock_pelt(rq), rq, 1) if (p->policy != SCHED_RR) return; // SCHED_FIFO 一直运行 if (--p->rt.time_slice) return; // SCHED_RR 时间片没有耗尽,继续运行 p->rt.time_slice = sched_rr_timeslice; // 重新更新时间片 requeue_task_rt(rq, p, 0); // 添加到队列末尾 resched_curr(rq); // 标记需要进程切换FAIR 调度器 task_tick_fair() 函数判定当前进程需要切换的完整条件是
task_tick_fair entity_tick(cfs_rq, se, queued) // 在对应的 cfs_rq 上做实体级别的 tick update_curr(cfs_rq, se) delta_exec = update_se(rq, curr) curr->vruntime += calc_delta_fair(delta_exec, curr) // 更新虚拟运行时间 resched = update_deadline(cfs_rq, curr) // EEVDF 算法判断是否需要切换进程 if (vruntime >= deadline) return true; // 到达 deadline,需要切换 if (resched || !protect_slice(curr)) resched_curr_lazy(rq); // 需要进程切换 update_load_avg(cfs_rq, curr, UPDATE_TG) update_cfs_group(curr)这里仅仅时标记需要进程切换,需要等待"安全点"到来才真正切换进程
应用进程调用 sched_yield() 函数主动放弃 CPU,或者陷入内核态进入 sleep 某些事件未就绪时调用 schedule() 放弃主动放弃 CPU
schedule __schedule(SM_NONE) cpu = smp_processor_id(); rq = cpu_rq(cpu); prev = rq->curr; update_rq_clock(rq); next = pick_next_task(rq, rq->donor, &rf) rq_set_donor(rq, next) if (prev != next) context_switch(rq, prev, next, &rf)立即进行进程切换
当更高优先级进程被唤醒,尝试抢占当前进程,标记需要进程切换,需要等待"安全点"到来才真正切换进程
void wakeup_preempt(struct rq *rq, struct task_struct *p, int flags){struct task_struct *donor = rq->donor; // 被唤醒进程 p 和正在运行进程 donor 属于同一个调度类 if (p->sched_class == donor->sched_class) donor->sched_class->wakeup_preempt(rq, p, flags); // 不同调度类,被唤醒进程优先级更高,标记需要进程切换 else if (sched_class_above(p->sched_class, donor->sched_class)) resched_curr(rq); /* * A queue event has occurred, and we're going to schedule. In * this case, we can save a useless back to back clock update. */ if (task_on_rq_queued(donor) && test_tsk_need_resched(rq->curr)) rq_clock_skip_update(rq);}DEADLINE 调度器比较截止时间 deadline:deadline 更小的优先级更高
static void wakeup_preempt_dl(struct rq *rq, struct task_struct *p, int flags){ // 被唤醒进程 deadline 更小,需要进程切换 if (dl_entity_preempt(&p->dl, &rq->donor->dl)) { resched_curr(rq); return; } // deadline 相同,并且当前进程未设置切换标志 if ((p->dl.deadline == rq->donor->dl.deadline) && !test_tsk_need_resched(rq->curr)) check_preempt_equal_dl(rq, p);}当当前进程和正在运行的进程优先级相同,不能盲目地直接抢占,要优先尝试通过多核迁移(Push/Pull)来避免不必要的上下文切换
static void check_preempt_equal_dl(struct rq *rq, struct task_struct *p){ // 当前进程绑定当前 CPU // 或者其他 CPU 也没有适合运行当前进程 // 不需要切换 if (rq->curr->nr_cpus_allowed == 1 || !cpudl_find(&rq->rd->cpudl, rq->donor, NULL)) return; // 被唤醒进程没有绑定 CPU 并且由其他 CPU 可以运行,不需要切换 if (p->nr_cpus_allowed != 1 && cpudl_find(&rq->rd->cpudl, p, NULL)) return; // 当前任务可以迁移,但新任务 p 走不掉(比如 p 被绑死在当前核); // 或者两者都走不掉。 resched_curr(rq); // 需要切换}REALTIME 调度器比较优先级:被唤醒进程优先级更高就需要进程切换
static void wakeup_preempt_rt(struct rq *rq, struct task_struct *p, int flags){struct task_struct *donor = rq->donor; // 被唤醒进程优先级更高,需要进程切换 if (p->prio < donor->prio) { resched_curr(rq); return; } /* * If: * * - the newly woken task is of equal priority to the current task * - the newly woken task is non-migratable while current is migratable * - current will be preempted on the next reschedule * * we should check to see if current can readily move to a different * cpu. If so, we will reschedule to allow the push logic to try * to move current somewhere else, making room for our non-migratable * task. */ if (p->prio == donor->prio && !test_tsk_need_resched(rq->curr)) check_preempt_equal_prio(rq, p);}优先级相同尝试将当前进程和被唤醒进程迁移,都无法迁移切换进程
static void check_preempt_equal_prio(struct rq *rq, struct task_struct *p){ // 说明当前任务无法迁移到其他 CPU if (rq->curr->nr_cpus_allowed == 1 || !cpupri_find(&rq->rd->cpupri, rq->donor, NULL)) return; // p 可以迁移到其他 CPU if (p->nr_cpus_allowed != 1 && cpupri_find(&rq->rd->cpupri, p, NULL)) return; // 切换进程 requeue_task_rt(rq, p, 1); resched_curr(rq);}FAIR 调度器没有一个固定的优先级比较,而是根据 EEVDF 算法选出最 "Eligible" 的进程
static void check_preempt_wakeup_fair(struct rq *rq, struct task_struct *p, int wake_flags){enum preempt_wakeup_action preempt_action = PREEMPT_WAKEUP_PICK;struct task_struct *donor = rq->donor;struct sched_entity *nse, *se = &donor->se, *pse = &p->se;struct cfs_rq *cfs_rq = task_cfs_rq(donor); int cse_is_idle, pse_is_idle; if (unlikely(se == pse)) return; // 被唤醒进程以及被节流,无法抢占 if (task_is_throttled(p)) return; // 当前进程已经被标记进程切换,返回等待进程切换进行 if (test_tsk_need_resched(rq->curr)) return; // 不支持抢占 if (!sched_feat(WAKEUP_PREEMPTION)) return; find_matching_se(&se, &pse); WARN_ON_ONCE(!pse); cse_is_idle = se_is_idle(se); pse_is_idle = se_is_idle(pse); // 1. 当前进程是 IDLE 进程,被唤醒进程是非 IDLE 进程,无条件抢占 if (cse_is_idle && !pse_is_idle) { /* * When non-idle entity preempt an idle entity, * don't give idle entity slice protection. */ preempt_action = PREEMPT_WAKEUP_SHORT; goto preempt; } // 当前进程是非 IDLE,被唤醒进程是 IDLE,不能抢占 if (cse_is_idle != pse_is_idle) return; /* * BATCH and IDLE tasks do not preempt others. */ if (unlikely(!normal_policy(p->policy))) return; cfs_rq = cfs_rq_of(se); update_curr(cfs_rq); /* * If @p has a shorter slice than current and @p is eligible, override * current's slice protection in order to allow preemption. */ // 2. 被唤醒进程比当前进程 slice 时间片小,判断是不是最 Eligible if (sched_feat(PREEMPT_SHORT) && (pse->slice < se->slice)) { preempt_action = PREEMPT_WAKEUP_SHORT; goto pick; } /* * Ignore wakee preemption on WF_FORK as it is less likely that * there is shared data as exec often follow fork. Do not * preempt for tasks that are sched_delayed as it would violate * EEVDF to forcibly queue an ineligible task. */ // 刚 fork 出来的子进程不抢占,因为通常紧接着会 exec // sched_delayed: 处于延迟调度状态的任务不能强制入队 if ((wake_flags & WF_FORK) || pse->sched_delayed) return; /* Prefer picking wakee soon if appropriate. */ if (sched_feat(NEXT_BUDDY) && set_preempt_buddy(cfs_rq, wake_flags, pse, se)) { /* * Decide whether to obey WF_SYNC hint for a new buddy. Old * buddies are ignored as they may not be relevant to the * waker and less likely to be cache hot. */ if (wake_flags & WF_SYNC) preempt_action = preempt_sync(rq, wake_flags, pse, se); } switch (preempt_action) { case PREEMPT_WAKEUP_NONE: return; case PREEMPT_WAKEUP_RESCHED: goto preempt; case PREEMPT_WAKEUP_SHORT: fallthrough; case PREEMPT_WAKEUP_PICK: break; }pick: nse = pick_next_entity(rq, cfs_rq, preempt_action != PREEMPT_WAKEUP_SHORT); /* If @p has become the most eligible task, force preemption */ if (nse == pse) goto preempt; /* * Because p is enqueued, nse being null can only mean that we * dequeued a delayed task. If there are still entities queued in * cfs, check if the next one will be p. */ if (!nse && cfs_rq->nr_queued) goto pick; if (sched_feat(RUN_TO_PARITY)) update_protect_slice(cfs_rq, se); return;preempt: if (preempt_action == PREEMPT_WAKEUP_SHORT) cancel_protect_slice(se); resched_curr_lazy(rq);}