虚拟内存

        这两天花时间学习了一下虚拟寻址的原理,之前写驱动程序只知道把物理地址转换为虚拟地址(VA),CPU实际使用的也是虚拟地址,其经内存管理单元(MMU)最终转换为物理地址(PA),对其中涉及的一些概念不清楚,导致对整个嵌入式系统的整体理解存在偏差,所幸花时间学习一下。 

物理和虚拟寻址

        首先明确虚拟地址空间和物理地址空间的概念。在一个带虚拟内存的系统中,假设为n位系统(如常见的32位和64位),CPU从这个有N = 2^n个地址的地址空间中生成虚拟地址,这个地址空间称为虚拟地址空间。概念上而言,虚拟内存被组织为一个由存放在磁盘上的个连续地字节大小的单元组成的数组,也就是说,虚拟内存是存储在磁盘上的。可能是经验少,写驱动程序时就没接触过核心模块的Flash,所以这时候我存在一个疑问:我们的核心模块有Flash吗?Flash有多大?直接问CPU板工程师,得到的答案是4GB()。物理地址空间就是很简单地对应为系统中主存(SDRAM)的个字节。

        ** 物理寻址就是程序中访问的内存地址都是实际的物理内存地址。早期的PC就是使用物理寻址,当计算机中同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小,下面通过实例来说明当同时运行多个程序时操作系统是如何为这些程序分配内存的:假设某台计算机总的物理内存大小是128M,同时运行A和B两个程序,A占用内存10M,B占用内存110M,计算机在给程序分配内存时会采取这样的方式:先将内存的前10M分配给程序A,接着再从内存中剩下的118M中划出110M分配给程序B,虽然这种分配方法可以保证程序A和B同时运行,但同时这种简单的内存分配策略问题很多。一:进程地址空间不隔离,理由很简单,由于程序都是直接访问物理内存,所以进程之间可以随意修改其他进程的内存数据;二:效率低,如果此时又运行了程序C,占用内存20M,但是此时系统只剩下8M空间可以使用,那怎么办,只能在已运行的程序中选择一个将其数据保存到磁盘上,暂时释放出空间供程序C使用,整个过程效率十分低下。虚拟寻址就是操作系统为每个进程创建一个4GB(32位系统)的虚拟内存空间,CPU通过生成一个虚拟地址来访问主存,这个虚拟地址在被送到主存之前先转换成合适的物理地址,这个过程叫地址翻译**,而完成这个过程的硬件叫做内存管理单元(MMU),如图,这样就解决了物理寻址存在的问题。

页、页框和页表

        和存储器层次结构中其他缓存一样(比如,SRAM做DRAM的缓存),DRAM缓存表示虚拟内存的缓存。磁盘上的数据被分割成块,这些块作为磁盘和主存之间的传输单元,为了换入换出的方便,物理内存也按同样大小分割成若干个块,这些块在虚拟内存中称为,而在物理内存中称为页框(Page Frame,也成页帧)。在Linux系统中,页和页框的大小一般为4KB(恍然大悟写驱动程序时物理地址映射为虚拟地址函数ioremap为什么是按照4KB映射,即使第二个参数设置了一个很小的整数,这个函数在后面还会提到)。物理内存和虚拟内存被分成页和页框以后,其每个地址被分成两部分,高位分别叫做页框码和页码,低位分别叫做页框偏移量和页内偏移量,假设一个32位系统其页大小为4KB,则其虚拟内存和物理内存分页情况如下图,其中四字节的前20bit表示页框码或页码,后12bit表示偏移量:

同任何缓存一样,虚拟内存系统必须有某种方法判断一个虚拟页是否缓存在DRAM中的某个地方,还必须确定这个虚拟页存放在哪个物理页中以及在物理页中选择一个牺牲页并将虚拟页从磁盘复制到DRAM中,这些功能是由软硬件联合提供的,包括操作系统软件、MMU中的地址翻译硬件和一个存放在物理内存中叫页表的数据结构。页表的示意图如下所示:

前面提到,页表是存放在物理内存中的,如果一个页大小为4KB,那么就存在1MB个页,所以页表长度很大,对内存的负担也很大,这时就出现了多级页表的概念,在此不想过多解释,不耽误我们理解就好。在页模式下,虚拟地址、物理地址转换关系的示意图如下所示:

首先从虚拟地址得到页码,然后查页表得到页框码,页框码和虚拟地址的偏移量结合生成物理地址。当然,页表的结构肯定要比这复杂得多,比如判断页是否缓存在内存标志位,如果有缓存,则直接访问得到的物理地址即可,否则我们甚至需要做换入换出动作才可以访问物理内存。

Linux内存管理

        针对32位x86 Linux系统而言,4GB虚拟地址空间具体又分为两个部分:用户空间和内核空间。用户空间的地址一般分布为03G,剩下的3G4G为内核空间,用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址,用户进程只有通过系统调用等方式才能访问到内核空间。每个进程的用户空间是完全独立互不相干的,用户进程各有自己的页表,而内核空间由内核负责映射,并不会跟着进程改变,是固定的。内核空间的虚拟地址到物理地址映射是被所有进程共享的,内核的虚拟空间独立于其他程序。

        1GB内核空间又被划分为物理内存映射区、虚拟内存分配区、高端页面映射区等多个区域。早期的PC物理内存较小,所以为了提高内核通过虚拟地址访问物理地址内存的速度,内核空间的虚拟地址与物理内存地址采用了一种从低地址向高地址依次一一对应的固定映射方式,如下图所示:

可以看到,这种简单的直接一一映射关系比页表映射简单的多,内核虚拟地址和物理地址仅仅相差了PAGE_OFFSET,所以当内核使用虚拟地址访问物理页框时,只需在虚拟地址上减去PAGE_OFFSET即可得到实际物理地址,比使用页表的方式要快得多。

由于这种做法几乎就是直接使用物理地址,所以这种按固定映射方式的内核空间也就叫做“物理内存空间”,简称物理内存。另外,由于固定映射方式是一种线性映射,所以这个区域也叫做线性映射区

那么内核空间剩余的内核虚拟空间怎么办呢?当然还是按照普通虚拟空间的管理方式,以页表的非线性映射方式使用物理内存。具体来说,在整个1GB内核空间中去除固定映射区,然后在剩余部分中再去除其开头部分的一个8MB隔离区,余下的就是映射方式与用户空间相同的普通虚拟内存映射区。在这个区,虚拟地址和物理地址不仅不存在固定映射关系,而且通过调用内核函数vmalloc()获得动态内存,故这个区就被称为vmalloc分配区。所以现在你知道vmalloc分配的内存虚拟地址连续而物理地址不一定连续的原因了吧。

随着计算机的实际物理内存越来越大,从而使得内核固定映射区也越来越大。显然,如果不加以限制,当实际物理内存达到1GB时,vmalloc分配区(非线性区)将不复存在。于是以前开发的、调用了vmalloc()的内核代码也就不再可用,显然为了兼容早期的内核代码,这是不能允许的。解决上述问题的方法就是:对内核空间固定映射区的上限加以限制,使之不能随着物理内存的增加而任意增加。Linux规定,内核映射区的上边界的值最大不能大于一个小于1G的常数high_menory,当实际物理内存较大时,以3G+high_memory为边界来确定物理内存区。对于32位x86 Linux系统而言,内存映射区最大长度即high_menory=896M,当系统物理内存大于896M时,超过896M的物理内存如何访问?很简单,跟访问普通虚拟内存方式一样,仍然通过页表的非线性映射方式,这部分内存称为高端内存。由此可见,对于32位x86系统而言,34GB的内核空间从低地址到高地址依次为:物理内存映射区(线性映射)->隔离带->vmalloc虚拟内存分配区->隔离带->高端内存映射区->专用页面映射区->保留区(专用页面映射区之后再讲)。补充一句,直接线性映射的896M物理内存映射区又分为两个区域,低于16MB的区域为DMA区域,16M896M的区域成为常规(NORMAL)区域

到这里,可能有人会问:写ARM驱动的时候对于IO内存,它既不属于进程空间,又不是物理内存,那它们被映射在虚拟内存的什么位置呢?我查到的答案是:Linux并没有为这些已知的外设IO内存资源的物理地址预分配虚拟地址范围,驱动程序不能直接通过物理地址访问IO内存资源,而必须将它们映射到虚拟内存地址范围(页表),然后才能访问,ioremap用来将IO内存资源的PA映射到内存虚拟地址空间(3GB~4GB)中。这个范围很笼统,个人感觉就是映射到了高端内存中(ARM的内核空间和x86内核空间

大同小异)。理论上我们就可以像读写RAM那样直接访问IO内存了(以前看韦东山老师写驱动程序就是直接使用指针访问虚拟地址),但为了保证跨平台的可移植性,应使用特定函数访问IO内存(readb/w/l,writeb/w/l)。

个人感觉通过了解虚拟内存技术,会对嵌入式系统的整体有更深刻的理解,对一些细节的地方也会知其所以然,特别是对软件工程师来说。对于Linux系统中的内存动态申请,我会再花一点时间去学习,未完待续

 

 

 

 

代码交流 2021