一、虚拟内存的提出
单片机
- 没有操作系统
- 只能运行一个程序
- 每次都要借助工具把代码烧录进去(后面的程序会把之前的覆盖)
单片机的 CPU 是直接操作内存的「物理地址」
现在的问题是
- 有操作系统
- 需要同时运行多个程序(把进程所使用的地址「隔离」开来)
- 每个进程最好都不能直接访问物理地址(后面的程序就不会把之前的覆盖)
解决思路:让操作系统为每个进程分配独立的一套「虚拟地址」,然后操作系统会提供一种机制,将不同进程的虚拟地址和不同内存的物理地址映射起来。
虚拟内存地址(Virtual Memory Address):程序所使用的内存地址
物理内存地址(Physical Memory Address):实际存在硬件里面的空间地址
内存管理单元(MMU):包含虚拟地址和物理地址的映射关系,在CPU芯片中
二、操作系统是如何管理虚拟地址与物理地址之间的关系?
1.内存分段
内存分段是根据程序的逻辑角度,分成了栈段、堆段、数据段、代码段等,这样可以分离出不同属性的段,同时是一块连续的空间。
虚拟地址
- 段选择因子(在段寄存器中):段号、标志位
- 段内偏移量
映射关系
- 段表:段基地址、段界限、特权级
物理地址
- 段起始地址(段基地址)
- 段界限
- 偏移量
分段机制:分段机制会把程序的虚拟地址分成 4 个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。
缺点
- 存在外部内存碎片的问题,解决办法就是内存交换
- 但内存交换效率低,因为每一次内存交换,都需要把一大段连续的内存数据写到硬盘上,而硬盘的访问速度要比内存慢太多。若交换的是一个占内存空间很大的程序,整个机器都会显得卡顿
内存碎片
- 内部内存碎片:没有根据实际需求分配内存,比如很小的程序分配很大的内存
- 外部内存碎片:由于每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生了多个不连续的小物理内存,导致新的程序无法被装载
内存交换
每个进程都有自己的虚拟空间,而物理内存只有一个,所以当启用了大量的进程,物理内存必然会很紧张,于是操作系统会通过内存交换技术,把不常使用的内存暂时存放到硬盘(换出),在需要的时候再装载回物理内存(换入)。
2.内存分页
分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。在 Linux 下,每一页的大小为 4KB
。
分页是怎么解决分段的「外部内存碎片和内存交换效率低」的问题?
内存分页由于内存空间都是预先划分好的,也就不会像内存分段一样,在段与段之间会产生间隙非常小的内存,这正是分段会产生外部内存碎片的原因。而采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。
但是,因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对内存分页机制会有内部内存碎片的现象。
如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。一旦需要的时候,再加载进来,称为换入(Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
虚拟地址
- 虚拟页号:页表的索引
- 页内偏移
映射关系
- 页表(在内存中):虚拟页号、物理页号
- 内存管理单元(MMU) :做将虚拟内存地址转换成物理地址的工作
当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。
物理地址
- 物理页号:每页所在物理内存的基地址
- 偏移量
分页机制:
- 把虚拟内存地址,切分成页号和偏移量;
- 根据页号,从页表里面,查询对应的物理页号;
- 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。
缺点
- 存在内部内存碎片的问题
- 页表会占用非常大的内存空间,解决方法就是多级页表
3.多级页表
首先,要知道一级页表必须覆盖整个虚拟地址空间,不然虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。
如果不使用二级分页,一级页表的占用内存会非常庞大。
但如果使用了二级分页,根据计算机系统的局部性原理,假如某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。
对于64位的系统,使用四级页表目录
- 全局页目录项 PGD(Page Global Directory);
- 上层页目录项 PUD(Page Upper Directory);
- 中间页目录项 PMD(Page Middle Directory);
- 页表项 PTE(Page Table Entry);
缺点
- 降低了虚拟、物理地址转换速度,带来了时间上的开销,解决办法就是页表缓存TLB
4.页表缓存TLB
页表缓存(TLB):专门存放程序最常访问的页表项的CPU Cache
内存管理单元(MMU):完成地址转换和 TLB 的访问与交互
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。
三、段页式内存管理
段页式内存管理 = 内存分段 + 内存分页
实现方式:先分段,每段再分页
地址结构:段号、段内页号、页内位移
数据结构:每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号
段页式地址变换
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。
四、Linux内存管理
1.Intel 处理器
逻辑地址:程序所使用的地址,通常是没被段式内存管理映射的地址
线性地址:通过段式内存管理映射的地址,也叫虚拟地址
80386 的页式内存管理设计:建立在段式内存管理的基础上,这就意味着,页式内存管理的作用是在由段式内存管理所映射而成的地址上再加上一层地址映射。
此时由段式内存管理映射而成的地址不再是“物理地址”了,Intel 就称之为“线性地址”(也称虚拟地址)。于是,段式内存管理先将逻辑地址映射成线性地址,然后再由页式内存管理将线性地址映射成物理地址。
2.Linux内存管理
Linux 系统主要采用了分页管理,但是由于 Intel 处理器的发展史,Linux 系统无法避免分段管理。于是 Linux 就把所有段的基地址设为 0
,也就意味着所有程序的地址空间都是线性地址空间(虚拟地址),相当于屏蔽了 CPU 逻辑地址的概念,所以段只被用于访问控制和内存保护。
3.Linux虚拟地址空间分布
虚拟地址空间
- 内核空间:只有进入内核态后,才可以访问内核空间的内存
- 用户空间:进程在用户态时,只能访问用户空间内存
- 不同位数的系统,地址空间的范围也不同
虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。
3.1.内核空间分布
省略
3.2.用户空间分布
以 32 位系统为例:
用户空间内存分布,从低到高分别是 6 种不同的内存段:
- 代码段,包括二进制可执行代码;
- 数据段,包括已初始化的静态常量和全局变量;
- BSS 段,包括未初始化的静态变量和全局变量;
- 堆段,包括动态分配的内存,从低地址开始向上增长;
- 文件映射段,包括动态库、共享内存等,从低地址开始向上增长;
- 栈段,包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是
8 MB
。当然系统也提供了参数,以便我们自定义大小;- 保留区(灰色部分),之所以要有保留区这是因为在大多数的系统里,我们认为比较小数值的地址不是一个合法地址,例如,我们通常在 C 的代码里会将无效的指针赋值为 NULL。因此,这里会出现一段不可访问的内存保留区,防止程序因为出现 bug,导致读或写了一些小内存地址的数据,而使得程序跑飞。
五、补充
虚拟内存有什么作用?
- 可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
六、参考
小林 coding