如果你写了一阵子多线程程序,大概会遇到这样一个困惑:代码里明明开了两个进程,各自循环打印,单核cpu上却看起来"同时"在跑。这个错觉的制造者,就是linux内核的进程切换。进程切换(context switch)是操作系统里最日常、也最容易被轻视的底层机制:用户态你看到的是十几个进程井井有条,内核态实际是一场精密到纳秒级的接力赛。这篇文章不打算给你背源码,而是从硬件视角、调度器主流程、触发路径到性能开销、调试工具,完整拆解一次切换从发生到结束的全过程。适合想深入理解linux底层原理的后端工程师、嵌入式开发者,以及正在学习内核的初学者。
1. 单核cpu上的"并发"是怎么骗过我们的
1.1 时间片:几十毫秒的轮番上场
进程切换本质上不是"切换软件",而是"切换硬件状态"。单核cpu只有一套寄存器、一条指令流水线,它要同时服务进程a和进程b,唯一办法是:跑一会a,把a的现场保存好;再跑一会b,把b的现场恢复出来继续跑。这个"保存a现场、恢复b现场"的动作,就是上下文切换。
现代cpu的主频动辄几ghz,时间片往往只有几毫秒到几十毫秒,中间切换造成的停顿在人眼和业务逻辑层面根本感知不到,于是产生了"同时运行"的错觉。多核时代同样离不开切换:核多不代表不切换,进程等待io、被高优先级任务抢占、或者时间片耗尽,都会让出cpu。区别只在于,多核系统里切换发生在某个特定的核上,而不是全局只有一个调度点。
从工程角度看,"切换"是一个系统性的度量单位。你可以用 vmstat 看到每秒上下文切换次数,用 pidstat 看单个进程的切换次数,这些数字直接反映系统的调度压力。很多性能问题,比如应用假死、cpu使用率不高但响应慢,最后都能在"切换次数异常"这个指标上找到线索。
1.2 "现场"到底包括哪些家当
一个正在运行的进程,它的"继续运行所需全部状态"至少包括:
- 通用寄存器:rax、rbx、rcx、rdx、rsi、rdi、rbp、r8-r15等,这些是当前指令流的工作变量
- 指令指针rip和栈指针rsp:决定了下一行代码在哪、函数调用栈顶端在哪
- 标志寄存器rflags:记录进位、零标志、方向标志等条件状态
- 页表基址cr3:决定进程看到的内存视图,也就是地址空间
- fpu/向量寄存器:xmm/ymm、mxcsr等浮点和simd状态
- 内核态的执行现场:进程自己的内核栈上保存的函数调用链、局部变量
- 调度元数据:运行时间、优先级、信号掩码等
其中寄存器数量很有限,保存恢复的成本其实不高;真正昂贵的是cr3切换带来的tlb失效,以及后续的缓存污染。举个粗浅的例子:换寄存器好比换一双鞋,几秒钟搞定;但换完鞋之后你还需要重新走回房间、重新找到刚才读的那页书,这个"重新热身"的成本才是切换的大头。很多人以为切换贵在汇编指令本身,实际上贵在切换后cpu缓存和tlb的"冷启动"。
1.3 三层递进:换指令流、换栈、换地址空间
进程切换可以拆成三个层次。第一层是换指令流:把rip从a的代码位置换到b的代码位置,决定下一条执行哪条指令;第二层是换栈:把rsp从a的内核栈换到b的内核栈,决定函数调用链在哪个栈上展开;第三层是换地址空间:把cr3换成b的页表,决定内存地址翻译到哪些物理页面。
线程切换只做到前两层,因为同一进程的线程共享地址空间;协程在用户态连内核栈都不换,只换用户栈和寄存器。很多初学者把这三种"切换"混为一谈,后面理解调度器和性能问题时就会绕晕。尤其是协程和goroutine,看起来快得离谱,但如果它发生一次真正的阻塞式系统调用,最终还是要落到内核进程切换上,这一点后面专门讲。
2. 硬件视角:switch_to的汇编里到底做了什么
2.1 x86_64的__switch_to_asm:一次"偷梁换柱"的栈迁移
内核里真正执行寄存器保存恢复的,在x86_64上是一小段汇编,位于 arch/x86/entry/entry_64.s ,函数名叫 __switch_to_asm 。去掉一些与具体硬件相关的装饰后,核心逻辑非常直白,下面这段就是它的骨架:
sym_func_start(__switch_to_asm)
pushq %rbp
pushq %rbx
pushq %r12
pushq %r13
pushq %r14
pushq %r15
movq %rsp, task_threadsp(%rdi) # 把当前rsp保存到prev的thread_struct
movq task_threadsp(%rsi), %rsp # 从next的thread_struct取rsp
popq %r15
popq %r14
popq %r13
popq %r12
popq %rbx
popq %rbp
ret
sym_func_end(__switch_to_asm)
这段汇编的精妙之处在于:它先把你当前的寄存器压进prev的内核栈,然后直接切换rsp到next的内核栈,再原样弹出next上次压入的寄存器。最终那条 ret 指令弹出的返回地址,是next上一次被切出时在 __switch_to_asm 之后的下一条指令。也就是说,next重新获得cpu后,会从上次被打断的地方继续往下走,仿佛根本没离开过。
注意这里保存的寄存器是abi里定义的callee-saved集合:rbx、rbp、r12-r15。像rax、rcx、rdx这类caller-saved寄存器不需要保存,因为它们本来就允许被函数调用破坏,调用方自己会处理。内核复用普通函数调用约定,就省掉了一半寄存器的保存开销。这个细节对理解"为什么切换本身很快"很关键。
2.2 为什么必须钻进内核态才能切换
用户态代码没能力、也没有权力做这种切换。第一,写cr3是特权指令,用户态直接操作页表会触发保护异常;第二,调度器要维护全局的runqueue、锁和统计信息,这些内核数据结构用户态根本碰不到;第三,进程随时可能被时钟中断打断,只有内核态的中断处理程序才知道"现在该不该换人"。
所以所有切换都发生在内核态。用户进程通过系统调用、异常、中断三个入口被动进入内核,由内核在合适的位置决定是否让出cpu。这里补充一个很多人不明确的地方:从用户态陷入内核态时,cpu会自动从tss(任务状态段)里读取该任务的内核栈顶地址,把rsp切到内核栈。所以每次系统调用,其实已经发生了一次"用户栈到内核栈"的切换,只不过它不涉及进程换人,上下文的重量级完全不同。
有了这个背景,"进程切换"和"系统调用"就容易区分了:系统调用只是当前进程换个栈继续干活,进程切换则是把整个执行权交给另一个进程。两者都会损失一些性能,但量级完全不同。
2.3 内核栈:每个进程的"私人更衣室"
内核栈是进程切换里最容易被忽略、却最关键的一环。x86_64上每个进程有独立的内核栈,通常配置为16kb,它存的是该进程在内核态执行时产生的函数调用链和局部变量。当进程a被切出时,它的内核栈里放着"内核代码正在执行到哪里"的完整痕迹;换入进程b时,只要把rsp切过去,内核后续的函数调用就自然发生在b的栈上。
这里有个硬件层面的细节:cpu从用户态进入内核态时,需要从tss里读当前任务的内核栈顶地址。所以 __switch_to 这个c函数的一项工作,就是更新tss里的sp0为next的内核栈顶。没有这一步,下次b陷入内核时会跑错栈。这也是为什么 switch_to 宏既调用c函数又调用汇编——c函数负责那些没法用几条汇编搞定的状态(tss、fs/gs基址、调试寄存器),汇编负责干最核心的寄存器与栈迁移。
把内核栈想成每个进程的"私人更衣室"很形象:进程切走时,所有私人衣物留在更衣室里;切回来时,直接进自己的更衣室换上昨天的衣服,接着干活。线程呢?线程也有自己独立的更衣室(内核栈),但它们共享同一个"房子"(地址空间),所以换衣服时不必重新认路。
3. 调度器主流程:从__schedule到context_switch
3.1 __schedule的骨架:关中断、挑next、换环境
调度器的核心入口是 __schedule() ,在 kernel/sched/core.c 里。去掉锁和rcu细节,主干长这样:
static void __sched __schedule(bool preempt)
{
struct task_struct *prev, *next;
struct rq *rq;
local_irq_disable();
rcu_note_context_switch(preempt);
prev = rq->curr;
next = pick_next_task(rq, prev);
if (likely(prev != next)) {
rq->curr = next;
context_switch(rq, prev, next);
}
balance_callback();
local_irq_enable();
}
几个关键点:进入后先关本地中断,防止在调度过程中又被中断打断导致 schedule() 重入;接着从当前cpu的运行队列里挑下一个任务 pick_next_task ;如果挑出来的next不是当前进程prev,就把 rq->curr 换成next,然后调用 context_switch 真正切换;最后打开中断,处理 balance_callback 等收尾。
传统cfs挑选next的依据是虚拟运行时间vruntime,谁的vruntime最小谁先跑;6.6以后的内核用eevdf(延迟虚拟运行时间)替代cfs的挑选逻辑,但"从runqueue中选择最合适的实体"这个框架没有变。理解调度器不用一上来扣每个版本细节,抓住"pick_next_task挑选 + context_switch切换"这个主线就够了。真正决定系统延迟的,往往不是pick_next_task用了什么算法,而是"什么时候允许调用schedule"。
3.2 context_switch的三板斧
context_switch() 的职责非常集中,主要做三件事。
第一件事是 switch_mm_irqs_off() ,切换内存描述符。具体动作是刷新tlb并加载新进程的页表基址到cr3。这里还包含asid/pcid的细节:如果硬件支持pcid,可以给不同进程分配不同pcid,切换时不必全量刷新tlb,这是现代x86降低切换开销的关键手段。对没有pcid的老硬件,每次cr3切换都意味着tlb里几乎所有翻译结果作废,后续指令执行会伴随大量缺页级的内存访问。
第二件事是 switch_to() ,执行前面讲的c函数 __switch_to 加汇编 __switch_to_asm ,完成tss更新、寄存器保存恢复和内核栈切换。
第三件事是切换完成后调用 finish_task_switch() ,处理rcu的上下文跟踪、更新统计信息,并通知一些等待调度器事件的子系统。这一步常常被忽视,但在高版本内核里, finish_task_switch 做了很多"善后",例如对membarrier用户空间内存屏障语义的支持。光看函数名容易觉得它只是清理工作,实际上它直接决定了一次切换能否正确被rcu、perf、调度统计等子系统感知。
3.3 线程切换为什么能"跳过"地址空间
如果next和prev属于同一个进程(线程切换), switch_mm_irqs_off 会发现 next->mm 等于prev的mm,直接跳过cr3的重新加载。这个跳过的意义非常大:cr3不换,tlb里的翻译结果还能继续用,进程的缓存热数据也不会因为地址空间切换而大量失效。这也是"线程切换比进程切换便宜"的硬件级原因。
注意,即使是线程切换,寄存器保存、内核栈切换、tss更新一样都不能少。每个线程都有自己的内核栈和 thread_struct ,它们只是共享mm。所以从调度器视角看,线程切换也是完完整整的一次上下文切换,只是省略了最贵的那部分——地址空间切换。这个结论对性能调优很有用:如果你把一个多进程应用改造成多线程应用,省下来的主要是tlb和缓存开销,而不是"寄存器保存"这个动作本身。
4. 切换的两种触发方式:主动让出与被动抢占
4.1 主动让出:进程困了就去睡
大量切换并不是调度器"抢"来的,而是进程自己让的。进程调用 read() 读磁盘、调用 mutex_lock() 拿不到锁、调用 sleep() 等待定时器,都会把自身状态改成睡眠,然后主动调用 schedule() 让出cpu。这种让出叫主动切换,对应的统计指标是 voluntary_ctxt_switches 。
内核里还有一个特殊的主动让出入口: cond_resched() ,专门给那些在内核态执行长循环、长时间不返回用户态的代码用。内核代码在for循环里隔一段就调用一次 cond_resched() 查一下要不要调度,避免一个内核线程长时间霸占cpu让其他进程饿死。写驱动的人对这个函数应该很熟,如果你在设备驱动或内核模块里写过遍历大量链表、长时间持锁的循环,几乎必然要面对"要不要加cond_resched"的问题。
从用户态角度看,主动让出的典型场景是阻塞io。比如一个socket没有数据可读, read() 返回前会进入等待队列,把当前进程状态置为睡眠,然后调度出去;当数据到达时,内核再唤醒它。这里的每次睡眠和唤醒,都会产生一前一后两次切换。所以"高并发阻塞模型"的问题不只是线程数多,还有大量进程马不停蹄地睡、醒、切换。
4.2 时钟中断与tif_need_resched:抢占是怎么酝酿的
被动切换的源头通常是时钟中断。系统每个tick(hz=1000时每1ms一次)进入时钟中断处理函数 scheduler_tick() ,更新当前进程的调度实体时间片统计。如果当前进程时间片耗尽,或者有个更高优先级的进程被唤醒、需要抢占当前进程,内核不会立刻切换,而是只把当前进程的 thread_info 里 tif_need_resched 这个标志位置1。
这是一个非常重要的设计:中断处理程序只负责"标记需求",不负责"执行切换"。真正执行切换的时机在后面的中断返回路径上。之所以这么设计,是因为时钟中断返回路径是内核回到用户态的必经之路,在这里统一检查调度标志,比在中断上下文里直接切换要安全得多——中断上下文里很多内核数据结构处于不可睡眠状态,直接在中断里跑调度器容易踩到锁和rcu的坑。
另外,现代内核在tickless( no_hz_full )模式下,不只是每个tick都打断每个cpu;但只要有事件中断发生,返回路径上的调度检查依然存在。所以"高优任务醒来立刻抢占"并不是字面意义的"立刻",它总要等到某个安全的调度点。
4.3 中断返回路径上的临门一脚
中断处理完,cpu开始逐层返回:从异常/中断处理程序,回到中断返回的汇编代码 ret_from_intr 。这里会检查两个东西: preempt_count 是否为零(表示当前是否处于不可抢占的临界区),以及 tif_need_resched 是否被置位。如果条件满足,就调用 schedule() 完成真正的切换。
值得区分两种返回目标。如果中断发生在用户态、要返回用户态,那无论内核配置哪种抢占模型,只要有 need_resched 就会调度,这叫用户态抢占。如果中断发生内核态、要返回内核态,则要看内核是否开启了内核抢占( config_preempt ):开启时preempt_count为0就能调度,否则只能继续跑当前内核代码,直到它自己返回用户态或调用 cond_resched 。
这个"临门一脚"的位置,是很多实时性抖动案件的案发现场。一个高优先级任务被唤醒,理论上应该立刻抢占低优先级任务,但低优先级任务可能正处在一个关中断的内核临界区里,或者内核态抢占被关闭,导致高优任务只能等下一个返回点。后面讲调试时我会给一个真实案例,这里先记住一句话:调度延迟高,先找"决策点被推迟"的原因。
4.4 抢占模型选型:服务器、桌面、嵌入式各取所需
内核编译期可以通过 config_preempt 系列选项决定抢占粒度,常见选项和适用场景差别很大。
| 配置 | 行为 | 典型场景 |
|---|---|---|
| config_preempt_none | 内核态几乎不可抢占,只在返回用户态时调度 | 高吞吐服务器 |
| config_preempt_voluntary | 内核代码在长循环中显式让出 | 通用发行版 |
| config_preempt | 临界区外几乎处处可抢占 | 桌面、android、交互式系统 |
| config_preempt_rt | 几乎所有内核代码可抢占、中断线程化 | 工业实时、音频 |
选型没有绝对好坏。我在嵌入式linux项目里见过有人把服务器内核搬过来,结果音视频任务周期性卡顿;也有人为了追求实时性全开preempt,结果吞吐下降明显。关键是要按业务延迟预算来:如果你的任务有硬实时要求,优先考虑rt配置;如果只是普通web服务,preempt_none配合足够的核数往往更稳。
5. 切换的代价:一片被搅乱的缓存比寄存器贵得多
5.1 直接开销与间接开销
如果只看寄存器保存恢复,一次切换其实只有几十条指令,亚微秒级就能完成。真正贵的是间接开销。
换个角度理解:寄存器保存像你换鞋,几秒钟;但换了鞋之后你要重新走回房间、重新找到刚才读的那页书,这个"从新开始"的成本才是大头。对cpu来说,切换到b后,l1/l2里装的几乎全是a的热数据,b要重新把它的热数据从内存/llc拉回缓存;tlb里的地址映射也大多是a的,b一跑就大量miss。这些虽然不直接出现在切换代码里,却会实实在在拖慢切换后的一段时间。
实践中,x86_64上一次进程切换的用户可感知成本通常在1到10微秒量级,进程越多、缓存敏感度越高,感受越明显。 lmbench 的 lat_ctx 测试能测出这个数字,但要注意它测的是"纯粹切换成本",实际业务里的切换往往叠加了缓存重填、锁竞争、唤醒延迟,整体开销会更高。这也是为什么很多高性能程序宁愿用事件循环忙等一小会,也不愿意让线程频繁睡眠——睡眠后唤醒带来的缓存冷启动,可能比忙等消耗的cpu周期更贵。
5.2 进程切换 vs 线程切换的真实差距
前面说过线程切换不用换mm、不用重新加载cr3、不用大量flush tlb,所以通常比进程切换便宜一半左右。这个说法在工程上依然成立,但别把它当救命稻草。
线程切换的便宜,换来的是共享地址空间带来的同步问题。你为了省切换开销改用线程,结果线程之间用锁保护共享数据,锁竞争激烈时会唤醒/睡眠一堆线程,制造出的调度压力可能比省下的切换成本还大。我在实践中见过一个典型的反面案例:有人把几千个"进程"改成"线程"后,吞吐没提升反而锁等待暴涨。切换成本只是系统性能的一个切面,必须和锁竞争、缓存一致性放在一起看。
有一类服务很适合用进程而不是线程:各worker之间几乎不通信、只需定期上报心跳。这种情况下进程天然隔离、崩溃不互相连累,共享状态用共享内存+原子标志就能解决,完全没必要为了"省切换"硬上线程。
5.3 工程上减少切换的常见手段
按"性价比从高到低"排序,常用的手段有:
- 事件驱动取代阻塞模型:epoll、io_uring都比"一连接一线程阻塞"少很多上下文切换;io_uring还减少了syscall次数,顺带省掉用户态/内核态切换。
- 绑核:用
taskset或sched_setaffinity把关键进程绑到固定cpu核,避免进程在不同核间迁移导致整个cache热数据作废。numa环境下绑核还有内存亲和性的额外收益。 - cpu隔离:对延迟敏感任务,用
isolcpus、nohz_full把某几个核从通用调度器中隔离出来,配合irq affinity,让关键任务几乎不被其他进程打扰。 - 注意避免惊群:大量线程同时等待同一事件,事件到达时一堆线程被唤醒,只有少数能拿到锁,其余马上又睡回去,制造一大串无意义的切换。
epollexclusive就是用来缓解这个问题的。
上面每个手段本质都不是"消灭切换动作",而是"减少切换发生的频率和破坏面"。搞清了这一点,很多优化思路自然就通了。
6. 亲手验证:用ftrace和perf把切换抓出来
6.1 ftrace的sched_switch事件
linux内核自带的ftrace可以直接记录每一次进程切换。现代系统tracefs通常挂载在 /sys/kernel/tracing ,全程不需要装额外软件:
sudo mount -t tracefs tracefs /sys/kernel/tracing 2>/dev/null echo 1 > /sys/kernel/tracing/events/sched/sched_switch/enable sleep 3 head -n 20 /sys/kernel/tracing/trace_pipe
常见输出形如:
# task-pid cpu# timestamp function
chrome-2321 [003] 5432.001001: sched_switch: prev_comm=chrome prev_pid=2321 prev_state=s ==> next_comm=python3 next_pid=4012 next_prio=120
prev_state=s 表示切出前的进程处于睡眠状态,说明是主动让出;如果 prev_state=r ,说明是被动抢占。所以 sched_switch 事件不仅告诉你"谁换谁",还告诉你"为什么换"。想连续记录一段时间,最好用 trace-cmd record -e sched_switch -- sleep 3 && trace-cmd report ,比直接读trace_pipe更省事。
一个小提醒:在高负载生产机上直接开ftrace事件会产生大量trace记录,io开销不可忽视。建议先在低峰期试跑几秒,或者在容器外、专用调试机上做。
6.2 perf sched:把调度延迟量化
perf是排查调度的利器。三步就能拿到时间线:
perf sched record -- sleep 5 perf sched latency --sort max perf sched timehist
perf sched latency 会输出每个进程的平均/最大调度延迟(从任务被唤醒到真正上cpu的时间)。如果你发现某个进程的最大延迟明显异常,顺着 timehist 看它的唤醒源和抢占它的进程,基本就能锁定是谁在捣乱。
这里给一个常用的判断口径:平均延迟好看、最大延迟难看,那往往是某个低优先级进程持锁时间过长,或者中断返回路径上的抢占延迟在特定时刻放大。结合前面的preempt知识,这类问题的根因通常不在"切换动作本身",而在"切换决策点被推迟"。perf还支持 perf sched timehist -s 做汇总统计,方便给每个进程排个"谁最容易被延迟"的榜单。
6.3 系统指标:先看数字再动手
不想上ftrace时,先用粗粒度指标判断"切换是否过多":
vmstat输出的cs列是每秒上下文切换次数;如果cs高(比如几万以上)同时cpu使用率不高,大概率是大量任务在睡眠/唤醒循环。- 看单个进程:
/proc/pid/status里的voluntary_ctxt_switches和nonvoluntary_ctxt_switches,前者多说明进程频繁睡眠等待,后者多说明经常被抢占或时间片耗尽。 pidstat -w 1可以实时刷新这两个数字。切换次数快速上涨时,去strace看系统调用,通常能找到真正的原因:比如某个库在while循环里反复打开文件、反复poll短超时。
粗指标只能给方向,精确定位还得靠事件级工具。这也是我推荐先用vmstat/pidstat粗筛、再用ftrace/perf精确定位的原因,一上来直接开trace在高负载机器上非常费io,而且数据量大到看不完。
7. 关于进程切换的几个常见误区与调试经验
7.1 "线程切换便宜,所以多开线程很划算"
线程切换确实比进程切换便宜,但这只回答了"切换本身"的成本。线程共享地址空间带来的数据竞争、锁等待、cache一致性流量,都是隐形成本。如果业务本质是"各干各的、很少通信",用进程反而更干净:崩溃隔离更彻底,mmap共享内存做大数据传输照样能有局部缓存优势。
要不要上线程,取决于通信模式而不是切换成本。十核机器上开几万个线程,调度器光挑下一个任务、处理唤醒队列就够呛,这又回到了"切换与调度是两件事"这个话题:调度器的pick_next_task和负载均衡成本,往往比一次寄存器切换显眼得多。曾经有项目把任务拆到几千个线程,结果大量时间耗在 schedule() 里,改成线程池加事件循环后,吞吐翻了几倍。
7.2 协程/goroutine:用户态调度也躲不开内核
goroutine切换只在用户态换栈和寄存器,一次可能只要十几纳秒,确实比内核进程切换快几个数量级。但goroutine一旦发生真正的阻塞式系统调用(比如磁盘读)、或者进入无法立刻完成的锁等待,go运行时会把当前线程m让出去,调度器再从p的队列里拉goroutine绑定到另一个m上。这个"让出m"的过程,最终还是内核进程/线程切换。
所以用户态调度器的意义是"减少但无法消灭"内核切换。写网络服务时,go会自动把非阻塞io放到netpoller,尽量不让goroutine睡在内核里,这也是为什么go的百万并发比c的"一进程一阻塞连接"模型能扛——不是它不用内核切换,而是它把内核切换的频率压到了极低。理解这层关系,就不会被"协程比线程快一万倍"这种说法带偏。
7.3 一个真实抖动案例:延迟不在切换动作本身
最后分享一次排查经历。某嵌入式linux设备跑音频采集任务,采样线程优先级已经设到很高,但每隔几百毫秒会出现一次明显卡顿。抓 sched_switch 后看到诡异现象:高优task被唤醒后,并没有立刻被调度,而是等了接近一个tick周期才上cpu。
进一步检查发现,唤醒它的低优驱动程序在中断返回路径上持有一把spinlock,而设备的linux内核配置是 config_preempt_voluntary ,内核态抢占被压缩到很少的主动让出点,spinlock临界区又关闭了本地中断/抢占;高优任务只能等临界区退出后,在中断返回路径的检查点才获准切换。方案不是改优先级,而是压缩临界区、把配置换成 config_preempt 、并对音频任务绑核加irq affinity,最终最大延迟从几毫秒压到几十微秒。
这件事给我的经验是:看到"调度延迟"不要第一时间怀疑调度器本身。切换动作本身快得很,问题大多出在"什么时候才允许切换"这个决策上——中断返回路径、preempt_count、锁临界区,才是真正的重点。
平时调linux性能,我总会先看一眼上下文切换次数和调度延迟,而不是急着优化业务代码。很多看起来"莫名其妙"的卡顿,最后都在 sched_switch 事件里现了原形。建议你也找一台测试机,把上面讲的ftrace、perf sched跑一遍,亲手抓出一次进程切换,再对照着理解寄存器、内核栈和cr3的关系。这套底层直觉一旦建立,再看并发、锁、io模型都会通透不少。
以上就是深度解析linux中进程切换的原理与性能优化的详细内容,更多关于linux进程切换的资料请关注代码网其它相关文章!
发表评论