良许Linux教程网 干货合集 内存管理:详解虚拟地址空间-MMU

内存管理:详解虚拟地址空间-MMU

虚拟内存

当你的电脑物理内存不足时,系统会将部分硬盘空间作为虚拟内存来使用,这部分硬盘空间被称为虚拟内存。

由于硬盘的传输速度远远慢于内存传输速度,因此虚拟内存的效率远低于物理内存。

值得注意的是,虚拟内存中的数据在断电后会丢失。

虚拟地址空间

虚拟地址空间是一个极为抽象的概念,从字面上解释如下:

它用于加载程序数据,如果物理内存不够,数据会加载到虚拟内存中。

它对应着一段连续的内存地址,起始位置为0。

之所以称之为虚拟,是因为这个起始地址0是虚拟出来的,并非物理内存上的0地址。

操作系统决定虚拟地址空间的大小,例如32位操作系统虚拟地址空间大小为4GB(2^32字节),而64位系统的操作系统虚拟地址空间大小为18EB(2^64字节),数量庞大,欲知详情请自行计算。

关于虚拟4GB内存的描述和分析:

每个进程使用的虚拟地址由内存区域表来管理,实际上没有用完4GB。这些内存区域通过页表映射到物理内存。

因此,每个进程可以使用相同的虚拟地址,因为它们的物理地址实际上是不同的。内核使用高于3G的1G虚拟地址,其中896M直接映射到物理地址,128M根据需要映射到高于896M的高位内存。各个进程共享相同的内核。

首先要弄清“可寻址”和“实际使用”的区别。

实际上每个进程都有4GB的虚拟地址空间,都可以“寻址”4GB,这意味着虚拟地址0-3GB对进程的用户态和内核态都可访问,而3-4GB只有进程内核态能访问。并不意味着进程会用完整个空间。

其次,“独立拥有的虚拟地址”指的是每个进程都可以访问自己的0-4GB虚拟地址。虚拟地址是“虚拟”的,需要转换为“真实”的物理地址。

类似于你有自己的地址簿,我也有自己的地址簿。你和我都有1、2、3、4页,但每页的内容实际上不同,我的第1页写着3,而你的第1页写着4,对于你和我来说都是使用第1页(虚拟),实际上使用的是不同的第3、4页(物理),互不冲突。

内核使用的896M虚拟地址是直接映射的,即将虚拟地址减去一个偏移量(3G)得到物理地址。同样,并非实际使用前就分配内存。此外,896M只是最大值。如果物理内存较小,内核可使用的有效内存也较小。

进程的虚拟地址空间分为用户区(0-3GB)和内核区(3-4GB),其中内核区受保护,用户无法对其进行读写操作。

所有进程共享的内核区;系统中所有进程对应的虚拟地址空间内核区都映射到同一块物理内存上(系统只有一个内核)。

image-20240510224924250
image-20240510224924250

虚拟地址空间中用户区地址范围是 0~3G,里边分为多个区块:

  • 保留区: 位于虚拟地址空间的最底部,未赋予物理地址。任何对它的引用都是非法的,程序中的空指针(NULL)指向的就是这块内存地址。

  • .text段: 代码段也称正文段或文本段,通常用于存放程序的执行代码 (即 CPU 执行的机器指令),代码段一般情况下是只读的,这是对执行代码的一种保护机制。

  • .data段: 数据段通常用于存放程序中已初始化且初值不为 0 的全局变量和静态变量。数据段属于静态内存分配 (静态存储区),可读可写。

  • .bss段: 未初始化以及初始为 0 的全局变量和静态变量,操作系统会将这些未初始化的变量初始化为 0

  • **堆(heap)**:用于存放进程运行时动态分配的内存。

    • 堆中内容是匿名的,不能按名字直接访问,只能通过指针间接访问。
    • 堆向高地址扩展 (即 “向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。
  • **内存映射区(mmap)**:作为内存映射区加载磁盘文件,或者加载程序运作过程中需要调用的动态库。

  • 栈(stack): 存储函数内部声明的非静态局部变量,函数参数,函数返回地址等信息,栈内存由编译器自动分配释放。栈和堆相反地址 “向下生长”,分配的内存是连续的。

  • 命令行参数:存储进程执行的时候传递给 main() 函数的参数,argc,argv [],env[]

  • 环境变量: 存储和进行相关的环境变量,比如:工作路径,进程所有者等信息

内存管理单元MMU

MMU位于CPU内,作用:

  • 程序中使用的地址均是虚拟内存地址,进程中的数据是如何进入到物理内存中的呢?
  • MMU完成虚拟内存到物理内存的映射,即虚拟地址映射为物理地址;
  • 流水线中预取指令取到的地址是虚拟地址,需要MMU转换以及设置访问权限

MMU采用分页机制(即按页来划分物理内存)

用MMU的是:Windows、MacOS、Linux、Android;

不用MMU的是:FreeRTOS、VxWorks、UCOS……

与此相对应的:CPU也可以分成两类,带MMU的、不带MMU的。

带MMU的是:Cortex-A系列、ARM9、ARM11系列;

不带MMU的是:Cortex-M系列……(STM32是M系列,没有MMU,不能运行Linux,只能运行一些UCOS、FreeRTOS等等)。

虚拟地址和物理地址的映射关系存储在页表中,而现在页表又是分级的

页表

实现从页号到物理块号的地址映射。

逻辑地址转换成物理地址的过程是:用页号p去检索页表,从页表中得到该页的物理块号,把它装入物理地址寄存器中。同时,将页内地址d直接送入物理地址寄存器地块内地址字段中。这样,物理地址寄存器中的内容就是由二者拼接成的实际访问内存的地址,从而完成了从逻辑地址到物理地址的转换。

TLB快表

TLB是MMU中的一块高速缓存,也是一种Cache.

TLB就是页表的Cache,其中存储了当前最可能被访问到的页表项,其内容是部分页表项的一个副本。只有在TLB无法完成地址翻译任务时,才会到内存中查询页表,这样就减少了页表查询导致的处理器性能下降。

如果没有TLB,则每次取数据都需要两次访问内存,即查页表获得物理地址和取数据.

虚拟地址空间以为单位进行划分,而相应的物理地址空间也被划分,其使用的单位称为页帧,页帧和页必须保持相同,因为内存与外部存储器之间的传输是以页为单位进行传输的。例如,MMU可以通过一个映射项将VA的一页0xb70010000xb7001fff映射到PA的一页0x20000x2fff,如果CPU执行单元要访问虚拟地址0xb7001008,则实际访问到的物理地址是0x2008。

虚拟内存的哪个页面映射到物理内存的哪个页帧是通过页表(Page Table)来描述的,页表保存在物理内存中MMU会查找页表来确定一个VA应该映射到什么PA。

**内存访问级别的设置和修改(内存保护)**,在完成映射的同时,会设置CPU访问该段内存的访问级别(3,2,1,0 Linux只有用户空间3,内核空间0),

如图:

ro表示read only

0和3表示访问级别

程序运行了两次,产生两个独立的进程,因此虚拟地址空间不一样

两个进程共用一个内核区,映射一份(图中也要通过MMU映射,懒得话而已)即可,其中两个进程的PCB不一样

用户区需要单独映射

image-20240510224928948
image-20240510224928948

MMU执行过程

OS和MMU是这样配合的:

操作系统在初始化或分配、释放内存时会执行一些指令在物理内存中填写页表,然后用指令设置MMU,告诉MMU页表在物理内存中的什么位置。

设置好之后,CPU每次执行访问内存的指令都会自动引发MMU做查表和地址转换操作,地址转换操作由硬件自动完成,不需要用指令控制MMU去做。

我们在程序中使用的变量和函数都有各自的地址,在程序被编译后,这些地址就成了指令中的地址,指令中的地址就成了CPU执行单元发出的内存地址,所以在启用MMU的情况下, 程序中使用的地址均是虚拟内存地址,都会引发MMU进行查表和地址转换操作。(注意理解这句话)

内存保护机制

中断和异常

中断由外部设备产生,而 异常由CPU内部产生的

中断产生与CPU当前执行的指令无关,而异常是由于当前执行的指令出现问题导致的g

处理器一般有用户模式(User Mode)和特权模式(privileged Mode)之分。操作系统可以在页表中设置每个页表访问权限,有些页表不可以访问,有些页表只能在特权模式下访问,有些页表在用户模式和特权模式下都可以访问,同时,访问权限又分为可读可写可执行三种。这样设定之后,当CPU要访问一个VA(Virtual Address)时,MMU会检查CPU当前处于用户模式还是特权模式,访问内存的目的是读数据、写数据还是取指令执行,如果与操作系统设定的权限相符,则允许访问,把VA转换成PA,否则不允许执行,产生异常(Exception)

在正常情况下处理器在用户模式执行用户程序,在中断或异常情况下处理器切换到特权模式执行内核程序,处理完中断或异常之后再返回用户模式继续执行用户程序。

段错误我们已经遇到过很多次了,它是这样产生的:

用户程序要访问的一个虚拟机地址,经MMU检查无权访问。

MMU产生一个异常,CPU从用户模式切换到特权模式,跳转到内核代码中执行异常服务程序。

内核把这个异常解释为段错误,把引发异常的进程终止掉。

用户空间与内核通信方式有哪些?

1)系统调用。用户空间进程通过系统调用进入内核空间,访问指定的内核空间数据;

2)共享映射区mmap。在代码中调用接口,实现内核空间与用户空间的地址映射,在实时性要求很高的项目中为首选,省去拷贝数据的时间等资源,但缺点是不好控制;

3)驱动程序。用户空间进程可以使用封装后的系统调用接口访问驱动设备节点,以和运行在内核空间的驱动程序通信;

4)copy_to_user()、copy_from_user(),是在驱动程序中调用接口,实现用户空间与内核空间的数据拷贝操作,应用于实时性要求不高的项目中。

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部