操作系统常见面试题总结(下)
这篇《操作系统常见面试题总结(下)》承接上篇,重点放在 内存管理、虚拟内存、分页分段、TLB、缺页中断、页面置换、I/O 多路复用、零拷贝、文件系统和磁盘调度。
如果说上篇更偏“进程、线程和并发控制”,这篇更偏“程序运行时到底怎么用内存、怎么做 I/O、怎么和文件系统交互”。这些内容看起来离业务代码比较远,但很多后端问题最终都会落到这里:为什么 mmap 能少一次拷贝?为什么 epoll 能撑住大量连接?为什么同一个虚拟地址在不同进程里互不影响?为什么频繁缺页会让系统变慢?
建议阅读时抓住一条主线:操作系统通过虚拟内存管理地址和隔离,通过 I/O 机制管理数据流动,通过文件系统管理持久化数据。把这条线串起来,很多零散概念就不再只是名词。
内存管理
内存管理主要做了什么?

面试里回答“内存管理做什么”,可以从 5 件事说起:
- 分配和回收内存:用户态分配器会通过
brk()、mmap()等接口向内核申请或释放虚拟地址区域;内核再管理物理页和内核对象。 - 地址转换:程序访问虚拟地址,CPU 中的 MMU 配合页表和 TLB,把虚拟地址翻译成物理地址。
- 进程隔离和权限保护:每个进程都有自己的地址空间,页表项还能记录读、写、执行、用户态/内核态等权限。
- 页面回收和换页:物理内存紧张时,内核会回收文件页或匿名页,必要时把匿名页写入 Swap。
- 共享和映射:动态库共享、共享内存 IPC、
mmap()文件映射、写时复制(COW)都依赖虚拟地址到物理页的映射能力。
面试里可以抓住一条主线:内存管理把程序看到的地址和真实物理内存隔开,再用分配、映射、保护和回收把内存管起来。
VSZ、RSS 和 PSS 有什么区别?
Linux 里看进程内存,最容易混的是 VSZ、RSS 和 PSS。
- VSZ:进程已经映射的虚拟地址空间大小。它包含尚未真正驻留的匿名映射、文件映射、共享库和预留地址,不能直接当成物理内存占用。
- RSS:当前已经驻留在 RAM 中、并映射给该进程的页面总量。共享库、共享内存和 Page Cache 中的共享页也会算进相关进程的 RSS。
- PSS:按比例分摊共享页后的内存占用。一个物理页如果被 4 个进程共享,每个进程的 PSS 只算四分之一。
所以,看单个进程常驻内存可以参考 RSS;要估算多个进程合计占用,PSS 更合适。直接把多个进程的 RSS 相加,通常会把共享页重复算进去。
常用命令:
grep -E 'VmSize|VmRSS|RssAnon|RssFile|RssShmem|VmSwap' /proc/<pid>/status
cat /proc/<pid>/smaps_rollup什么是内存碎片?
内存碎片是由内存的申请和释放产生的,通常分为下面两种:
- 内部内存碎片(Internal Memory Fragmentation,简称为内存碎片):已经分配给进程使用但未被使用的内存。导致内部内存碎片的主要原因是,当采用固定比例比如 2 的幂次方进行内存分配时,进程所分配的内存可能会比其实际所需要的大。举个例子,一个进程只需要 65 字节的内存,但为其分配了 128(2^7)大小的内存,那 63 字节的内存就成为了内部内存碎片。
- 外部内存碎片(External Memory Fragmentation,简称为外部碎片):由于未分配的连续内存区域太小,以至于不能满足任意进程所需要的内存分配请求,这些小片段且不连续的内存空间被称为外部碎片。也就是说,外部内存碎片指的是那些并未分配给进程但又不能使用的内存。我们后面介绍的分段机制就会导致外部内存碎片。

内存碎片会导致内存利用率下降,如何减少内存碎片是内存管理要非常重视的一件事情。
常见的内存管理方式有哪些?
内存管理方式可以简单分为下面两种:
- 连续内存管理:为一个用户程序分配一个连续的内存空间,内存利用率一般不高。
- 非连续内存管理:允许一个程序使用的内存分布在离散或者说不相邻的内存中,相对更加灵活一些。
连续内存管理
块式管理 是早期计算机操作系统的一种连续内存管理方式,存在严重的内存碎片问题。块式管理会将内存分为几个固定大小的块,每个块中只包含一个进程。如果程序运行需要内存的话,操作系统就分配给它一块,如果程序运行只需要很小的空间的话,分配的这块内存很大一部分几乎被浪费了。这些在每个块中未被利用的空间,我们称之为内部内存碎片。除了内部内存碎片之外,由于两个内存块之间可能还会有外部内存碎片,这些不连续的外部内存碎片由于太小了无法再进行分配。
在 Linux 系统中,连续内存管理采用了 伙伴系统(Buddy System)算法 来实现,这是一种经典的连续内存分配算法,可以有效解决外部内存碎片的问题。伙伴系统的主要思想是将内存按 2 的幂次划分(每一块内存大小都是 2 的幂次比如 2^6=64 KB),并将相邻的内存块组合成一对伙伴(注意:必须是相邻的才是伙伴)。
当进行内存分配时,伙伴系统会尝试找到大小最合适的内存块。如果找到的内存块过大,就将其一分为二,分成两个大小相等的伙伴块。如果还是大的话,就继续切分,直到到达合适的大小为止。
假设两块相邻的内存块都被释放,系统会将这两个内存块合并,进而形成一个更大的内存块,以便后续的内存分配。这样就可以减少内存碎片的问题,提高内存利用率。

虽然解决了外部内存碎片的问题,但伙伴系统仍然存在内存利用率不高的问题(内部内存碎片)。这主要是因为伙伴系统只能分配大小为 2^n 的内存块,因此当需要分配的内存大小不是 2^n 的整数倍时,会浪费一定的内存空间。举个例子:如果要分配 65 大小的内存块,依然需要分配 2^7=128 大小的内存块。

对于小对象频繁分配带来的内部内存碎片和性能问题,Linux 还会使用 SLAB/SLUB 这类分配器优化。它们会把常用内核对象缓存起来,按对象类型复用已经初始化过的内存块,减少重复分配、初始化和释放的成本。由于这部分内容不是本篇文章的重点,这里点到为止。
非连续内存管理
非连续内存管理存在下面 3 种方式:
- 段式管理:以段(一段连续的物理内存)的形式管理/分配物理内存。应用程序的虚拟地址空间被分为大小不等的段,段是有实际意义的,每个段定义了一组逻辑信息,例如有主程序段 MAIN、子程序段 X、数据段 D 及栈段 S 等。
- 页式管理:把物理内存分为连续等长的物理页,应用程序的虚拟地址空间也被划分为连续等长的虚拟页,是现代操作系统广泛使用的一种内存管理方式。
- 段页式管理机制:结合了段式管理和页式管理的一种内存管理机制,把物理内存先分成若干段,每个段又继续分成若干大小相等的页。
虚拟内存
什么是虚拟内存?有什么用?
虚拟内存(Virtual Memory) 是操作系统提供的一层内存抽象。程序看到的是连续、私有的虚拟地址空间,真正的数据放在物理内存的哪些位置,由操作系统和硬件共同决定。
简单来说,虚拟内存把“程序使用的地址”和“内存条上的真实地址”隔开了。进程访问虚拟地址时,CPU 中的 MMU 会根据页表等映射关系,把虚拟地址转换成物理地址,再去访问真正的内存。

总结来说,虚拟内存主要提供了下面这些能力:
- 隔离进程:每个进程都有自己的虚拟地址空间和页表。不同进程即使使用相同的虚拟地址,也可以映射到不同的物理页,彼此不会直接踩内存。
- 提升物理内存利用率:操作系统不需要把进程的全部代码和数据一次性装进物理内存,只把当前真正会用到的页加载进来。
- 简化内存管理:进程看到的是一片连续的虚拟地址空间,物理内存可以是离散的页帧,复杂的拼接工作交给页表和 MMU。
- 多个进程共享物理内存:进程在运行过程中会加载许多操作系统动态库,这些库对于多个进程而言可以共用同一份物理页;多个进程也可以通过共享内存 IPC 主动映射同一块物理内存,用于高效交换数据。
- 提高内存使用安全性:控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性。
- 提供更大的可使用内存空间:当物理内存不够用时,可以把暂时不用的页换出到磁盘,需要时再换回来。这样程序感知到的可用内存空间可以超过实际物理内存,不过频繁换页会明显拖慢系统。
没有虚拟内存有什么问题?
如果没有虚拟内存的话,程序直接访问和操作的都是物理内存,看似少了一层中介,但多了很多问题。
具体有什么问题呢? 这里举几个例子说明(参考虚拟内存提供的能力回答这个问题):
- 用户程序可以访问任意物理内存,可能会不小心操作到系统运行必需的内存,进而造成操作系统崩溃,严重影响系统的安全。
- 同时运行多个程序容易崩溃。比如你想同时运行一个微信和一个 QQ 音乐,微信在运行的时候给内存地址 1xxx 赋值后,QQ 音乐也同样给内存地址 1xxx 赋值,那么 QQ 音乐对内存的赋值就会覆盖微信之前所赋的值,这就可能会造成微信这个程序会崩溃。
- 程序运行过程中使用的所有数据或指令都要载入物理内存,根据局部性原理,其中很大一部分可能都不会用到,白白占用了宝贵的物理内存资源。
- ……
什么是虚拟地址和物理地址?
物理地址(Physical Address) 是真正的物理内存中地址,更具体点来说是内存地址寄存器中的地址。程序中访问的内存地址不是物理地址,而是 虚拟地址(Virtual Address)。
也就是说,我们编程开发的时候实际就是在和虚拟地址打交道。比如在 C 语言中,指针里面存储的数值就可以理解成为内存里的一个地址,这个地址也就是我们说的虚拟地址。
操作系统一般通过 CPU 芯片中的一个重要组件 MMU(Memory Management Unit,内存管理单元) 将虚拟地址转换为物理地址,这个过程被称为 地址翻译/地址转换(Address Translation)。在现代系统里,这个转换通常依赖页表完成,TLB 会缓存最近使用过的地址转换结果,减少查页表的开销。

通过 MMU 将虚拟地址转换为物理地址后,CPU 再访问对应的物理内存位置,完成读写请求。也正是因为有这层转换,不同进程里数值相同的虚拟地址,最终可以落到完全不同的物理页上。
这也是进程隔离和进程间通信的基础:默认情况下,一个进程不能直接读写另一个进程的用户态地址空间;如果两个进程确实需要交换数据,就要借助管道、消息队列、共享内存、Socket 等 IPC 机制。关于 IPC 的系统总结可以看:进程间通信(IPC)详解:管道、消息队列、共享内存、Socket 与 Binder。
MMU 将虚拟地址翻译为物理地址的主要机制有两种:分段机制 和 分页机制。
什么是虚拟地址空间和物理地址空间?
- 虚拟地址空间是虚拟地址的集合,是虚拟内存的范围。每一个进程都有一个一致且私有的虚拟地址空间。
- 物理地址空间是物理地址的集合,是物理内存的范围。
虚拟地址与物理内存地址是如何映射的?
MMU 将虚拟地址翻译为物理地址的主要机制有 3 种:
- 分段机制
- 分页机制
- 段页机制
其中,现代操作系统广泛采用分页机制,需要重点关注!
分段机制
分段机制(Segmentation) 按程序的逻辑结构来划分地址空间,比如代码段、数据段、堆、栈等。每个段的长度可以不同,段本身有明确的语义,也可以配合权限控制。
段表有什么用?地址翻译过程是怎样的?
分段管理通过 段表(Segment Table) 映射虚拟地址和物理地址。段表项通常会记录段基址、段界限(段的长度)、访问权限等信息。
分段机制下的虚拟地址由两部分组成:
- 段号:标识着该虚拟地址属于整个虚拟地址空间中的哪一个段。
- 段内偏移量:相对于该段起始地址的偏移量。
具体的地址翻译过程如下:
- MMU 首先解析得到虚拟地址中的段号;
- 通过段号去该应用程序的段表中取出对应的段信息(找到对应的段表项);
- 检查段内偏移量是否超过段界限,检查访问权限是否合法;
- 合法的话,用段基址加上段内偏移量得到最终的物理地址。

举个例子,要访问段 3、偏移量 500 的地址,如果段 3 的基地址是 7000,且偏移量没有越界,那么最终物理地址就是 7000 + 500 = 7500。
通过段号一定要找到对应的段表项吗?得到最终的物理地址后对应的物理内存一定存在吗?
不一定。段表项可能并不存在:
- 段表项被删除:软件错误、软件恶意行为等情况可能会导致段表项被删除。
- 段表项还未创建:如果系统内存不足或者无法分配到连续的物理内存块就会导致段表项无法被创建。
分段机制为什么会导致内存外部碎片?
分段机制容易出现外部内存碎片,根本原因是段的长度不固定,并且每个段通常需要一块连续的物理内存。进程不断创建和释放段后,物理内存里会留下很多零散空洞;这些空洞总量可能够用,但单个空洞不够大,仍然无法分配给新的大段。
举个例子:假设可用物理内存为 5G 的系统使用分段机制分配内存。现在有 4 个进程,每个进程的内存占用情况如下:
- 进程 1:0~1G(第 1 段)
- 进程 2:1~3G(第 2 段)
- 进程 3:3~4.5G(第 3 段)
- 进程 4:4.5~5G(第 4 段)
此时,我们关闭了进程 1 和进程 4,则第 1 段和第 4 段的内存会被释放,空闲物理内存还有 1.5 GB。由于这 1.5 GB 物理内存并不是连续的,导致没办法将空闲的物理内存分配给一个需要 1.5 GB 连续物理内存的进程。

外部碎片可以通过内存紧凑来缓解,也就是把还在使用的段搬到一起,腾出连续空间。但搬移大段很费时间,如果还伴随换出、换入磁盘,系统会明显变慢。
分页机制
分页机制(Paging) 把虚拟地址空间和物理内存都切成固定大小的块。虚拟地址空间里的块叫虚拟页,物理内存里的块叫物理页或者页帧。Linux 下,一页通常是 4 KB。
注意:这里的页是连续等长的,不同于分段机制下不同长度的段。
在分页机制下,应用程序虚拟地址空间中的任意虚拟页可以映射到物理内存中的任意物理页帧上,因此物理内存可以离散分配。分页按照固定页大小管理内存,基本消除了分段机制中的外部内存碎片;不过最后一页可能装不满,会产生少量内部碎片。
分页还有一个很重要的能力:支持按需调页。程序并不需要一启动就把所有页装进物理内存,只有真正访问到某个虚拟页时,操作系统才把对应数据加载进来。
页表有什么用?地址翻译过程是怎样的?
分页管理通过 页表(Page Table) 映射虚拟地址和物理地址。页表项记录虚拟页号和物理页帧号的对应关系,还会记录访问位、脏位、权限位、存在位等状态信息。我这里画了一张基于单级页表进行地址翻译的示意图。

在分页机制下,每个进程都会有自己的页表。正因为页表是进程私有的,不同进程相同的虚拟页号可以映射到不同的物理页帧,从而实现地址空间隔离。
分页机制下的虚拟地址由两部分组成:
- 页号:通过虚拟页号可以从页表中取出对应的物理页帧号;
- 页内偏移量:物理页帧起始地址 + 页内偏移量 = 物理内存地址。
具体的地址翻译过程如下:
- MMU 首先解析得到虚拟地址中的虚拟页号;
- 通过虚拟页号去该进程的页表中取出对应的物理页帧号(找到对应的页表项);
- 用物理页帧号对应的起始地址加上虚拟地址中的页内偏移量,得到最终的物理地址。

如果页表项不存在,或者存在位表示该页还不在物理内存中,就会触发缺页异常,由内核决定是加载页面、建立映射,还是判定为非法访问。
通过虚拟页号一定能找到对应的物理页帧号吗?找到了物理页帧号得到最终的物理地址后,对应的物理页一定存在吗?
不一定!可能会存在 页缺失。也就是说,物理内存中没有对应的物理页或者物理内存中有对应的物理页但虚拟页还未和物理页建立映射(对应的页表项不存在)。关于页缺失的内容,后面会详细介绍到。
单级页表有什么问题?为什么需要多级页表?
以 32 位环境为例,虚拟地址空间范围共有 2^32(4 GB)。假设一页大小是 2^12(4 KB),那就需要 4 GB / 4 KB = 2^20 个页表项。每个页表项占用 4 字节,整张单级页表大约就是 4 MB。也就是说,一个进程即使只用了一小段虚拟地址空间,单级页表也要为整个 4 GB 地址空间预留页表项。
系统运行的进程多起来后,这部分开销就很明显。更要命的是,绝大多数进程只会使用虚拟地址空间中的一小部分,单级页表里的大量页表项其实都是空的。
为了解决这个问题,操作系统引入了 多级页表。多级页表的核心思路是:顶层页表覆盖整个虚拟地址空间,下级页表按需创建;某段虚拟地址完全没用到,就不需要为它创建下级页表。
这里以二级页表为例进行介绍:一级页表共有 1024 个页表项,每个一级页表项可以指向一张二级页表;每张二级页表同样有 1024 个页表项。只有某个一级页表项覆盖的地址范围真的被使用时,才需要创建对应的二级页表。
假设只需要 2 个二级页表,那两级页表的内存占用情况为:4 KB(一级页表占用) + 4 KB * 2(二级页表占用) = 12 KB。

多级页表是在省页表空间:多走几层索引,换来更小的页表内存占用。实际系统会配合 TLB 缓存常用页表项,所以多级页表的额外查表开销不会每次都完整发生。
TLB 有什么用?使用 TLB 之后的地址翻译流程是怎样的?
为了提高虚拟地址到物理地址的转换速度,操作系统在 页表方案 基础之上引入了 转址旁路缓存(Translation Lookaside Buffer,TLB,也被称为快表)。

在主流的 AArch64 和 x86-64 体系结构下,TLB 属于 MMU(Memory Management Unit,内存管理单元)内部的单元,本质上就是一块高速缓存(Cache),缓存了虚拟页号到物理页帧号的映射关系,你可以将其简单看作是存储着键(虚拟页号)值(物理页帧号)对的哈希表。
使用 TLB 之后的地址翻译流程是这样的:
- 用虚拟地址中的虚拟页号作为 key 去 TLB 中查询;
- 如果能查到对应的物理页的话,就不用再查询页表了,这种情况称为 TLB 命中(TLB hit)。
- 如果不能查到对应的物理页的话,还是需要去查询主存中的页表,同时将页表中的该映射表项添加到 TLB 中,这种情况称为 TLB 未命中(TLB miss)。
- 当 TLB 填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页。

由于页表也在主存中,因此在没有 TLB 之前,CPU 访问一个虚拟地址,往往要先访问内存查页表,再访问真正的数据;多级页表下查表次数还会更多。有了 TLB 之后,命中时可以跳过页表查询,直接拿到物理页帧号,地址转换会快很多。
TLB 的设计思想非常简单,但命中率往往很高,效果很好。这依赖的还是局部性原理:程序在一段时间内频繁访问的页通常只是少数几个。
看完了之后你会发现快表和我们平时经常在开发系统中使用的缓存(比如 Redis)很像,的确是这样的,操作系统中的很多思想、很多经典的算法,你都可以在我们日常开发使用的各种工具或者框架中找到它们的影子。
换页机制有什么用?
换页机制的思想是:当物理内存不够用时,操作系统选择一些暂时不常用的物理页,把它们换出到磁盘;等进程再次访问这些页时,再把它们换回物理内存。也就是说,换页机制利用磁盘这种更低成本的存储设备,从逻辑上扩展了可用内存。
这也就解释了一个日常使用电脑常见的问题:为什么操作系统中所有进程运行所需的物理内存即使比真实的物理内存要大一些,这些进程也是可以正常运行的,只是运行速度会变慢。
这同样是一种时间换空间的策略,用页的调入调出时间,换来更大的可用内存空间。问题也很直接:一旦频繁发生主缺页和磁盘换页,系统会明显变慢,严重时会出现抖动(Thrashing),CPU 大部分时间都耗在换页上。
什么是页缺失?
根据维基百科:
页缺失(Page Fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由 MMU 所发出的中断。
常见的页缺失可以从性能统计角度分成下面两类:
- 主缺页(Major Page Fault,也常被叫作硬缺页):目标页不在物理内存中,需要从磁盘文件或 Swap 读取页面,开销较大。
- 次缺页(Minor Page Fault,也常被叫作软缺页):目标页其实已经在物理内存中,只是当前进程还没建立对应页表映射,比如共享库映射、写时复制(COW)触发的新映射等,通常不需要读盘。
发生上面这两类缺页时,应用程序访问的虚拟地址通常是合法的,只是页面暂时不在内存,或者映射关系还没建立。如果访问的是非法地址,比如野指针或越权访问,硬件同样会触发 page fault 进入内核,但内核一般会向进程发送 SIGSEGV,这属于错误处理,不应和主缺页、次缺页混为一类。

常见的页面置换算法有哪些?
当发生主缺页时,如果物理内存中没有空闲的物理页面可用,操作系统就必须将物理内存中的一个物理页淘汰出去,这样就可以腾出空间来加载新的页面。
用来选择淘汰哪一个物理页的规则叫做 页面置换算法,我们可以把页面置换算法看成是淘汰物理页的规则。
页缺失太频繁的发生会非常影响性能,一个好的页面置换算法应该是可以减少页缺失出现的次数。
常见的页面置换算法有下面这 5 种(其他还有很多页面置换算法都是基于这些算法改进得来的):

- 最佳页面置换算法(OPT,Optimal):优先淘汰未来最长时间不会再被访问的页面,理论上缺页率最低。但它需要预知未来,现实中无法实现,通常作为衡量其他算法的基准。
- 先进先出页面置换算法(FIFO,First In First Out):总是淘汰最早进入内存的页面,实现简单,但容易误伤热点页,并且可能出现 Belady 异常。
- 最近最久未使用页面置换算法(LRU,Least Recently Used):淘汰最久没有被访问的页面。它利用的是时间局部性,效果接近 OPT,但精确实现需要维护时间戳或链表,成本较高。
- 最少使用页面置换算法(LFU,Least Frequently Used):淘汰一段时间内访问次数最少的页面。它关注访问频率,但容易让早期频繁访问、后来不再使用的页面长期留在内存中,因此实际使用时常配合计数衰减。
- 时钟页面置换算法(Clock):也叫二次机会算法,是 LRU 的一种低成本近似实现。它给每个页面维护一个访问位,页面排成环形队列;访问位为 1 时先清零并跳过,访问位为 0 时才淘汰。

FIFO 页面置换算法性能为何不好?
主要原因有二:
- 经常访问或者需要长期存在的页面会被频繁调入调出:较早调入的页往往是经常被访问或者需要长期存在的页,这些页会被反复调入和调出。
- 存在 Belady 现象:被置换的页面并不是进程不会访问的,有时就会出现分配的页面数增多但缺页率反而提高的异常现象。出现该异常的原因是因为 FIFO 算法只考虑了页面进入内存的顺序,而没有考虑页面访问的频率和紧迫性。
哪一种页面置换算法实际用的比较多?
LRU 及其近似算法在实际系统中应用较多,因为它比较符合程序的局部性规律。不过,真实系统通常不会原样照搬教科书算法,而是做大量工程化改造。比如 Linux 内核不是简单地在 OPT/FIFO/LRU/CLOCK 里挑一个,而是使用活跃/非活跃 LRU、workingset、refault 检测等机制做近似回收;InnoDB Buffer Pool 也对传统 LRU 做了改进,避免预读和全表扫描把热点页挤出去。

Swap、工作集和抖动分别是什么?
Swap 是磁盘上的后备空间。匿名页没有对应的文件来源,物理内存紧张时,如果内核要回收这类页面,就可能把它们写入 Swap;以后进程再次访问,再从 Swap 读回内存。
Swap 不是免费的内存扩容。磁盘比内存慢很多,Swap 活跃时,业务延迟通常会变差。
工作集 是进程在一段时间内真正频繁访问的页面集合。只要物理内存能放下主要进程的工作集,缺页就比较可控;如果放不下,页面会被频繁换出又换入。
这种状态叫 抖动(Thrashing)。抖动时,CPU 不一定忙在业务计算上,可能大量时间耗在缺页处理、页面回收和磁盘 I/O 上。
排查时可以看这些指标:
free -h
vmstat 1
grep -E 'pgfault|pgmajfault|pswpin|pswpout|pgscan|pgsteal' /proc/vmstat
cat /proc/pressure/memoryOvercommit 和 OOM 是什么关系?
Linux 允许进程申请的虚拟内存超过当前 RAM 和 Swap,这叫 Overcommit。它适合那些会预留很大地址空间、但不一定真正使用完的程序。
因此,malloc() 或 mmap() 成功,通常只表示虚拟地址空间申请成功,并不代表所有物理页已经准备好。很多物理页要等到首次访问时才会真正分配。
当进程实际访问页面时,如果内核无法通过回收、写回或 Swap 获得足够内存,就可能触发 OOM Killer,选择一个或多个进程杀掉来释放内存。
在容器环境里,还要看 cgroup 限制。宿主机还有空闲内存,不代表容器还能继续用;容器达到 memory.max 后,也可能先触发 cgroup 范围内的 OOM。
mmap、COW 和共享内存有什么关系?
mmap() 会在进程虚拟地址空间里创建一段映射。它既可以映射文件,也可以创建匿名映射。映射建立时不一定马上读入数据,真正访问到某个页面时,才可能触发缺页异常。
多个进程映射同一个文件时,内核可以让它们共享 Page Cache 中的物理页。共享内存 IPC 也是类似思路:不同进程的虚拟地址映射到同一批物理页,进程之间读写数据不需要每次经过内核拷贝。
COW(Copy-On-Write,写时复制) 常见于 fork()。父子进程刚创建时可以共享同一批物理页,页表先标成只读;谁先写,谁触发缺页异常,内核再复制一份页面给写入方。
详细介绍:操作系统内存管理详解:分页、分段、页面置换、Swap 与 OOM
分页机制和分段机制有哪些共同点和区别?
共同点:
- 都是非连续内存管理的方式。
- 都采用了地址映射的方法,将虚拟地址映射到物理地址,以实现对内存的管理和保护。
区别:
- 划分依据不同:分页按固定大小切分地址空间和物理内存,页是内存管理的物理粒度;分段按程序的逻辑结构切分,比如代码段、数据段、堆、栈,段是更贴近程序语义的逻辑单位。
- 大小是否固定不同:页大小固定,常见为 4 KB;段大小不固定,取决于程序中对应逻辑区域的大小。
- 碎片问题不同:分段容易产生外部碎片,因为每个段需要连续空间;分页基本消除了外部碎片,但最后一页可能装不满,会产生少量内部碎片。
- 地址结构不同:分页地址通常由页号和页内偏移组成,通过页表完成映射;分段地址由段号和段内偏移组成,通过段表完成映射。
- 工程使用不同:现代通用操作系统主要依赖分页管理内存。以 x86 为例,硬件历史上支持分段和分页,但 Linux 基本把段基址设为 0,让分段“弱化”为权限和兼容机制,实际内存管理主要靠分页完成。
段页机制
结合了段式管理和页式管理的一种内存管理机制。程序视角中,内存被划分为多个逻辑段,每个逻辑段进一步被划分为固定大小的页。
在段页式机制下,地址翻译的过程分为两个步骤:
- 段式地址映射(虚拟地址 -> 线性地址):
- 虚拟地址 = 段选择符(段号)+段内偏移。
- 根据段号查段表,找到段基址,加上段内偏移得到线性地址。
- 页式地址映射(线性地址 -> 物理地址):
- 线性地址 = 页号 + 页内偏移。
- 根据页号查页表,找到物理页框号,加上页内偏移得到物理地址。
局部性原理
要想更好地理解虚拟内存技术,必须要知道计算机中著名的 局部性原理(Locality Principle)。另外,局部性原理既适用于程序结构,也适用于数据结构,是非常重要的一个概念。
局部性原理是指在程序执行过程中,数据和指令的访问存在一定的空间和时间上的局部性特点。其中,时间局部性是指一个数据项或指令在一段时间内被反复使用的特点,空间局部性是指一个数据项或指令在一段时间内与其相邻的数据项或指令被反复使用的特点。
在分页机制中,页表的作用是将虚拟地址转换为物理地址,从而完成内存访问。在这个过程中,局部性原理的作用体现在两个方面:
- 时间局部性:由于程序中存在一定的循环或者重复操作,因此会反复访问同一个页或一些特定的页,这就体现了时间局部性的特点。为了利用时间局部性,分页机制中通常采用缓存机制来提高页面的命中率,即将最近访问过的一些页放入缓存中,如果下一次访问的页已经在缓存中,就不需要再次访问内存,而是直接从缓存中读取。
- 空间局部性:由于程序中数据和指令的访问通常是具有一定的空间连续性的,因此当访问某个页时,往往会顺带访问其相邻的一些页。为了利用空间局部性,分页机制中通常采用预取技术来预先将相邻的一些页读入内存缓存中,以便在未来访问时能够直接使用,从而提高访问速度。
总之,局部性原理是计算机体系结构设计的重要原则之一,也是许多优化算法的基础。在分页机制中,利用时间局部性和空间局部性,采用缓存和预取技术,可以提高页面的命中率,从而提高内存访问效率。
虚拟内存是如何实现地址转换和进程隔离的?
面试里问虚拟内存,不要只背“隔离进程”。可以按这条线回答:虚拟内存把进程看到的地址和真实物理地址隔开,再由 MMU、页表和 TLB 完成地址翻译。
进程访问的是虚拟地址(VA),真正落到内存条上的是物理地址(PA)。每个进程都有自己的虚拟地址空间和页表,所以不同进程即使使用相同的虚拟地址,也可以映射到不同的物理页,从而实现进程隔离。

不过,隔离不代表进程之间完全不能共享数据。操作系统可以有控制地让多个进程映射同一批物理页,例如动态库共享、mmap 文件映射、共享内存 IPC。区别在于:默认隔离由页表权限保证,共享则必须由内核显式建立映射并配合权限控制;如果是共享内存 IPC,还需要额外处理同步问题。
分页机制把虚拟地址空间和物理内存都切成固定大小的页,通过页表记录“虚拟页号 -> 物理页帧”的映射。这样基本消除了分段容易产生的外部碎片,也让物理内存可以离散分配;页表本身会占空间,所以现代系统会用多级页表按需创建下级页表。
TLB 可以理解为页表项缓存。CPU 先查 TLB,命中就直接拿到物理页帧号;未命中才去查多级页表,并把结果回填到 TLB。如果页表项显示页面不在内存,就会触发缺页中断,内核再判断访问是否合法,合法则分配页帧、必要时换出旧页、从文件或 Swap 调入页面,最后更新页表并重新执行那条指令。
页面置换算法可以抓住一句话:换出去的页,最好是后面最晚再用到的页。OPT 是理论最优但无法实现,LRU 接近 OPT 但实现成本高,CLOCK 用访问位近似 LRU,FIFO 简单但可能出现 Belady 异常。
详细介绍:虚拟内存详解:地址转换、TLB、缺页中断与页面置换
I/O
什么是 I/O 多路复用?
I/O 多路复用解决的不是“单次读写更快”,而是一个线程如何同时等待多个文件描述符(fd)的就绪事件。
一次网络读取通常分成两个阶段:先等数据从网卡到达并进入内核缓冲区,再把数据从内核缓冲区拷贝到用户缓冲区。阻塞 I/O 的问题在第一阶段:一个线程调用 recv 后,如果这个连接没数据,线程就只能卡在那里等。

I/O 多路复用把一批 fd 交给内核,让线程阻塞在 select、poll 或 epoll 这类系统调用上。只要其中任意 fd 就绪,调用就返回,应用再去处理对应的连接。这样一个线程就能管理成千上万个连接,特别适合大量连接空闲、少量连接活跃的场景,比如 Redis、Nginx、Netty 这类高性能网络程序。
需要注意:I/O 多路复用仍然属于同步 I/O。内核只是通知“可以读/可以写了”,真正的 read/recv 还得应用自己调用,数据从内核缓冲区拷到用户缓冲区这一步并没有被省掉。
详细介绍:I/O 多路复用详解:select、poll、epoll 原理与区别
select、poll 和 epoll 有什么区别?
结论:select 和 poll 每次等待都要把完整监听集合交给内核,并在返回后线性扫描;epoll 把监听集合长期维护在内核里,epoll_wait 主要返回已经就绪的事件,更适合“连接很多但活跃连接较少”的场景。
select 使用固定大小的 fd_set 位图,Linux glibc 下通常受 FD_SETSIZE 限制,只能安全处理编号 0~1023 的 fd。每次调用前都要重新设置监听集合,返回后还要遍历位图找出哪些 fd 就绪。
poll 把位图换成了 pollfd 数组,绕开了 FD_SETSIZE 的限制,但本质上还是每次把完整数组传入内核,返回后遍历整个数组检查 revents。所以连接数量很大、活跃比例很低时,扫描成本仍然明显。
epoll 通过 epoll_ctl 维护监听集合,通过 epoll_wait 获取就绪事件。内核会维护 interest list 和 ready list,fd 就绪后进入 ready list,应用等待时只取这批就绪事件。它还支持 LT(水平触发)和 ET(边缘触发):LT 只要缓冲区还有数据就会反复通知;ET 只在状态变化时通知一次,必须配合非阻塞 fd,并循环读到 EAGAIN。

不过,epoll 不是所有场景都更快。如果连接数量很少,或者所有连接都很活跃,epoll_ctl、回调、就绪链表等维护成本也要算进去。它的主场是海量长连接、大部分时间空闲的服务端程序。
详细介绍:I/O 多路复用详解:select、poll、epoll 原理与区别
什么是零拷贝?
零拷贝不是完全没有拷贝,而是尽量避免 CPU 在内核缓冲区和用户缓冲区之间搬运数据,从而减少 CPU 拷贝和用户态/内核态切换。
以传统 read + write 文件发送为例,数据通常要经历 4 次拷贝:磁盘到内核缓冲区是 DMA 拷贝,内核缓冲区到用户缓冲区是 CPU 拷贝,用户缓冲区到 Socket 缓冲区还是 CPU 拷贝,Socket 缓冲区到网卡是 DMA 拷贝。这里最浪费的是中间两次 CPU 拷贝,因为应用并没有修改数据,只是让数据到用户空间绕了一圈。

零拷贝的思路就是让数据尽量留在内核路径里转发。比如 sendfile 可以把文件数据从 Page Cache 直接送到 Socket;如果网卡支持 SG-DMA,Socket 缓冲区里甚至可以只放描述信息,payload 由 DMA 从内核缓冲区直接送到网卡。
零拷贝很适合文件原样转发、大文件传输、消息队列日志发送这类场景。Kafka 消费端把日志段文件发送给消费者时,就很适合走 FileChannel.transferTo 这类 sendfile 路线。
详细介绍:零拷贝详解:mmap、sendfile 与 splice
mmap、sendfile 和 splice 有什么区别?
结论:要改数据,用 mmap;只是文件到 Socket 原样转发,用 sendfile;更一般的 fd 之间转发,再考虑 splice。
mmap + write 利用虚拟内存映射,把文件映射到进程地址空间。应用访问这段内存时,实际访问的是 Page Cache 对应的物理页,省掉了传统 read 中“内核缓冲区 -> 用户缓冲区”的那次 CPU 拷贝。不过后续 write 到 Socket 缓冲区通常还会有一次 CPU 拷贝。它适合需要在发送前读取、修改、解析数据的场景。
sendfile 更适合“文件 -> Socket”的原样发送。数据不进入用户态,系统调用次数也更少;在支持 SG-DMA 的网卡上,还能把 CPU payload 拷贝降到 0。静态文件服务器、Kafka 日志段发送这类场景很典型。
splice 借助 pipe 在内核中移动页引用,适合更一般的描述符之间转发,比如 socket 到 socket、文件到管道再到 socket。它的限制是路径里通常要有 pipe,而且文件到 socket 往往需要两次 splice 调用,代码复杂度和系统调用次数都要考虑。

零拷贝也有失效场景:TLS 加密、压缩、格式转换、内容过滤、水印处理等都需要应用真正处理 payload,数据就很难一直停留在内核路径里。小文件或随机访问下,映射、缺页、管道的固定成本也可能盖过收益。
详细介绍:零拷贝详解:mmap、sendfile 与 splice
文件系统
文件系统主要做了什么?
文件系统负责把存储设备上的块组织成应用能理解的文件和目录。面试里可以从这 6 件事回答:

- 命名:用路径和文件名找到目标文件,例如
/var/log/app.log。 - 组织:用目录树管理文件和目录,让不同文件有清晰层次。
- 定位:把文件的第 N 个字节映射到底层数据块。
- 空间管理:分配、释放和复用磁盘块,记录哪些块空闲、哪些块已使用。
- 权限保护:记录所有者、权限、时间戳等元数据,并在访问时做检查。
- 缓存和恢复:用 Page Cache 提升读写性能,用日志机制减少崩溃后的文件系统结构损坏。
不是所有文件系统都对应本地磁盘。tmpfs 主要以内存作为后端,procfs 暴露内核运行状态,NFS 则把远端文件系统接入本地目录树。Linux 通过 VFS 给这些文件系统提供统一接口。
文件、目录、inode、dentry 有什么关系?
在 Linux/Unix 文件系统里,文件名通常不存放在 inode 中。

- 目录项:保存文件名到 inode 号的映射。
- inode:记录文件类型、权限、所有者、大小、时间戳、链接计数,以及数据块或 extent 的映射信息。
- dentry:VFS 在内存里维护的目录项缓存,用来加速路径查找。它通常指向 inode,也可能缓存“不存在”的查找结果,也就是 negative dentry。
- 数据块或 extent:保存普通文件的实际内容。
这也解释了为什么同一个文件可以有多个名字:多个目录项可以指向同一个 inode。mv a.txt b.txt 如果发生在同一个文件系统内,很多时候只是修改目录项,文件内容本身不用移动。
可以用这些命令观察 inode 和元数据:
ls -li app.log
stat app.log
df -iopen 一个文件时发生了什么?
open() 不会把整个文件读进内存,它主要做路径解析和打开对象创建。

大致流程是:
- 从根目录或当前目录开始解析路径,逐级查目录项和 dentry 缓存。
- 找到目标 inode 后,检查权限、打开标志和文件类型。
- 创建内核里的打开文件对象,记录当前文件偏移量、打开状态、读写标志等。
- 在当前进程的文件描述符表里分配一个最小可用的非负整数,也就是 fd。
fd 是进程文件描述符表里的索引,不是 inode。dup()、fork() 之后,多个 fd 可能引用同一个打开文件对象,所以它们共享文件偏移量;两个进程分别 open() 同一个文件,通常会得到不同的打开文件对象,各自维护偏移量。
文件在磁盘上怎么存放?
文件系统会把一个分区或卷划分成很多块,再用元数据记录文件内容和块之间的关系。教材里常见三种分配方式:

- 连续分配:文件占用一段连续块,顺序读写和随机访问都快;缺点是文件增长麻烦,容易产生外部碎片。
- 链式分配:每个块指向下一个块,不要求连续空间;缺点是随机访问差。
- 索引分配:把数据块地址集中放在索引块中,随机访问更方便;缺点是需要额外索引空间。
经典 ext2/ext3 使用直接块指针、一级间接、二级间接和三级间接块定位文件数据。ext4 通常使用 extent tree,一个 extent 记录一段连续物理块的逻辑起点、物理起点和长度;对连续大文件来说,它比“每个块记录一个地址”更省元数据。
硬链接和软链接有什么区别?
硬链接和软链接都能让一个路径关联到另一个文件,但它们指向的对象不同。

| 对比项 | 硬链接 | 软链接 |
|---|---|---|
| 指向对象 | 同一个 inode | 另一个路径 |
| 是否创建新 inode | 不创建新的目标文件 inode,只新增目录项并增加链接计数 | 软链接本身是独立文件,有自己的 inode |
| 删除源文件后 | 只要还有硬链接,数据还在 | 软链接可能变成悬空链接 |
| 是否能跨文件系统 | 不能 | 可以 |
| 是否能链接目录 | Linux 不允许普通用户对目录创建硬链接 | 可以 |
可以用这几个命令做个小实验:
echo hello > a.txt
ln a.txt hard.txt
ln -s a.txt soft.txt
ls -li a.txt hard.txt soft.txta.txt 和 hard.txt 的 inode 号相同,soft.txt 的 inode 号不同。删除 a.txt 后,hard.txt 还能读到内容,soft.txt 会指向一个不存在的路径。
硬链接为什么不能跨文件系统?
硬链接指向的是 inode,而 inode 号只在当前文件系统内有意义。每个文件系统都有自己的 inode 表,另一个文件系统里的同一个 inode 号不代表同一个文件。
软链接保存的是路径字符串,解析时按路径重新查找目标文件,所以可以跨文件系统。
write 成功后数据一定落盘了吗?
不一定。对于普通 buffered I/O,write() 成功通常只表示数据已经被内核接收,常见情况是进入 Page Cache 并被标记为脏页,不代表数据已经持久化到底层设备。

如果应用需要更强持久性,要调用:
fsync():同步文件数据和关联元数据。fdatasync():同步文件数据,以及后续读取数据所必需的元数据,例如文件大小。- 带同步语义的打开标志,例如
O_SYNC、O_DSYNC。
还要注意两个细节:
write()可能只写入部分字节,调用方要处理 partial write。- 创建新文件、
rename()或unlink()后,如果要求掉电后目录项也可靠持久化,通常还需要对父目录 fd 调用fsync()。
日志文件系统解决了什么问题?
文件系统一次操作常常要改多处元数据。比如创建文件时,要分配 inode、分配数据块、更新目录项、更新位图。机器在中途断电,文件系统可能停在不一致状态。
日志文件系统会先把即将进行的元数据变更写入日志区域,再更新正式位置。系统恢复时扫描日志,已经完整提交但还没写回正式位置的事务可以重放,没有完整提交的事务会被丢弃。
以 ext4 为例,常见数据模式有三种:
| 模式 | 说明 |
|---|---|
data=writeback | 只保证元数据日志,不保证相关数据块先于元数据写入 |
data=ordered | 默认模式,元数据进日志前,相关数据块会先写到主文件系统 |
data=journal | 数据和元数据都先写日志,再写最终位置,写放大更明显 |
日志文件系统主要保证文件系统结构一致性,不等于帮应用保证所有业务数据都不丢。事务级持久性仍然要靠应用正确使用 fsync()、写入顺序和恢复逻辑。
提高文件系统性能的方式有哪些?
可以从访问模式、缓存、元数据和硬件几方面答:
- 尽量顺序读写:顺序 I/O 更容易被预读、合并写和 extent 这类连续空间映射利用。
- 利用 Page Cache:普通文件读写会经过 Page Cache,命中时可以少访问磁盘;但缓存不是越大越好,还要看内存压力和回写压力。
- 减少小文件和元数据操作:大量小文件会放大 inode、目录项、权限检查、创建删除等元数据成本。
- 控制刷盘频率:频繁
fsync()会降低吞吐,但完全不刷盘又会扩大掉电丢数据窗口,需要按业务持久性要求取舍。 - 选择合适的文件系统和挂载参数:ext4、XFS、Btrfs 等实现不同,日志模式、atime、barrier 等参数也会影响性能和可靠性。
- 使用更合适的硬件:SSD、NVMe、RAID、磁盘缓存策略都会影响读写延迟和吞吐。
排查时常看这些命令:
df -h
df -i
lsof +L1
iostat -x 1df 很满但 du 找不到大文件时,优先查被删除但仍被进程打开的文件;小文件特别多时,df -i 可能比 df -h 更早暴露问题。
详细介绍:操作系统文件系统详解:inode、VFS、Page Cache 与日志机制
常见的磁盘调度算法有哪些?
磁盘调度算法是操作系统中对磁盘访问请求进行排序和调度的算法,其目的是提高磁盘的访问效率。
一次磁盘读写操作的时间由磁盘寻道/寻找时间、延迟时间和传输时间决定。磁盘调度算法可以通过改变到达磁盘请求的处理顺序,减少磁盘寻道时间和延迟时间。
常见的磁盘调度算法有下面这 6 种(其他还有很多磁盘调度算法都是基于这些算法改进得来的):

- 先来先服务算法(First-Come First-Served,FCFS):按照请求到达磁盘调度器的顺序进行处理,先到达的请求先被服务。FCFS 算法实现起来比较简单,不存在算法开销。不过,由于没有考虑磁头移动的路径和方向,平均寻道时间较长。同时,该算法容易出现饥饿问题,即一些后到的磁盘请求可能需要等待很长时间才能得到服务。
- 最短寻道时间优先算法(Shortest Seek Time First,SSTF):也被称为最佳服务优先(Shortest Service Time First,SSTF)算法,优先选择距离当前磁头位置最近的请求进行服务。SSTF 算法能够最小化磁头的寻道时间,但容易出现饥饿问题,即磁头附近的请求不断被服务,远离磁头的请求长时间得不到响应。实际应用中,需要优化一下该算法的实现,避免出现饥饿问题。
- 扫描算法(SCAN):也被称为电梯(Elevator)算法,基本思想和电梯非常类似。磁头沿着一个方向扫描磁盘,如果经过的磁道有请求就处理,直到到达磁盘的边界,然后改变移动方向,依此往复。SCAN 算法能够保证所有的请求得到服务,解决了饥饿问题。但是,如果磁头从一个方向刚扫描完,请求才到的话,这个请求就需要等到磁头从相反方向过来之后才能得到处理。
- 循环扫描算法(Circular Scan,C-SCAN):SCAN 算法的变体,只在磁盘的一侧进行扫描,并且只按照一个方向扫描,直到到达磁盘边界,然后回到磁盘起点,重新开始循环。
- 边扫描边观察算法(LOOK):SCAN 算法中磁头到了磁盘的边界才改变移动方向,这样可能会做很多无用功,因为磁头移动方向上可能已经没有请求需要处理了。LOOK 算法对 SCAN 算法进行了改进,如果磁头移动方向上已经没有别的请求,就可以立即改变磁头移动方向,依此往复。也就是边扫描边观察指定方向上还有无请求,因此叫 LOOK。
- 均衡循环扫描算法(C-LOOK):C-SCAN 只有到达磁盘边界时才能改变磁头移动方向,并且磁头返回时也需要返回到磁盘起点,这样可能会做很多无用功。C-LOOK 算法对 C-SCAN 算法进行了改进,如果磁头移动的方向上已经没有磁道访问请求了,就可以立即让磁头返回,并且磁头只需要返回到有磁道访问请求的位置即可。
举个简单例子:假设磁头当前在 50 号磁道,请求序列是 82、170、43、140、24。FCFS 会按请求到达顺序处理,磁头移动路径可能很长;SSTF 会先找离 50 最近的 43,再逐步选择最近请求,平均寻道距离通常更短,但远处请求可能一直被推迟;SCAN/LOOK 则会固定一个扫描方向,沿途处理请求,更像电梯上下运行,公平性更好。
参考
- 《计算机操作系统—汤小丹》第四版
- 《深入理解计算机系统》
- 《重学操作系统》
- 《现代操作系统原理与实现》
- 王道考研操作系统知识点整理:https://wizardforcel.gitbooks.io/wangdaokaoyan-os/content/13.html
- 内存管理之伙伴系统与 SLAB:https://blog.csdn.net/qq_44272681/article/details/124199068
- 为什么 Linux 需要虚拟内存:https://draveness.me/whys-the-design-os-virtual-memory/
- 程序员的自我修养(七):内存缺页错误:https://liam.page/2017/09/01/page-fault/
- 虚拟内存的那点事儿:https://juejin.cn/post/6844903507594575886
写在最后
如果内容对你有帮助的话,欢迎顺手给 JavaGuide 点一个免费的 Star 支持一下:GitHub | Gitee。
JavaGuide 已持续维护近七年,累计 6100+ 次提交,来自 620+ 位贡献者共同完善。你的 Star、反馈和 PR,都是这个项目继续更新的动力。
如果你正在准备后端/AI 应用开发面试,也可以了解一下我的知识星球,里面包括后端和 AI 实战项目、简历优化、一对一提问和高频考点资料,已经持续维护六年。

