现代操作系统都使用了虚拟内存系统来进行内存管理
1. 进程直接访问物理内存造成的问题
早期的操作系统,进程直接访问物理内存,存在以下问题
- 内存不足
当进程访问的内存地址超出物理内存范围时,内存不足,程序会崩溃。
举个例子,32位CPU的系统,地址线为32条,可以访问的寻址空间为 ,如果机器只有1GB内存,当进程访问一个超过 1GB 范围的内存地址时,程序崩溃。 - 内存碎片化
程序频繁启动和退出,会导致内存的频繁申请和释放,进而产生内存碎片,当进行内存分配申请时,有可能没有任何一块连续的内存区域足够大,而导致分配失败,尽管有足够多的碎片,总量超过申请量。 - 内存访问冲突
多个进程可能访问相同的物理内存地址,造成数据误修改、错乱等。因为进程并不知道哪些内存被占用了。
如何解决上述这些问题呢?操作系统引入了虚拟内存系统来解决。
2. 什么是虚拟内存
基于以上问题,操作系统使用了虚拟内存的技术方案来进行进程见内存管理。可以理解成进程和物理内存之间的一层映射或容器,进程使用虚拟内存地址,也叫逻辑地址,被MMU硬件单元(Memory Manager Unit)映射到对应的物理内存地址。我们可以看看虚拟内存系统是如何解决上述的直接访问物理内存的问题?
- 解决内存不足
当进程申请内存,发现物理内存不足时,系统会根据页面置换算法把暂时不用的内存置换到硬盘上,并把对应的逻辑地址映射到硬盘,而把释放出来的物理内存返回给最新的进程,并维护该逻辑地址到物理地址的映射关系,产生一种无限内存的错觉。 - 解决内存碎片
虚拟内存系统映射表可以找到多块物理内存碎片合并到一个逻辑块中。 - 内存访问冲突
不同进程相同的虚拟地址,但是每个进程有自己的映射表,通常会映射到不同的物理内存,不会互相干扰。
有些情况下,进程间需要共享内存,那么就是不同进程的虚拟地址可以映射到相同的物理地址即可。
3. 虚拟内存的原理
3.1 内存分段
最早的虚拟内存管理方式为内存分段的方式,在进程启动时,为进程分配一块连续的内存,划分为代码段、数据段、栈段、堆段等部分,进程维护一个段表,记录每个段在物理内存的起始地址(段基地址)和该段的最大偏移(段界限),虚拟地址由段号和段内偏移值组成,在映射时,根据段号从段表中查询物理地址起始位置,再加上偏移值得到物理内存地址
内存分段容易导致两个问题
- 物理内存容易出现内存碎片
- 当需要进行内存置换到硬盘时,只能以段为单位,导致内存交换效率较低
3.2 内存分页
为了解决内存分段方案中较大内存碎片和内存交换空间大导致效率低的问题,内存分页的方案被提出。该方案的主要做法是:
- 将整个虚拟和物理内存空间分成若干份固定尺寸的大小,每一份称为一页,通常在 Linux 下,每一页大小为 4KB
- 虚拟地址和物理地址通过页表来进行映射
3.2.1 如何解决内存分段的大块外部碎片?
内存分页,最小的内存分配单位为页,通常要比段小的多,页之间不需要保证连续,可以将多个不连续的页组装成一块较大的内存区域,因此不会产生大块的外部内存
3.2.2 如何解决内存分段的内存交换效率低问题?
内存分段在产生内存 Swap Out 和 Swap In 到硬盘时,都是以段为单位的,通常较大,而内存分页可以以页为单位,更小更精细,在交换时的效率更高。
3.2.3 虚拟地址如何映射到物理地址
虚拟地址由页号和业内偏移量组成,内存中存放一份页表,保存了虚拟页和物理页的对应关系,根据虚拟地址中的虚拟页号到页表中读取物理页号,再加上虚拟地址中的偏移量,就能得到物理地址
3.2.4 内存分页有什么缺陷吗?
(1) 有内部碎片
页的大小固定为4KB,当程序分配的内存需求小于 4KB 时,操作系统也要分配4KB,容易造成页内碎片
(2)页表占用内存较大
页表本身也占用了内存,例如在 32 位环境下,虚拟地址空间为 ,一个页大小是 ,那么页数为 大约为 100 万页,每个页表项需要 4 个字节,则需要 4MB 的空间来存储页表,每个进程都有自己的一份页表,4MB 还是有点大了,我们可以通过多级页表 来减少页表占用的内存大小
3.3 段页式管理
3.3.1 实现方式
分段和分页并不是对立的,Linux 将两种方式组合起来,得到段页式内存管理方案
- 现将进程内存进行分段、每段有各自的逻辑意义
- 再将每个段进行分页
- 虚拟地址由段号、段内页号和页内偏移量组成
-
每个进程对应一张段表、每个段对应一张页表,段表中的地址为页表的起始地址、页表中的地址为该页的物理地址
3.3.2 地址映射
段页式内存的地址映射过程:
- 从虚拟地址中取出段号,访问段表,得到页表起始地址
- 从虚拟地址中取出段内页号,访问对应的页表,得到物理页号
- 从虚拟地址中取出页内偏移,和物理页号组合,得到物理地址
4. Linux 程序的内存布局
4.1 用户空间和内核空间
Linux 系统将虚拟地址空间划分为内核空间和用户空间,对于 32 位环境,低地址的 3G 为用户空间,高地址 1G 为内核空间
内核空间和用户空间的区别:
- 进程在用户态时,只能访问用户空间内存
- 进程只有进入内核态时,才能访问内核空间内存
虽然每个进程都有各自独立的虚拟内存空间,但是 <font color=red>每个进程的内核地址,都关联相同的物理内存地址</font>
4.2 用户空间分段
用户空间被划分成 6 种不同的内存段
- 代码段:程序编译后的可执行代码(指令)存放区域,在编译时确定了,同一个程序在不同机器上、在同一个机器上的不同次运行,同一个方法的入口地址都是确定的
- 数据段:存放程序已初始化的静态常量和全局变量
- BSS 段:存放未初始化的静态常量和全局变量
- 堆:动态分配的内存区域,从低地址向高地址增长,大小不固定
- 栈:存放局部变量、函数调用上下文等,栈的大小在程序启动时就固定了,一般是 8MB,系统提供了参数可以修改