4.1 为什么要有虚拟内存?
虚拟内存
单片机的 CPU 是直接操作内存的「物理地址」。
- 而每个程序可能都想要访问相同的地址,那么就会出现无法同时运行两个程序
- 因此操作系统为每个进程分配独立的一套「虚拟地址」,每个程序不能直接访问物理地址,都可以访问相同的虚拟地址。
- 而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常
操作系统通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存,将不同进程的虚拟地址和不同内存的物理地址映射起来。
- 我们程序所使用的内存地址叫做虚拟内存地址(Virtual Memory Address)
- 实际存在硬件里面的空间地址叫物理内存地址(Physical Memory Address)。
内存分段
程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(Segmentation)的形式把这些段分离出来。
分段机制下的虚拟地址由两部分组成,段选择因子和段内偏移量。
段选择因子就保存在段寄存器里面。段选择因子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。
虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。
分段的办法很好,解决了程序本身不需要关心具体的物理内存地址的问题,但它也有一些不足之处:
第一个就是内存碎片的问题。
- 每次分配地址释放之后,可能其他程序不够用,造成内存浪费
- 内部内存碎片:分配了冗余的内存给一个程序
- 外部内存碎片:每个段长度不固定,每次分配都可能剩余一些小物理内存,不够其他程序使用,可以通过内存交换解决
第二个就是内存交换的效率低的问题。
- 每次交换需要交换一大段,成本较高,可能造成机器卡顿
内存分页
把整个虚拟和物理内存空间切成一段段固定的连续内存空间(页)。在 Linux 下,每一页的大小为 4KB。
在加载程序的时候,不再需要一次性都把程序加载到物理内存中。只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
页比较小,进行内存交换效率高。
在分页机制下,虚拟地址分为两部分,页号和页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址
一级分页的问题:
因为操作系统是可以同时运行非常多的进程的。每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。就需要很大的页表。
因此出现了多级分页,每次只需要直接创建一级页表,低级的页表只有被使用的时候,才生成低级页表,这样也能覆盖全部虚拟空间(而一级分页的话想要覆盖全部虚拟空间,就需要生成所有的页表)
同时,为了更好的利用内存,提高效率,出现了TLB(Translation Lookaside Buffer),缓存最常访问的页表项,
有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表,命中率通常较高。
页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
段页式内存管理
段页式内存管理实现(结合内存分段和内存分页)的方式:
- 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
- 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
这样,地址结构就由段号、段内页号和页内位移三部分组成。
用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。提高内存使用效率。
Linux 内存管理
Linux 内存主要采用的是页式内存管理,但同时也不可避免地涉及了段机制。
在 Linux 操作系统中,虚拟地址空间的内部又被分为内核空间和用户空间两部分,不同位数的系统,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,如下所示:

通过这里可以看出:
32位系统的内核空间占用1G,位于最高处,剩下的3G是用户空间;64位系统的内核空间和用户空间都是128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。
再来说说,内核空间与用户空间的区别:
- 进程在用户态时,只能访问用户空间内存;
- 只有进入内核态后,才可以访问内核空间的内存;
虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

