《初学者对内存管理的常见疑惑》


20220710.No.5122


https://www.xuetangx.com/learn/XIYOU08091001441/XIYOU08091001441/10321548/video/17342639

大家好,我是奔跑吧Linux内核一书的作者,笨叔,很高兴能在陈莉君老师的《linux内核分析与应用》,这门慕课中和大家分享一点关于内存管理的理解。陈老师在这个课程里已经对linux内核的内存管理做了很全面和系统的介绍,那,我在这期节目想和大家聊聊,初学者对内存管理的常见8大疑惑,这些疑惑是我多年工作经验的总结,它们可能是这门课 期末考试的必考内容,也有可能是以后你们职场技术面试的必考题哟,我们一起来看看都有哪些疑惑吧。


P1:疑惑一:关于虚拟地址

我觉得初学者对内存管理最常见的第一个疑惑是关于虚拟地址?比如说,为什么现代处理器访问的地址是虚拟地址而不直接采用物理地址?第二问题,为什么处理器内部需要标配一个MMU的硬件单元?

这两个问题需要同学们对计算机体系结构的发展历史需要有一点的了解,那么会对这个问题容易理解一些。在上个世纪50年代,那时候系统软件还没有操作系统的概念,系统只有一个进程,我们把它称为单道处理系统,用户程序总是加载到同一个内存地址上运行,所以内存管理很简单。实际上不需要任何的内存管理单元,程序使用的地址就是物理地址,而且也不需要地址保护。但是缺点也很明显:第一点,无法运行比实际物理内存大的程序;其二,系统只运行一个程序,造成资源浪费;其三,无法迁移到其他的计算机中运行。

那么,后来出现多道处理系统,系统可以同时运行多个进程。在内存管理方面,因为要处理多个进程的问题,所以历史上出现了固定分区和动态分区两种技术。

所谓固定分区,就是在系统编译阶段,把内存被划分成许多静态分区,进程可以装入大于或者等于自身大小的分区。这个实现简单,操作系统管理开销也比较小。


P2:动态分区法

固定分区方法有缺点,人们自然就想到了动态分区的方法。动态分区的思想也比较简单,就是在一整块内存中首先划出一块内存是给操作系统本身使用,那么剩下的内存空间给用户进程使用。当第一个进程A运行时,先从这一大片内存中切割一块进程A需要的内存大小,给进程A使用。当第二个进程B准备运行时,我们可以从剩下的空闲内存中继续切割一块进程B所需要的内存,给进程B使用,依次类推。这样进程A和进程B以及后面进来的进程就可以实现动态分区了。

我们来看这个图,假设现在有一块32MB大小的内存,一开始操作系统使用了最低的4MB大小,剩余的内存要留给4个用户进程使用(如图(a)所示)。进程A使用了操作系统往上的10MB内存,进程B使用了进程A往上的6MB内存,进程C使用了进程B往上的8MB内存。剩余的4MB内存不足以装载进程D,因为进程D需要5MB内存((b)所示),那么这样,这个内存末尾就形成了第一个空洞。假设某个时刻,操作系统需要运行进程D,这时系统中没有足够的内存,就需要选择一个进程来换出,以便为进程D腾出足够的内存空间。假设操作系统选择进程B来换出,这样进程D就装载到了原来进程B的地址空间里,但是呢,产生了第二个空洞(如图(c)所示)。假设操作系统某个时刻需要进程B运行,也需要选择一个进程来换出,假设进程A被换出,那么系统中又产生了第三个空洞(如图(d)所示)。这个就是动态分区法的工作原理。


P3:直接使用物理地址的缺点

刚才提到了固定分区法和动态分区法都是处理器直接访问的物理地址的,但是呢,这种内存管理的方式对于现代操作系统来说,充满了挑战,这些挑战,我大概列举3个方面挑战:

1. 进程地址空间保护(isolation)和安全问题(security)。

所有的用户进程都可以访问全部的物理内存,如果有恶意的程序,那么它可以修改其他程序的内存数据,这样破坏了系统管理软件和其他进程的内部数据。即使系统里所有的进程都不是恶意进程,但是进程A依然可能不小心修改了进程B的数据,从而导致进程B运行崩溃。这明显违背了“进程地址空间需要保护”的原则,也就是地址空间要相对独立。所以,操作系统需要保证,每个进程的地址空间都应该受到保护,不应该被其他进程有意或者无意地伤害。

2. 内存使用效率低。

如果即将要运行的进程所需要的内存空间不足,就需要选择一个进程进行整体换出,这种机制导致有大量的数据需要换出和换入,效率非常低下,通常换出是把数据写入到交换分区,导致大量的磁盘IO。

3. 程序运行地址重定位问题。

从前一页的ppt里看到,进程在每次换出换入时运行的地址都是不固定的,这给程序的编写带来一定的麻烦,因为访问数据和指令跳转时的目标地址通常是固定的,这就需要重定位技术了。

上面三个问题,如果纯粹从软件来解决,貌似很难,所以人们开始思考是否可以从处理器体系结构,这个的角度来思考,如何解决问题。


Pn:地址空间的抽象

对于一个进程,站在内存使用的角度来看,大概有三个地方需要用到内存:

  1. 代码段,数据段用来存储程序本身需要的数据。

  2. 栈。程序运行时候,需要分配一个内存空间来保存 函数调用关系,保存局部变量、函数参数以及函数返回值等内容,这些也是需要消耗内存的。

  3. 堆空间。程序运行时候,需要动态分配程序需要使用的内存,比如存储 程序需要用的数据等等。

不管是刚才提到的静态分区法,还是动态分区法,对于一个进程来说,都需要包含上述三种内存。但是,如果我们直接使用物理内存的话,我们在编写这样一个程序时候,就需要每时每刻去关心: 分配的物理内存地址是多少,内存空间够不够?等等这些问题。

后来,人们对内存也有一个伟大的抽象,就是把上述三个用到的内存,抽象成地址空间或者虚拟内存,也就是说,对于进程来说,它不用关心分配的内存 在哪个地址,它只管分配使用就行,由处理器来把进程对内存的需求或者请求,中间做一个转换,把进程请求的地址 转换成 物理地址,也就是物理内存DDR的地址。这个转换过程,称为地址转换(address translation),而进程请求的地址,我们可以理解为虚拟地址(virtual address),因为我们在处理器里做了一个 虚拟抽象,让进程感觉到自己可以拥有全部的物理内存。进程可以发出请求,那至于这些请求能不能得到百分之百的满足,那就是处理器的事情了。总之,进程地址空间是对内存的一个重要的抽象,让内存虚拟化得到了实现,它和进程的CPU虚拟化,以及文件对存储地址空间的抽象,这三个是操作系统中三大抽象,组成了操作系统的三个元素。

进程地址空间的概念引入了虚拟内存,而这个思想可以解决刚才提到的三个问题。

  1. 隔离性和安全性。虚拟内存机制可以提供这样的隔离性,因为每个进程感觉全部拥有了整个地址空间,它可以随意访问整个地址空间,然后由处理器来转换到实际的物理地址,所以,进程A没法访问到进程B的物理内存,也就没办法做到破坏了。

  2. 效率。后来出现的分页机制可以解决动态分区法出现的碎片化和效率的问题。

  3. 重定位问题。进程换入换出时访问的地址变成相同的虚拟地址。它不用关心物理地址。这个虚拟地址我们称为链接地址。


Pn:分页机制的基本概念

基于进程地址空间这个概念,在计算机历史发展过程中,出现分段机制和分页机制。分段机制比较粗糙,它把进程的地址空间分成几个逻辑段:代码段,栈和堆。这些逻辑段可以放到不同的物理内存区域。但是,由于分段机制的粒度比较粗,导致外部碎步片问题。后来大家想到一个更好的办法,就是分页机制,分页机制不再按照代码段、堆,栈等逻辑段来分,而是按照固定大小的单元来分配内存,这个固定大小的单元 就是页帧(Page Frame)。

所以,在使能了分页机制的处理器中,我们通常把处理器能寻址的地址空间称为虚拟地址空间(Virtual Address space)。和虚拟地址空间对应的是物理地址空间,它对应着系统中使用的物理存储设备的地址空间,比如DDR内存颗粒等。在没有使能分页机制的系统中,处理器直接寻址物理地址,把物理地址发送到内存控制器中,而在使能了分页机制的系统中,处理器直接寻址虚拟地址,这个地址不会直接发给内存控制器,而是先发送给一个叫做 MMU(Memory Management Unit)的硬件单元。MMU负责虚拟地址到物理地址的转换和翻译工作。在虚拟地址空间里,按照固定大小来分页,这个是虚拟页面,典型的页面的大小为4KB一个页,现代处理器都支持大粒度的页面,比如16KB、64KB甚至2MB的巨页(hugepage)。而在物理内存中也是分成和虚拟地址空间大小相同的块,称为页帧(Page Frame)。程序可以在虚拟地址空间里任意分配虚拟内存,但只有当程序需要访问或修改虚拟内存时操作系统才会为其分配物理页面,这个过程叫做请求调页(Demand Page)或者缺页异常(Page Fault)。

相信大家现在已经知道了为什么现代处理器访问地址通常使用虚拟地址,而MMU硬件单元就是用来实现这个地址空间抽象 最重要的一个硬件部件,我们后续还会继续和大家详细介绍和MMU硬件相关的一些常见疑惑。


Pn:疑惑二:关于多级页表

下面来讲第二个常见的疑惑,是关于多级页表的。我把多级页表的常见疑惑,总结出来,主要有三个。第一个,为什么页表要设计成多级页表?直接使用一级页表是否可行?多级页表又引入了什么问题?第二个,为什么页表存放在主内存中而不是放在芯片内部的寄存器?第三个,在2级页表系统中,访问一次内存地址A,最多需要访问几次内存?

Pn:虚拟地址到物理地址映射过程

我们先来简单回顾一下,虚拟地址是怎么转换成物理地址的。比如在这个图上,一个虚拟地址(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硬件单元来查询页表,处理器得到了物理地址,最后把物理地址发送给内存控制器,从而能对内存地址进行读写操作了。

Pn:一级页表

下面以最简单的一级页表为例,假设处理器采用一级页表,虚拟地址空间位宽是32位,寻址范围是4GB大小,物理地址空间位宽也是32位,最大支持4GB物理内存,另外页面的大小是4KB。为了能映射整个4GB地址空间,那么需要4GB/4KB=1M个页表项,每个页表项占用4字节,则需要4MB大小的物理内存来存放这张页表。

我们刚才提到,VA[11:0]是页面偏移,VA[31:12]这20个比特位是VPN,作为索引值在页表中查询页表项。页表其实类似一个数组,VPN类似数组的下标,查找数组中对应的成员。

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

Pn:采用一级页表的缺点

我们来讲一下一级页表的缺点,如果处理器采用一级页表,假设虚拟地址空间位宽是32位,物理地址空间位宽也是32位,最大支持4GB物理内存,页面的大小是4KB。为了能映射整个4GB地址空间,那么需要4GB/4KB=1M个页表项,每个页表项占用4字节,则需要4MB大小的物理内存来存放这张页表。那是需要相当大的存储空间的。通常操作系统支持多进程调度,进程调度器会在合适的时间切换进程A到进程B来运行,比如进程A使用完时间片,调度器选择另外一个进程B来运行。另外,分页机制也让每个进程都感觉自己拥有了全部的虚拟地址空间。每个进程拥有了一套属于自己的页表,在进程切换时需要切换页表基地址。比如,采用一级页表的话,每个进程需要为其分配4MB的连续物理内存来存储页表,这是不能接受的,因为这样太浪费内存了。举个例子,如果系统中1GB的内存,有100个进程,那么光页表就要占用了400MB的内存,这相当浪费内存。那怎么办呢?

解决办法就是:设计了多级页表来减少页表所占用的内存空间。例如,二级页表分成一级页表和二级页表,页表基地址寄存器指向一级页表的基地址,一级页表的页表项里存放了一个指针,指向二级页表的基地址。当处理器执行程序时它只需要把一级页表加载到内存中,并不需要把所有的二级页表都装载到内存中,然后根据物理内存分配和映射情况逐步创建和分配二级页表。我们假设一级页表有4096个页表项,每个页表项占用4个字节,那么一级页表只需要16KB内存即可。所以,新创建一个进程,我只需要分配4个4KB的页面来存储一级页表即可,至于二级页表,那是用到才动态映射。如果采用刚才提到的一级页表方案,新创建一个进程需要分配4MB连续内存来存储页表,所以,二级页表的方案比一级页表的方案 节省内存。

Pn:多级页表 – 二级页表

接下来,我们来看一下二级页表。这页ppt是一个某款处理器二级页表的例子,虚拟地址VA[31:20]用作一级页表的索引值,一共有12个比特位,最大可以索引4096个页表项,VA[19:12]用作二级页表的索引值,一共有8个比特位,最大可以索引256个页表项。当操作系统fork一个新进程时,首先会创建一级页表,分配16KB页面。在这个例子中,一级页表有4096个表项,每个表项占4字节,因此一级页表一共是16KB。当操作系统准备让这个进程运行时,设置一级页表在物理内存中的起始地址到页表基地址寄存器中。进程执行过程中需要访问物理内存,因为一级页表的表项是空的,触发缺页异常。在缺页异常里分配一个二级页表,并且把二级页表的起始地址填充到一级页表的相应表项中。接着,分配一个物理页面,然后把这个物理页面的PFN填充到二级页表的对应页表项中,从而完成了页表的填充。随着进程的执行,它需要访问越来越多的物理内存,那么操作系统会逐步地把页表填充和建立起来。

所以,采用二级页表的情况下,二级页表是动态构建的,而不是进程创建的时候都全部预先分配好,这个和一级页表的方案有本质的区别。

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

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

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

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

Pn:疑惑三:页表的基地址

接下来,我们来看第三个常见的疑惑。我们刚才和大家详细解释了关于页表的问题,页表的作用是帮助MMU硬件单元实现虚拟地址到物理地址的转换,有不少细心同学可能会产生这样一个疑问:页表项中有指向下一级页表基地址的指针,那它指向的是下一级页表基地址的物理地址还是虚拟地址?比如这张图上是某个处理器上的页表项,其中第12~47比特位 作为下一级页表的基地址,那么这个基地址是指向 下一级页表的虚拟地址还是物理地址呢?

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

Pn:疑惑四:Linux内核软件遍历页表

接下来,我们来看第四个常见的疑惑。这是一个非常有意思的问题,有不少同学在阅读linux内核关于页表的代码时,都产生这样的疑惑。我们刚才分析二级页表,MMU硬件单元会遍历页表,但是呢,有细心的读者会发现Linux内核也遍历这个页表,比如walk_pgd()、__create_pgd_mapping()、follow_page()等函数。MMU硬件单元遍历页表比较容易理解, MMU硬件单元从页表基地址寄存器得到了PGD页表(一级页表)基地址的物理地址,然后从虚拟地址中得到每级页表的索引值,从而找到对应的页表项,页表项中存储了下一级页表物理基地址,以此类推,很容易遍历整个页表。但是,站在软件的视角,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类型的变量。所以,这是一个比较有趣的问题。

Pn:软件遍历页表的简单例子

这里列出一个软件遍历页表的简单例子,比如这个函数walk_pagetable(), 第32行的,pgd_offset()这个宏可以很方便找到虚拟地址address对应的pgd页表项的虚拟地址,这里pgdp是一个指针,它指向pgd页表项的虚拟地址。

init_mm_p是内核页表的基地址,系统中所有的内核线程都使用相同的内核页表,它在内核里称为swapper_pg_dir,在init_mm数据结构中的pgd成员也指向这个页表。

接下来比较难理解的是,Linux内核是如何去查找到下一级页表基地址的虚拟地址?因为,pgd_t数据结构里并没有存储一个指针来指向下一级页表的虚拟地址。

我们知道,在Linux内核里,物理内存会线性映射到内核空间里,偏移地址为PAGE_OFFSET。在内核空间可以很方便地实现虚拟地址和物理地址映射的转换。Linux内提供两个宏,其中__pa()宏用于计算内核中线性映射的虚拟地址到物理地址;而__va()宏用来计算内核线性映射的物理地址到虚拟地址。

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

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


Pn:疑惑五:线性映射

我们接下来看常见疑惑5.可能有的同学对线性映射有点疑惑,为什么内核在初始化的时候需要把整个DDR内存都线性映射到内核空间呢,对于64位的处理器是全部线性映射了物理内存,但是对于32位处理器来说,由于内核空间比较小,比如按3:1来划分4GB进程地址空间的话,内核空间也就1个G,若物理内存有2个GB,那是无法把2GB内存都线性映射到内核空间的,这里面有一个高端内存的概念,我们先不讨论这个话题,我们假定处理器是64bit,我们来关注线性映射的问题。你之前不是说,多级页表是 按需映射,动态映射吗?为什么linux内核要全部线性映射物理内存到内核空间呢?

首先要明白一点,怎么映射是由软件来定义的,而页表机制是由硬件提供的,也就是硬件提供了页表映射的机制,至于究竟哪些虚拟地址映射到哪些物理地址,这是软件可以定义的,本质上来说,软件来填充页表,而MMU硬件单元只是根据页表的页表项内容,来完成虚拟地址到物理地址的映射。所以,程序猿让MMU映射哪里,它就映射哪里,MMU不会自作聪明 自动给你建立映射的。

第二点,多级页表的按需映射,是页表的基本功能,只是用来节省页表占用内存空间,而内核的线性映射和这个多级页表的按需映射,其实是两回事,大家需要区别对待。

第三点,用户地址空间和内核地址空间。内核空间是所有进程共享的一个空间。CPU虚拟化也就是进程的抽象,内存虚拟化也就是地址空间的抽象,在这两个概念下,进程感觉它拥有了全部的地址空间,包括用户空间和内核空间,用户空间是它独有的,而内核空间是所有进程共享的。

当进程陷入到内核空间时,它访问的地址同样是虚拟地址,只不过是它访问了内核地址空间。但是,有一点不一样的是,进程在用户空间访问虚拟地址,如果这个虚拟地址没有映射物理内存时,处理器会触发缺页异常,然后陷入到内核态的缺页异常中来修复这个映射。但是如果在内核态访问一个没有映射的内核地址空间,那么内核陷入崩溃状态,这就是我们常常看的oops错误。这是因为运行在内核空间的程序需要稳定性和安全性。假设内核空间的虚拟地址没有预先映射,内核运行在内核空间里也常常需要访问物理内存,那么内核就会常常处于缺页异常中,若缺页异常无法修复错误的话,那么整个系统就挂掉了。所以,为了操作系统的安全性和稳健性,对内核空间虚拟地址是不做缺页异常处理的,在内核空间里访问一个空指针常常会引发系统崩溃。所以,通常做法是:在内核初始化时,把全部物理内存都线性映射到内核地址空间,这样预先映射有不少好处,比如可以减少在内核空间崩溃的几率。第二,也能提高系统性能,因为在内核空间也是常常会分配内存,比如伙伴系统,slab机制分配内存,这些分配的内存,不需要重新来建立虚拟地址到物理地址的映射了,因为系统初始化时,已经预先映射好了。

Pn:疑惑六:内核空间的内存布局

接下来,我们来看疑惑6.很多初学者对 内核空间的内存布局也是常常感觉到疑惑,比如刚才提到的线性映射,还有vmalloc区,内核image等等,它们都在内核空间的什么地方。很多初学者对它们究竟在内核空间 什么位置,没有太多的感官的认识。

我们以64位的ARM处理器为例。

ARM64架构处理器采用48位物理寻址机制,最大可以寻找256TB的物理地址空间。对于目前的应用来说已经足够了,不需要扩展到64位的物理寻址。虚拟地址也同样最大支持48位寻址,所以在处理器架构设计上,把虚拟地址空间划分为两个空间,每个空间最大支持256TB。Linux内核在大多数体系结构上都把两个地址空间划分为用户空间和内核空间。

q 用户空间:0x0000_0000_0000_0000到0x0000_ffff_ffff_ffff。

q 内核空间:0xffff_0000_0000_0000到0xffff_ffff_ffff_ffff。

64位Linux内核中没有高端内存这个概念了,因为48位的寻址空间已经足够大了。

在QEMU Virt实验平台中,ARM64架构的Linux 5.0内核的内存分布图,这部分信息的打印是在mem_init()函数中实现的。

q Modules区域:0xffff000008000000到0xffff000010000000,大小为128MB。

q vmalloc区域:0xffff000010000000到0xffff7dffbfff0000,大小为129022GB。

q 固定映射(Fixed)区域:0xffff7dfffe7f9000到0xffff7dfffec00000,大小为4124KB。

q PCI I/O区域:0xffff7dfffee00000到0xffff7dffffe00000,大小为16MB。

q vmemmap区域:0xffff7e0000000000到0xffff800000000000,大小为2048GB。

q 线性映射区:0xffff800000000000到0xffffffffffffffff,大小为128TB。

这个图里,我们还打印了一些我们想感兴趣的值。

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

当系统刚初始化时,内核映像是通过段映射的方式映射到KIMAGE_VADDR + TEXT_OFFSET的虚拟地址上,因此kimage_voffset表示内核映像虚拟地址和物理地址之间的偏移,有点类似线性映射之后的PAGE_OFFSET。

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

我们来看一下,这个图里,有指出 内核image每个段的虚拟地址是在哪里。

编译器在编译目标文件并且链接完成之后,即可知道内核映像文件最终的大小,接下来打包成二进制文件,该操作由arch/arm64/kernel/vmlinux.ld.S控制,其中也划定了内核的内存布局。

内核image本身占据的内存空间从_text段到_end段,并且分为如下几个段。

q 代码段:_text和_etext为代码段的起始和结束地址,包含了编译后的内核代码。

q init段:__init_begin和__init_end为init段的起始和结束地址,包含了大部分模块初始化的数据。

q 数据段:_sdata和_edata为数据段的起始和结束地址,保存大部分内核的变量。

q BSS段:__bss_start和__bss_stop为BSS段的开始和结束地址,包含初始化的或者初始化为0的全局变量和静态变量。

上述几个段的大小在编译链接时根据内核配置来确定,因为每种配置的代码段和数据段长度都不相同,这取决于要编译哪些内核模块,但是起始地址_text总是相同的。内核编译完成之后,会生成一个System.map文件,查询这个文件可以找到这些地址的具体数值。

Pn:疑惑七:关于映射

关于映射,初学者常常有这样疑惑,一个虚拟地址可以同时映射多个物理地址吗?一个物理地址可以同时映射多个虚拟地址吗?我相信有不少初学者就犯晕了。

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

举个例子,前面提到内核的线性映射,把物理内存线性映射到内核空间,同时,对于内核映像,内核在初始化时又做了另外一个映射,把内核映像映射到内核空间的另一个地方。所以,虚拟地址和物理地址怎么映射,这是软件可以定义的事情。

当然,上述的说法是考虑在同一张页表的情况,但是linux系统可能同时存在多张页表,即多个进程地址空间,在这种复杂场景下,是有可能一个相同的虚拟地址映射到同一个物理地址上,但是它们只是虚拟地址的数值相等,但是进程地址空间确是不一样的。

Pn:疑惑八:虚拟地址打架

我们来看最后一个常见的疑惑。假设系统中有进程A和进程B,分别使用testA和testB函数分配内存,使用printf打印指针bufA和bufB指向的地址是一样的,那么在内核中这两块虚拟内存是否冲突了呢?这是非常有意思的问题。

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

总的来说,就是我们在第一个疑惑里提到的,进程地址空间是 操作系统对内存的抽象,进程看是自己进程地址空间的虚拟内存,理解了地址空间这个概念,对这个疑惑就容易理解了。

我们今天的课程就聊到这里,希望对大家有帮助,谢谢大家。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值