ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

从课堂练习到线上排查:进程切换原理与实测

从课堂练习到线上排查:进程切换原理与实测 做操作系统课的时候课堂练习 3.4 这道题看起来是整章里最不起眼的一个——它既不像银行家算法那样有花哨的表格也不像页面置换那样能手算一堆命中率就八个字进程的切换。但我后来发现这道题其实是整门课的分水岭。能把进程切换讲明白的人后面看进程调度、看锁竞争、看进程池和线程池的选型甚至看一台服务器为什么莫名其妙 CPU 不高却响应很慢都会顺很多讲不明白的人就会一直停留在进程是程序的一次执行这种背下来的句子里。这篇东西是我自己做这道练习时留下的一整套笔记中间补了很多当时没搞懂、后来在实际工作里才补上的细节。不管你是刚上完课要交作业的学生还是工作几年后回头想把地基重新夯一遍的人都能从里面拿到能直接用的东西一段能跑起来量切换开销的 C 代码、几条能直接看到切换现场的命令、一张能对照着查问题的速查表以及几个我自己踩过的坑。1. 进程切换到底在切什么先建立一个能用手比划的心智模型1.1 从切环境这件更常见的事说起很多人第一次被切换这个词搞晕不是因为进程切换有多难而是因为日常生活中切换这个词用得太泛了。你在终端里敲uv切换 Python 环境、敲nvm切换 Node 版本那是换一套依赖路径用 Git 敲checkout切分支那是换一套工作区快照家里路由器配了个主备链路检测到主线路断了就切到备线那是换一条转发路径硬件工程师画一块 28V 电源的切换电路或者 485 收发自动切换电路那是换一条电气通路。它们的共同点是发生切换的那一刻系统必须保证切换完成之后新的那一侧能接着往下走而不是从头开始。进程切换也是这个逻辑只不过它切换的对象是CPU 的执行权。CPU 在任意一个瞬间只能执行一条指令流它手上有几样东西是当下这一刻独有的一堆寄存器的值程序计数器 PC、栈指针 SP、通用寄存器、当前生效的页表、以及一堆跟这段执行流相关的内核状态。所谓进程切换就是把属于进程 A 的这一整套当下原封不动地收进 A 的档案袋里再把 B 的档案袋摊开、一样一样装回硬件上。装完之后CPU 下一条指令取到的就是 B 上次被打断的位置B 甚至察觉不到自己曾经被换下去过。这里有个关键认知很多人做练习的时候会忽略进程切换不是从头启动进程而是恢复一个被冻结的现场。这两者的成本差着数量级。一个新进程启动要走fork/execve、要建页表、要加载可执行文件而一次切换只是搬几十个寄存器的值加换一次页表基址。理解这一点后面你才能理解为什么进程池能省下大量开销——它省的是创建的开销但省不掉切换的开销这两笔账必须分开算。1.2 三样东西寄存器现场、内核栈、地址空间把现场这个词拆开具体是三大块。第一块是 CPU 寄存器现场。在 x86-64 上用户态程序被打断进入内核时硬件和内核入口代码会把当时的rip、rsp、rflags、rax、rbx、rbp等一整套寄存器压到内核栈上形成一个叫pt_regs的结构。这一步是保存现场的前半段注意它压的是内核栈不是进程自己的用户栈——因为进程的用户栈地址此刻已经不可信任了内核要有一个绝对安全的落脚点。第二块是内核栈本身。每个进程严格说是每个任务包括线程在内核里都有自己独立的一小段栈空间。x86-64 的 Linux 上这个栈默认是 16KB也就是 4 个物理页因为要同时容纳pt_regs、中断处理、以及可能嵌套的系统调用路径。进程被切走的时候内核栈上的内容要保留栈指针也就是task_struct里的thread.sp要更新成当前栈顶位置。下一次这个进程被调度到时内核从这个thread.sp把栈换回来之前压进去的寄存器再逐个弹出执行流就续上了。所以task_struct里那个thread_struct是切换的真正核心数据结构它存的不是进程信息而是这个进程被冻结时CPU 的骨架长什么样。第三块是地址空间也就是页表。这是进程切换和线程切换最大的差别所在。每个进程有自己独立的虚拟地址空间对应一套页表页表基址寄存器x86-64 上是CR3指向这套页表的根。切换进程的时候如果两个进程不属于同一个地址空间就必须换CR3。换CR3这件事看着只是一条mov指令但它会带来一个非常贵的副作用TLB 里的地址翻译缓存大面积失效。TLB 是 CPU 里缓存虚拟地址到物理地址映射的小表本来访问内存只要查 TLB 就够了一旦它失效后面一段时间的内存访问都得走一次完整的页表遍历那可能是几十个时钟周期起步的代价。现代 CPU 用 PCIDProcess Context Identifier给 TLB 表项打标签让不同进程的 TLB 表项能共存切换时不用整片刷掉只标记一下当前用哪个标签这才把进程切换的间接开销压了下来。但你得记住这是个优化后的结果而不是本来就不用花钱。1.3 进程切换和线程切换差在哪一张表讲透同一进程内的两个线程共享地址空间所以它们之间切换不需要换CR3TLB 基本不受影响这也解释了为什么大家张口就说线程比进程轻。对比维度进程切换同进程内线程切换寄存器现场保存恢复需要需要内核栈切换需要需要页表基址CR3切换需要不需要TLB 影响大除非有 PCID 缓解基本没有用户态页表部分完全换一套共用文件描述符表、信号处理表各自独立共享典型直接开销1~3 微秒量级亚微秒到 1 微秒间接开销Cache/TLB 污染显著可能到几微秒甚至更高较小注意最后两行的措辞——直接开销和间接开销必须分开谈。直接开销就是你保存和恢复寄存器、切换栈、换页表花掉的那点纯 CPU 周期间接开销是切换完成之后新进程发现自己的数据和代码不在缓存里、TLB 命中也掉了于是接下来的几万条指令都在慢速路径上跑。很多性能问题真正疼的不是前者是后者。你在做课堂练习时如果能在报告里把这层区分写出来基本就跟身边同学的作业不是一个层次了。2. 谁按下了切换键触发时机与调度链路拆解2.1 自愿切换和非自愿切换一定要分清进程切换的触发分两大类这个分类在/proc/pid/status里直接有对应字段voluntary_ctxt_switches和nonvoluntary_ctxt_switches。自愿切换是进程自己认命了它去读一个还没准备好的磁盘数据、去等一个还没到的网络包、去抢一个已经被占住的锁、或者主动调用sleep于是它把自己挂到一个等待队列上明确告诉内核我现在没法跑了你换别人上。这种切换是它主动配合的所以叫自愿。自愿切换多通常说明这个进程是 I/O 密集型或者锁竞争比较多不一定是坏事。非自愿切换是进程不想让但被内核强制剥夺了执行权时间片用完了或者有一个优先级更高的任务被唤醒了。这种切换多通常说明 CPU 上是真的挤大家都在抢。一个进程的nonvoluntary_ctxt_switches涨得飞快基本上可以判定它所在的那台机器 CPU 资源比较紧张或者它自己被设成了低优先级、一直在被抢占。这两类数字的实际意义完全不同我见过有人把两个加起来当总切换次数去对比性能然后得出一个很离谱的结论。排查性能问题时先看非自愿切换占多大比例这一眼就能定方向比例高就往 CPU 竞争方向查比例低就往 I/O 等待方向查。2.2 从时钟中断到switch_to的完整链路课堂练习最容易被老师追问的就是这条链路。以时间片用完这种非自愿切换为例大致的路是这样走的硬件定时器到点产生时钟中断CPU 跳进内核的中断入口顺手把当前寄存器压到内核栈上形成pt_regs。中断处理程序更新调度相关的统计量比如当前任务的虚拟运行时间并检查它是不是该让位了。如果判断该让位就给当前任务打上一个标志位TIF_NEED_RESCHED。中断处理结束准备返回用户态之前内核会检查这个标志位。注意这一步很关键——内核并不在中断处理程序里直接切换因为中断上下文里不能随便睡眠切换要推迟到一个安全的时机。检查到标志位后进入调度函数挑出下一个应该运行的任务。进入真正的切换函数context_switch它干两件事switch_mm负责换地址空间要换的话switch_to负责换寄存器和栈。switch_to在 x86-64 上是一段汇编动作就是把当前寄存器存进当前任务的thread_struct把下一个任务的存进去的值取出来装上同时把栈指针换成下一个任务的内核栈。从下一个任务的角度看它上一次调用switch_to的那行代码刚刚返回了于是它继续往下走顺着返回路径一路退回用户态接着执行自己被打断的地方。这里有三个特别容易被忽略的细节考试和面试都爱问。第一内核态和用户态的任务在内核里看起来是同一个执行流。switch_to并不区分我切的是用户进程还是内核线程它切的是任务。一个普通进程在系统调用里阻塞了被切走时它在内核态下一次被唤醒恢复时它也是从内核态继续直到返回系统调用出口才回到用户态。第二选择下一个任务和执行切换是两件分开的事。前者是纯计算在一堆候选里挑一个后者是有实际代价的动作。Linux 的调度器设计上刻意把这两件事分开好处是选谁的过程可以随时被中断、被重新计算而真正切换只发生一次。第三切换不等于立刻发生。打了标志位之后可能要等到返回用户态、或者等到下一个可以安全调度的点才真正切。这中间有一段延迟窗口学名叫调度延迟。2.3 抢占延迟为什么内核要忍一下既然有切换需求为什么不立刻切因为在很多位置立刻切会出事。设想内核正在遍历一个链表或者正握着一把锁、已经改了半个数据结构。这时候如果被切走另一个任务上来也要动同一个结构就会看到不一致的中间状态。所以内核明确划定了一些不允许抢占的区域比如持有自旋锁、关中断、或者某些显式的抢占计数非零的区间。在这些地方TIF_NEED_RESCHED标志位照样会被打上但切换动作被推迟到退出这些区域之后。这就带来了一个很有意思的现象一个进程该被换下去了和真的被换下去了之间可能隔着几微秒甚至更久。对普通应用来说这点延迟无所谓但对硬实时场景就是致命的。所以在实时内核里抢占是被允许发生在更多位置的代价是内核代码写起来更难要更小心地处理并发。顺带说一句Linux 的默认可抢占粒度是除内核临界区外可抢占加上任务的动态优先级不会随便乱变所以同一台机器上跑一个高优先级的实时任务和一个低优先级的批处理任务批处理那边会明显感到被压制。这是设计选择不是 bug。3. 动手把切换看出来四种观测手段实测课堂上讲原理最怕的就是全在脑子里跑。我后来养成了一个习惯任何讲切换的场合只要手边有 Linux 机器就先让数据出来说话。下面四种手段按成本从低到高排你可以按需取用。3.1 第一层vmstat和pidstat的读法最省事的是vmstat每秒打一行其中cs那一列就是每秒上下文切换次数。注意它统计的是任务级别的切换包含线程不只是进程。# 每秒采样一次采 10 次 vmstat 1 10典型的输出长这样字段含义我标在下面procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 3 0 0 812340 62180 998220 0 0 4 12 920 1450 8 3 88 1 0r是运行队列长度在跑或者等着跑的b是阻塞的进程数in是每秒中断数cs是每秒上下文切换数。看这台机器cs一千五in九百多ussy才 11%CPU 挺闲。这说明切换的主要来源不是 CPU 竞争很可能是大量 I/O 等待带来的自愿切换。看切换次数的时候一定要跟 CPU 使用率一起看单看cs没有意义。要看具体是哪个进程在切用pidstat -wpidstat -w 1 5输出里有两列很关键cswch/s每秒自愿切换和nvcswch/s每秒非自愿切换。这个工具比vmstat好用的地方在于它能定位到进程。如果某个进程的nvcswch/s一直很高那基本就是这个进程在被反复抢占如果cswch/s高而nvcswch/s很低那就是它在频繁等待。提示vmstat的cs在高负载机器上过万是很常见的不要一看到大数字就喊切换太多有问题。判断标准是切换次数和实际吞吐、延迟之间的关系而不是绝对数字。3.2 第二层读两个/proc文件第一手数据其实就在/proc里不需要装任何工具。# 看一个进程的切换统计 grep ctxt /proc/12345/status输出两行voluntary_ctxt_switches: 12453 nonvoluntary_ctxt_switches: 218这个进程绝大部分是自愿切换说明它在密集做 I/O 或者等锁被抢占的次数才两百多CPU 竞争不激烈。更详细的数据在/proc/pid/sched里cat /proc/12345/sched里面有几行对做作业特别有用se.nr_migrations : 12 nr_switches : 12671 nr_voluntary_switches : 12453 nr_involuntary_switches : 218 se.load.weight : 1024 se.avg.util_avg : 143nr_switches是总切换次数后面两个是自愿和非自愿的分解和status里对得上。se.avg.util_avg是这个任务的平均利用率单位是 1024 分之一所以 143 大约相当于 14% 的 CPU 占用。这个文件还有一个隐藏用途se.nr_migrations记录了这个任务被迁移到别的 CPU 核心上的次数迁移次数异常高往往意味着调度器在核心之间来回搬它这本身也会带来缓存失效的代价。读这两个文件的好处是你可以在程序里直接把数字读出来用来验证自己写的多线程/多进程代码到底切了多少次而不用靠外部工具猜。做课堂练习的时候在报告里贴一段我的程序跑了 N 秒总共发生 M 次自愿切换比空谈原理有说服力得多。3.3 第三层用 ftrace 抓一次真实的切换现场想看内核里到底发生了什么ftrace是最直接的工具。它靠内核里预埋的追踪点工作开销很低不需要重新编译内核。# 需要 root 权限 cd /sys/kernel/debug/tracing # 打开调度切换追踪点 echo 1 events/sched/sched_switch/enable # 清空旧数据开始记录 echo trace sleep 2 # 关掉追踪看结果 echo 0 events/sched/sched_switch/enable cat trace | head -50你会看到这样的行bash-3127 [002] d... 1234.567890: sched_switch: prev_commbash prev_pid3127 prev_prio120 prev_stateS next_commsshd next_pid890 next_prio120这一行信息量很大。prev_stateS表示前一个任务是因为进入可中断睡眠状态而让位的也就是自愿切换如果是prev_stateR那是被抢走的就是非自愿切换。[002]是发生在哪个 CPU 上。prev_prio和next_prio是两边任务的优先级。你只要看prev_state这一个字段就能当场给一次切换定性。如果嫌手敲路径麻烦trace-cmd更顺手trace-cmd record -e sched_switch -p function sleep 2 trace-cmd report | head -30第一次看到真实的切换流在屏幕上滚过去那种感觉比看一页图要实在得多。你会直观地看到有些任务切得极其频繁有些任务一睡就是好几毫秒。注意追踪点全开会带来可观的性能开销绝对不要在线上服务器上随手echo 1到某个开关就忘了关。用完立刻关掉或者干脆用trace-cmd这种会自动收尾的方式。3.4 第四层写个 C 程序把开销量出来这是我觉得这道课堂练习最值得做的一步。原理非常简单用两个进程通过管道来回传球测一次完整往返的时间除以二就是一次切换的近似成本。#define _GNU_SOURCE #include stdio.h #include stdlib.h #include unistd.h #include time.h #include string.h static long long now_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return (long long)ts.tv_sec * 1000000000LL ts.tv_nsec; } int main(int argc, char **argv) { int n (argc 1) ? atoi(argv[1]) : 100000; /* 往返次数 */ int p2c[2], c2p[2]; char buf[1] {0}; if (pipe(p2c) 0 || pipe(c2p) 0) { perror(pipe); return 1; } pid_t pid fork(); if (pid 0) { perror(fork); return 1; } if (pid 0) { /* 子进程收到一个字节就回一个字节 */ close(p2c[1]); close(c2p[0]); for (;;) { if (read(p2c[0], buf, 1) ! 1) break; if (write(c2p[1], buf, 1) ! 1) break; } _exit(0); } /* 父进程发出去再收回来量一次往返 */ close(p2c[0]); close(c2p[1]); long long start now_ns(); for (int i 0; i n; i) { if (write(p2c[1], buf, 1) ! 1) break; if (read(c2p[0], buf, 1) ! 1) break; } long long elapsed now_ns() - start; printf(往返 %d 次总耗时 %.3f ms\n, n, elapsed / 1e6); printf(单次往返 %.1f ns单次切换约 %.1f ns\n, (double)elapsed / n, (double)elapsed / n / 2.0); return 0; }编译并跑gcc -O2 -o pingpong pingpong.c ./pingpong 100000实测的典型结果是这样一次往返大约落在2 到 6 微秒这个区间除以二就是单次切换 1 到 3 微秒。数字会随机器差别很大——更快的 CPU、更小的缓存压力会把它压下去开了防护补丁、页表切换更频繁的机器会把它推上去。这里有几个必须写进报告的注意事项否则结论会站不住脚这个测量包含的不只是切换本身。它包含两次系统调用read和write的进出开销、两次唤醒另一个进程的调度开销、以及管道缓冲区拷贝。真实切换的直接开销比这个数字小。所以正确的说法是这是切换的上界估计不是精确值。进程要绑到不同的核上做对照。如果两个进程刚好在同一个核上跑是真正的切换加调度如果被调度器分到了两个核上那测的其实是核间通信IPI和缓存一致性协议的成本完全是另一回事。用taskset绑核可以做个对照。# 绑到同一个核 taskset -c 0 ./pingpong 100000 # 绑到两个核 taskset -c 0,1 ./pingpong 100000把同样的逻辑换成两个线程再跑一遍对比数字。你会发现线程版本的往返时间明显更短因为省掉了页表切换和 TLB 失效这部分。这一组对照数据是整个练习里最能说明进程切换贵在哪的证据。心得做这个实验时先跑一次预热再正式计时。第一次跑往往会明显慢一截因为代码和数据都还在从磁盘往上搬缓存是冷的。等跑过一轮之后再统计数据才稳定、可复现。4. 进程池、线程池和 IPC切换成本如何反向塑造架构4.1 进程池到底省了什么没省什么面试里问我进程池和线程池的区别我一般会从哪部分开销被省了这个角度答。fork一个进程的代价是很实在的要复制页表结构、建立新的task_struct、设置地址空间、就算有写时复制COW让物理页不立刻复制光是页表本身就得实打实建一份。进程池的做法是提前把这些创建动作做掉让请求到来时直接复用已有的进程。所以进程池省掉的是创建开销和销毁开销。但请求处理过程中的上下文切换开销一分没省。池子里的进程之间如果要在同一个核上轮流处理请求页表该换还是得换TLB 该失效还是得失效。所以进程池的价值主要体现在两处一是避免高并发时创建销毁带来的抖动二是可以用来做资源隔离——每个进程有独立的地址空间一个进程里的内存越界不会直接把隔壁搞崩。线程池的账就更清楚了。线程共享地址空间创建时不需要建页表切换时不需要换CR3。所以它在创建和切换两个环节都比进程便宜代价是隔离性差线程之间共享全局变量、共享文件描述符一个线程里的野指针可以直接把整个进程带走。选哪个不是看谁更先进而是看你的请求处理是 CPU 密集还是 I/O 密集、对隔离性的要求有多高。4.2 进程通信的每一次往来本质都在增加切换次数这一条是我做练习时怎么都想不明白、后来才彻底理顺的。进程之间要交换数据就得有通道管道、消息队列、共享内存、Unix 域套接字、信号。除了共享内存这种直接读一块物理页的方式以外其余方式大多需要内核参与而一旦内核参与就很可能伴随着任务状态的切换。举个最直观的例子。用管道做生产者—消费者生产者把数据写进管道、消费者从管道读。当管道是空的时候消费者会在read上阻塞这是一次自愿切换它睡下去生产者被唤醒又是另一次切换方向的变化。生产完一批数据消费者醒来再切一次。所以两个进程之间通信越频繁切换次数就越高而且这种切换是你切我、我切你成对出现的。共享内存之所以被当成高性能 IPC 的默认选择重要原因之一就是它可以让两个进程在不进入内核、不发生切换的情况下交换数据。当然前提是得自己处理同步——信号量、原子变量、自旋锁这些同步机制本身又可能带来新的等待和切换所以真实的性能还是要实测不能想当然。顺带说个常见误区有人觉得用了共享内存就一定快。实际测下来如果两个进程在同一台机器上频繁争抢同一块缓存行性能反而可能比走管道还差因为缓存一致性协议在核心之间来回同步那条缓存行代价并不低。同步粒度比通信方式重要得多。4.3 一个特别贴近日常的切换排查案例U 盘拔不出来提示设备正忙这个场景几乎人人都遇到过而它本质上就是一个进程占用的问题。它的排查思路跟排查进程切换异常其实是同一套方法找出谁在占用、看它在干什么、决定是等它还是处理它。# 找出是哪个进程占着 U 盘挂载点 lsof D /media/yourname/USBDRIVE # 或者用 fuser 直接列出占用的进程号 fuser -vm /media/yourname/USBDRIVE拿到进程号之后再看一眼它在干什么# 看这个进程的 I/O 和状态 cat /proc/pid/status | grep -E State|ctxt ls -l /proc/pid/fd | grep USBDRIVEState显示D表示不可中断睡眠通常是在做实际的磁盘 I/O这种情况你等一会儿它自己就好了显示S表示可中断睡眠那可能是某个后台程序文件管理器、索引服务、杀毒软件都可能攥着一个目录句柄不放。这里有一个特别容易踩的坑kill -9杀不掉的D状态进程是杀不掉的。因为D状态的进程在进行不可中断的内核操作它不响应任何信号包括SIGKILL。这时候唯一能做的是等 I/O 完成或者解决底层的硬件/驱动问题。很多人遇到这种情况反复kill -9然后以为系统坏了其实只是没搞清信号的分发机制。同类的场景还有虚拟机报另一个程序已锁定文件的一部分进程无法访问。这就是另一个进程握着虚拟磁盘文件的锁处理方式同出一辙用lsof找到持锁的进程确认它是不是自己开的一个残留实例是的话关掉它而不是急着去重装软件。5. 常见问题与排查速查表5.1 六个高频疑问逐个拆问题一cs很高是不是一定有问题不一定。如果机器在做大量小 I/O比如频繁读写小文件、频繁收发小报文切换次数高是正常结果。要看的指标是切换次数和吞吐、延迟的组合。只有当切换次数高、CPU 使用率也高、但吞吐上不去的时候才说明切换本身成了瓶颈。问题二为什么我改了代码逻辑切换次数没怎么变因为切换次数主要取决于阻塞和唤醒的次数而不是代码行数。如果你的优化只是让计算更快但阻塞的节奏没变那切换次数当然不变。要降低切换次数得从减少阻塞次数、合并 I/O、适当批处理这个方向入手。问题三为什么两个进程交替执行比一个进程干完再干另一个慢这么多因为交替执行意味着频繁切换而每次切换都要付出缓存和 TLB 冷启动的代价。这就是为什么批量处理通常比流水线处理更快——省的不是 CPU 计算时间是缓存重建时间。问题四线程数开得越多切换开销越大吗会。可运行线程数超过物理核心数之后多出来的线程只能排队调度器就要在它们之间来回切每个线程的缓存工作集还会互相挤压。这也是为什么无脑加大线程数经常不涨反降。问题五为什么同样的程序在容器里跑切换次数比物理机上高容器的 CPU 配额机制会在超过配额时把任务挂起一段时间再放行这一挂一放就多出了切换。另外容器里看到的 CPU 数量可能和宿主实际核心数不一致很多运行时按错误的核数去设置并行度间接增加了竞争。问题六怎么判断一次切换是自愿的还是非自愿的最直接的办法是在/proc/pid/sched里分开看两个计数想做动态追踪就用前面说的sched_switch追踪点看prev_state字段S或D就是自愿R是被抢。5.2 排查速查表把上面这些整理成一张能直接对着用的表现象优先怀疑方向第一条命令关键判断依据系统整体响应变慢CPU 却不高I/O 等待引起大量自愿切换vmstat 1 10b列持续大于 0wa偏高某进程 CPU 占用不高但延迟抖动大被反复抢占pidstat -w -p pid 1nvcswch/s持续偏高切换次数极高但吞吐上不去缓存/TLB 抖动pidstat -w 1 5perf stat缓存未命中率同步上升进程数远超核心数调度排队sar -q 1 5运行队列长度长期大于核心数设备提示正忙无法卸载有进程持有句柄fuser -vm 挂载点列出持有者进程号进程kill -9无效处于不可中断睡眠cat /proc/pid/statusState显示D容器内切换次数明显高于宿主机CPU 配额受限cat /sys/fs/cgroup/cpu.max配额被限流触发挂起这张表里的每一条我都是先在被怀疑的对象上执行第一条命令拿到粗判再往下钻。不要一上来就perf全量采样那玩意儿输出太大很容易把自己淹掉。6. 课堂练习怎么做才拿分我踩过的坑和加分项6.1 三个最常见的扣分点第一类扣分是把进程切换和进程调度讲成一件事。调度是决策——在候选集合里挑一个切换是动作——把 CPU 的执行权交出去。两者的关系是先决策后动作。我在报告里一开始把两个概念混着写被老师圈出来一大片。建议的做法是在报告开头就用一句话把这两个概念分开定义后面所有论述都严格遵守这个用法。第二类扣分是把开销只写成一个数字。很多人量出一次切换约 2 微秒就结束了。但这个数字依赖于什么依赖于机器型号、是否开了页表隔离、绑定到同一个核还是不同核、是否有缓存预热。这些条件必须写清楚否则这个数字没有任何可比性。我后来改的做法是把测量条件列成一个小表包括 CPU 型号、是否绑核、往返次数、预热轮数、十次测量取中位数然后说明为什么不取平均值因为偶尔会有一次异常长的延迟把平均值拉高。第三类扣分是只测进程、不测线程没有对照组。单独一个数字说服力有限但如果能给出同进程内线程往返 X 纳秒跨进程往返 Y 纳秒比值约 Z这种对照结论立刻就立住了。对照实验是这类练习性价比最高的加分项。6.2 想拉开差距可以加这几块内容如果时间还宽裕下面几件事做了会明显不同。把 TLB 的影响证据拿出来。找一个内存访问量大的程序分别在单进程场景和两进程频繁切换场景下跑用性能计数工具看 TLB 未命中次数的变化。能把这组数据摆出来说明你不只是背了结论而是真的做过实验。把切换的完整链路画成一张时间轴。从时钟中断进入到标志位设置到返回用户态前的检查到真正切换标出每一步大概的时间量级。不需要画得多漂亮只要每一步的因果关系是对的这份图就是整份报告的核心。给一个优化前后的小对比。比如把一个频繁读写小块的程序改成批量处理然后对比切换次数和总耗时。哪怕提升只有百分之几只要是你自己测出来的价值就比抄一段教材高得多。加一节我遇到的问题。比如第一次跑测量程序时忘了预热、比如trace-cmd输出把终端刷爆了、比如taskset绑核之后数字变化很大让我一度怀疑程序写错了。真实的踩坑记录是加分项不是减分项因为它证明你真的动过手而不是复述了一遍概念。6.3 最后一点个人经验我自己在这个过程中最大的收获是意识到进程切换这个概念的真正价值不在于它的定义而在于它是一个可以测量的物理现象。你可以用几行 C 代码把它量出来可以用vmstat把它观测到可以用ftrace把它抓下来看。一旦它从课本里的名词变成了屏幕上会跳动的数字后面所有跟并发、性能相关的问题就都有了一个具体的抓手。还有一个小提醒给正在做这个练习的人写报告的时候先把实验做完再回去补理论解释不要把顺序反过来。先把数字跑出来你会带着问题去看内核的知识理解速度会比先读理论快得多。我试过两种顺序差别非常明显。踩过几次坑之后我才明白先动手再补理论学到的东西是活的先啃理论再动手学到的往往只是能背出来的句子。
返回列表