本文完全参考
- 虚拟内存
- 内存分段
- 内存分页
- 段页式内存管理
- Linux内存管理
一、虚拟内存
1. 单片机的绝对物理地址
以单片机作为引子,它没有操作系统,每次写完程序是借助工具将程序烧录进单片机,程序才能运行。
单片机由于没有操作系统,其CPU之间操作内存的物理地址。
这种设计,要在内存中同时运行两个程序是不可能的。比如程序一在2000位置写入一个值,将会擦除掉程序二存放在当前位置的所有内容,两个程序都会崩溃。
单片机的问题:两个程序都直接操作绝对物理内存。
操作系统如何解决这个问题呢?【虚拟内存】
为了避免不同的进程共用一段相同的绝对物理地址,可以把进程所使用的地址「隔离」开来,即让操作系统为每个进程分配独立的一套「虚拟内存」,不同进程使用的内存彼此独立、互不干涉,前提是每个进程都不能访问物理内存。
两种内存地址:
虚拟内存地址(Virtual Memory Address):程序所使用的内存地址;
物理内存地址(Physical Memory Address):实际存在硬件里面的空间地址;
操作系统提供一种映射机制,将不同进程的虚拟地址与不同的内存物理地址映射起来,当程序访问虚拟地址时,操作系统转换成不同的物理地址,这样不同进程运行时,写入的是不同的物理地址,这样就避免了冲突,实现了进程内存隔离。
这个映射机制由CPU中的内存管理单元(MMU)实现,进程持有的虚拟内存可以通过MMU转变成物理地址,然后通过物理地址访问内存,如图:
操作系统具体是如何管理虚拟内存和物理地址的关系?
- 内存分段;
- 内存分页;
两种方法,下面逐一介绍:
二、内存分段
1. 程序分段机制
程序是由若干个逻辑分段(Segmentation)组成的,不同段有不同属性,包括:代码分段、数据分段、栈分段、堆分段等。
2. 分段机制下,虚拟地址和物理地址如何映射?
分段机制下的虚拟地址由两部分组成,段选择因子和段内偏移量。
段选择因子:
- 位置:保存在段寄存器中
- 段号:段选择因子中最重要的是段号,用作段表索引
- 段表: 段表保存着这个段的基地址、段的界限和特权等级等。
段内偏移量:
- 范围:段内偏移量位于0和段界限之间;
- 规则:如果段内偏移量合法,就将段基地址+段内偏移量=物理内存地址。
即虚拟地址是通过段表与物理地址进行映射,分段机制把程序的虚拟地址分为4个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,就能找到物理内存中的地址。
比如要访问段3中偏移量为500的虚拟地址,可以计算出目标物理地址 = 段3基地址7000 + 偏移量 500 = 7500.
分段内存解决了程序本身不需要关心物理内存地址的问题,但是问题在于:
- 内存碎片;
- 内存交互效率低;
3. 内存碎片问题
3.1 内存碎片产生原因
如图,由于分段内存机制,真实物理内存中分出了三片内存用于不同的进程,比如游戏、音乐、浏览器。当我们关闭中间的浏览器内存时,就产生了内存碎片。真实物理内存中有200MB以上的内存空闲,但是由于内存分段机制与内存碎片问题,无法打开200MB的进程!
3.2 内存碎片分类
内存分段带来的内存碎片主要是外部内存碎片问题。
内存碎片分为内部内存碎片和外部内存碎片。
内存分段管理可以做到段实际需求分配内存,所以可以有多少需求就分配多少的段,不会出现内部内存碎片问题。
但是由于每个段的长度不固定,多个段未必恰好使用所有的内存空间,会产生多个不连续的小物理内存,导致新的程序无法被装载,出现外部内存碎片问题。
解决外部内存碎片问题就是用内存交换
3.3 内存交换
可以将音乐程序占用的256MB内存暂时写进磁盘,然后再从磁盘写回内存,但是不是写回去原来位置,而是紧紧跟在已占用的512MB内存后面,这样就解决了外部内存碎片问题,腾出了256MB内存空间,于是新的200MB以上的程序就可以装载进来了!
这个内存交换空间,在Linux系统中称为swap空间,这块空间是从硬盘划分出来的用于内存与硬盘的空间交换。
3.4 内存分段的效率问题
对于多进程系统,用分段内存的方式映射虚拟内存和物理内存很容易产生外部内存碎片,那就不得不用swap内存区域进行内存交换,这个过程涉及到磁盘I/O,而磁盘速度相比内存慢太多,每次内存交换都需要把一大段连续内存数据写入磁盘。
所以如果内存交换时,交换的是一个占用很大内存空间的程序,整个机器都会卡顿!
3.5 内存分段总结
外部内存碎片问题和内存交换效率极低问题,为了解决这两个问题,我们用到内存分页机制。
三、内存分页
分段的好处就是能产生连续的内存空间,但是会出现「外部内存碎片和内存交换的空间太大」的问题。
有什么办法能减少内存碎片问题?或者当需要内存交换的时候,让需要swap的数据少一些,减少磁盘装载数据,就可以解决内存分段的问题了。
内存分页(Paging)把整个虚拟和物理内存切成一段段固定尺寸的大小,一个连续且尺寸固定的内存空间,我们称为页(Page),Linux下,每一个内存页大小为4KB。
3.1 页表
虚拟内存与物理内存之间通过页表来映射:
页表:
页表是存储在内存中的,内存管理单元(MMU)负责将虚拟内存地址转换成物理地址。
缺页异常处理:
当进程访问的虚拟内存在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存,更新进程页表,最后返回用户空间,恢复进程运行。
缺陷:内部内存碎片问题:
内存分页后,页与页之间紧密排列,所以不会产生外部内存碎片,但是由于内存分页机制的最小单位是一页4KB,所以即便是程序需求不足一页大小,我们最少也只能分配一页,所以页内可能会出现内存浪费,即内存分页机制存在内部内存碎片问题。
优势1:内存交换效率高:
如果内存空间不够了,操作系统会把其他正在运行的进程中最近没被使用的内存页面给释放掉,注意并不是完全关闭,而是Swap out(换出)到磁盘空间,一旦需要调用,再加载到内存空间,即Swap in (换入)。所以一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换效率高。
优势2:不必一次性加载全部内存页,分步加载需要的进程即可。
分页内存的机制,使得我们不需要一次性把程序加载到物理内存,完全可以进行虚拟内存和物理内存页之间的映射后,并不真的把全部虚拟内存页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令或数据时,再将其加载到物理内存中。
分页机制下,虚拟地址和物理地址具体映射机制?
虚拟地址分为两个部分,页号和页内偏移
- 页号是页表的索引,页表包含物理页每页所在的物理地址的基地址,基地址与页内偏移的组合形成了物理内存地址。
分页内存机制的内存地址转换,分三步:
- 把虚拟地址切分为页号和偏移量
- 根据页号,从页表查询对应的物理页号
- 物理页号+偏移量 = 物理内存地址
通过以上映射,虚拟内存中的页通过页表映射到了物理内存中的页:
以上是简单的分页内存机制,一视同仁的全部分成等大的页,有何缺陷?
空间缺陷:
因为操作系统是可以同时运行非常多的进程的,那这不就意味着页表会非常的庞大。
32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB(2^12),那么就需要大约 100 万个页,每个页表项就需要4字节大小来存储,整个4GB空间映射就需要有4MB的内存来存储页表。
那么,100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了,更别说 64 位的环境了。
3.2 多级页表
单页表的实现方式,在 32 位 和 页大小 4KB 的环境下,一个进程的页表需要装下 100多万个页表项,并且每个页表项是占用4字节大小的,于是相当于每个页表需要占用4MB大小的空间。
把这100 多万个页表项的单级页表进行再分页,将页表(一级页表)分为1024个二级页表,每个二级页表中包含了1024个页表项目,形成二级分页。
“单级页表 vs 二级页表的内存占用对比”
论证多级页表如何解决单级页表的内存浪费问题,核心逻辑如下:
- 单级页表的痛点:必须“全占满”内存
页表的职责是 覆盖整个虚拟地址空间(否则虚拟地址无法转物理地址,程序崩溃)。
以 4GB 虚拟地址空间、4KB 页大小为例:
- 页表项数量 = 4 G B 4 K B = 100 万 + ( 即 2 20 项 ) 页表项数量 = \frac{4GB}{4KB} = 100万+(即2^{20}项) 页表项数量=4KB4GB=100万+(即220项)
- 若每项占 4 字节,单级页表总内存 = 100万 ✖ 4B = 4MB
问题:即使程序只用了 20% 的虚拟地址,单级页表仍需预先分配 4MB 内存存所有页表项(包括未使用的 80%),造成巨大浪费。
- 二级页表的优化:分层 + 按需创建
二级页表将页表拆为 “一级页表(占位) + 二级页表(按需建)”:
- 一级页表:先建 1024 项(覆盖整个 4GB 虚拟地址空间),每项存“二级页表的位置索引指针”,仅占 1024 ✖ 4B = 4KB
- 二级页表:仅当程序访问某段虚拟地址时,才创建对应二级页表(未访问的虚拟地址,其二级页表不建,节省内存)。
计算对比(假设仅 20% 一级页表项被使用):
总内存 = 一级页表(4KB) + 20% × 二级页表总容量(4MB) ≈ 0.804MB,远小于单级页表的 4MB。
- 核心结论:分层让“全覆盖”和“省内存”共存
单级页表必须为所有虚拟地址预先建页表项,导致内存浪费;
二级页表通过 “一级页表先覆盖全空间(满足地址翻译的必要性),二级页表按需创建(避免未使用地址的内存开销)” ,实现了“覆盖全虚拟地址 + 节省内存”的平衡。
简言之,多级页表的本质是 用“分层占位 + 按需加载”的策略,解决单级页表“全量预分配”的内存臃肿问题。
多级页表概念
多级页表是为解决单级页表在大地址空间下的内存浪费问题而设计的分层地址转换结构,核心是 “按需存储页表项”。
基本实现思路:
将虚拟地址转换所需的页表拆分成多个层级(比如二级、三级),虚拟地址被分成对应层级的索引段。只有实际被程序使用的页表层级才会加载到内存,未使用的层级可以保存在外存(如硬盘),避免单级页表 “不管用不用都占满内存” 的问题。
举例(二级页表):
虚拟地址分为三部分:页目录索引→页表索引→页内偏移。
- 第一级(页目录):存储指向第二级页表的指针(仅记录被使用的页表位置);
- 第二级(页表):存储实际的物理页地址;
- 转换时,先通过页目录索引找到对应的页表,再通过页表索引找到物理页,最后结合页内偏移得到物理地址。
核心作用:
大幅减少页表对内存的占用(尤其适合 64 位等大地址空间系统),同时保持地址转换功能。代价是地址转换时可能需要多次访问内存(可通过 TLB 缓存缓解)。
对于64位系统,二级分页也不够,变成了四级分页:
- 全局页目录项 PGD(Page Global Directory);
- 上层页目录项 PUD(Page Upper Directory);
- 中间页目录项 PMD(Page Middle Directory);
- 页表项 PTE(Page Table Entry);
TLB
TLB(Translation Lookaside Buffer,地址转换高速缓存)是 多级页表的“性能补丁” ,专门解决多级页表地址转换时的速度瓶颈,核心逻辑如下:
为什么多级页表需要TLB?
多级页表通过“分层 + 按需创建”节省了内存,但地址转换时 需要多次访问内存(比如二级页表要先查一级页表,再查二级页表),导致速度变慢。
TLB的作用是 “缓存常用的地址映射关系” ,避免每次都走多级页表的繁琐流程。TLB是什么?
TLB是CPU内部的 高速缓存,存储 最近使用的虚拟地址→物理地址的映射(即页表项的副本) 。
可以理解为:把多级页表里 常用的“虚拟页→物理页”翻译结果 ,临时存在CPU附近的“快捷栏”里。TLB如何工作?(结合多级页表)
当CPU需要转换虚拟地址时,流程如下:
- ① 先查TLB:如果TLB里有对应的虚拟页→物理页映射(TLB命中),直接用物理地址访问内存,跳过多级页表的查询。
- ② 若TLB没命中:才去遍历多级页表(比如一级页表→二级页表),找到物理页后,把这次的映射存入TLB(TLB满了就按策略替换,比如LRU淘汰最久没用的项)。
- TLB和多级页表的关系:互补设计
- 多级页表:解决 “内存占用大” 问题(按需创建页表,不浪费内存);
- TLB:解决 “地址转换慢” 问题(缓存常用映射,减少多级页表的内存访问次数)。
两者结合,让大地址空间(如64位系统)的内存管理 既省内存,又高效 。
通俗类比:
把多级页表想象成 “分层的字典”(一级目录→二级字典),查单词(地址转换)需要先翻目录、再翻字典,步骤多;
TLB就是 “把常用单词的翻译记在便利贴上” ,下次直接看便利贴,不用再翻字典,速度飞起!
TLB也称快表、页表缓存、转址旁路缓存等,CPU中封装了内存管理单元(Memory Management Unit)芯片,用来完成地址转换和TLB访问交互。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。
四、段页式内存管理
内存分段和内存分页并不是对立的,而是可以组合在一个系统里面使用,组合后称为段页式内存管理。
段页式内存管理实现:
- 先分段:将程序划分为多个有逻辑意义的段,代码段、数据段、栈和堆等;
- 再分页:对分段划分出来的连续空间,再划分固定大小的页;
段页式内存管理是 “段式 + 页式的混合方案” ,核心是 “先按逻辑分段,再把每个段分页管理” ,结合两者优势、规避缺点:
1. 核心设计:“段套页”的两层结构
第一步:按逻辑分段
把虚拟地址空间按 逻辑功能划分成段(如代码段、数据段、栈段),每个段有独立的 段描述符(存段的基址、长度、访问权限等)。
→ 实现 逻辑模块化(比如代码段只读,数据段可读写,方便权限管理和共享)。第二步:段内分页
每个段内部,再按 固定大小(如4KB)分成页,用 页表 管理“虚拟页→物理页”的映射。
→ 解决 段式的内存碎片问题(段式按段分配易产生碎片,分页后碎片粒度变小),同时支持 虚拟内存(页可换入换出硬盘)。
2. 地址转换流程(以虚拟地址→物理地址为例)
虚拟地址被拆成三部分:段号 + 页号 + 页内偏移,转换分两步:
查段描述符:
根据段号找到对应的段描述符,检查:- 权限(如代码段是否被非法写操作访问);
- 范围(页号是否超过段的长度,避免越界)。
若合法,获取 段的基址(段在虚拟地址空间的起始位置)。
查页表(段内分页):
结合段基址 + 页号,定位到 页表项,获取物理页框号,最后加上 页内偏移,得到物理地址。
→ 若页表项标记“页不在内存”,会触发 缺页中断,将页从硬盘换入内存。
地址转换举例子:
用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号,如图所示:
3. 对比段式、页式,段页式的优劣势
特性 | 段式 | 页式 | 段页式 |
---|---|---|---|
逻辑管理 | 清晰(按功能分段) | 模糊(无逻辑划分) | 清晰(段负责逻辑,页负责物理) |
内存碎片 | 易产生大碎片(按段分配) | 碎片小(按页分配) | 碎片小(继承页式优点) |
权限/共享 | 方便(段级控制) | 麻烦(需额外标记) | 方便(段级控制) |
地址转换开销 | 一次查表(段→物理) | 一次查表(页→物理) | 两次查表(段→页表→物理),依赖TLB加速 |
4. 通俗类比:“文件夹套文件”
- 段 ≈ 文件夹:按功能分类(代码文件夹、数据文件夹),每个文件夹有“属性”(只读/可写);
- 页 ≈ 文件夹里的文件:每个文件夹里的内容按固定大小(如4KB)分成多个文件,方便存储和搬运(换入换出硬盘);
- 地址转换 ≈ 找文件:先找文件夹(段),再在文件夹里找具体文件(页),最后定位文件内的位置(偏移)。
核心结论:
段页式通过 “段管逻辑、页管物理” ,平衡了 模块化(权限、共享) 和 内存效率(碎片、虚拟内存) ,代价是地址转换更复杂(需两次查表),因此依赖 TLB(高速缓存) 加速常用地址的转换。
现代操作系统(如Linux、Windows)的内存管理,本质都是段页式的变种(结合了虚拟内存、TLB优化)。
五、Linux内存管理
5.1 从intel处理器发展历史讲起
要理解 Intel 处理器发展中 内存分段与虚拟内存 的演变,可按 “问题→方案→升级” 的逻辑,结合关键 CPU 型号拆解:
一、早期 CPU(8080/8085):无虚拟内存,直接物理寻址
- 核心问题:程序直接操作物理内存,多进程会互相覆盖数据(比如进程 A 改写进程 B 的代码),且寄存器位数和地址总线一致(如 8 位 CPU 只能寻址 8 位空间),扩展能力差。
二、8086(1978):内存分段解决“寻址能力不足”,但无保护
- 矛盾:8086 是 16 位寄存器,但地址总线是 20 位(可寻址 1MB 内存)。16 位寄存器最多只能表示 64KB(2¹⁶),如何访问 1MB 空间?
- 方案:内存分段:
- 物理地址公式:
物理地址 = 段基址 × 16 + 段内偏移
。 - 举例:段寄存器
CS=0x1000
(段基址),指令指针IP=0x0005
(段内偏移),则物理地址 =0x1000×16 + 0x0005 = 0x10005
。 - 本质:把 1MB 内存切成多个 64KB 的段(段内偏移最大 0xFFFF,即 64KB),让 16 位寄存器间接访问 20 位地址。
- 物理地址公式:
- 缺陷:仍为 实模式,段基址可随意设置,程序能直接篡改物理内存,多进程无法安全共存。
三、80286(1982):保护模式 + 分段,虚拟内存雏形
- 目标:解决实模式的安全问题,支持多进程。
- 升级:保护模式的分段:
- 段寄存器不再存“段基址”,而是 段选择子(类似“索引”)。
- 通过段选择子,去 全局描述符表(GDT) 查找 段描述符(存段基址、段长度、权限,如“代码段只读”)。
- 操作系统管理 GDT,进程不能随意修改段选择子,必须通过 中断/调用门 切换特权级(如用户态→内核态),防止非法访问。
- 意义:首次实现 内存保护(段级权限控制)和 多进程隔离(不同进程的段描述符不同,地址空间互不干扰),是虚拟内存的雏形(但未解决内存碎片和交换效率问题)。
四、80386(1985):段页式结合,虚拟内存成熟
80386 除了完成并完善从 80286 开始的段式内存管理的同时还实现了页式内存管理。但没有绕开段式内存管理,而是建立在段式内存管理基础上,这就意味着页式内存管理则作用是:由段式内存管理所映射而成的地址上再加一层地址映射。
由于此时由段式内存管理映射而成的地址不再是物理地址,intel就称之为线性地址(虚拟地址)。于是段式内存管理先将逻辑地址映射成了线性地址,然后由页式内存管理将线性地址映射成了物理地址。
- 逻辑地址:程序所使用的地址,通常没有被段式内存管理映射的地址;
- 线性地址(虚拟地址):通过段式内存管理映射的地址;
- 新增:分页机制:
- 在保护模式基础上,引入 分页:把虚拟地址和物理地址切成 固定大小的页(如 4KB),用 页表 管理“虚拟页→物理页”的映射。
- 地址转换流程:
- 先通过 分段 得到 线性地址(段基址 + 段内偏移);
- 再通过 分页 将线性地址转成物理地址(若关闭分页,线性地址直接是物理地址)。
- Linux 的优化:平坦内存模型:
- Linux 为简化设计,将所有段的 段基址设为 0,段界限设为整个虚拟地址空间(如 32 位下 4GB),让分段“名存实亡”(只剩权限检查功能,如代码段只读),实际用 分页 主导虚拟内存管理(多级页表、Swap、缺页中断等)。
五、64 位时代(如 x86-64):分段弱化,分页主导
- 趋势:64 位 CPU 中,分段机制进一步简化:
- 段基址被强制设为 0,段界限覆盖整个 64 位地址空间,实际变成 平坦模式,分段仅保留最基本的权限检查(如代码段不可写)。
- 分页成为虚拟内存的核心,发展出 多级页表(解决页表内存占用)、TLB(加速地址转换)等优化,支撑 TB 级虚拟地址空间。
关键逻辑总结:
- 内存分段的诞生:是 8086 为解决“16 位寄存器访问 20 位地址”的权宜之计,后来在保护模式中承担 内存保护 职责。
- 虚拟内存的实现:保护模式(分段)实现 地址隔离和权限控制,分页解决 内存碎片和 Swap 效率问题,两者结合形成段页式。
- Linux 的取舍:通过“平坦模型”弱化分段的寻址功能,仅用其保护特性,让 分页成为虚拟内存管理的核心。
- Intel 的兼容性:新处理器仍保留分段机制,只为向后兼容,实际应用中已几乎不用分段的寻址能力。
理解这段历史,就能明白:内存分段是硬件历史遗留的“补丁”,而分页才是现代虚拟内存的灵魂,Linux 等系统通过巧妙设计,让分段“退居幕后”,分页“挑大梁”。
5.2 Linux 采用了什么方式管理内存?
Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。
从 整体架构→空间划分→用户态布局 层层拆解,Linux内存管理核心逻辑如下:
一、硬件历史倒逼:Linux如何应对Intel的“分段”要求?
Intel x86 CPU的设计规则是:程序地址必须先经过“分段映射”,再进行“分页映射”(历史遗留,源于8086的分段寻址)。但Linux的核心需求是 “简单、高效的虚拟内存” ,于是采取“折中策略”:
- 让分段“形同虚设”:把所有段的起始地址设为0,段长度覆盖整个虚拟地址空间(如32位下占4GB)。这样,分段后的“线性地址”和原始虚拟地址几乎一致,相当于屏蔽了分段的“寻址功能”,只保留其 “权限控制” (如代码段只读、数据段可读写)。
Linux系统中每个段都是从0地址开始的整个4GB的虚拟内存空间,也就是所有段的起始地址都一样,这意味着Linux系统中的代码,包括操作系统本身的代码和应用程序代码,所面对的地址空间都是线性地址空间(虚拟地址),这种做法相当于屏蔽了段内存分配的段寻址,处理器中逻辑地址的概念,段只有访问控制和内存保护的功能。
二、Linux虚拟地址空间:内核与用户的“楚河汉界”
Linux 操作系统中,虚拟地址空间被划分为 内核空间 和 用户空间 ,不同位数系统的地址空间范围划分不同:
系统位数 | 虚拟地址总大小 | 内核空间占比 | 用户空间占比 | 特点 |
---|---|---|---|---|
32位 | 4GB | 1GB(高地址,如0xC0000000~0xFFFFFFFF ) |
3GB(低地址,如0x00000000~0xBFFFFFFF ) |
内核占比小,用户空间充足 |
64位 | 超大(如256T) | 128T(最高地址段) | 128T(最低地址段) | 中间大量地址未定义 |
内核与用户空间的核心区别:
- 访问权限:
- 进程在 用户态 时,只能访问 用户空间 内存;
- 只有进入 内核态(如系统调用、中断),才能访问 内核空间 。
- 物理映射:
所有进程的 内核虚拟地址 ,都映射到同一块物理内存(内核代码、数据全局共享)。这样,进程切换到内核态后,无需重新加载内核数据,直接访问即可。
三、用户空间布局:从低到高的“分段江湖”(以32位为例)
用户空间从 低地址→高地址 ,依次分布7个区域(含保留区),每个区域分工明确:
1. 保留区(最底部,接近0地址)
- 作用:拦截非法地址访问(如
NULL
指针)。 - 原理:系统规定“低数值地址(如0x00000000附近)为非法”,若程序访问这些地址(比如空指针解引用),会触发段错误(Segmentation Fault),避免程序崩溃后乱改内存。
2. 代码段(.text)
- 内容:存放程序的可执行二进制代码(如函数指令)。
- 特性:只读(防止程序意外修改自身代码)。
3. 数据段(.data)
- 内容:存放已初始化的全局变量、静态常量(如
int g_var = 10;
)。 - 特性:可读写,程序运行时能修改这些变量。
4. BSS段(.bss)
- 内容:存放未初始化的全局变量、静态变量(如
int g_var;
,默认值为0)。 - 特性:可读写,加载时会自动清零。
5. 堆(Heap)
- 内容:动态分配的内存(如
malloc()
申请的空间)。 - 增长方向:从低地址→高地址向上扩展,需手动管理(
free()
释放)。
6. 文件映射段
- 内容:动态库(如
libc.so
)、共享内存(如mmap()
映射的文件)。 - 增长方向:从低地址→高地址向上扩展(和堆同方向,中间可能有未使用的间隙)。
7. 栈(Stack)
- 内容:局部变量、函数调用的上下文(如参数、返回地址)。
- 增长方向:从高地址→低地址向下扩展,大小固定(默认8MB,可通过
ulimit -s
调整)。 - 风险:栈溢出(如递归过深)会导致程序崩溃。
动态分配的秘密:
- 堆 + 文件映射段 是用户空间的“动态区域”:
- 堆用
malloc()
分配,由程序员手动管理; - 文件映射段用
mmap()
分配,可映射文件或匿名内存(如共享内存)。
- 堆用
- 栈是“静态区域”,大小固定,满了就溢出。
核心逻辑总结:
- 硬件妥协:Linux为兼容Intel的分段机制,让分段“名存实亡”,实际以 页式管理 为主。
- 空间隔离:虚拟地址分为内核(高权)和用户(低权)空间,保障系统安全。
- 用户态细节:从低到高的分段布局,让代码、数据、动态内存、栈各司其职,保留区防止空指针踩雷,堆和文件映射支持灵活分配。
理解这套设计,就能明白:Linux的虚拟内存,是“硬件约束”和“软件高效”妥协后的产物,既兼容历史,又通过分层布局实现了安全与灵活。
操作系统内存管理总结
虚拟内存是操作系统应对 “多进程共存、物理内存有限、安全访问” 难题的核心方案,其设计围绕 “硬件妥协+效率优化” 展开,核心逻辑可总结为:
一、诞生背景:解决三大痛点
多进程环境下,直接使用物理内存会出现:
- 地址冲突:进程互相篡改数据(如进程A覆盖进程B的代码);
- 内存不足:物理内存撑不起大量进程/大程序;
- 安全风险:程序非法访问系统核心内存(如篡改内核)。
二、实现进化:从分段到分页的迭代
1. 分段:逻辑清晰,但低效
- 思路:按“代码、数据、栈”等逻辑功能划分虚拟地址,每个段连续,方便权限管理(如代码段只读)。
- 缺陷:段大小不固定,产生 外部内存碎片(空闲内存分散,无法分配大块空间),且换入换出效率低(整段操作)。
2. 分页:固定大小,解决碎片
- 思路:将虚拟/物理地址切成 等长页(如Linux的4KB),用页表记录“虚拟页→物理页”的映射。
- 优势:
- 碎片粒度小(仅4KB),解决外部碎片;
- 换入换出高效(仅操作单个页)。
- 新问题:页表太大(如32位系统需百万级页表项),占用内存多。
3. 多级页表 + TLB:优化空间与速度
- 多级页表:将页表分层(如二级、三级),按需加载页表层级(未使用的层级不占内存),解决页表臃肿问题。
- TLB(地址转换缓存):在CPU内缓存 常用页表映射,避免每次地址转换都遍历多级页表,加速地址转换。
4. Linux的妥协:兼容Intel,弱化分段
因Intel处理器强制“先分段再分页”,Linux采取 “段基址设为0” 的策略:
- 让所有段的起始地址相同,虚拟地址近似“线性空间”,屏蔽分段的寻址功能,仅用分段做 权限检查(如代码段只读);
- 实际以 分页+多级页表+TLB 主导虚拟内存管理。
三、核心价值:虚拟内存的三大作用
突破物理内存限制:
利用 局部性原理(程序仅频繁访问部分内存),将“不常用页”换出到硬盘(Swap),需要时再换入,让程序“以为”拥有超大内存。实现多进程隔离:
每个进程有独立页表,虚拟地址空间互不干扰(进程A的虚拟地址≠进程B的物理地址),防止地址冲突和非法访问。增强内存安全:
页表项的 权限标记(如只读、存在位)+ 分段的权限控制,防止程序越权访问(如用户态程序篡改内核内存)。
逻辑闭环:
虚拟内存是 “硬件约束(Intel分段)”与“软件需求(高效、安全、多进程)”妥协的产物:
从分段的“逻辑清晰但低效”,进化到分页的“高效但页表臃肿”,再通过多级页表和TLB优化,最终Linux以“弱化分段、强化分页”落地,实现 “内存扩容、进程隔离、安全防护” 的核心目标。
这就是虚拟内存的底层逻辑——用复杂的硬件适配和算法优化,让程序跑得更稳、更自由。