【12】初学者对内存管理的常见疑惑

一、关于虚拟地址

1、 为什么处理器访问的地址是虚拟地址而不是物理地址?

1.1 直接使用物理内存

单道处理系统:用户进程加载同一个内存地址运行,不需要任何内存管理单元,使用物理地址,整个系统只有一个进程,不需要地址保护,缺点是:

  • 无法运行比实际物理内存大的进程
  • 系统只有一个程序,造成资源浪费
  • 进程无法迁移到其他计算机上去运行

多道处理系统:固定(静态)分区(在系统编译时将内存分成多个静态分区,进程可以装载大于等于自身大小的分区中)和动态分区(操作系统内存,其它内存分给用户进程使用)

直接使用物理内存的缺点

  • 进程地址空间保护和安全问题:所有用户进程都可以访问全部的物理内存,恶意程序可以修改其他程序的内存数据,OS需要保证进程地址空间相互独立、受到保护
  • 内存使用效率低:即将运行的进程所需要的内存空间不足,需要选择一个进程整体换出,这种机制导致由大量数据需要换出和换入,效率非常低下
  • 程序运行地址重定位问题
1.2 地址空间抽象

进程使用内存:程序代码、栈、堆
地址空间抽象OS三大抽象:进程CPU虚拟化、进程地址空间抽象、文件对存储地址空间的抽象

  • 隔离性和安全性:进程A无法访问进程B的物理内存
  • 效率:分页机制可以解决动态分区法出现的碎片化和效率问题
  • 重定位问题:进程换入换出访问的是相同的虚拟地址(链接地址)

2、 为什么CPU需要MMU?

MMU硬件单元是用来实现虚拟地址空间抽象的重要硬件部件

  • 分段机制将进程空间分成几个逻辑段(代码段、数据段、堆、栈等),逻辑段放在不同的物理内存的区域里,分段机制力度粗糙导致外部碎片化的问题
  • 分页机制按照固定大小的单元(页帧)来分配内存

在使能分页机制系统,处理器寻址的是虚拟地址,这个地址不直接发送至内存控制器,而是先发送至MMU硬件单元,负责虚拟地址到物理地址的转换和翻译工作

在虚拟地址空间里,按照固定大小来分页,这个是虚拟页面,典型的页面的大小为4KB一个页,现代处理器都支持大粒度的页面,比如16KB、64KB甚至2MB的巨页(hugepage)。而在物理内存中也是分成和虚拟地址空间大小相同的块,称为页帧(Page Frame)。程序可以在虚拟地址空间里任意分配虚拟内存,但只有当程序需要访问或修改虚拟内存时操作系统才会为其分配物理页面,这个过程叫做请求调页(Demand Page)或者缺页异常(Page Fault)

所以现代处理器访问地址通常使用虚拟地址,而MMU硬件单元就是用来实现这个地址空间抽象 最重要的一个硬件部件


二、关于多级页表

1、 为什么页表存放在主内存中而不是放在芯片内部的寄存器?

1.1 虚拟地址到物理地址映射过程

虚拟地址到物理地址映射过程 一个虚拟地址(VA[31:0])可以分成两部分,一部分是虚拟页面内的偏移(Page Offset),以4KB页为例,VA[11:0]是虚拟页面偏移;剩下的部分用来寻找属于哪个页,称为虚拟页帧号VPN(Virtual Page Frame Number)。
相对于物理地址也是类似,PA[11:0]表示物理页帧的偏移,剩余部分表示物理页帧号PFN(Physical Frame Number)。MMU的工作内容就是把虚拟页帧号转换成物理页帧号。处理器通常使用一张表来存储VPN到PFN的映射关系,这个表称为页表(Page Table,PT)。页表中每一个表项称为页表项(Page Table Entry,PTE)

如果把整张页表存放在寄存器中,会占用很多硬件资源,比如说在32位系统里,一个32位的寄存器最多就存储32位,即4个字节,这些寄存器是需要消耗硬件资源,因此通常的做法是把页表放在主内存里,通过一个叫做页表基地址寄存器来指向这种页表的起始地址。处理器发出的地址是虚拟地址,通过MMU硬件单元来查询页表,处理器得到了物理地址,最后把物理地址发送给内存控制器,从而能对内存地址进行读写操作


2、 为什么页表要设计成多级页表?直接使用一级页表是否可行?多级页表又引入了什么问题?

2.1 一级页表

在这里插入图片描述页表项中包含两部分:

  • 一是PFN,它代表页面在物理内存中的帧号即页帧号,页帧号加上VA[11:0]页内偏移就组成最终物理地址PA;
  • 另外一部分是页表项的属性,比如图中的v表示有效位。有效位为1表示这个页表项对应的物理页面在物理内存中,处理器可以访问这个页面的内容;有效位为0表示这个物理页面不在内存中,可能在交换磁盘中。如果访问该页面,那么操作系统会触发一个缺页异常,在缺页异常中处理这种情况;
  • 当然实际的处理器中还有很多其他的属性比特位,比如描述这个页面是否为脏,是否可读可写等。

缺点:

  • 处理器采用一级页表,虚拟地址空间位宽是32位,寻址范围是4GB大小,物理地址空间位宽也是32位,最大支持4GB物理内存,另外页面的大小是4KB。为了能映射整个4GB地址空间,那么需要4GB/4KB=1M个页表项,每个页表项占用4字节,则需要4MB大小的物理内存来存放这张页表
  • 每个进程拥有了一套属于自己的页表,在进程切换时需要切换页表基地址。比如,采用一级页表的话,每个进程需要为其分配4MB的连续物理内存来存储页表,这是不能接受的,因为这样太浪费内存了(如果系统中1GB的内存,有100个进程,那么光页表就要占用了400MB的内存,这相当浪费内存)
2.2 多级页表-二级页表

多级页表-二级页表页表基地址寄存器指向一级页表的基地址,一级页表的页表项里存放了一个指针,指向二级页表的基地址。当处理器执行程序时它只需要把一级页表加载到内存中,并不需要把所有的二级页表都装载到内存中,然后根据物理内存分配和映射情况逐步创建和分配二级页表
(假设一级页表有4096个页表项,每个页表项占用4个字节,那么一级页表只需要16KB内存即可。所以,新创建一个进程,只需要分配4个4KB的页面来存储一级页表,至于二级页表,那是用到才动态映射)

当操作系统准备让这个进程运行时,设置一级页表在物理内存中的起始地址到页表基地址寄存器(TTBRx)中。进程执行过程中需要访问物理内存,因为一级页表的表项是空的,触发缺页异常。在缺页异常里分配一个二级页表,并且把二级页表的起始地址填充到一级页表的相应表项中。接着,分配一个物理页面,然后把这个物理页面的PFN填充到二级页表的对应页表项中,从而完成了页表的填充。随着进程的执行,它需要访问越来越多的物理内存,那么操作系统会逐步地把页表填充和建立起来。


3、 在2级页表系统中,访问一次内存地址A,最多需要访问几次内存?

页表查询的过程,也比较简单。首先,处理器访问页表基地址寄存器,页表基地址寄存器中存放着一级页表的基地址。

1)处理器根据虚拟地址的Bit[31:20]作为索引值,在一级页表中找到页表项,一级页表一共有4096个页表项。

2) 第一级页表的表项中存放有二级页表的物理基地址。处理器使用虚拟地址的Bit[19:12]作为索引值,在二级页表中找到相应的页表项,二级页表有256个页表项。

3)二级页表的页表项里存放有4KB页的物理基地址。这样,处理器就完成了页表的查询和翻译工作。


三、页表的基地址

页表的作用是帮助MMU硬件单元实现虚拟地址到物理地址的转换

1、页表项中有指向下一级页表基地址的指针,那它指向的是下一级页表基地址的物理地址还是虚拟地址?

页表项在使能了MMU之后,CPU直接寻址虚拟地址,而MMU硬件单元负责虚拟地址到物理地址的转换和翻译工作,地址转换和翻译的依据是页表。页表项的内容是由操作系统负责填充的。如果下一级页表的基地址是虚拟地址的话,那么MMU还需要查询另外一个页表才能找到这个虚拟地址对应的物理地址,这样MMU就会陷入死循环了,因此这里下一级页表的基地址采用的是物理地址


四、Linux内核软件遍历页表

1、Linux内核里有不少函数遍历页表,如何遍历的?

问题:MMU硬件单元会遍历页表,但Linux内核提供了软件遍历页表的函数,比如walk_pgd()、__create_pgd_mapping()、follow_page()等函数,站在软件的视角,Linux内核的pgd_t、pud_t、pmd_t以及pte_t数据结构中并没有存储一个指向下一级页表的指针(即站在CPU角度来看,CPU访问这些数据结构时以虚拟地址来访问的),它是如何遍历的呢?pgd_t、pud_t、pmd_t以及pte_t数据结构定义在arch/arm64/include/asm/pgtable_types.h头文件中,它们是u64类型的变量

函数walk_pagetable() 遍历页表

pgdp = pgd_offset(init_mm_p, address);

pgd_offset()这个宏可以很方便找到虚拟地址address对应的pgd页表项的虚拟地址,这里pgdp是一个指针,它指向pgd页表项的虚拟地址,init_mm_p是内核页表的基地址,系统中所有的内核线程都使用相同的内核页表,它在内核里称为swapper_pg_dir,在init_mm数据结构中的pgd成员也指向这个页表

pgd_t数据结构里并没有存储一个指针来指向下一级页表的虚拟地址,在Linux内核里,物理内存会线性映射到内核空间里,偏移地址为PAGE_OFFSET。在内核空间可以很方便地实现虚拟地址和物理地址映射的转换。Linux内提供两个宏,其中__pa()宏用于计算内核中线性映射的虚拟地址到物理地址;而__va()宏用来计算内核线性映射的物理地址到虚拟地址

/* 虚拟地址到物理地址 */
_pa = x - PAGE_OFFSET + PHYS_OFFSET;
/* 物理地址到虚拟地址 */
_va = x - PHYS_OFFSET + PAGE_OFFSET;

Linux内核软件遍历页表mm->pgd存储了指向PGD页表的基地址,这里指向的是PGD页表的虚拟地址。通过虚拟地址的PGD索引域,可以在PGD页表中找到PGD页表项,在pgd页表项中存储了指向下一级页表基地址的物理地址,那么通过__va()这个宏,我们可以快速地把物理地址转换成内核空间的虚拟地址,从而找到下一级页表基地址的虚拟地址。 所以,我们不需要在pgd_t数据结构中存储一个指针来执行下一级页表基地址

这里巧妙的运用了线性映射的技巧以及硬件页表的知识


五、线性映射

1、为什么内核要把整个DDR内存全部线性映射到内核空间?

  • 映射是由软件来定义的,而页表机制是由硬件提供的,本质上来说,软件来填充页表,而MMU硬件单元只是根据页表的页表项内容,来完成虚拟地址到物理地址的映射
  • 多级页表的按需映射,是页表的基本功能,只是用来节省页表占用内存空间,而内核的线性映射和这个多级页表的按需映射,其实是两回事
  • 用户地址空间和内核地址空间。内核空间是所有进程共享的一个空间。CPU虚拟化也就是进程的抽象,内存虚拟化也就是地址空间的抽象,在这两个概念下,进程感觉它拥有了全部的地址空间,包括用户空间和内核空间,用户空间是它独有的,而内核空间是所有进程共享的。

当进程陷入到内核空间时,它访问的地址同样是虚拟地址,只不过是它访问了内核地址空间。但是,有一点不一样的是:

  • 进程在用户空间访问虚拟地址,如果这个虚拟地址没有映射物理内存时,处理器会触发缺页异常,然后陷入到内核态的缺页异常中来修复这个映射。
  • 但是如果在内核态访问一个没有映射的内核地址空间,那么内核陷入崩溃状态,这就是我们常常看的oops错误,这是因为运行在内核空间的程序需要稳定性和安全性。假设内核空间的虚拟地址没有预先映射,内核运行在内核空间里也常常需要访问物理内存,那么内核就会常常处于缺页异常中,若缺页异常无法修复错误的话,那么整个系统就挂掉了。所以,为了操作系统的安全性和稳健性,对内核空间虚拟地址是不做缺页异常处理的,在内核空间里访问一个空指针常常会引发系统崩溃

通常做法是:在内核初始化时,把全部物理内存都线性映射到内核地址空间,这样预先映射有不少好处:

  • 第一,可以减少在内核空间崩溃的几率;
  • 第二,也能提高系统性能,因为在内核空间也是常常会分配内存,比如伙伴系统,slab机制分配内存,这些分配的内存,不需要重新来建立虚拟地址到物理地址的映射了,因为系统初始化时,已经预先映射好了。

六、内核空间内存布局

ARM64 Linux内核空间布局:
ARM64 Linux内核空间布局ARM64架构处理器采用48位物理寻址机制,最大可以寻找256TB的物理地址空间,虚拟地址也同样最大支持48位寻址,所以在处理器架构设计上,把虚拟地址空间划分为两个空间,每个空间最大支持256TB。Linux内核在大多数体系结构上都把两个地址空间划分为用户空间和内核空间

ARM64 Linux内核空间布局* PAGE_OFFSET,它表示物理内存在内核空间里做线性映射(Linear Mapping)的起始地址,在ARM64的Linux中该值定义为0xffff800000000000。Linux内核在初始化时会把物理内存全部做一次线性映射,映射到内核空间的虚拟地址上

  • kimage_voffset表示内核映像虚拟地址和物理地址之间的偏移
  • PHYS_OFFSET表示物理内存的偏移,比如有的系统,它的物理内存不是从地址空间的0地址开始的,而是有一个偏移量,这个偏移量在SOC芯片设计的时候就定下来了

七、关于映射

1、一个虚拟地址可以同时映射多个物理地址吗?一个物理地址可以同时映射多个虚拟地址吗?

页表的作用是让MMU实现虚拟地址到物理地址的映射,而且映射规则是根据虚拟地址来作为各级页表的索引。一个虚拟地址,它在页表中,它就有了固定的页表项,因为索引值是由虚拟地址来确定,因此,一个虚拟地址只能对应一个物理地址。反过来,一个物理地址可以对应多个虚拟地址,只要根据这个物理地址来生成一个 页表项的内容,然后在多个页表项中复用这个页表项的内容,那么就可以实现多个虚拟地址映射到这个物理地址

内核的线性映射,把物理内存线性映射到内核空间,同时,对于内核映像,内核在初始化时又做了另外一个映射,把内核映像映射到内核空间的另一个地方。上述的说法是考虑在同一张页表的情况,但是linux系统可能同时存在多张页表,即多个进程地址空间,在这种复杂场景下,是有可能多个相同的虚拟地址映射到同一个物理地址上,但是它们只是虚拟地址的数值相等,但是进程地址空间确是不一样的


八、虚拟地址打架

1、假设系统中有进程A和进程B,分别使用testA和testB函数分配内存,使用printf打印指针bufA和bufB指向的地址是一样的,那么在内核中这两块虚拟内存是否冲突了呢?

虚拟地址打架每个用户进程有自己的一份页表,mm_struct数据结构中有一个pgd成员指向这个页表的基地址,在fork新进程时会初始化一份页表。每个进程有一个mm_struct数据结构,包含一个属于进程自己的页表、一个管理VMA的红黑树和链表。进程本身的VMA会挂入属于自己的红黑树和链表,所以即使进程A和进程B使用malloc分配内存返回相同的虚拟地址,但其实它们是两个不同的VMA,分别被不同的两套页表来管理

进程地址空间是操作系统对内存的抽象,进程只能看到自己进程地址空间的虚拟内存

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值