当前位置:首页>Linux>不懂上下文切换,无法真正吃透 Linux 内核进程管理

不懂上下文切换,无法真正吃透 Linux 内核进程管理

  • 2026-08-18 23:11:29
不懂上下文切换,无法真正吃透 Linux 内核进程管理

大家好,我是蟹老板~

这篇文章的目标就一个:从原理到源码到线上排查,把进程上下文切换讲透。

一、为什么要吃透进程上下文切换?

1.1 上下文切换在 Linux 调度体系中的核心地位

很多人第一次接触上下文切换,是在 top 或 vmstat 里看到一个叫 cs 的数字。

一秒几千次,看着还挺正常。一秒几十万次,心里开始发毛。再高一点,群里通常就会出现一句熟悉的话。

“是不是上下文切换太多了?”

然后大家开始调线程池,改协程,绑 CPU,甚至重启机器。折腾一圈,接口延迟没怎么变,代码倒是被改得亲妈都不认识。

以我这么多年的踩坑经验来说,上下文切换就像体检报告里的胆固醇。数字高不一定马上有病,数字不高也不能证明身体倍儿棒。真正重要的是,它为什么发生,谁在切换,切换之后 CPU 在干什么,以及你的业务有没有为这些切换付出不必要的代价。

Linux 调度器的工作,说穿了就是决定哪一个可运行任务能够占用 CPU。决定做完以后,光喊一句“你上”没用,还要把当前任务的执行现场收起来,再把另一个任务的现场恢复出来。这个交接动作就是上下文切换。

没有上下文切换,调度算法只是写在纸上的排班表。

CFS、EEVDF、实时调度、CPU 亲和性、负载均衡,这些机制最终都要落到一个极其具体的问题上——CPU 下一条指令到底属于哪个任务。

1.2 区分核心概念:进程切换、线程切换、中断上下文切换

“上下文切换”这个词经常被混着用,技术讨论一旦混词,后面基本就要开始鸡同鸭讲。

Linux 内核真正调度的对象是任务,也就是 task_struct 所描述的调度实体。 一个传统意义上的进程可以只有一个任务,也可以包含多个共享地址空间的线程。站在调度器角度,线程和进程没有一堵泾渭分明的墙,大家都是可以进入运行队列的任务。

两个不同进程之间发生切换,通常需要切换地址空间。

同一个进程内的两个线程发生切换,它们往往共享 mm_struct,不需要重新切换用户页表,但寄存器、内核栈、线程局部存储、FPU 状态等线程私有内容仍然要处理。

中断上下文又是另一回事。

硬件中断到来时,CPU 暂停当前指令流,保存必要现场,转去执行中断处理程序。此时并没有天然发生进程切换,current 仍然指向被中断的任务。硬中断处理程序不能睡眠,也不能在硬中断上下文里直接进行普通任务调度。

不过,中断处理程序可以让某个任务变为可运行,也可以设置 TIF_NEED_RESCHED。等中断退出并回到允许调度的位置,内核才可能进入 schedule(),随后发生真正的任务切换。

所以,说“中断绝对不会引起进程切换”不准确。更准确的说法是,硬中断现场本身不执行普通进程切换,但中断可能创造调度条件,切换发生在中断返回路径上的合法调度点。 Linux 6.6 的 __schedule() 注释也明确说明,TIF_NEED_RESCHED 会在中断、异常和用户态返回路径被检查。

1.3 性能视角:上下文切换的开销与线上服务性能瓶颈关联

上下文切换不是无损的。

直接成本包括保存和恢复寄存器、切换栈指针、维护调度状态、处理架构相关状态。跨地址空间切换还可能涉及 CR3、PCID、ASID、TLB 代际和页表上下文处理。

间接成本更麻烦。

任务 A 刚把热点数据塞进 L1、L2 缓存,任务 B 就被调度上来。B 的工作集可能逐渐挤掉 A 的缓存行。等 A 再次运行,代码还是原来的代码,数据还是原来的数据,可 CPU 要重新从更慢的缓存层级甚至内存读取。

这种损失不会在 vmstat 的 cs 一列里直接写出来。

线上系统最难查的性能问题,往往也藏在这里。CPU 使用率看着只有 60%,负载却很高。线程数量一大坨,吞吐上不去,P99 还抖得像心电图。你以为机器没吃满,实际上 CPU 正忙着唤醒、抢占、迁移、等待锁,再把缓存弄得一地鸡毛。

我见过太多人把“多线程”理解成“多干活”。线程确实多了,干活的未必多。


二、核心基础:进程上下文与切换前置知识

2.1 什么是进程上下文?用户上下文 vs 内核上下文

所谓上下文,可以理解成让一段程序从暂停位置继续执行所需的全部状态。

CPU 不会记得“刚才那个订单线程执行到哪儿了”。它只认识寄存器、内存地址和当前特权级。一个任务被切走之后,未来想继续执行,就必须能够恢复足够多的状态。

用户上下文主要包含用户态可见的执行现场。

其中包括用户指令指针、用户栈指针、通用寄存器、标志寄存器、浮点和向量状态,以及线程局部存储相关信息。系统调用、中断或异常进入内核时,架构入口代码会把用户态现场保存到内核栈上的 pt_regs 等结构中。

内核上下文则是任务在内核态执行时的现场。

这里包括内核栈、内核调用链、需要跨调用保存的寄存器、调度状态、地址空间信息和架构私有状态。任务因为等待锁、等待 I/O 或主动调用 schedule() 而被切走时,将来必须从内核调用链的某个位置继续。

用户态与内核态切换,不等于进程上下文切换。

一次普通系统调用进入内核,处理完再返回同一个任务,期间发生了特权级转换,却可能完全没有换任务。反过来,一个任务在内核里阻塞,调度器换成另一个任务,这才是调度意义上的上下文切换。

2.2 进程核心资源:task_struct 结构体核心字段解析

task_struct 是 Linux 描述任务的核心结构。

它很大。真的很大。

第一次打开 include/linux/sched.h,很多人会产生一种错觉,Linux 内核是不是把所有东西都塞进了这个结构。这个感觉不算完全错,只是内核开发者已经尽量克制了。

下面截取一组和调度、切换关系比较紧密的字段。代码经过删减,只保留理解主线所需内容。

struct task_struct {#ifdef CONFIG_THREAD_INFO_IN_TASKstruct thread_info thread_info;#endif    unsigned int            __state;    void                    *stack;#ifdef CONFIG_SMP    int                     on_cpu;#endif    int                     on_rq;    int                     prio;    int                     static_prio;    int                     normal_prio;    unsigned int            rt_priority;struct sched_entity     se;struct sched_rt_entity  rt;struct sched_dl_entity  dl;    conststruct sched_class *sched_class;    unsigned int            policy;    const cpumask_t         *cpus_ptr;    cpumask_t               cpus_mask;struct mm_struct        *mm;struct mm_struct        *active_mm;    pid_t                   pid;    pid_t                   tgid;    unsigned long           nvcsw;    unsigned long           nivcsw;struct thread_struct    thread;};
  • • __state 表示任务当前的睡眠或运行相关状态。
  • • stack 指向任务的内核栈。
  • • on_rq 表示任务是否位于运行队列中,on_cpu 表示任务是否正在某个 CPU 上运行。这两个字段看起来差不多,语义却不能混。 一个任务可以处于可运行状态并在运行队列里等着,也可以已经被某个 CPU 选中执行。
  • • priostatic_prionormal_prio 与优先级计算有关。sched_class 指向任务所属的调度类,普通任务通常进入公平调度类,实时任务则可能进入 RT 调度类。
  • • se 是公平调度实体,里面维护虚拟运行时间、权重、运行树节点,以及 Linux 6.6 中使用的 deadline、lag 相关状态。
  • • mm 指向任务自己的用户地址空间。内核线程没有独立用户地址空间,所以 mm 通常为空。active_mm 表示 CPU 当前实际使用的内存上下文,内核线程运行时可以临时借用前一个用户任务的地址空间。
  • • nvcsw 记录自愿上下文切换次数,nivcsw 记录非自愿上下文切换次数。Linux 6.6 的 __schedule() 会根据任务是主动阻塞还是被抢占,选择更新其中一个计数。
  • • thread 是架构相关的线程状态。x86_64 下的栈指针、FS/GS、FPU、调试寄存器、PKRU 等内容都和它有关。

Linux 6.6 的源码把 thread_struct 放在 task_struct 尾部,因为 x86 的 FPU 状态可能是动态大小,源码甚至特意写了大写警告,别往后面加字段。

2.3 进程栈与内核栈机制、栈空间布局详解

每个用户线程都有自己的用户栈,也有自己的内核栈。

注意,是每个线程,不是整个进程共享一个内核栈。

用户栈位于用户地址空间,用来保存用户函数调用、局部变量、返回地址等内容。线程库创建线程时,通常会为新线程分配独立用户栈。

内核栈位于内核空间。线程通过系统调用、异常或中断进入内核后,需要一个可信、隔离且不会被用户随意篡改的栈。这个栈属于当前任务。

在 x86_64 上,可以把一个任务的内核栈粗略理解成下面的布局。具体细节会受到内核配置、入口机制和架构变化影响,但这张图足够帮助理解。

高地址┌──────────────────────────┐│ pt_regs                  ││ 用户态寄存器保存区域       │├──────────────────────────┤│ 内核函数调用栈             ││ 局部变量、返回地址等        ││                          ││ 栈向低地址方向增长          │├──────────────────────────┤│ 栈保护与架构相关区域        │└──────────────────────────┘低地址

任务在内核中调用 schedule() 时,当前函数调用链就在这个内核栈上。

switch_to() 切换任务时,一个关键动作就是把旧任务的内核栈指针保存到 prev->thread.sp,再把 next->thread.sp 加载进 %rsp

这一换可不是普通变量赋值。

换完 %rsp 后,CPU 当前所处的调用栈已经属于另一个任务。原本那个任务的局部变量、返回地址和调用链都留在它自己的栈上。新任务恢复的,是它上一次被切走时留下的调用现场。

这也是上下文切换最反直觉的地方。

函数 context_switch() 是任务 A 调进去的,switch_to() 之后继续往下执行的,却可能是曾经在这里暂停的任务 B。函数代码没变,栈换了,世界就换了。

2.4 寄存器上下文:CPU 切换的核心保存对象

CPU 执行程序时,大量临时状态都在寄存器里。

x86_64 的通用寄存器包括 RAXRBXRCXRDXRSIRDIRBPRSP 和 R8 到 R15。除此之外,还有指令指针、标志寄存器、段寄存器、控制寄存器、调试寄存器、FPU 和 SIMD 状态。

并不是所有寄存器都由 switch_to() 在同一个位置保存。

用户态寄存器通常在进入内核时保存到 pt_regs。编译器调用约定又把通用寄存器分为调用者保存和被调用者保存。执行底层任务切换时,x86_64 的 __switch_to_asm重点保存 callee-saved 寄存器,也就是跨函数调用必须保持的那一组。

Linux 6.6 的核心汇编片段:

SYM_FUNC_START(__switch_to_asm)    pushq   %rbp    pushq   %rbx    pushq   %r12    pushq   %r13    pushq   %r14    pushq   %r15    movq    %rsp, TASK_threadsp(%rdi)    movq    TASK_threadsp(%rsi), %rsp    popq    %r15    popq    %r14    popq    %r13    popq    %r12    popq    %rbx    popq    %rbp    jmp     __switch_toSYM_FUNC_END(__switch_to_asm)

这段代码先把旧任务需要保留的寄存器压入旧任务内核栈,随后保存 %rsp,加载新任务的 %rsp,再从新任务的栈里弹出寄存器。

我第一次认真看这里时也有一种感觉,就这?Linux 把一个任务换掉,就靠这么几行?

对,最核心的栈切换就是这么几行。但周边还有地址空间、FPU、TLS、FS/GS、调试状态、内存屏障、锁和统计信息。真正复杂的不是 movq 本身,而是怎样保证这一刀切下去以后,系统还能继续保持一致。

2.5 进程状态与调度触发条件(就绪、运行、阻塞状态流转)

教科书喜欢把进程状态画成运行、就绪、阻塞三个圆。

Linux 真实状态比这复杂,但这三个概念仍然适合建立基础模型。

任务处于运行状态时,可能正在 CPU 上执行,也可能位于运行队列中等待执行。Linux 的 TASK_RUNNING 同时覆盖了传统意义上的 running 和 runnable。

任务等待可中断事件时,可以进入 TASK_INTERRUPTIBLE。等待期间若收到合适信号,可以提前醒来。

任务等待不可中断事件时,可能进入 TASK_UNINTERRUPTIBLE。这类任务常见于某些 I/O 等待路径,用户在 ps 中会看到 D 状态。

停止、跟踪停止、僵尸和死亡任务还有其他状态。

从业务排查角度,不要只盯着状态字母。 一个线程处于 S 状态,可能是在正常等待网络请求,也可能在高频争抢互斥锁。看起来都在睡,含义差了十万八千里。

典型状态流转可以简化成这样:

                    被选中运行可运行队列  ─────────────────────>  CPU运行    ▲                                  │    │                                  │ 时间片、抢占    │                                  ├──────────────┐    │                                  │              │    │ 事件完成、锁释放、I/O完成          ▼              │阻塞等待  <────────────────────── 主动睡眠             │    ▲                                                 │    └─────────────────────────────────────────────────┘                      重新进入运行队列

唤醒任务不等于任务马上执行。

唤醒操作通常只是把任务放回运行队列。若它比当前任务更适合运行,调度器会设置重新调度标志,然后在最近的合法调度点进行切换。Linux 6.6 的源码注释专门强调,wake-up 本身并不直接调用 schedule()


三、上下文切换核心原理与分类

3.1 进程上下文切换的本质:CPU 执行权的转移

上下文切换的本质不是“保存一堆数据”。

真正的本质是 CPU 执行权发生了转移,而保存和恢复状态是为了让这次转移可逆。

假设任务 A 正在执行。

调度器决定让任务 B 运行后,需要完成几件事。

  • • A 的可恢复状态必须留下。
  • • B 的可恢复状态必须装入 CPU。
  • • 当前任务指针、运行队列状态和统计数据必须同步更新。
  • • 地址空间必须与 B 匹配。

切换完成后,CPU 的下一段执行流必须落入 B 上次暂停的位置,或者落入新线程的初始入口。

从宏观上看,CPU 从 A 交给了 B。

从微观上看,只是寄存器和栈指针变了,页表上下文可能变了,当前任务指针变了。CPU 不知道谁叫 A,谁叫 B。那些名字都是操作系统赋予硬件状态的意义。

3.2 两类切换场景解析

3.2.1 主动切换:进程主动放弃 CPU(sleep、wait、yield)

主动切换也叫自愿上下文切换。

任务发现自己暂时无法继续工作,于是主动进入等待状态,让调度器选择其他任务。

常见情况包括等待互斥锁、等待条件变量、等待管道数据、等待磁盘或网络 I/O、调用休眠函数,以及通过某些同步原语进入等待队列。

我们来看一个典型的内核等待模式:

for (;;) {    prepare_to_wait(&wait_queue, &wait, TASK_INTERRUPTIBLE);    if (condition_ready())        break;    schedule();}finish_wait(&wait_queue, &wait);

任务先设置状态并进入等待队列,条件不满足时调用 schedule()

yield 也属于主动让出 CPU,但别把它当万能优化按钮。 对于普通的 SCHED_OTHER 任务,滥用 sched_yield() 往往说明应用调度设计出了问题。Linux 手册对这件事说得挺不客气,基本可以翻译成“你大概率写歪了”。 (man7.org)

不过主动切换不一定是坏事。

高并发网络服务本来就会频繁等待 socket、epoll、futex 和队列。真正需要判断的是,每次睡眠是否有必要,等待时间是否合理,唤醒是否精准,以及有没有几十个线程围着同一把锁蹦迪。

3.2.2 被动切换:时间片耗尽、高优先级进程抢占

被动切换也叫非自愿上下文切换。

当前任务仍然想运行,但调度器决定暂时不让它继续。

普通任务可能因为运行时间已经不占优势,被公平调度器换下。高优先级实时任务变为可运行时,也可能立刻抢占普通任务。多核系统中的负载均衡和任务迁移同样会影响实际切换行为。

Linux 通过 TIF_NEED_RESCHED 等机制表达重新调度需求。

定时器 tick 可以更新调度时间并设置重新调度标志。新任务被唤醒后,若调度类判断它应该抢占当前任务,也会设置该标志。到达合法调度点后,内核进入调度流程。

pidstat -w 中的 nvcswch/s 可以观察每秒非自愿上下文切换次数。手册将其描述为任务被迫让出处理器的次数。 (man7.org)

3.3 进程切换 vs 线程切换:资源共享差异与开销区别

进程和线程的切换主干流程很接近,因为 Linux 调度的都是任务。

差异主要来自共享资源。

同一进程内的线程通常共享用户地址空间、代码段、堆、共享库映射和大部分文件资源。线程切换时,prev->mm 与 next->mm 往往相同,内核可以避免真正切换到另一套用户页表。

跨进程切换时,mm 通常不同,需要执行地址空间切换逻辑。x86 下会进入 switch_mm_irqs_off(),处理 CR3、ASID 或 PCID、TLB 代际、LDT 和其他内存上下文状态。Linux 6.6 的 context_switch() 也明确区分了用户任务与内核线程之间的四种组合。

但线程切换并不等于“几乎没成本”。

  • • 寄存器和内核栈还是要切。
  • • 两个线程可能访问完全不同的数据集,照样会互相污染缓存。
  • • 线程迁移到另一个 CPU 后,数据可能需要经过缓存一致性协议重新获取。
  • • TLS、FPU 和调试状态也可能不同。

所以,进程切换通常比同地址空间线程切换更重,但不能张嘴就说固定重两倍、五倍或十倍。 具体差距取决于硬件、PCID 配置、安全缓解措施、工作集大小、NUMA 拓扑和任务是否迁核。

一个线程在 64 个核心之间乱窜,缓存代价完全可能比两个固定在同一核心、工作集很小的进程切换更难看。

3.4 上下文切换 vs 中断上下文切换(无进程切换、无调度)

硬中断发生时,CPU 会保存一部分现场,切换到中断入口执行。

这确实是一种执行上下文变化,但它和调度器进行任务切换不是一回事。

处理中断时,current 仍然代表被中断的任务。中断处理程序使用的是中断相关入口现场,在部分架构和配置下还会使用专门的 IRQ 栈。

硬中断上下文不能调用可能睡眠的函数。

原因并不玄学。睡眠意味着当前执行实体要进入等待状态,并由调度器选择另一个任务。可硬中断不是一个普通的可调度任务,没有独立的用户进程身份,也没有可以像普通任务一样挂进等待队列后再恢复的调度语义。

硬中断可以唤醒线程,可以触发软中断,也可以设置重新调度请求。

等退出硬中断、恢复到可抢占任务上下文后,调度器才有机会切换任务。PREEMPT_RT 会把大量中断处理线程化,让相关工作在可调度线程上下文运行,但仍有少数不能线程化的中断例外。 


四、Linux 内核进程上下文切换完整流程(全景拆解)

4.1 调度入口:schedule() 函数核心作用

很多阻塞路径最终都会进入 schedule()

但 schedule() 不是一个孤零零的入口。内核还有 preempt_schedule()schedule_preempt_disabled()schedule_idle() 等不同入口,它们针对不同调用环境处理抢占计数和调度模式。

普通 schedule() 的逻辑可以粗略概括成下面这样。

void __sched schedule(void){struct task_struct *tsk = current;    sched_submit_work(tsk);    do {        preempt_disable();        __schedule(SM_NONE);        sched_preempt_enable_no_resched();    } while (need_resched());    sched_update_worker(tsk);}

真正的核心在 __schedule()

外层需要处理待提交工作、抢占状态以及调度结束后是否又产生了新的 need_resched。这也是为什么你看内核源码时,不能只搜一个函数名就宣布“调度器我懂了”。

我年轻时干过这种事。

看了 schedule() 二十几行,合上电脑,自信满满。第二天继续看调用链,发现昨天的自信主要来自无知。挺好的,程序员的成长有时就是稳定地发现自己不太行。

4.2 进程选择:CFS 调度器挑选下一个运行进程

在 Linux 5.15 一类经典 CFS 实现中,公平调度器使用红黑树维护可运行调度实体。实体按 vruntime 排序,虚拟运行时间较小的任务更接近红黑树左侧。

经典选取逻辑会从最左实体开始,再结合 buddy 机制照顾唤醒延迟和缓存局部性。

static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr){struct sched_entity *left = __pick_first_entity(cfs_rq);struct sched_entity *se;    if (!left || (curr && entity_before(curr, left)))        left = curr;    se = left;    /* 省略 skip、next、last buddy 处理 */    return se;}

红黑树最左侧并不意味着“运行时间最短”,而是加权后的虚拟运行时间更小,意味着它获得的公平 CPU 份额相对不足。

到了 Linux 6.6,情况变了。

公平调度类仍然保留 vruntime、红黑树和 CFS 的大量基础设施,但选取逻辑开始转向 EEVDF。任务需要满足 eligibility 条件,然后调度器倾向选择虚拟截止时间更早的实体。

Linux 6.6 的主线代码中可以直接看到 pick_eevdf()

static struct sched_entity *pick_next_entity(        struct cfs_rq *cfs_rq,        struct sched_entity *curr){    return pick_eevdf(cfs_rq);}

所以,面试时回答“CFS 永远选择 vruntime 最小的进程”已经有点危险。

在 Linux 5.x 语境下,这是便于理解的近似。

在 Linux 6.6 及后续内核里,更靠谱的回答是:公平调度仍以虚拟运行时间和权重建模公平性,但任务选择已经引入 EEVDF 的 lag、eligibility 和虚拟截止时间机制。

4.3 上下文切换核心步骤

4.3.1 保存当前进程寄存器上下文与栈信息

任务进入 __schedule() 后,调度器先获得当前 CPU 的运行队列和当前任务。

cpu  = smp_processor_id();rq   = cpu_rq(cpu);prev = rq->curr;

选出 next 后,如果 prev != next,内核进入 context_switch()

真正的寄存器和栈切换发生在架构相关的 switch_to() 中。x86_64 会把需要跨调用保存的寄存器压入旧内核栈,将旧 %rsp 写入 prev->thread.sp,再加载 next->thread.sp

任务的用户态寄存器并不是此时才全部保存。它们通常早已在系统调用、异常或中断入口处进入 pt_regs

把这些保存点分开,是性能和调用约定共同作用的结果。不是所有寄存器都需要在每一层重复保存,不然 CPU 真要被搬家式切换折腾死。

4.3.2 更新进程状态、调度统计信息

__schedule() 会读取 prev->__state

如果当前不是抢占模式,且旧任务已经设置了睡眠状态,内核会判断是否存在待处理信号。没有合适信号时,任务会从运行队列移除。

prev_state = READ_ONCE(prev->__state);if (!(sched_mode & SM_MASK_PREEMPT) && prev_state) {    if (signal_pending_state(prev_state, prev)) {        WRITE_ONCE(prev->__state, TASK_RUNNING);    } else {        deactivate_task(rq, prev,                        DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);    }    switch_count = &prev->nvcsw;}

默认情况下,switch_count 先指向 nivcsw。主动睡眠时改为 nvcsw

这就是用户空间看到自愿与非自愿切换统计的源头之一。

接着,调度器选择下一个任务,清除旧任务的重新调度标志,更新 rq->curr、切换总数、压力统计和 tracepoint。

next = pick_next_task(rq, prev, &rf);clear_tsk_need_resched(prev);clear_preempt_need_resched();if (likely(prev != next)) {    rq->nr_switches++;    RCU_INIT_POINTER(rq->curr, next);    ++*switch_count;    trace_sched_switch(...);    rq = context_switch(rq, prev, next, &rf);}

trace_sched_switch 很重要。perf sched、ftrace、BPF 工具和大量调度分析能力,都围绕类似的调度事件建立。

4.3.3 加载新进程的寄存器、栈、地址空间

context_switch() 主要分成两个大块。

一个是地址空间。

另一个是寄存器和内核栈。

static __always_inline struct rq *context_switch(struct rq *rq,               struct task_struct *prev,               struct task_struct *next,               struct rq_flags *rf){    prepare_task_switch(rq, prev, next);    arch_start_context_switch(prev);    if (!next->mm) {        enter_lazy_tlb(prev->active_mm, next);        next->active_mm = prev->active_mm;    } else {        switch_mm_irqs_off(prev->active_mm, next->mm, next);    }    prepare_lock_switch(rq, next, rf);    switch_to(prev, next, prev);    barrier();    return finish_task_switch(prev);}
  • • 用户任务切换到另一个用户任务时,会处理新的 mm
  • • 切换到内核线程时,内核线程没有自己的用户地址空间,于是进入 lazy TLB 模式并借用 active_mm
  • • 然后 switch_to() 换寄存器和栈。

注意 switch_to(prev, next, prev) 这个写法。第三个参数仍然叫 prev,看起来像故意为难后来人。它表示切换回来以后,返回“上一次从谁那里切换过来”。

这里需要一点脑内栈模拟。别急着背。背下来也会忘,想明白才有用。

4.3.4 刷新 CPU 缓存、TLB,完成执行权切换

上下文切换并不会例行把 CPU 缓存全部刷新。

L1、L2、L3 缓存通常继续保留原有内容。硬件缓存以地址和一致性协议管理数据,不会因为 current 换了就主动把缓存倒进垃圾桶。

真正的问题是新任务运行后会加载自己的代码和数据,逐步挤占旧任务的缓存工作集。这属于间接污染,不是内核执行了一条“清空缓存”命令。

TLB 也不一定全量失效。

早期或缺少地址空间标识能力的系统,在切换页表时更容易产生广泛 TLB 失效。现代 x86 可以利用 PCID,ARM 等架构可以利用 ASID,为不同地址空间的 TLB 项打标签,从而避免每次切换都清空全部翻译缓存。

Linux 6.6 的 x86 switch_mm_irqs_off() 会读取当前加载的 mm、ASID 和 TLB generation,再决定是否需要刷新以及使用哪个地址空间标识。它绝不是简单粗暴地每次重载 CR3 然后全冲掉。

把上下文切换描述成“刷新 CPU 缓存和 TLB”太糙。

更准确的说法是,地址空间切换可能需要更新页表上下文并按需处理 TLB,而 CPU 缓存通常不会被显式清空,但任务切换会破坏缓存局部性。

4.4 切换收尾:进程重新执行的指令点位解析

上下文切换结束后,finish_task_switch() 负责完成收尾工作。

它会处理旧任务状态、性能事件、tick、架构钩子、运行队列锁,以及可能延迟释放的 mm

最有意思的是执行位置:

任务 A 调用 schedule()

A 在 switch_to() 中被换下。

任务 B 恢复自己的内核栈,从它上一次暂停的 switch_to() 之后继续执行。

B 随后进入 finish_task_switch(),退出调度器,回到 B 当初调用 schedule() 的位置。

等未来 A 被选中,A 也会从自己的 switch_to() 后继续。

这就像两个程序员共用一张办公桌,每个人离开前都把桌面原样封存。下一位坐下时,不是从办公室门口重新走流程,而是直接回到自己上次写到一半的那行代码。

新创建的线程没有“上一次暂停点”。

内核会提前构造它的初始栈帧,让第一次切换进去时落到 ret_from_fork_asm 和 ret_from_fork,随后进入内核线程函数或返回用户态。Linux 6.6 的汇编源码中直接写着,新 fork 的任务第一次上下文切换会进入这个地址。


五、关键源码深度解析

5.1 核心切换函数:__schedule() 源码流程拆解

把 __schedule() 压缩成一条主线,可以得到下面这张流程图:

读取当前 CPU 与运行队列          │          ▼关闭本地中断并锁定 rq          │          ▼更新运行队列时钟          │          ▼判断 prev 是否主动睡眠          │          ├── 是 ──> 从运行队列移除          │          ▼pick_next_task()          │          ▼清理 need_resched          │          ▼prev == next ?    │             │    │ 是          │ 否    ▼             ▼解锁返回     更新 rq->curr 与统计                  │                  ▼             context_switch()
  • • 显式阻塞会进入调度器。
  • • 定时器或唤醒路径可以设置 TIF_NEED_RESCHED
  • • 可抢占内核会在外层 preempt_enable()、中断返回等位置寻找机会。
  • • 不可抢占内核则更多依赖显式 schedule()cond_resched() 或返回用户态的路径。

这里纠正一个常见的错误点。

唤醒高优先级任务时,并不是唤醒函数直接把 CPU 切走。它通常先把任务入队,判断是否需要抢占,设置标志。真正切换仍然要到允许调度的位置。

调度器非常在意锁、抢占计数和中断状态。不是因为开发者保守,而是因为运行队列属于每 CPU 的核心共享状态。选任务选到一半,被另一个 CPU 或中断路径改了队列,后果不会只是偶发慢请求,可能直接变成内核一致性灾难。

5.2 架构相关核心函数:switch_to 汇编级实现

5.2.1 x86_64 架构 switch_to 汇编代码逐行解析

x86_64 的 switch_to 宏最终调用 __switch_to_asm()

#define switch_to(prev, next, last)           \do {                                          \    ((last) = __switch_to_asm((prev), (next))); \} while (0)

汇编入口约定 %rdi 保存 prev%rsi 保存 next

pushq %rbppushq %rbxpushq %r12pushq %r13pushq %r14pushq %r15

这一段把 callee-saved 寄存器压到旧任务内核栈。

movq %rsp, TASK_threadsp(%rdi)

把旧任务栈指针保存到 prev->thread.sp

movq TASK_threadsp(%rsi), %rsp

加载新任务的栈指针。

执行完这条指令后,CPU 已经站在新任务的内核栈上。 后面的 popq 读取的是新任务之前保存的寄存器。

popq %r15popq %r14popq %r13popq %r12popq %rbxpopq %rbp

随后跳入 C 函数 __switch_to()

jmp __switch_to

这里用 jmp 而不是普通 call,和当前栈帧布局及返回语义有关。__switch_to() 返回时,会沿新任务栈中原有的返回地址继续。

这几行代码真的有点东西。

它没有执行“恢复指令指针”的显式指令,却通过切换栈和恢复返回地址,让控制流自然回到新任务原来的位置。调度器不是拿着地图寻找暂停点,暂停点本来就在那个任务的栈里。

5.2.2 通用架构切换逻辑与架构差异化

调度核心尽量保持架构无关。

它负责运行队列、任务状态、调度类选择、统计和切换时序。

真正涉及寄存器、页表控制寄存器、TLS、异常入口和 FPU 的工作,则交给架构代码。

x86_64 通过 %rsp、FS/GS、CR3、PCID 和 xsave 体系完成相关工作。

ARM64 使用自己的栈指针、页表基址寄存器和 ASID 机制。

RISC-V、PowerPC 等架构也有各自的寄存器保存集合和 MMU 上下文切换方式。

这也是 switch_to() 必须作为架构接口存在的原因。CFS 或 EEVDF 可以跨架构共用,可 CPU 寄存器长什么样,编译器说了不算,硬件说了算。

5.3 进程地址空间切换:mm_switch、pgd 页表切换逻辑

不同内核版本和架构中,函数名称与封装层次可能不同。调度核心常见入口是 switch_mm_irqs_off(),更底层再处理 PGD、CR3、ASID 或 PCID。

mm_struct 描述一个用户地址空间。

pgd 指向顶级页表。

当两个用户进程拥有不同 mm 时,CPU 需要转入新地址空间对应的页表上下文。x86 最终会围绕 CR3 和 PCID 处理,其他架构则使用相应的页表基址寄存器和地址空间标识。

同进程线程通常共享 mm

此时不需要切换到另一套用户页表,TLB 中与该地址空间相关的翻译也更有机会继续使用。这是线程切换常见的直接优势。

内核线程的 mm 为空。

它不访问普通用户地址空间,所以可以借用前一个任务的 active_mm。这种 lazy TLB 设计避免了为了内核线程专门切换到无意义的用户页表上下文。

但“借用”不代表内核线程可以随便访问前一个进程的用户内存。mmactive_mm 和用户访问语义是不同层面的概念,别看到同一个页表就开始放飞自我。

5.4 上下文保存与恢复:thread_struct 结构体作用

thread_struct 保存架构相关的线程私有状态。

x86_64 中常见字段包括下面这些。

struct thread_struct {struct desc_struct tls_array[GDT_ENTRY_TLS_ENTRIES];    unsigned long sp;    unsigned short es;    unsigned short ds;    unsigned short fsindex;    unsigned short gsindex;    unsigned long fsbase;    unsigned long gsbase;struct perf_event *ptrace_bps[HBP_NUM];    unsigned long virtual_dr6;    unsigned long ptrace_dr7;    unsigned long cr2;    unsigned long trap_nr;    unsigned long error_code;struct io_bitmap *io_bitmap;    u32 pkru;struct fpu fpu;};
  • • sp 保存任务不运行时的内核栈指针。
  • • FS/GS 相关字段与线程局部存储和段状态有关。
  • • 调试寄存器状态用于断点、单步和 ptrace。
  • • PKRU 与内存保护键有关。
  • • FPU 结构保存浮点、SSE、AVX 等扩展状态。

现代 CPU 的扩展寄存器状态越来越大。如果每次上下文切换都不加区分地搬运全部数据,代价会很难看。因此内核会利用硬件能力和延迟恢复策略,尽量避免不必要的保存恢复。

Linux 6.6 的 x86 __switch_to() 会处理 FPU、FS/GS、TLS、PKRU、当前任务指针、栈顶和额外线程状态。它不是单纯换一个 %rsp 就拍拍屁股走人。

5.5 线程独有切换逻辑:轻量级进程切换优化点

Linux 线程通常通过 clone() 创建,共享 CLONE_VM、文件表、信号处理等资源。

同一线程组内任务的 tgid 相同,具体线程 ID,也就是 pid 字段,可以不同。

线程切换的主要优化来自共享 mm

不需要切换用户页表上下文,TLB 更容易保留,地址空间相关安全与同步工作也可能减少。

共享文件表并不会直接让 switch_to() 更快,因为文件表不是每次切换都重新加载进 CPU 的硬件状态。这个区别要说清楚。很多文章把“线程共享文件描述符”直接列为线程切换更快的原因,逻辑中间少了好几层。

真正直接影响切换路径的是地址空间和线程私有架构状态。

另外,同一服务的工作线程若处理相似代码和数据,指令缓存与共享只读数据也可能具有更好的局部性。

但线程太多后,优势会被竞争吃掉。

共享地址空间也意味着更容易共享锁、队列和缓存行。几十个线程更新同一个原子计数器,地址空间确实没换,缓存一致性流量却已经把性能按在地上摩擦了。


六、上下文切换的开销详解与底层成因

6.1 直接开销:寄存器读写、栈数据保存恢复、页表切换

直接开销是切换路径本身消耗的 CPU 周期。

其中包括调度器函数调用、运行队列加锁、时钟更新、任务选择、统计处理、寄存器压栈和出栈、栈指针切换,以及架构私有状态处理。

跨地址空间时,还要处理 mm 切换。

有些资料喜欢给出一个固定数字,比如“一次上下文切换需要 1 微秒”。

这种说法方便传播,不方便负责。

任务切换耗时会受到 CPU 代际、内核版本、编译配置、安全缓解措施、虚拟化、PCID、FPU 状态、调度类和测量方法影响。只测一对管道 ping-pong 线程,得到的是特定条件下的往返切换成本,不是所有线上负载的通用常数。

想知道自己的机器是多少,最好自己测。

下面是一个简单的线程 ping-pong 思路,两个线程通过条件变量交替唤醒,计算往返时间。

#include <pthread.h>#include <stdint.h>#include <stdio.h>#include <time.h>static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;static int turn = 0;static const int loops = 1000000;static uint64_t now_ns(void){struct timespec ts;    clock_gettime(CLOCK_MONOTONIC_RAW, &ts);    return (uint64_t)ts.tv_sec * 1000000000ULL + ts.tv_nsec;}static void *worker(void *arg){    for (int i = 0; i < loops; ++i) {        pthread_mutex_lock(&lock);        while (turn != 1)            pthread_cond_wait(&cond, &lock);        turn = 0;        pthread_cond_signal(&cond);        pthread_mutex_unlock(&lock);    }    return NULL;}int main(void){    pthread_t tid;    pthread_create(&tid, NULL, worker, NULL);    uint64_t begin = now_ns();    for (int i = 0; i < loops; ++i) {        pthread_mutex_lock(&lock);        while (turn != 0)            pthread_cond_wait(&cond, &lock);        turn = 1;        pthread_cond_signal(&cond);        pthread_mutex_unlock(&lock);    }    pthread_join(tid, NULL);    uint64_t elapsed = now_ns() - begin;    printf("round trip %.2f ns\n",           (double)elapsed / loops);    return 0;}

这个程序测到的不只有内核切换,还包括锁、条件变量、唤醒和用户代码成本。想进一步分离,需要使用更严格的绑核、预热、统计和基线扣除方案。

6.2 间接开销:TLB 失效、CPU 缓存命中率下降

直接切换可能只需要很短时间,间接成本却可能持续很久。

任务重新运行后,如果热点指令和数据已经被挤出缓存,就会出现更多 L1、L2、LLC miss。

地址空间切换若导致部分 TLB 项不可复用,后续访存需要重新进行页表遍历。页表遍历本身也会访问缓存和内存,进一步增加延迟。

任务迁移到不同 CPU 更麻烦。

原 CPU 私有缓存中的数据无法直接在新 CPU 的 L1 中使用。共享数据还可能涉及缓存行所有权转移。若跨 NUMA 节点,内存访问距离也可能变化。

因此,减少上下文切换只是一个方向。减少任务迁移、保持工作集局部性、避免共享缓存行抖动,有时比单纯把 cs 数字压低更重要。

6.3 软开销:调度队列遍历、进程状态计算、统计更新

调度器还要做大量“管理工作”。

  • • 运行队列时钟需要更新。
  • • 调度实体的运行时间、vruntime、lag 或 deadline 需要维护。
  • • 调度类需要选择任务。
  • • 压力信息、perf 事件、RCU、rseq 和 tracepoint 需要处理。
  • • 多核系统还涉及负载均衡、CPU 容量、NUMA、大小核差异、CPU 隔离和亲和性。

现代公平调度器的单次选取并不是简单遍历所有进程,但复杂的层级调度、cgroup 和多核平衡仍然会增加软成本。

尤其在超大核心数机器上,调度域和负载平衡设计很重要。内核的 cpuset 文档明确提到,负载平衡的算法成本和对共享内核数据结构的影响,会随着参与平衡的 CPU 数量增加而增长。 (内核文档)

6.4 进程切换与线程切换的开销量化对比

这里不给大家一个很精确的固定倍率。

成本项目
同进程线程切换
跨进程切换
通用寄存器与内核栈
需要
需要
调度器状态更新
需要
需要
FPU、TLS 等线程状态
可能需要
可能需要
用户页表上下文
通常相同
通常不同
TLB 地址空间复用
更有利
取决于 PCID、ASID 等机制
缓存工作集影响
取决于线程工作集
取决于进程工作集
NUMA 与迁核代价
可能很高
可能很高

在同 CPU、同 mm、工作集相近的情况下,线程切换通常更轻。

在任务跨核、工作集巨大或共享数据竞争严重时,线程与进程之间的理论差距可能被缓存和同步成本淹没。

所以排查性能时,不要问“线程切换是不是一定比进程快很多”。

应该问的是,这两个任务是否共享 mm,是否在同一 CPU,工作集是否重叠,是否频繁迁移,是否争用共享数据,以及切换发生后产生了多少 cache miss。

问题问对了,答案才有意义。


七、线上环境上下文切换性能问题实战分析

7.1 核心观测工具:top、vmstat、pidstat、perf 使用指南

先看系统整体。

vmstat 1

重点关注 rbincsussyid 和 wa

cs 表示每秒上下文切换次数,in 表示每秒中断数量。r 表示可运行任务数量,b 表示阻塞等待 I/O 的任务数量。 (man7.org)

但只看数字绝对值没意义。

一台 128 核机器每秒 50 万次切换,可能比一台 2 核机器每秒 10 万次更健康。至少要结合 CPU 核数、吞吐、运行队列长度和延迟看。

再看具体进程。

pidstat -w 1

加入线程维度。

pidstat -w -t -p <PID> 1

其中 cswch/s 是自愿切换速率,nvcswch/s 是非自愿切换速率。

也可以直接读取进程状态。

grep -E 'voluntary|nonvoluntary' /proc/<PID>/status

输出类似这样。

voluntary_ctxt_switches:        183920nonvoluntary_ctxt_switches:       4281

/proc/<PID>/status 从 Linux 2.6.23 起提供这两类计数。 (man7.org)

想看具体事件和总切换数量,可以用 perf stat

perf stat \  -e context-switches,cpu-migrations,cycles,instructions,\cache-references,cache-misses \  -p <PID> \  sleep 10

想看任务什么时候被唤醒、什么时候真正运行,用 perf sched

perf sched record -a -- sleep 10perf sched latencyperf sched timehist

perf sched latency 适合看任务调度延迟,timehist 适合看时间轴、等待时间和运行时间。这个命令有点东西,遇到线程唤醒后迟迟跑不起来的情况,比盯着 top 猜强多了。

7.2 高上下文切换的典型业务场景

线程数量远高于 CPU 核心数,是最常见场景。

例如 16 核机器开 1000 个活跃计算线程。每个线程都觉得自己应该运行,调度器只能不停轮换。

锁竞争也很典型。

大量线程争抢同一把 mutex,拿不到锁的线程睡眠,锁释放后又被唤醒。若唤醒多个等待者却只有一个能拿锁,就会形成惊群。大家刚醒就再次睡,CPU 忙得很,业务没前进多少。

短任务加频繁队列传递也会制造切换。

请求从网络线程交给业务线程,再交给 RPC 线程、序列化线程、日志线程。一个请求像在机关单位盖章,代码看起来分层优雅,CPU 看起来像在跑接力赛。

阻塞式 I/O 配合过大线程池,同样容易产生高切换。

还有一种很隐蔽。

某个监控任务每隔极短时间轮询一次,调用 sleep 或定时等待,醒来检查一个几乎不变化的状态,然后继续睡。单个线程不起眼,几十个组件都这么干,系统就开始有节奏地抽搐。

7.3 高频问题排查

7.3.1 自愿切换过高:业务阻塞、锁竞争问题定位

自愿切换高,说明任务频繁主动阻塞。

先别骂调度器。

调度器只是接住了业务代码递过来的辞职信。

排查可以从系统调用开始。

strace -f -c -p <PID>

重点观察 futexepoll_waitpollreadrecvfromnanosleep 等调用。

epoll_wait 多不一定有问题,事件驱动服务本来就要等待事件。

futex 调用很多也不能直接判定锁竞争严重,因为用户态锁在无竞争时可能不进入内核。真正需要结合等待时长、线程栈和吞吐判断。

可以使用 perf 观察调用栈。

perf record -g -p <PID> -- sleep 30perf report

也可以采集调度事件。

perf sched record -p <PID> -- sleep 10perf sched timehist -p <PID>

下面讲一个综合案例。

这个故事是我把几类常见线上事故揉在一起的。

某服务在促销期间 CPU 使用率只有 55%,接口 P99 却从 40 毫秒涨到 600 毫秒。

vmstat 看到 cs 很高。

pidstat -w -t 显示一批工作线程的自愿切换暴涨。

线程栈里大量时间落在连接池条件变量和一个全局限流锁上。

根因不是 CPU 不够,也不是 Linux 调度器突然发疯。连接池只有几十个连接,业务线程却开了几百个。大量线程拿不到连接后睡眠,连接归还时又发生密集唤醒。醒来的线程还要争全局限流锁。

后来没有做什么内核优化。

工作线程数降下来,连接池容量与下游承载能力重新匹配,全局锁拆成分片状态,P99 就下来了。

但性能优化大部分时候就是把设计中不合理的并发还掉,不是把汇编写得更花。

7.3.2 非自愿切换过高:CPU 资源抢占、线程过载

非自愿切换高,说明任务经常在仍想运行时被调度器换下。

常见原因包括 CPU 过载、可运行线程太多、实时任务抢占、容器 CPU 配额、任务频繁迁移,以及运行时间分配过于碎片化。

先看运行队列。

vmstat 1uptime

r 长期显著高于可用 CPU 数量,通常意味着可运行任务在排队。

再看线程数量和 CPU 分布。

ps -eLo pid,tid,psr,pcpu,stat,comm \  --sort=-pcpu | head -50

查看调度策略。

ps -eLo pid,tid,cls,rtprio,pri,ni,psr,pcpu,comm

检查 CPU 亲和性。

taskset -pc <PID>

检查容器或 cgroup CPU 限制也很重要。

应用看到宿主机还有空闲 CPU,不代表自己所在的 cgroup 还能用。配额耗尽后,任务可能被 throttling。此时团队很容易对着整机 CPU 图吵半天,完全没看到容器头上的天花板。

7.4 性能优化通用方案:线程池调优、锁优化、调度策略调整

线程池不要按“越多越安全”设置。

CPU 密集型任务的活跃线程数通常应靠近可用 CPU 数量,再根据阻塞比例小幅调整。I/O 密集型任务可以多一些,但上限应由下游容量、内存、连接数和尾延迟共同决定。

锁优化不要只想着换一把“更快的锁”。

先减少共享状态。

能分片就分片,能线程私有就线程私有,能批量更新就别每条请求都抢一次。

缩短临界区也很重要。锁里面不要做网络调用、磁盘 I/O、复杂日志格式化和不可控回调。

避免无效唤醒。

条件变量和队列要尽量唤醒真正有机会工作的线程。无脑广播唤醒,看起来豪迈,实际上像半夜拉响全楼警报,只为了叫一个人下楼取外卖。

CPU 亲和性要谨慎使用。

对延迟敏感且工作集稳定的线程,合理绑核可能改善缓存局部性。可绑得太死,又可能导致某些 CPU 过载、其他 CPU 空闲。

内核工作队列本身也在持续加强基于 LLC、NUMA 和 CPU 范围的亲和性策略,用来兼顾缓存局部性与并发管理。 (内核文档)

调度策略别乱改。

把业务线程改成 SCHED_FIFO 并不是性能优化捷径。实时线程若不阻塞、不让出 CPU,可能长期压制普通任务。监控、SSH、日志甚至关键系统线程都可能受到影响。

这不是“真的夯爆了”,这是很可能把机器夯死了。


八、进阶:Linux 上下文切换高级特性

8.1 调度策略对切换的影响:SCHED_OTHER、SCHED_FIFO、SCHED_RR

SCHED_OTHER 是普通任务常用的默认策略,对应公平调度类。

它通过动态公平机制分配 CPU,nice 值会影响任务权重。

SCHED_FIFO 是实时先进先出策略。

同优先级下,没有时间片轮转。任务会一直运行,直到主动阻塞、主动让出 CPU,或者被更高优先级实时任务抢占。

SCHED_RR 在 SCHED_FIFO 基础上增加了同优先级任务的时间片轮转。任务用完时间片后,会被放到同优先级队列尾部。

Linux 的实时优先级通常为 1 到 99,数值越大优先级越高。实时任务会优先于普通公平调度任务。 (man7.org)

策略不同,会改变切换触发方式。

  • • SCHED_FIFO 同优先级任务之间不会因为普通时间片耗尽自动轮转。
  • • SCHED_RR 会按时间片发生同优先级轮转。
  • • SCHED_OTHER 则依据公平调度模型决定任务是否继续运行。

实时策略不是为了让程序“跑得更快”,而是为了控制调度确定性和优先级。 吞吐程序乱上实时策略,经常像给送外卖的电动车装火箭发动机。听着猛,刹车系统完全没准备。

8.2 内核抢占机制与上下文切换的触发时机优化

Linux 内核可以配置不同抢占模型。

  • • 非抢占内核中,正在内核态运行的普通代码不会随时被另一个普通任务抢占。调度更多发生在显式调度点、返回用户态或特定检查位置。
  • • 自愿抢占模型会在长内核路径中加入 cond_resched() 等主动让出点。
  • • 可抢占内核允许在满足条件时,于更广泛的内核执行位置触发任务抢占。
  • • PREEMPT_RT 则进一步减少不可抢占区域,把大量中断和锁转换为更适合实时调度的形式,目标是降低最坏调度延迟,而不只是追求平均吞吐。 (内核文档)

可抢占内核的交互响应速度通常更好,但切换机会也可能增加。

服务器吞吐、桌面交互和硬实时系统,对调度延迟的目标不同。不存在一套配置统治所有场景。

8.3 实时进程与普通进程切换优先级差异

高优先级实时任务变为可运行后,可以抢占普通任务。

普通任务的 nice 值再高,也不会越过实时调度类的优先级边界。

实时任务之间先比较实时优先级,再根据 FIFO 或 RR 规则安排。

但这会带来一个风险:

高优先级实时线程持有某把锁,低优先级线程也需要这把锁,或者反过来,低优先级线程持锁后被中优先级线程压制,高优先级线程又在等低优先级线程释放锁。

这就是优先级反转的土壤。

内核和用户态实时同步机制可以利用优先级继承缓解问题,让持锁的低优先级任务临时获得更高优先级,尽快释放资源。

实时系统分析上下文切换时,不能只看次数。

还要看从唤醒到运行的最坏延迟、不可抢占区长度、中断线程优先级、锁依赖和 CPU 隔离。

平均值很漂亮,偶尔卡 20 毫秒,对音视频可能只是抖一下,对工业控制可能就是事故了。

8.4 新版内核切换优化:调度时延优化、缓存亲和性调度

Linux 6.6 的公平调度开始转向 EEVDF。

EEVDF 会利用任务 lag 判断其是否有资格运行,再根据虚拟截止时间选择任务。这种方式希望在保持公平性的同时,更好地照顾延迟敏感任务和不同 slice 请求。 (内核文档)

多核调度也越来越关注拓扑。

SMT 线程、物理核心、共享 LLC、NUMA 节点和异构大小核都有不同迁移成本。

把任务从一个 CPU 移到另一个 CPU,不能只问“那边是不是空闲”。还要考虑缓存热度、CPU 容量、能耗和唤醒关系。

新版内核会记录任务最近使用 CPU、唤醒关系和调度域信息,尽量把任务放在既能及时运行、又不至于完全丢失局部性的位置。

这不是完美算法。

调度器面对的是动态负载,信息永远有延迟。任务下一秒要访问什么数据,内核也没有水晶球。

所以应用仍然要做自己的并发设计。不要指望调度器替你修复 2000 个线程争 8 个核心的架构。调度器按规距办事,不负责创造算力。


九、常见面试核心问题

9.1 进程切换为什么比线程切换开销大?

Linux 调度器切换进程和线程时,都要切换 task_struct 对应的执行上下文,包括内核栈、必要寄存器和调度状态。

同进程线程通常共享 mm_struct,不需要切换到不同用户地址空间。因此可以减少页表上下文切换及相关 TLB 处理,更有利于地址翻译缓存复用。

不同进程通常拥有不同 mm,需要执行 switch_mm 相关逻辑。

但线程切换也可能产生严重缓存污染、CPU 迁移和同步成本,因此不能笼统地认为线程切换始终便宜很多。

一句适合面试现场的回答:

进程和线程的核心寄存器、栈切换流程类似,主要差异来自地址空间。线程共享 mm 时通常可以避免真正的页表上下文切换,因此直接成本更低,但缓存和迁核代价仍取决于实际工作集。

9.2 中断上下文为什么不能发生进程切换?

硬中断不是普通可调度任务。

它没有独立的任务睡眠与恢复语义,也不能像进程一样进入等待队列。硬中断处理中还处于特殊中断状态,部分锁、中断屏蔽和抢占计数条件不允许普通调度。

因此,硬中断处理程序不能调用会睡眠的函数,也不能直接进行普通任务调度。

不过,中断可以唤醒任务或设置 need_resched。等中断退出,恢复到合法调度点后,内核可以调用调度器并切换任务。

面试时只回答“中断不能切换进程”有点粗糙。把“中断退出后可能触发调度”补上,味道就对了。

9.3 switch_to 为什么需要汇编实现?C 语言能否完成切换?

C 语言无法完整、可靠地表达底层上下文切换所需操作。

编译器会自行分配寄存器、生成函数序言和结尾,并假设当前栈在函数执行期间遵守正常调用约定。

上下文切换却需要精确控制哪些寄存器何时压栈,什么时候保存旧 %rsp,什么时候加载新 %rsp,以及切换栈以后怎样继续执行。

栈指针一旦改变,普通 C 函数对局部变量和返回地址的假设就可能失效。

因此,最底层的栈与寄存器切换必须使用汇编或编译器明确支持的架构机制。

切换前后的高层逻辑可以用 C 实现,Linux 也正是这么做的。__schedule()context_switch() 和 __switch_to() 大量使用 C,真正无法交给编译器自由发挥的部分才落到汇编。

9.4 高并发场景如何避免无效上下文切换?

  1. 1. 控制活跃线程数量。线程总数可以大于 CPU 核数,但同时处于 runnable 状态的计算线程不宜长期远超 CPU 容量。
  2. 2. 减少锁竞争。拆分全局锁,降低共享数据写入,缩短临界区,避免持锁执行慢操作。
  3. 3. 避免惊群。让唤醒具有针对性,不要一个资源到位就把几十个线程全部叫醒。
  4. 4. 使用事件驱动或异步模型时,也不要迷信协程。协程可以减少内核线程切换,但协程运行时仍然需要合理调度。如果协程里混入阻塞系统调用,或者所有任务争同一把用户态锁,换个名字也救不了。
  5. 5. 合理设置线程池、连接池和队列长度。这三个东西必须和 CPU、下游吞吐和延迟目标一起设计。单独把线程池调大,通常只是把等待从队列搬到了调度器和锁里面。
  6. 6. 保持 CPU 与内存局部性。延迟敏感任务可以考虑 CPU 亲和性、NUMA 绑定和分片架构,但要防止绑核后负载失衡。
  7. 7. 持续测量。vmstat 用来观察整体,pidstat 找具体任务,perf sched 看调度延迟,perf stat 看迁移和缓存,火焰图看 CPU 去了哪里。

别一看到 cs 高就开始优化上下文切换。

上下文切换通常是结果。你要找的是制造它的业务行为。


十、日常开发与运维中的落地实践建议

平时写服务时,少创建一些“以防万一”的线程。

给线程池设置边界,也给任务队列设置边界。

监控里保留系统 cs、进程自愿和非自愿切换、CPU migration、运行队列长度与 P99 延迟。

压测时别只看平均吞吐。

观察线程数增加后,吞吐是否仍然线性增长。如果线程翻倍,吞吐不涨,切换、锁等待和缓存 miss 却猛增,那不是并发能力提升,只是系统更忙了。

升级内核或更换发行版时,也别把旧调度器知识直接复制过去。

Linux 6.6 的公平调度选择已经和经典 CFS 教程出现明显差异。函数名可能还叫 fair.c,结构里也还有 cfs_rq,但任务选择语义正在演进。

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-08-21 21:46:51 HTTP/2.0 GET : https://f.mffb.com.cn/a/507642.html
  2. 运行时间 : 0.193814s [ 吞吐率:5.16req/s ] 内存消耗:4,703.10kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=7738e79475d2c7eafd4b81b626638f30
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000901s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001425s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000665s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000591s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001185s ]
  6. SELECT * FROM `set` [ RunTime:0.000488s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001311s ]
  8. SELECT * FROM `article` WHERE `id` = 507642 LIMIT 1 [ RunTime:0.001212s ]
  9. UPDATE `article` SET `lasttime` = 1787320011 WHERE `id` = 507642 [ RunTime:0.020843s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 67 LIMIT 1 [ RunTime:0.000579s ]
  11. SELECT * FROM `article` WHERE `id` < 507642 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000954s ]
  12. SELECT * FROM `article` WHERE `id` > 507642 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001143s ]
  13. SELECT * FROM `article` WHERE `id` < 507642 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001630s ]
  14. SELECT * FROM `article` WHERE `id` < 507642 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001542s ]
  15. SELECT * FROM `article` WHERE `id` < 507642 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.001664s ]
0.197282s