ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

操作系统核心笔记:进程调度、虚拟内存、文件系统与IO多路复用

2026/9/29 1:38:03 拓冰建站 浏览量
操作系统核心笔记:进程调度、虚拟内存、文件系统与IO多路复用 操作系统这门课很多人是这么过来的期末前一周翻开课本死记硬背进程调度算法和页面置换算法考完试就还给老师。等真正工作几年后线上服务突然CPU飙到百分之百、内存缓慢泄漏、磁盘IO莫名其妙打满被逼着回头翻那些当年觉得没用的知识才发现进程状态机、虚拟内存、页表这些东西一个都躲不掉。我最近花了两周时间把小林Coding的操作系统笔记从头到尾啃了一遍边读边在本地Linux机器上做验证整理出这份读书笔记。它不是什么知识点罗列而是我自己消化之后重新组织的理解路径加实操记录。如果你正在准备操作系统相关的面试、考研复习或者单纯想把这块基础补扎实这份笔记应该能帮你少走一些弯路。下面我按笔记的脉络把核心概念、实操验证、踩过的坑全部摊开来讲。1. 为什么值得花时间系统啃一遍操作系统1.1 操作系统的知识在整个技术栈里到底处于什么位置我先讲一个很实际的感受。很多人学编程是从写业务代码入手的调个接口、连个数据库、跑个框架日子也能过。但当你遇到性能问题、并发问题、资源调度问题的时候会发现所有上层框架的底层都压着同一套东西进程怎么被调度、内存怎么被分配、IO怎么被处理。小林Coding这份笔记的价值在于它没有一上来就堆名词而是沿着「一个程序从磁盘上被执行起来中间到底发生了什么」这条线去串。我的理解是操作系统的知识体系可以拆成四块硬骨头进程与线程的抽象、内存管理、文件系统、IO模型。这四块之间不是孤立的。进程要运行就得占内存内存不够就要换出到磁盘换出换入又牵扯到文件系统而进程等待IO的过程又涉及调度。所以任何一块单独背知识点都很容易忘只有把它当成一条链路去理解才能记住。这也是我读这份笔记时的核心方法每读一章都问自己这个机制解决的是上一步留下的什么问题。举个例子虚拟内存这个概念很多教材上来就给定义说它是把物理内存抽象成连续的地址空间。但真正的「为什么」是早期程序直接操作物理地址多道程序并发时一个程序改到另一个程序的内存就崩了而且程序大小受限于物理内存。虚拟内存用页表加MMU这套硬件机制把这两个问题一起解决了。理解了这一层你再看缺页中断、页面置换就不是孤立的知识点而是这条逻辑链上的必然环节。1.2 为什么选择小林Coding的笔记作为主线市面上的操作系统资料大致分三类一类是经典教材比如《操作系统概念》那本厚书理论完整但读起来累例子偏抽象一类是考研辅导书知识点密集但为了应试很多地方只告诉你结论不告诉你推导还有一类是网络上的技术笔记优点是图多、语言通俗、抓重点。小林Coding的笔记属于第三类里做得比较系统的。我选它作为主线的原因有三个。第一它的行文方式是先给场景再给机制比如讲进程通信会先问「两个进程要交换数据为什么不能直接读写对方内存」然后再引出管道、消息队列、共享内存这些方案符合我的理解习惯。第二它把很多面试高频考点和原理串在一起讲比如「线程切换为什么比进程切换开销小」这个问题在笔记里是从页表、上下文、CPU缓存三个角度拆开的比单纯背一句话管用。第三篇幅适中适合当复习主线遇到不够细的地方我再回去翻教材补效率比直接啃大厚书高很多。需要说明的是笔记本身是别人整理的知识框架我这份读书笔记的做法是沿着它的脉络走但每个关键机制我都会在本地环境里动手验证一遍把验证结果和自己的理解记下来。所以你会看到很多命令行、参数、实测现象这些是我自己加的不是照搬。提示选主线资料的时候不要贪多。同时开三本书的结果通常是三本都读不完。选一本逻辑顺的当骨架其他资料只用来查漏补缺这是我试过最省时间的组合。2. 进程与线程先分清抽象再看调度2.1 进程、线程、协程到底差在哪这一块是我读笔记时停留最久的地方因为进程和线程的定义几乎人人都能背但真正理解它们的关系要落到三个维度资源拥有、调度单位、切换开销。进程是资源分配的基本单位操作系统会给每个进程分配独立的虚拟地址空间、文件描述符表、信号处理表这些东西。线程是CPU调度的基本单位同一进程内的多个线程共享地址空间和文件描述符但各自有独立的栈和寄存器上下文。我用一个生活类比来记进程像一栋独立的房子有自己完整的水电管网线程像同一栋房子里的几个租客共用厨房和卫生间但各睡各的房间。租客之间沟通成本低但一个人把厨房炸了全屋都遭殃——这正好对应线程共享内存带来的安全问题。协程则是用户态的调度单位切换完全在用户空间完成不经过内核所以开销比线程还小。笔记里提到协程的一个关键点是它适合IO密集型场景因为等待IO时可以主动让出不需要内核参与。我实测过用Go的goroutine跑十万个并发任务内存占用远小于开十万个线程原因就是goroutine的栈是动态增长的初始只有几KB而线程栈通常是固定的MB级别。衡量切换开销我习惯从三个角度算账一是上下文保存恢复的寄存器数量二是是否需要切换页表也就是换地址空间三是CPU缓存和TLB的失效代价。进程切换要换页表TLB会大面积失效缓存命中率下降线程切换在同一地址空间内这两项代价都小得多。所以在高并发场景下多线程模型通常比多进程模型更划算代价是编程复杂度上升共享数据要加锁。2.2 进程调度算法怎么动手推演才记得住调度算法光看名字很容易混先来先服务、短作业优先、时间片轮转、多级反馈队列背完就忘。我的办法是拿一组具体数据手动推演一遍算出来才记得住。假设有四个进程到达时间和需要运行的时间如下表进程到达时间运行时间A08B14C29D35如果按先来先服务调度执行顺序就是A、B、C、DA在0时刻开始跑8个单位B等到8时刻才跑如此类推平均等待时间会很长。如果换成短作业优先需要先比较各进程的运行时间但由于只能在到达后才能决策实际执行顺序会变成A先跑因为0时刻只有A之后B、D、C按短到长排。你可以自己把时间轴画出来算出每个进程的等待时间和周转时间再算平均值对比两种算法的差距。我做完这个推演之后才真正理解为什么短作业优先理论上平均等待时间最短以及为什么它会导致长作业饥饿。多级反馈队列更复杂一点它的核心设计是新进程进最高优先级队列用完一个时间片没跑完就降到下一级只有高优先级队列空了才调度低优先级。这个设计同时照顾了短作业和IO密集型进程因为IO型进程经常在时间片内就主动让出。手动推演的时候我会在纸上画几条队列模拟时钟滴答看进程怎么在队列之间迁移。这个过程比看任何文字描述都管用。2.3 进程间通信的几种方式各自适合什么场景进程之间不能直接读写对方内存这是隔离性带来的代价所以需要专门的通信机制。笔记里列了管道、消息队列、共享内存、信号量、信号、套接字这几种。我按「数据量大不大、是否跨机器、是否要同步」三个维度做了个对照通信方式数据量是否跨主机同步机制典型场景匿名管道小否自带阻塞父子进程简单传递命名管道小否自带阻塞无亲缘关系进程消息队列中否自带异步解耦共享内存大否需配合信号量高频大数据交换套接字视情况是自带网络服务我的经验是共享内存是这几种里速度最快的因为它直接映射同一块物理内存不涉及数据拷贝。但快有快的代价共享内存本身不提供同步多个进程同时写会出问题所以必须配合信号量或互斥锁使用。我在本地写过一个生产者消费者的小实验用共享内存加信号量两个进程交替读写一个整型数组实测下来吞吐比用管道高了将近一个数量级。这个实验让我彻底记住了共享内存的定位它解决的是数据搬运开销不解决并发控制。注意共享内存用完之后一定要记得解除映射并删除否则会残留在系统里占用内存。我在实验时因为进程崩溃没走到清理逻辑导致/dev/shm下残留了文件一开始还以为是内存泄漏排查了半天。用ipcs和ipcrm这两个命令可以查看和清理。3. 内存管理从物理地址到虚拟地址的关键跃迁3.1 虚拟内存到底解决了什么问题虚拟内存这一章如果只记「给每个进程独立的地址空间」这句话面试能答但理解不深。我把它的价值拆成三个层面来讲。第一是隔离。没有虚拟内存的时候程序直接操作物理地址恶意或有bug的程序能读写别的程序甚至内核的内存系统毫无安全性可言。有了页表映射进程A根本看不到进程B的物理页越界访问会触发缺页中断或段错误。第二是容量扩展。程序不再受物理内存大小限制可以用「部分装入」的方式运行暂时用不到的页放在磁盘的交换区里用到的时候再换进来。这就是请求分页的思想。我在本地用一个小程序验证过申请一块远超物理内存的数组并逐页写入观察swap分区的变化确实能看到内存被换出换入。第三是共享和复用。动态库在物理内存里只存一份多个进程的虚拟地址映射到同一份物理页既省内存又省加载时间。fork系统调用里的写时复制也是同一个思路子进程初始和父进程共享物理页只有当某一方要写的时候才复制一份避免了大量无谓拷贝。理解虚拟内存的关键对象是页表。页表记录虚拟页号到物理页框号的映射每个进程一张。页表项里除了物理页号还有几个重要的标志位有效位表示这一页是否在内存里脏位表示是否被修改过访问位表示最近是否被访问过。后面讲页面置换算法的时候这些标志位就是决策依据所以一定要先记清楚。3.2 页面置换算法的手动计算过程页面置换算法是必考内容我把它当成记忆的硬骨头用同一个访问序列对比四种算法的表现算一遍就清楚了。设访问序列为3、4、5、4、3、6、3、4、5、6物理块数为3。下面分别推演。先进先出算法的思路最简单淘汰最早进入内存的页。我模拟下来缺页次数比较多因为最早进来的页可能还要被频繁访问。这种算法的缺点是会出现「Belady异常」就是物理块数增加反而缺页次数上升这在实际系统里很反直觉所以现代系统基本不用纯FIFO。最近最久未使用算法淘汰最长时间没被访问的页它利用了程序访问的局部性原理理论表现好但实现代价高因为要精确记录每一页的访问时间硬件支持成本大。实际系统里常用的是它的近似实现比如时钟算法。时钟算法把页组织成一个环形链表每页有一个访问位指针转动时检查访问位为1就置0并跳过为0就淘汰。这个算法实现简单效果接近最近最久未使用是我觉得性价比最高的方案。最佳置换算法是理论上的最优解淘汰未来最长时间内不会被访问的页但它需要预知未来所以只能用来做参照衡量其他算法的差距。我建议你至少手动推演两种算法把每一步内存里的页、缺页次数、置换次数都写出来。纸上推一遍比看十遍图都管用。我当时推完最大的收获是算法之间的差距本质上取决于「预测未来访问模式」的准确度谁猜得准谁缺页少这个视角一建立所有算法的设计意图都串起来了。3.3 内存碎片与分配策略的实际影响内存碎片分两种内部碎片是分配出去但用不完的部分外部碎片是空闲但太小无法利用的零散空间。分页机制会产生内部碎片因为最后一页通常填不满分段机制会产生外部碎片因为段的大小不固定。分配策略我印象最深的是伙伴系统Linux就是用这个来管理物理页框的。它的思路是把内存按2的幂次划分申请时找到最小的能容纳的块不够就向上分裂释放时如果相邻的伙伴块也空闲就合并。这个设计把分裂和合并的复杂度控制得很好也方便管理不同大小的请求。读这一节的时候我在本地观察过/proc/buddyinfo能看到不同阶数的空闲页框数量。长期运行的机器上高阶页框往往很少因为内存被切成碎片了。这解释了一个实际问题为什么有时候申请大块连续内存会失败明明总空闲内存还有很多。原因就是没有连续的大块了。要缓解这个问题一是尽早申请大块内存二是用内存池预先分配三是调整分配器的行为。这些都是笔记里没细讲但实际工作中很有用的点。4. 文件系统与IO被忽视但极其重要的一章4.1 文件系统的分层设计思路很多人学操作系统会跳过文件系统觉得不如进程和内存重要。但从排查问题的角度看文件系统的知识用得非常多。我先讲它的分层最上面是给用户和程序用的接口层open、read、write这些系统调用往下是逻辑文件系统负责目录结构和文件元数据再往下是文件组织模块负责逻辑块到物理块的映射最底层是设备驱动和存储介质。这个分层的好处是每层只关心自己的职责。比如你要换一种文件系统只要实现对应的逻辑层和映射层上层的系统调用接口不用动。Linux里可以把ext4、xfs、btrfs这些不同的文件系统挂载在同一棵目录树下靠的就是虚拟文件系统这一层抽象。虚拟文件系统定义了一套通用接口具体文件系统各自实现用户无感知。inode是文件系统里最重要的概念之一。它记录文件的所有元数据大小、权限、时间戳、数据块指针唯独不记录文件名。文件名放在目录项里目录项指向inode。这解释了一个现象同一个文件可以有多个硬链接因为它们指向同一个inode而软链接是一个独立的文件内容是目标文件的路径。4.2 从一次文件读取看零拷贝的价值read系统调用把一个文件读进内存再发到网络传统路径要经过四次拷贝磁盘到内核缓冲区、内核缓冲区到用户缓冲区、用户缓冲区到socket缓冲区、socket缓冲区到网卡。每一次拷贝都是CPU在搬数据数据量大时开销非常明显。零拷贝技术要解决的就是这个重复搬运问题。mmap加write的方式把内核缓冲区和用户缓冲区映射到同一块物理内存省掉一次拷贝sendfile更进一步数据直接从内核缓冲区送到socket完全不经过用户空间。我在本地用dd和简单的socket程序做了个粗糙对比发送一个大文件用传统read加write和用sendfileCPU占用率差距肉眼可见。当然这只是定性观察精确数据要看具体场景和文件大小。这里的关键认知是性能优化很多时候不是让CPU算得更快而是让它少干重复的活。零拷贝就是一个典型例子。理解了这一点你再看IO多路复用、异步IO这些技术思路就清晰了——它们都在减少等待和搬运的浪费。4.3 IO多路复用的三种实现对比IO多路复用是网络编程的基础select、poll、epoll这三种机制经常被拿来对比。我的理解是它们解决同一个问题的三代方案核心差异在数据结构和事件通知方式上。select用位图表示文件描述符集合有数量上限通常是1024每次调用都要把所有描述符从用户态拷到内核态返回后还要遍历一遍找出就绪的。poll改进了数量限制用链表存描述符但拷贝和遍历的问题还在。epoll把描述符集合放在内核里维护用红黑树管理就绪的描述符通过回调放进一个就绪链表调用时直接返回就绪的完全避免了遍历和重复拷贝。我在本地写过一个echo服务器分别用select和epoll实现压测下epoll在连接数上千后优势非常明显。这个实测让我记住了一个结论连接数少且活跃的时候几种机制差别不大连接数多但活跃比例低的时候epoll的机制优势才体现出来因为它只处理就绪的那部分。这个结论和很多资料上讲的一致但自己动手测过之后印象完全不一样。提示学习IO模型时一定要分清「同步异步」和「阻塞非阻塞」这两个维度。它们描述的是不同层面阻塞非阻塞说的是调用发起后是否立即返回同步异步说的是数据拷贝这个动作由谁完成。两两组合有四种模型笔记里的分类表我建议自己画一遍。5. 网络与并发操作系统视角下的连接处理5.1 一个网络请求在内核里走过的路径把进程、内存、IO三块知识串起来的场景就是一个网络请求的处理过程。客户端发起连接数据包到达网卡网卡通过DMA写入内核缓冲区触发中断内核协议栈逐层解析最终把数据放到对应socket的接收缓冲区唤醒等待在这个socket上的进程。这个链路里每一步都能对应到前面学过的机制。中断处理对应并发和调度协议栈解析涉及内存分配socket缓冲区涉及内存管理进程被唤醒涉及调度器。我强烈建议你把这个链路完整地理解一遍因为它是排查网络问题的基础框架。当线上出现请求延迟高的时候你可以在这个链路的每一环去问是数据包没到还是到了没被处理还是处理了但进程没被调度上来。5.2 并发模型选型的几个考量点服务器处理并发连接常见的模型有多进程、多线程、IO多路复用加线程池、协程。选哪个不是拍脑袋要看几个具体因素。连接数规模是关键。几百个连接多线程够用上万甚至几十万连接多线程的栈内存和切换开销就撑不住了需要IO多路复用或者协程。业务类型也重要CPU密集型任务适合线程数接近核数避免过度切换IO密集型任务可以开更多并发单元因为它们大部分时间在等。还有一个容易被忽视的点是错误隔离。多进程模型下一个进程崩了不影响其他进程适合对稳定性要求极高的服务多线程模型下一个线程崩溃可能拖垮整个进程所以要做好异常处理。这个取舍在笔记里没有展开但我在实际项目里吃过亏所以特意补上。并发模型适合连接数内存开销隔离性适用场景多进程中低高好稳定性优先的服务多线程中中差通用业务IO多路复用高低中高并发网关协程极高很低中IO密集高并发5.3 用strace观察系统调用的实操理论学再多不如亲眼看看程序在系统调用层面干了什么。strace这个工具能跟踪进程的所有系统调用我读笔记的时候用它验证过不少结论。比如验证一次文件读取可以用strace -e traceopenat,read,write,close追踪一个cat命令你能清楚看到openat打开文件、read读数据、write输出、close关闭的完整序列。再比如验证前面说的零拷贝对比sendfile和read加write两种实现从系统调用序列上能直接看出数据经过了哪些环节。用法上要注意几个参数-f跟踪子进程-T显示每个调用耗时-c做统计汇总。我排查一个启动慢的问题时用strace -c发现某次启动在某个系统调用上反复重试一下就定位到了。这个工具是理论到实践的桥梁学操作系统不亲手用一次太可惜了。注意strace会显著拖慢被跟踪进程因为它要拦截每一次系统调用。生产环境上排查问题要谨慎使用最好在测试环境复现。另外它只能看到系统调用层面的信息看不到用户态的耗时分布这时候要配合perf或者火焰图一起用。6. 复习过程中的典型卡点与排查速查表6.1 我踩过的几个认知陷阱第一个陷阱是混淆「并发」和「并行」。并发说的是多个任务在时间上重叠可能是在单个CPU上轮流切换实现的并行说的是多个任务真正同时执行需要多核。很多人说起多线程就说并行其实在单核上一段时间内只有一个线程在跑那是并发。这个区分在分析性能瓶颈时很重要如果是并发瓶颈可能在调度开销如果是并行瓶颈可能在核心数和任务划分。第二个陷阱是把页表当成一个简单的数组。实际系统里用的是多级页表因为单级页表太大。以32位地址空间、4KB页大小为例页内偏移12位页号20位单级页表就是2的20次方个表项每个4字节就是4MB每个进程一张进程一多内存就爆了。多级页表把页号再分段用两级或三级索引大部分中间表项可以不存在省下大量空间。理解了多级页表的必要性你才能理解地址翻译为什么需要多次访存以及TLB为什么这么重要。第三个陷阱是以为有了虚拟内存就可以无限申请内存。虚拟地址空间确实很大但物理内存和交换区是有限的。申请的时候是惰性分配真正写入才建立映射写入量超过物理内存加交换区的时候进程就会被系统杀掉这就是常说的内存不足。我在本地故意申请并写满超大内存亲眼看到进程被终止这个现象比任何描述都深刻。6.2 高频问题排查速查表我把学习和实操中遇到的问题整理成表方便你遇到类似情况时快速定位现象可能原因排查方向CPU使用率高但吞吐低频繁上下文切换或锁竞争看上下文切换次数看锁等待内存缓慢增长泄漏或缓存未释放看进程内存曲线看页缓存磁盘IO高但应用不忙日志刷盘或换页看IO分布看swap活动连接建立慢队列溢出或端口不足看连接队列看端口范围进程被系统终止内存超限看系统日志看内存水位这张表的用法是先根据现象缩小范围再用具体工具验证。比如CPU高先用top看是用户态还是内核态占的用户态高看是哪个函数内核态高看是不是系统调用太多。一步步排除比盲目猜测高效得多。6.3 关于怎么把笔记变成自己的东西我最后分享一个方法。读完一份笔记或者一本书判断学没学会的标准不是能不能复述而是能不能用它解释一个你没见过的问题以及能不能动手验证一个结论。我的做法是每读完一块内容就给自己出一道验证题。读完调度算法题目是「写一个程序观察多进程和多线程在大量计算下的上下文切换差异」读完内存管理题目是「用命令观察一个进程的虚拟地址空间布局」读完文件系统题目是「对比两种IO方式发送同一文件的开销」。这些题目不需要多复杂但做完之后知识就从纸面变成了手上的工具。笔记是别人的知识框架你的理解才是自己的。同样一份小林Coding的操作系统笔记在不同人手里价值差别很大差别就在于有没有动手验证、有没有用自己的话重新组织。我这两周做下来的体会是操作系统这门课最忌只读不练凡是能跑起来看的结论都值得跑一遍。后续如果你想把这块内容继续往深走可以从两条线扩展一条是沿着Linux内核源码看具体实现比如调度器、内存分配器、虚拟文件系统另一条是结合性能分析工具做实战perf、ftrace、eBPF这些能让你看到内核里的真实行为。两条线都挺硬但走进去之后你对整个计算机系统的理解会上一个台阶。