Skip to content

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,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

再来说说,内核空间与用户空间的区别:

  • 进程在用户态时,只能访问用户空间内存;
  • 只有进入内核态后,才可以访问内核空间的内存;

虽然每个进程都各自有独立的虚拟内存,但是每个虚拟内存中的内核地址,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。


正在精进