资讯动态

操作系统概论不是背定义:一张资源管理地图帮你排查线上故障

发布时间:2026/10/28 3:10:16 来源:尧图企业网站定制
“操作系统概论”这几个字很多人一看就头大——课本第一章、考试必考、背完就忘。我当年也这么想直到一次线上故障把我按在地上摩擦一台机器 load average 飙到四十多CPU 空闲率还有九成所有人盯着应用日志查了两小时最后发现问题根本不在业务代码而是一批进程卡在不可中断睡眠状态上磁盘在疯狂换页。那次之后我才真正明白操作系统概论讲的从来不是几个名词定义而是一张地图——它告诉你一台机器里的资源是怎么被分配、被争抢、被回收的。这张地图你脑子里有没有直接决定你排查问题时是瞎猜还是有方向。这篇内容就是把这张地图摊开讲清楚操作系统到底在管什么、进程和内存这两块最容易讲糊涂的地方怎么理顺、系统调用这条边界线在哪、以及概论里的知识怎么落到日常排查上。刚入行的人可以当成入门路线写了几年业务代码但对底层始终模糊的人可以当成一次系统性的补课。1. 先把操作系统的位置摆正它到底在管什么1.1 从“一台裸机到一个能跑的程序”倒推我不喜欢从定义开始讲操作系统因为定义是结论不是过程。更有效的切入方式是倒推假设你现在手里只有一块主板、一颗 CPU、几条内存条和一块硬盘上面什么软件都没有。你要让一段自己写的代码跑起来中间缺了什么缺的第一样东西是“加载”。CPU 上电后只会从一个固定地址取指令执行它不知道你的程序在哪、有多大、该放到内存的哪个位置。缺的第二样东西是“隔离”。你的程序算错了一个下标把隔壁程序的数据覆盖了凭什么能拦住缺的第三样东西是“排队”。两颗核心、十个程序同时想跑谁先谁后、跑多久该让出来缺的第四样是“翻译”。你想读一个文件硬盘只认“第几柱面第几扇区”这种话你不可能自己去做这件事。把这四样缺的东西补齐就是操作系统存在的理由。所以我给它的定位是两句话向下它是对硬件的封装和抽象向上它是资源的管理者和仲裁者。这两句话看着简单但它解释了后面几乎所有的设计取舍。比如“抽象”这条线解释了为什么你写代码时可以完全不管硬盘是机械的还是固态的、内存条插了几根。你打开文件、读写字节底下那些磁道、扇区、块号、寻道时间全被文件系统这一层抽象吃掉了。抽象的好处是降低心智负担代价是抽象会漏——一旦硬件出问题或者负载超过设计假设抽象层兜不住问题就会以很奇怪的形式冒到应用层。这就是为什么很多诡异的线上问题最终都要往内核这一层挖。再比如“仲裁”这条线解释了为什么操作系统里到处是“策略”和“机制”的分离。机制负责“能做什么”策略负责“该怎么做”。调度器提供了“选择下一个运行的进程”这个机制具体按什么规则选是可以换的策略。理解这个分离你就能明白为什么同一个系统在不同场景下表现差异巨大——不是内核变了是策略参数被调过了。1.2 五个管理对象进程、内存、文件、设备、权限概论里会罗列一堆功能我习惯把它们收敛成五个管理对象。这个收敛不是为了考试是为了排查问题时能快速定位“这是哪一层的锅”。管理对象核心问题出问题时的典型现象进程与线程谁在跑、跑多久、怎么切换CPU 飙高、上下文切换暴涨、进程卡死内存谁用哪块、不够了怎么办内存持续上涨、被强制终止、系统变卡文件与存储数据放哪、怎么找、怎么保证一致磁盘写满、写入后掉电丢数据、句柄耗尽设备与 I/O怎么和外部设备对话、怎么等I/O 等待高、磁盘队列长、网卡丢包用户与权限谁能做什么、边界在哪权限被拒、越权访问、资源配额打满这张表我建议每个刚入行的人都默写一遍。它的价值在于当你面对一个“系统很慢”的模糊描述时能立刻把它拆成五个具体问题去验证而不是打开监控面板一顿乱看。举个我自己的例子。有次一个服务每隔十几分钟就抖一下延迟从 20 毫秒跳到 2 秒。按这张表走进程维度看CPU 不高、切换正常内存维度看占用平稳那就往设备和 I/O 上想一看磁盘队列果然在抖动的时间点上有一段长时间的写入峰值。再往下挖是一个日志组件在攒够一批之后统一刷盘。问题定位到这一步改法就很简单了——要么降低单次批量要么把日志盘和业务盘分开。1.3 为什么“抽象层”是概论里最值钱的概念我见过不少人学完概论能背出“操作系统有四大特征并发、共享、虚拟、异步”但一到实际问题上就懵。原因在于这些词是描述性的不是操作性的。真正有操作性的是“抽象层”这个概念。拿“虚拟”来说。虚拟内存这四个字很多人背得滚瓜烂熟但你问他“我一个进程的地址空间里0x7f 开头那段是什么”他答不上来。而恰恰是这种具体问题决定你能不能在内存问题面前站得住。抽象的层次可以这么理解物理内存是真实存在的、有限的、共享的虚拟地址空间是每个进程独享的、连续的、可以比物理内存大的。内核做的事就是维护一张映射表把虚拟地址翻译成物理地址并且在物理内存不够的时候把暂时不用的部分挪到磁盘上腾地方。这个“翻译 搬运”的机制就是虚拟内存。再往下推一层为什么有了虚拟内存程序就可以假设自己独占内存因为每个进程看到的地址空间是独立的进程 A 的 0x1000 和进程 B 的 0x1000 可以映射到完全不同的物理位置互相看不见。这个隔离是硬件和内核配合实现的不是靠程序自觉。提示概论里的每个“特征”背后都有一个具体的硬件机制或内核数据结构在支撑。学的时候一定要追问一句“这是靠什么实现的”否则这些词永远是空的。2. 进程与线程概论里最容易讲糊涂的一块2.1 进程不是“运行中的程序”这么简单“进程是程序的一次执行”这个定义正确但没用。我更愿意把进程拆成两个东西看一个资源容器加一条执行流。资源容器指的是这个进程独占的那一堆东西地址空间、打开的文件描述符表、信号处理方式、工作目录、环境变量、权限凭证。这些东西是“容器”里的内容进程被创建时初始化进程消失时回收。执行流指的是那条指令序列它有自己的程序计数器、栈指针、寄存器状态。CPU 上真正在跑的是执行流不是容器。这个拆分解释了线程的存在。线程是“共享同一个资源容器、但拥有独立执行流”的东西。同一进程内的多个线程地址空间是共享的所以一个线程 new 出来的对象另一个线程直接能看到但每个线程有自己的栈和寄存器所以能各跑各的。把这两层分清楚很多困惑就自动解开了。比如“为什么多线程比多进程轻”——因为创建线程不需要复制地址空间只需要分配一个栈和一套寄存器上下文。比如“为什么多线程程序容易出并发 bug”——因为地址空间共享两个线程同时改一个变量天然就有竞态。再比如“进程崩溃为什么不会拖垮别的进程”——因为资源容器是隔离的一个容器的地址空间坏了内核直接把它回收别的容器不受影响。反过来一个线程把共享数据改坏了整个进程里的所有线程都会受影响这就是多线程程序的脆弱之处。2.2 上下文切换的成本到底从哪来这是概论里最容易被一句“切换有开销”带过去的地方。开销到底在哪我给你拆成四块。第一块是寄存器的保存和恢复。CPU 里的通用寄存器、程序计数器、栈指针、状态标志切换时都得存到内存里的进程控制块中再把下一个进程的存回去。这一块是纯机械操作几十条指令的量级。第二块是页表切换。如果切的是不同进程地址空间的映射表要换这会导致地址翻译缓存用来加速虚拟地址到物理地址转换的那层高速缓存里的大量条目失效。切换之后的一小段时间内几乎每次地址翻译都要多走几级查表内存访问明显变慢。第三块是缓存局部性丢失。CPU 的各级高速缓存里存的是上一个进程的热数据切过来之后这些数据全成了垃圾新进程要从头把工作集灌进缓存。这个成本不体现在切换的瞬间而体现在切换之后的几百微秒里。第四块是调度器自身的时间。选下一个进程、更新统计、处理优先级这些也是要花 CPU 的。把四块加起来一次进程切换的量级大概在几微秒。听起来不多但你要乘上频率。假设一个系统每秒发生 20 万次切换单次按 3 微秒算就是 0.6 秒——相当于一个核心的六成算力被切换吃掉了一点正事没干。注意切换到线程比切换到进程便宜主要省在第二块和第三块。同进程内线程共享地址空间不用换映射表地址翻译缓存也不至于全废。这是“线程比进程轻量”的量化解释。2.3 调度策略所有取舍都围绕三个指标打架调度的目标说白了就是在三个互相冲突的指标之间找平衡吞吐单位时间干完多少活、响应延迟从提交到开始处理要多久、公平性每个任务都能分到合理的份额。这三个指标天然打架。想提高吞吐就该让长任务一次跑够减少切换——但这样短任务就得干等响应延迟变差。想改善延迟就该频繁切换让每个任务都快速得到一次机会——但切换本身又是开销吞吐会掉。所以你会看到调度器普遍采用“多级”的设计。交互型任务比如你敲一下键盘要有反应给高优先级、短时间片计算型任务给低优先级、长时间片让它跑得久一点。现代通用系统上常见的那套公平调度思路核心是给每个任务记一个“虚拟运行时间”谁跑得少谁的虚拟时间就小下次优先选它这样长期看大家分到的份额是公平的。实现上通常用一棵平衡树按虚拟时间排序选下一个任务就是取树的最左节点复杂度是对数级。实时任务的处理又是另一套逻辑。实时分两种一种是“必须在这个时间点之前完成超了就失败”另一种是“尽量快但不硬性保证”。前者要有优先级抢占后者靠调度类优先级排序。这里有个常见误区很多人以为把任务设成最高优先级就万事大吉结果一堆实时任务互相抢占反而谁都跑不完。优先级不是免费的午餐。2.4 并发的三个经典陷阱概论讲并发通常只讲互斥和同步但实际操作中真正让人掉头发的是下面三个。竞态条件。两个执行流对同一份数据“读—改—写”中间任何一步被打断结果就可能错。典型例子是计数器自增看起来是一条语句编译出来是“读、加、写”三步。两个人同时读到了 100各自加一写回 101实际应该加两次到 102。解决办法是让这三步变成不可分割的原子操作或者用锁把它们圈起来。死锁。四个条件同时成立才会发生资源互斥、持有并等待、不可剥夺、循环等待。反过来说破掉任何一个就能避免。工程上最常用的破法是破“循环等待”——给所有锁定一个全局顺序任何地方都按这个顺序加锁。我踩过一次坑两个模块各自封装了一套锁A 模块先拿自己的锁再调 BB 模块先拿自己的锁再调 A两边同时跑就锁死了。后来定了个规矩跨模块调用时不允许持有本模块的锁问题消失。优先级反转。这个最隐蔽。高优先级任务在等一把锁锁被低优先级任务持有这时候一个中优先级任务抢占了低优先级任务导致低优先级任务迟迟释放不了锁高优先级任务被间接卡住——它实际被中优先级任务阻塞了。解决办法是优先级继承持有锁的低优先级任务临时提升到等待者的优先级或者优先级天花板。这类问题在实时系统里是致命的在普通业务系统里表现就是“偶尔莫名其妙卡一下”。3. 内存管理虚拟内存是概论的分水岭3.1 地址翻译是怎么发生的我判断一个人操作系统有没有入门就看他能不能把“一个变量取值的地址翻译过程”讲清楚。过程是这样的你的代码里访问一个变量编译出来是一条访存指令里面的地址是虚拟地址。CPU 拿到这个虚拟地址交给内存管理单元去翻译。翻译的依据是页表页表把虚拟地址空间切成一页一页常见页大小是 4KB每一页对应一个物理页框。页表本身也很大所以通常是多级结构。以常见的 48 位虚拟地址为例低 12 位是页内偏移剩下 36 位分成四段每段 9 位对应四级页表索引。翻译时从根页表开始一级一级往下查最后拿到物理页框号加上页内偏移就是物理地址。四级查表意味着一次访存要额外做四次内存读这谁受得了。所以硬件里有一层专门的缓存叫地址翻译缓存TLB存最近用过的“虚拟页到物理页框”的映射。命中就直接出结果不命中才走完整查表流程。这就解释了为什么“进程切换会导致性能下降”——切换后地址翻译缓存里全是上一个进程的映射新进程要重新积累。也解释了为什么“大页”把 4KB 的页换成 2MB 的页能提速——同样大小的地址空间需要的页表项少了五百多倍缓存能装下的映射范围大了命中率自然高。3.2 缺页一次内存访问可能要等五个数量级正常访存在缓存命中时是纳秒级主存访问是百纳秒级。但如果这一页根本不在物理内存里就会触发缺页异常内核要把这一页从磁盘读进来这个代价就完全不一样了。我给个量级对比你感受一下主存访问大概 100 纳秒固态盘随机读大概 100 微秒量级机械盘随机读大概 10 毫秒量级。从 100 纳秒到 10 毫秒差了五个数量级。这就是为什么“内存不够开始换页”的系统会卡到没法用——不是慢一点是慢几万倍。缺页分几种情况处理成本差很多。第一种是“页从来没被加载过”比如程序刚启动代码段要按需从可执行文件读进来这叫按需调页。第二种是“页被换出去了”要从交换区读回来。第三种是“写一个共享页”需要先复制一份这叫写时复制。第四种是“访问了非法地址”直接触发段错误把进程干掉。写时复制这个机制值得单独说。创建子进程时内核并不真的复制父进程的整块内存而是让父子共享同一批物理页同时把这些页标记成只读。任何一方要写就触发缺页内核这时候才真正复制一份出来改掉映射。这个设计的精妙之处在于如果子进程创建后立刻执行一个新程序这是最常见的情况那整块内存根本不需要复制白白省下大量开销。如果父子都要读写那就按需复制只复制真正被改过的页。3.3 看懂内存指标为什么“剩余内存”是个伪概念很多人看内存只看“还剩多少”。这个指标在现代系统上基本没有意义因为内核会把空闲内存拿去做缓存看起来“用了很多”实际上是可回收的。正确的看法是看三个数已用、可回收、可用。可回收的主体是文件页缓存和可回收的 slab 缓存这部分在内存紧张时能被内核主动释放。可用 空闲 可回收这才是你该关心的数字。还有一个指标经常被误读虚拟内存大小。一个进程的虚拟内存可能显示几个 GB实际占用的物理内存只有几十 MB。这不矛盾因为虚拟地址空间只是“申请了”不代表“用了”。真正占物理内存的是驻留集大小。很多运行时环境会预先向系统申请一大块虚拟地址空间用的时候再一点点提交所以虚拟内存大得吓人实际占用很小。提示判断内存有没有问题看趋势不看瞬时值。持续上涨且可回收部分不增长才叫泄漏瞬时高但可回收多只是缓存用得多不用管。3.4 内存问题的三条排查路径第一条路径是泄漏。现象是进程驻留内存持续上涨涨到一定程度被系统强制终止内存不足时内核会按一套评分挑一个进程杀掉通常挑占用大、优先级低的。排查手段是看进程的内存分段堆是不是在涨涨的速率和请求量是否相关。堆内存泄漏最常见的原因是长生命周期容器里塞了短生命周期对象比如全局缓存没有淘汰策略、监听器注册了没反注册。第二条路径是换页抖动。现象是系统整体变慢磁盘读密集CPU 里有相当比例花在等 I/O 上。这时候要看交换区的读写量如果持续有大量换入换出说明工作集已经超过物理内存系统在颠簸。这种状态下调应用参数没用只能加内存或者减并发。第三条路径是碎片。现象是明明总空闲内存够但申请大块连续内存失败。这在长时间运行的服务上比较常见尤其是频繁分配释放不同大小对象的场景。缓解办法是用对象池、固定大小分配器或者干脆把大对象预分配好。4. 文件系统与 I/O概论里最贴近日常的那部分4.1 文件不只是“一个名字加一堆数据”文件系统这层抽象很多人理解得太浅以为就是“文件名对应内容”。实际上一个文件至少包含三部分名字、元数据、数据块。元数据存放在一个叫索引节点inode的结构里包含文件大小、权限、所有者、三个时间戳访问、修改、元数据变更、以及指向数据块的指针。目录本质上是一张表表项是“文件名到索引节点编号”的映射。这就是为什么同一个文件可以有多个名字硬链接——多个目录项指向同一个索引节点删掉一个名字只是减引用计数数据还在。这个结构解释了一堆奇怪现象。为什么重命名一个巨大文件是瞬间完成的因为只改了目录项里的名字数据一个字节都没动。为什么删除一个大文件可能很慢因为要释放大量数据块还要更新空闲块位图。为什么跨文件系统移动文件慢、同文件系统内移动快因为前者要复制数据后者只是改映射。还有一个特别容易忽略的点文件描述符是有限资源。每打开一个文件、一个网络连接都要占一个描述符。描述符耗尽的表现是“打开文件失败”“连不上数据库”但 CPU、内存看起来都很正常。这个坑我在早期项目里踩过一个连接池没设上限跑了一段时间之后所有新连接都建不上排查了半天才发现是描述符打满了。4.2 写入成功不等于落盘页缓存与回写这是概论里最实用、也最容易被跳过的一节。当你调用写入接口往文件里写数据数据并没有立刻到磁盘上而是先写进了内核的页缓存同时把对应的页标记为“脏”。真正写盘是由内核的后台回写线程周期性触发的触发条件通常是“脏页占比超过阈值”或者“脏页存在时间超过阈值”。这个设计的理由是性能。磁盘的随机写慢得离谱攒一批再写能大幅提升吞吐。但代价是数据一致性窗口从写入返回到数据真正落盘中间有一个时间差这个时间差里如果掉电或者内核崩溃数据就没了。要突破这个窗口就得用同步落盘的调用让数据强制刷到持久介质上。代价是每次写都要等磁盘确认性能会掉一到两个数量级。所以正确的做法是分级日志这类丢一点能接受的数据走普通写入账务、订单这类不能丢的写完必须强制刷盘而且要在关键节点批量刷不要每条都刷。注意这一点在容器环境里尤其容易出问题。数据卷的挂载参数、存储介质的缓存策略都会影响“落盘”的真实含义。做数据安全相关的功能一定要在目标环境上实测掉电场景别拿本地开发机的表现当结论。4.3 I/O 模型从“干等”到“事件驱动”I/O 模型这块概论通常讲五种阻塞、非阻塞、I/O 多路复用、信号驱动、异步。名字很唬人但核心问题只有一个当数据还没准备好时你的线程在干什么阻塞模型下线程就挂在那儿等什么也干不了。一个连接一个线程连接数一多线程数就爆炸光是线程栈和切换开销就能把机器拖垮。非阻塞模型下线程问一句“好了没”没好就立刻返回去干别的。但这样你得不停地轮询轮询太密浪费 CPU太疏延迟高。I/O 多路复用是主流的折中方案把一堆连接交给内核统一盯哪个就绪了内核告诉你你只处理就绪的那些。一个线程就能管成千上万个连接。它的关键改进在于“内核帮你做轮询”这件事的效率——早期实现每次调用都要把所有监听的描述符从用户态拷到内核态再遍历一遍连接数一多就退化成线性的后来的实现改成了内核里维护一张关注表事件到来时通过回调把就绪项挂到队列上取的时候只取就绪的部分复杂度降到和数据量相关。这里有个实操细节值得说水平触发和边缘触发的区别。水平触发是“只要还有数据没读完就一直通知你”安全但可能重复通知边缘触发是“只在状态变化时通知一次”效率高但要求你必须一次把数据读干净否则剩下的数据要等下一次状态变化才处理容易卡住。新手用边缘触发最常见的错误就是读了一次就返回剩下的数据晾在那儿表现为“连接偶发卡死”。4.4 设备与中断内核怎么和硬件对话设备管理这块概论讲得比较抽象。我用一个具体流程把它串起来你敲了一下键盘这个信号是怎么变成程序里读到的一个字符的键盘产生电信号键盘控制器把它转成一个扫描码通过总线发给 CPU触发一个中断。CPU 收到中断暂停当前执行流保存现场跳到中断向量表里登记的处理入口。内核的中断处理程序读取扫描码把它转成键值放进输入缓冲区然后结束中断恢复现场之前被打断的执行流继续跑。这个流程里有两个关键设计。第一是中断上下文要尽量短。中断处理期间很多资源是不可用的处理太久会影响其他中断的响应。所以内核把中断处理拆成两半上半部只做最紧急的事读硬件寄存器、清中断标志剩下的耗时工作推到下半部延后执行。第二是DMA直接内存访问。大批量数据传输比如网卡收包、磁盘读块不需要 CPU 一个字节一个字节搬设备可以直接往内存里写写完再发个中断通知 CPU“活干完了”。这样 CPU 在数据传输期间可以去干别的事。理解了这一层你就能明白为什么“中断使用率”是个值得看的指标——中断太密集会把 CPU 打散让它没有大块时间跑业务逻辑也能明白为什么网卡多队列、磁盘多队列能提升性能——多个队列对应多个中断能分散到不同核心上并行处理。5. 系统调用用户态和内核态之间那道门5.1 一次系统调用的完整旅程用户程序不能直接操作硬件这是硬性规定。所以所有需要内核帮忙的事都得走系统调用这道门。完整流程是这样的你把参数放到约定的寄存器或者栈上把系统调用号放进另一个约定的寄存器然后执行一条特殊指令不同架构上名字不同作用是主动触发一个异常。CPU 收到这个异常从用户态切到内核态根据异常类型跳到内核预设的入口入口代码再根据系统调用号去一张函数表里找对应的实现执行完把返回值放回寄存器最后执行返回指令切回用户态。这一趟下来开销在哪模式切换本身要保存和恢复一部分寄存器内核态和用户态的权限检查参数需要做合法性校验比如你传的指针是不是真的属于你的地址空间还有切换导致的一小部分缓存污染。所以有个经验法则系统调用的成本比普通函数调用高一到两个数量级。这解释了很多性能优化套路。为什么读文件要用缓冲区一次读一大块而不是一个字节一个字节读因为每次读都是系统调用。为什么高性能网络库要把多个小包攒起来一次发同理。为什么有的程序用内存映射文件代替读写调用因为它把“每次读都进内核”变成了“缺页时才进内核”。5.2 用工具把“看不见的调用”变可见概论讲到系统调用就停了但真正让你有感觉的是把它观测出来。最直接的工具是系统调用跟踪器。它能让目标程序跑起来把每一次系统调用、参数、返回值、耗时全打出来。我第一次用它看一个“启动很慢”的程序发现它在启动阶段做了上万次小的文件读取每次读几十字节。改成一次读几 KB启动时间直接砍掉一大半。用的时候有几个注意点。第一跟踪本身有开销会显著拖慢目标程序所以别在压测环境里开着它跑性能测试。第二只跟特定类型的调用比如只跟文件相关的、只跟网络相关的能大幅降低噪音。第三注意统计而不是逐条看几万行输出里你要找的是“哪个调用次数异常多”“哪个调用耗时异常长”这两个信息比逐行读有用得多。还有一个更轻量的办法看系统的全局统计。很多系统会暴露“系统调用总次数”“各类调用的分布”这类汇总数据虽然看不到细节但能快速判断“这个进程是不是在疯狂进内核”。如果发现某个进程每秒几百万次系统调用那基本可以确定它在一个字节一个字节地读写优化空间很大。5.3 性能排查的三件套CPU、内存、I/O 怎么看概论的知识最终要落到“机器慢了怎么办”上。我总结了一个固定的三步走。第一步看 CPU 的分布不只看总量。用户态占比高说明在跑计算内核态占比高说明系统调用多或者中断多等待 I/O 占比高说明在等磁盘空闲高但系统卡那问题不在 CPU往内存和 I/O 上找。这个拆分能直接把搜索范围缩小一大半。第二步看内存的压力重点看可回收空间和换页活动。前面说过看“可用”不看“空闲”看换入换出速率不看单次值。第三步看 I/O 的队列。这里有个特别容易被误读的指标——平均负载。很多人把平均负载当成 CPU 使用率其实它统计的是“正在运行 等待不可中断 I/O”的进程数量。所以一个纯 I/O 密集的机器CPU 可能只用了 10%平均负载却能到 20。看到负载高但 CPU 空闲第一反应应该是查磁盘和网络而不是加 CPU。提示排查顺序不要乱。先确认是计算密集、内存压力还是 I/O 瓶颈再往下钻具体是哪个进程、哪个文件、哪个连接。反过来做容易在错误的方向上深挖半天。6. 概论怎么学才不空三条可动手的路径6.1 别从概念背起从现象问起我给的建议很直接不要按章节顺序学按问题学。准备好三个问题带着它们去翻概论。第一个“我的程序从双击到界面出现中间发生了什么”。这个问题会逼你走一遍加载、地址空间建立、动态链接、入口执行。第二个“两个程序同时改一个文件为什么不会互相覆盖”。这个问题会逼你理解文件描述符、偏移量、加锁、原子操作。第三个“内存明明够为什么程序还是被杀了”。这个问题会逼你看资源配额、控制组限制、内核的内存评分机制。带着问题看书看到相关章节会有“原来是这样”的感觉按顺序看看到第三章就忘了第一章。这个差别我体会很深第一遍通读我基本什么都没记住第二遍带着具体问题去查才真正建立起连接。6.2 三个能自己跑一遍的小实验光看不动手理解永远是浮的。这三个实验成本很低一台普通机器就能做。实验一观察虚拟内存和物理内存的差距。写一个小程序申请一大块内存比如 1GB但只往里面每隔 4KB 写一个字节。跑起来之后看这个进程的虚拟内存和驻留内存——你会发现虚拟内存是 1GB 多驻留内存只有几 MB。原因是你只触碰了少数几页其余的页只是“分配了地址”没有真正映射物理页。这个实验能让你彻底搞明白“申请”和“占用”是两件事。实验二观察写时复制。程序先申请一块内存并写满数据然后创建子进程父子都睡一会儿再看各自的内存。你会发现创建子进程的瞬间内存没有翻倍。再让子进程往那块内存里写点东西这时候物理内存才会涨上去。这个实验比看十遍文字描述都管用。实验三观察缓存和落盘的差别。往一个文件里连续写入大量数据记录耗时然后调用同步落盘的接口再写一遍记录耗时。两个数字通常差一个数量级以上。再做个对照写完不刷盘直接强制关机虚拟机里做别拿生产机试看文件里剩多少数据。这个实验会让你对“数据丢了”这件事有非常具体的感受。6.3 一份给不同基础的人的阅读顺序如果你完全没接触过顺序是先看进程和内存这两章因为这两块是所有其他内容的基础然后看文件系统因为它最贴近日常最后看设备和并发这两块可以慢一点。如果你写过几年业务代码可以直接从系统调用和性能观测入手边看边在自己机器上验证。你已有的工程直觉会让你理解得很快缺的只是把直觉和内核机制对应起来。如果你做过运维或者排查过线上问题建议反过来——先把你遇到过的问题列出来一个个往回追。你会有很多“原来当时是这个原因”的时刻这种学习效率比顺序阅读高得多。7. 常见问题与排查速查7.1 概念混淆清单下面这些是我在带人时被问得最多、也最容易搞混的对照。容易混淆的一对区别要点进程 vs 线程资源容器是否独立切换成本差在地址空间和翻译缓存虚拟内存 vs 物理内存前者是每个进程独享的地址视图后者是真实有限的硬件资源缓冲区 vs 缓存缓冲区主要为了合并小操作缓存主要为了复用已有结果阻塞 vs 同步阻塞说的是线程状态同步说的是调用是否等待结果完成两者不等价平均负载 vs CPU 使用率前者还包含等待 I/O 的进程不能直接当 CPU 压力看并发 vs 并行并发是逻辑上同时推进并行是物理上同时执行硬链接 vs 软链接前者共享同一索引节点后者是独立文件存了个路径页缓存 vs 交换区前者缓存文件数据提升读性能后者是内存不够时的临时落脚点这张表建议存下来每次概念打架的时候对一眼。7.2 现象到原因的速查现象优先怀疑第一步验证CPU 高但吞吐没涨空转、锁竞争、频繁切换看上下文切换次数和内核态占比CPU 不高但响应很慢I/O 等待、锁等待、换页看等待 I/O 占比和磁盘队列内存持续上涨泄漏、缓存无淘汰看驻留内存趋势和堆分段系统整体卡顿、磁盘狂转换页抖动看换入换出速率提示打开文件失败描述符耗尽看进程的描述符上限和当前数量写入后重启数据丢了没有强制落盘检查关键路径是否调用了同步刷盘连接偶发卡死边缘触发没读干净检查读循环是否读到无数据为止偶发长时间停顿优先级反转、大页分配、后台回写峰值对照停顿时间点看后台任务7.3 几个我踩过的坑第一个坑把平均负载当 CPU 指标。早年我看到负载高就加 CPU 核数加完发现没用因为瓶颈在磁盘。后来养成习惯看到负载高先看 CPU 空闲率空闲高就绝不往计算资源上想。第二个坑以为写入返回就是落盘了。一个记录关键状态的功能测试环境跑得好好的线上一次机房供电切换丢了最后几秒的数据。事后复盘就是少了一次强制刷盘。这件事之后我定了个规矩凡是“丢了要背锅”的数据写路径上必须有明确的同步点而且要写下为什么加、加在哪。第三个坑忽略描述符上限。一个服务压测时表现良好上线后跑几天就开始报连接错误。查了半天是描述符上限设得太保守加上连接回收不及时慢慢就耗尽了。这类问题不会在短时间压测里暴露必须在长时间稳定性测试里才能看出来。第四个坑跨模块加锁顺序不一致导致死锁。前面提过两个模块互相调用时各自持有自己的锁形成一个环。这种问题在低并发下几乎不会出现一到高峰期就锁死。后来的做法是画一张锁的依赖图只要图里有环就重构。第五个坑以为虚拟内存大就是内存泄漏。有个服务虚拟内存显示十几个 GB被报警系统盯上了。实际查下来是运行时预申请的地址空间驻留内存只有几百 MB完全是虚惊。教训是监控指标要选对选错了就是自己给自己制造噪音。概论这门东西学一遍只能知道有这些事学两遍能对上号真正理解得靠你在具体问题上撞几次。我自己的体会是每次排查完一个底层问题回头再翻概论总能在某一页上看到当时该看却没看懂的那句话。所以别指望一次读透把它当工具书遇到问题就回来翻——翻的次数多了那张资源管理的地图自然就长在脑子里了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑