type
status
date
slug
summary
tags
category
password
虚拟内存管理
虚拟内存和物理内存
虚拟内存是操作系统提供的一种内存管理技术,它通过软硬件结合的方式,为每个进程提供了一个比实际物理内存更大的、连续的地址空间。
- 物理内存:实际的物理内存地址。如果进程可以直接使用绝对物理地址,容易造成同一个物理地址被两个不同的进程同时占用,从而导致进程出错的问题。
- 虚拟内存:操作系统为每个进程分配独立的独立的、连续的虚拟地址。当程序要访问虚拟地址时,再由操作系统通过MMU(内存管理单元)将虚拟地址转换为物理地址,然后再通过物理内存地址访问内存。

虚拟内存的工作流程:
- 进程访问虚拟地址
- MMU 查找页表进行地址转换
- 若页在内存中,完成访问
- 若页不在内存中(页错误),触发缺页中断
- 操作系统选择牺牲页,若被修改则写回磁盘
- 从磁盘调入所需页到内存
- 更新页表,重新执行导致页错误的指令
虚拟内存的作用:
- 扩展内存空间:使程序可以使用比实际物理内存更大的内存
- 内存隔离:不同进程的地址空间相互隔离,提高安全性
- 内存共享:允许多个进程共享同一段物理内存(如共享库)
- 内存保护:通过权限位控制对内存区域的访问权限
虚拟内存的相关技术:
- 内存分页:将虚拟内存和物理内存划分为固定大小的页(通常4KB),使用页表记录虚拟页到物理页的映射,支持按需调页和页面置换。
- 内存分段:按逻辑单元(代码段、数据段等)划分内存,每个段有不同的大小和属性。
- 页面置换算法:Linux 会在物理内存不足时,使用页面置换算法,将一些不经常使用的页面文件写到 swap 区,通过这种方式来释放物理内存;当需要用到原始的内容时,这些信息会被重新从 swap 区读入物理内存。当然
- FIFO(先进先出)
- LRU(最近最少使用)
- 时钟算法(Clock)
- 工作集算法
Swap区
交换(Swap)区是磁盘上的一块预留空间(分区或文件),用于临时存储从物理内存中“换出”(swap out)的页面。Swap 区的作用:
- 扩展可用内存:当物理内存不足时,将不活跃的页面移至交换区,腾出物理内存供其他进程使用。当然内存交换不一定在内存不足的时候才发生,Linux 系统会不时地进行页面交换操作,以保持尽可能多的空闲物理内存。即使并没有什么事情需要内存,Linux 也会交换出暂时不用的内存页面,因为这样可以大大节省等待交换所需的时间。有时我们会看到这么一个现象,Linux 物理内存还有很多,但是 swap 区也使用了很多,其实这并不奇怪。
- 休眠支持:在系统休眠(hibernate)时,将整个内存内容保存到交换区。
Swap 区频率的内存交换会影响系统的性能,一般情况下,如果已经用到 Swap 区说明硬件配置已经不太够了,需要升级硬件了。现代服务器通常会禁用掉 Swap 区的功能。
- 临时禁用Swap。
- 永久禁用Swap。编辑
/etc/fstab文件,找到包含swap的行,在行首添加#注释掉该行
交换区和虚拟内存的区别
- 虚拟内存:虚拟内存是操作系统提供的一种抽象机制,它让每个进程认为自己拥有连续的、独立的内存空间(虚拟地址空间),而实际上这些内存可能分散在物理内存或磁盘上。
- 物理内存充足时:进程的页面完全驻留在物理内存中,无需使用 swap。
- 物理内存不足时:操作系统将不活跃的页面移至 swap,腾出空间给活跃进程(可能引发性能下降,因为磁盘访问比 RAM 慢)。
- 交换区:交换区是磁盘上的一块预留空间(分区或文件),用于临时存储从物理内存中“换出”(swap out)的页面。
总的来说,虚拟内存是一个架构级概念,它包含了物理内存和交换区的协同管理。交换区是虚拟内存的物理支持之一(用于存储“溢出”的页面),但虚拟内存的实现不一定依赖交换区(例如,嵌入式系统可能禁用 swap)。
内核空间和用户空间
操作系统的虚拟地址空间分为内核空间(Kernel Space)和用户空间(User Space)两部分,用于隔离系统核心功能与用户程序,确保系统的安全性和稳定性。
- 内核空间(Kernel Space):操作系统内核运行的特权区域,拥有对硬件和系统资源的完全访问权限。
- 核心功能:
- 直接管理硬件(CPU、内存、设备驱动等)。
- 提供系统调用(System Calls)接口供用户程序请求服务。
- 处理进程调度、内存管理、文件系统、网络协议栈等核心任务。
- 特权级别:运行在CPU的最高特权级(如 x86 的
Ring 0,ARM 的EL1/EL2),可以执行所有指令(包括特权指令,如直接操作硬件)。 - 安全性:内核代码需高度可靠,漏洞可能导致系统崩溃或安全风险(如提权攻击)。
- 内存隔离:内核空间的内存通常被保护,用户程序无法直接访问。
- 用户空间(User Space):普通应用程序运行的非特权区域,权限受限。
- 核心功能:
- 运行用户程序(如浏览器、文本编辑器等)。
- 通过系统调用或库函数(如 glibc)间接访问内核功能。
- 特权级别:运行在低特权级(如 x86 的
Ring 3,ARM 的EL0)。无法直接执行特权指令或访问硬件。 - 安全性:程序崩溃通常不会影响其他进程或系统稳定性,通过权限检查防止越权操作(如访问其他进程的内存)。
- 内存隔离:每个进程有独立的虚拟地址空间,受内存管理单元(MMU)保护。一个进程崩溃通常不会影响操作系统本身或其他进程。

用户态和内核态的切换
内核空间和用户空间的切换触发场景:
- 系统调用:用户程序主动调用,如
fork,open,read,write。通过执行一条特殊的指令(如 x86 的int 0x80或syscall)触发。
- 异常:CPU执行指令时遇到的错误,如除零、页错误、访问非法内存等。
- 中断:来自外部设备的中断信号,如时钟中断、键盘输入、磁盘IO完成。
我们可以把整个切换过程分为几个阶段:
阶段一:触发与初始硬件响应(由CPU自动完成)
- 权限提升:
- CPU 当前的特权级从 用户态(Ring 3) 瞬间切换到 内核态(Ring 0)。这使CPU获得了执行特权指令和访问所有内存区域的能力。
- 上下文保存:
- CPU 将当前用户态程序的“执行现场”自动保存到当前进程的内核栈中。
- 关键点:CPU 会从 任务状态段(TSS) 中加载当前进程的内核栈的栈指针(SS:ESP),然后开始使用这个内核栈。这是至关重要的一步,因为它将用户栈和内核栈分离开,保证了内核的安全和稳定。
- 保存的上下文通常包括:
- 用户态的栈指针(SS:ESP)
- 标志寄存器(EFLAGS)
- 当前代码段(CS)
- 指令指针(EIP/RIP) - 即下一条要执行的用户指令地址
- 错误码(如果是异常)
- 以及其他一些寄存器状态(取决于架构)。
- 中断向量查找:
- CPU 根据触发原因(系统调用、中断号、异常类型)去查询 中断描述符表(IDT)。
- IDT 是由操作系统在启动时设置好的,它告诉CPU,当发生特定类型的中断/异常时,应该跳转到哪个内核函数去处理。
- 跳转到处理程序:
- CPU 从 IDT 中拿到对应的中断服务程序(ISR) 的入口地址,并开始执行该内核代码。至此,CPU 已经完全在内核的控制下运行。
阶段二:软件处理(由操作系统内核完成)
此时,CPU已经在内核态,但只是执行了一个通用的入口例程。内核需要做更多事情来完整地处理这次陷入。
- 保存完整的用户态上下文:
- 刚才CPU只自动保存了少数几个寄存器。内核的汇编入口代码需要继续将所有的通用寄存器(EAX, EBX, ECX, EDX...)也压入内核栈。这样,整个用户进程的执行现场就被完整地保存了下来,将来可以完美地恢复。
- 识别陷入原因:
- 对于系统调用,从特定的寄存器(如 EAX)中读出系统调用号。
- 对于中断,读出中断控制器的信息来确定是哪个设备。
- 对于异常,根据异常类型和错误地址进行相应处理(如发送信号杀死进程、或进行页面换入)。
- 执行核心服务:
- 系统调用:根据系统调用号,查询 系统调用表(sys_call_table),找到对应的内核函数(如
sys_read)并执行。 - 中断:执行设备驱动程序的中断处理程序。
- 异常:调用相应的异常处理函数(如
page_fault_handler)。
- 调度与信号处理(可选):
- 在从内核返回用户态之前,内核会检查当前进程是否需要被重新调度(
need_resched标志是否被设置)。如果需要,则会调用调度器切换到另一个进程。 - 内核也会检查是否有信号(Signal)需要递送给当前进程。如果有,会安排执行用户态的信号处理函数。
阶段三:返回用户态
核心服务执行完毕后,需要安全地返回到用户态程序。
- 恢复上下文:
- 内核代码从内核栈中弹出之前保存的通用寄存器,恢复用户进程的寄存器状态。
- 执行返回指令:
- 执行一条特殊的返回指令(如 x86 的
iret或sysret)。 - 这条指令会自动从内核栈中弹出阶段一中由CPU保存的上下文:包括 CS、EIP、EFLAGS、SS、ESP 等。
- 当 CS 和 EIP 被弹出时,CPU 的特权级会从内核态(Ring 0)降回用户态(Ring 3),因为 CS 中包含了特权级信息。
- 继续执行用户代码:
- CPU 现在回到了用户态,使用用户栈,并从当初被打断的那条指令(或下一条指令)继续执行。整个过程对用户程序来说是透明的。
内核空间和用户空间的地址分配
不同位数的系统,地址空间的范围也不同。比如最常⻅的 32 位和 64 位系统:
1、32 位系统的内核空间占用 1G ,位于最高处,供操作系统内核使用;剩下的 3G 是用户空间,供进程自身使用。这种划分在进程的页表中定义,通过 CPU 内的 MMU 来实现转换和权限检查。
- 用户空间 (0x00000000 - 0xBFFFFFFF):
- .text: 存放程序的可执行代码。
- .data: 存放已初始化的全局变量。
- .bss: 存放未初始化的全局变量。
- 堆: 动态分配的内存,向高地址增长。
- 内存映射区: 映射文件或共享库。
- 栈: 存放局部变量和函数调用信息,向低地址增长。
- 命令行参数和环境变量: 在栈的底部。
- 内核空间 (0xC0000000 - 0xFFFFFFFF):
- 映射了完整的物理内存(但通常不是直接1:1映射)。
- 存放内核代码和数据结构。
- 为每个进程维护进程控制块。
- 提供设备驱动程序访问硬件的地址映射。
- 中断处理程序和系统调用入口。
- 所有进程的内核空间地址布局都是相同的,因为内核是唯一的,为所有进程服务。
虽然每个进程的虚拟地址空间中都有内核空间,但用户进程无法直接访问它。当进程运行在用户模式时,CPU 不允许访问内核空间的页面。只有当发生系统调用或中断,CPU 切换到内核模式后,才能访问内核空间。
2、64 位系统的内核空间和用户空间都是 128T ,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义区域,任何访问都会导致错误。
- 用户空间: 通常从
0x0到0x00007FFFFFFFFFFF。
- 内核空间: 通常从
0xFFFF800000000000到0xFFFFFFFFFFFFFFFF。

内存分页机制
按照虚拟内存和物理内存的映射形式,可以分为:
- 内存分段
- 内存分页
内存分段
在内存分段的机制下,虚拟内存和物理内存通过内存段表进行映射。

- 把虚拟内存地址分为段选择子和段内偏移量。
- 段选择子就保存在段寄存器里面。段选择子里面最重要的是段号,用作段表的索引。
- 段内偏移量应该位于 0 和段界限之间,将段基地址加上段内偏移量得到物理内存地址。
- 将段表划分为这个段基地址、段界限和特权等级等,每一项段表项对应虚拟内存地址的一个段号。
- 分段机制会把程序的虚拟地址分成 4 个段, 每个段在段表中有一个。虚拟内存地址通过段号在段表找到对应项的段基地址,再加上段内偏移量,即可就能找到物理内存中的地址。

内存分段会有两个问题:
- 内存碎片问题。
- 内存交换的效率低问题。
内存分页
为了解决内存分段的问题,后面就出现了内存分页。分⻚是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间, 我们叫⻚(Page)。在 Linux 下,每一⻚的大小为 4KB 。虚拟地址与物理地址之间通过⻚表来映射。

注意:
- 页表是存在于内存的,一般是在内核态。
- 每个进程都有一个单独的页表,而且每个进程的页表都会映射全部物理内存。
- MMU是在CPU内部的,功能是将虚拟内存地址转换成物理地址。
在内存分页机制,虚拟内存和物理内存通过内存页表进行映射。

- 虚拟地址分为两部分:⻚号和⻚内偏移。⻚号作为⻚表的索引,页内偏移量等于物理地址上的偏移量。
- ⻚表分为虚拟页号和物理页号,虚拟页号对应虚拟地址的页号,物理⻚对应物理内存的基地址。
- 虚拟地址根据⻚号,从⻚表里面,查询对应的物理⻚号,再加上页内偏移量就得到了物理内存地址。

而且在分⻚的形式下,我们在加载程序的时候,不再需要一次性都把程序加载到物理内存中。我们只需要在进行虚拟内存和物理内存的⻚之间完成映射,而不用真的把⻚加载到物理内存里。只有在程序运行中,需要用到对应虚拟内存⻚里面的指令和数据时,再加载到物理内存里面去。
内存的加载和释放都是以页为单位的:
- 换出(Swap Out):将分页从物理内存写入硬盘。
- 换入(Swap In):将内存从硬盘写入物理内存。
多级页表
内存分页虽然解决了内存分段的碎片化问题,但是也存在空间消耗上的问题。
上面我们提到 linux 下的分页大小是 4KB,假如每一页对应的页表项大小为 4 个字节,那么分页和页表项的大小对比比例就是1000:1,换而言之,总内存和页表的大小对比比例就是1000:1。
假如32位系统下系统内存是4G,页表项大小为 4 个字节,那么就需要2^20个页表项,对应的页表大小就是4MB。因为每个进程都有单独的页表,假如有100个进程,就需要 400MB 的内存来存储⻚表。这是比较高的空间消耗了。
为了解决空间消耗的问题,提出多级⻚表(Multi-Level Page Table)的解决方案。

把 2^20 个「⻚表项」的单级⻚表进行二次分⻚:
- 一级页表包含1024个页表项,每项指向一个二级页表地址。
- 二级页表也包含1024个页表项,每项指向物理内存地址。
- 虚拟地址同时包含一级页号和二级页号。
理论上,使用二级页表需要的内存空间好像更多了,需要 4KB(一级⻚表)+ 4MB(二级⻚表),但实际情况是:一个进程往往用不到那么多内存,部分对应的⻚表项都是空的,根本没有分配,我们可以在进程真的用到内存的时候再分配内存。但是不管进程用了多少内存,⻚表映射一定要覆盖全部虚拟内存地址。
如果使用单个页表就需要有 2^20 个⻚表项来映射全部虚拟内存地址。使用二级页表的话,一级页表只需要 1024 个页表项就可以映射全部虚拟内存地址,二级页表在需要用到的时候再进行创建。
二级分⻚再推广到多级⻚表,就会发现⻚表占用的内存空间更少了,例如 64 位的系统下变成了四级目录:
- 全局⻚目录项 PGD(Page Global Directory)
- 上层⻚目录项 PUD(Page Upper Directory)
- 中间⻚目录项 PMD(Page Middle Directory)
- ⻚表项 PTE(Page Table Entry)

TLB
多级页表虽然解决了单表带来的空间消耗的问题,但是如果每次查找虚拟内存地址对应的物理内存地址都要经过多次查找,就会带来效率上的问题。
程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。利用这一特性,把程序最常访问的⻚表项存到 Cache 然后放到 CPU 里面,这个 Cache 就是 TLB(Translation Lookaside Buffer)。

有了 TLB 后,CPU 在通过 MMU 查找物理内存地址的时候,先查 TLB,如果没找到,才会继续查常规的⻚表。
内存页面置换算法
上面提到处理缺页中断的过程中,会在物理内存找到一个空闲⻚载入磁盘上的内存页。如果此时内存满了,就需要一个内存页面置换算法来腾出内存页,选择一个合适的内存页写入磁盘。
常⻅的⻚面置换算法有:
- 最佳⻚面置换算法(OPT)
- 先进先出置换算法(FIFO)
- 最近最久未使用的置换算法(LRU)
- 时钟⻚面置换算法(CLock)
- 最不常用置换算法(LFU)
最佳⻚面置换算法(OPT)
最佳⻚面置换算法(OPT)的思路是:置换在「未来」最⻓时间不访问的⻚面。

在这个请求的⻚面序列中,缺⻚共发生了
7 次(空闲⻚换入 3 次 + 最优⻚面置换 4 次),⻚面置换共发生了 4 次。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。但是最佳置换算法可以用来评价其他算法的效率。
先进先出置换算法(FIFO)
先进先出置换算法(FIFO)的思路:置换在内存驻留时间最⻓的⻚面。

在这个请求的⻚面序列中,缺⻚共发生了
10 次,⻚面置换共发生了 7 次,跟最佳⻚面置换算法比较起来,性能明显差了很多。最近最久未使用的置换算法(LRU)
最近最久未使用的置换算法(LRU)的思路:置换最⻓时间没有被访问的⻚面。

在这个请求的⻚面序列中,缺⻚共发生了
9 次,⻚面置换共发生了 6 次,跟先进先出置换算法比较起来,性能提高了。最近最久未使用算法近似于最优置换算法,最优置换算法是通过「未来」的使用情况来推测要淘汰的⻚面,而 LRU 则是 通过「历史」的使用情况来推测要淘汰的⻚面。
虽然 LRU 在理论上是可以实现的,但实现起来比较困难:为了完全实现 LRU,需要在内存中维护一个所有⻚面的链表,页面按照「最近访问时间」排序,最近最多访问的⻚面在表头,最近最少访问的⻚面在表尾。每次访问内存的时候都要遍历整个链表找到指定页面,然后把他移动到表头。
所以,LRU 虽然看上去不错,但是由于开销比较大,实际应用中比较少使用。
时钟⻚面置换算法(CLock)
LRU 算法的性能接近于 OPT,但是实现起来比较困难,且开销大;FIFO算法实现简单,但性能差。时钟⻚面置换算法(CLock)结合了两者的优点,现代操作系统置换算法很多都是时钟⻚面置换算法的变体。
时钟⻚面置换算法(CLock)的思路是:把所有的⻚面都保存在一个类似钟面的「环形链表」中,一个表针指向最老的⻚面。当发生缺⻚中断时,算法首先检查表针指向的⻚面:
- 如果它的访问位位是 0 就淘汰该⻚面,并把新的⻚面插入这个位置,然后把表针前移一个位置。
- 如果访问位是 1 就清除访问位,并把表针前移一个位置,重复这个过程直到找到了一个访问位为 0 的⻚面为止。

最不常用置换算法(LFU)
最不常用置换算法(LFU)的思路:置换访问次数最少的页面。
它的实现方式是:对每个⻚面设置一个「访问计数器」,每当一个⻚面被访问时,该⻚面的访问计数器就 累加 1。在发生缺⻚中断时,淘汰计数器值最小的那个⻚面。
但实际问题是:
- 如果要对这个计数器查找哪个⻚面访问次数最小,查找链表本身,如果链表⻓度很大,是非常耗时的,效率不高。
- 只考虑了频率问题,没考虑时间的问题,比如有些⻚面在过去时间里访问的频率很高,但是现在已经没有访问了,而当前频繁访问的⻚面由于没有这些⻚面访问的次数高,在发生缺⻚中断时,就会可能会误伤当前刚开始频繁访问,但访问次数还不高的⻚面。
由于最不常用算法效率不高,命中率也不高,实际很少应用。
内存映射
内存映射原理
内存映射(Memory Mapping)是 Linux 系统中一种重要的内存管理机制,用于将文件或设备映射到进程的虚拟内存空间,使得进程可以像访问内存一样访问这些资源。
内存映射主要通过系统调用
mmap()来实现:addr:建议的映射起始地址(通常设为 NULL 由内核决定)
length:映射区域的长度
prot:保护模式(PROT_READ/PROT_WRITE/PROT_EXEC/PROT_NONE)
flags:映射类型和属性(MAP_SHARED/MAP_PRIVATE/MAP_ANONYMOUS等)
fd:文件描述符(匿名映射时为-1)
offset:文件偏移量(通常为0)
mmap() 系统调用示例:内存映射的常见用途:
- 文件映射(内存映射文件):将文件直接映射到内存,避免频繁的
read/write系统调用,提高 I/O 性能。
- 匿名映射(动态内存分配):类似
malloc,但更灵活(可指定权限和共享行为)。
- 进程间共享内存(IPC):多个进程通过
MAP_SHARED映射同一文件/匿名区域,实现数据共享。
- 实现零拷贝(Zero-Copy):如网络编程中,直接将文件数据映射到内存供发送,避免内核与用户空间的数据拷贝。
缺页异常
缺页异常(Page Fault)是指当程序试图访问当前不在物理内存中的虚拟内存页时,由内存管理单元(MMU)触发的一种异常,这时会请求操作系统将所缺⻚调入到物理内存。
缺页异常的类型:
- 次要缺页异常(Minor Page Fault):所需页面已在物理内存中,但尚未映射到当前进程的页表中。这种异常处理简单,只需建立页表映射
- 主要缺页异常(Major Page Fault):所需页面不在物理内存中,需要从磁盘(交换空间或文件系统)加载。这种异常处理较慢,涉及 I/O 操作:
- 要访问的页面已经被 swapping 到了磁盘,访问时触发缺页异常。
- fork 子进程时,子进程共享父进程的地址空间,写是触发缺页异常(COW 技术)。
- 要访问的页面被 KSM 合并,写时触发缺页异常(COW技术)。
- 无效缺页异常(Invalid Page Fault):程序试图访问无效或受保护的内存地址,通常会导致段错误(Segmentation Fault)。
缺页异常的工作流程:

- 在 CPU 里访问一条 Load M 指令,然后 CPU 会去找 M 所对应的⻚表项。
- 如果该⻚表项的状态位是「有效的」,那 CPU 就可以直接去访问物理内存了,如果状态位是「无效的」,则 CPU 则会发送缺⻚中断请求。
- 操作系统收到了缺⻚中断,则会执行缺⻚中断处理函数,先会查找该⻚面在磁盘中的⻚面的位置。
- 找到磁盘中对应的⻚面后,需要把该⻚面换入到物理内存中,但是在换入前,需要在物理内存中找空闲⻚,如果找到空闲⻚,就把⻚面换入到物理内存中。
- ⻚面从磁盘换入到物理内存完成后,则把⻚表项中的状态位修改为「有效的」。
- 最后,CPU 重新执行导致缺⻚异常的指令。
- Author:mcbilla
- URL:http://mcbilla.com/article/1363c42f-d2c0-4ed0-b0e1-3d73fbc30474
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts
