基于ARMCPU的Linux物理内存管理Page1刘永生微信:eternalvita邮箱:yongshliu@sina.cn基于ARMCPU的Linux物理内存管理刘永生基于ARMCPU的Linux物理内存管理Page2刘永生微信:eternalvita邮箱:yongshliu@sina.cn本文一共分为四个部分第一部分介绍内存布局的演进。这样方便理解为什么内存管理中需要虚拟地址,物理内存和访问保护。第二部分介绍在ARMCCPU上是如何支持内存管理的。操作系统对内存的管理的目的就是满足应用程序(当然也有部分内核代码)的内存申请和释放,而内存的申请和释放都是围绕CPU硬件上的内存管理单元(MMU)而进行的。所以不了解ARMMMU对地址映射的一些概念和要求,就没办法理解内核中的某些数据结构和执行操作。如果对这部分比较了解,可以越过。第三部分介绍Linux内核对物理内存管理的思想和原理。如果能在原理和框架上理解内核对物理内存如何管理的,那么就能更快和深入地理解内核代码是如何实现内核管理的。第四部分在源代码中介绍Linux内核是如何实现物理内存管理的。注,本文只介绍了内核是如何管理物理内存的,并不包括内存管理的其他部分。本文的介绍内容到buddy系统建立为止,而建立在buddy系统之上的cache-cache机制,如Slob,Slab和Slub等则不在本文范围。基于ARMCPU的Linux物理内存管理Page3刘永生微信:eternalvita邮箱:yongshliu@sina.cn第一部分,内存布局的演进首先在一个设备上,CPU执行所需要的代码可以存放在Rom中,也可以存放在Ram里存放在ROM中的代码,虽然可以被直接执行,但因为Rom不能被改变(或需要复杂的命令序列来改变),所以系统还需要一些Ram来存放用于程序执行所需要的数据,例如全局变量和堆栈。因为RAM在断电的情况下是不能继续保持其中内容的,所以在RAM中的程序代码需要系统上电的时候从外面的存储介质中加载,可以是从一个Rom中把代码读入到Ram,也可以其他的存储介质,比如NandFlash或SD/MMC卡。而在PC上典型的是从硬盘(SATA接口)读入。无论哪种启动执行方式,CPU都需要一块能够可读写的RAM。我们的问题是,系统中RAM是如何使用的呢?最直接和简单就是把一块内存划分成不同的区域,用来存放不同目的数据,例如下图所示,直接把Ram划分成代码,数据和堆栈,也是程序执行必须的三要素。这是最直接的内存使用方式,没有什么技巧和也不需要管理,直接简单粗暴的把内存分为三部分。每部分有各自的使用目的和大小,在使用期间也不需要改变大小和使用目的。在某些单片机或DSP上就是这么使用物理内存的,直接高效。它的缺点就是只能运行一个程序,所解决问题的也比较单一和固定。随着使用需求的变化,这样的布局就出现了局限。如果我们需要运行多个程序,每个程序以时间片的方式共享CPU时间。如上图所示,系统中需要有三个程序来完成不同的任务,CPU会轮流地执行不同程序的代码。CPU在切换到不同程序前,要把当前程序的状态保留下来,以便之后再轮到当前程序运行时能从当前的状态开始继续执行。这个需要被保存下来的状态,也就做程序的CPU上下文(cpucontext)。每个程序都需要自己的运行数据,那么相应地,根据运行程序的数量也要把内存分成三部分。每部分为一个程序服务,又把内存划分成更小的代码,数据和堆栈区。如下图基于ARMCPU的Linux物理内存管理Page4刘永生微信:eternalvita邮箱:yongshliu@sina.cn系统中既然有了多于一个的CPU上下文,就需要CPU能不断地在不同的上下文之中切换。例如CPU可以先执行A,然后执行B,然后有要切换去执行C。那么CPU什么时候需要切换和如何进行切换?解决它,系统就要有一个独立于所运行程序之外的模块来仲裁和调度不同的程序,执行CPU上下文的切换。这样系统还需要一个管理CPU上下文切换的模块。如下图所示,这个模块我们先叫它为系统代码。代码A全局数据堆栈开始地址结束地址代码B全局数据堆栈代码C全局数据堆栈系统代码系统数据系统堆栈系统代码除了管理CPU上下文切换之外,还要管理一些外围设备并提供统一的使用接口。这样不同的应用程序就能直接调用这些通用接口来直接使用外设,而不需要每个程序各自编写自己的程序来控制外设。这些外设管理程序也被叫做去驱动。这样做的好处是-外设管理的代码统一集中,不需要需要每个程序自己都编写设备使用代码-易于定义统一通用的接口,这样在理想情况下,应用程序就可以在不改面代码的情况在其他操作上运行。-简化了应用程序使用外设的设计和编码-便于协调和同步不同程序使用同一外设,驱动程序可以决定应用程序是共享式的使用当前设备还是排他式的使用。此时,上图中的系统代码就有了管理CPU上下文切换和各种外部设备的功能,具有这么多功能的系统代码,也可以被称为操作系统(OperatingSystem)。这样每个程序都有了属于自己的,预定义的代码,数据和堆栈区间。但问题是不同的程序在运行的过程中,所需要的数据和堆栈空间是不一样的。如何给系统中的程序分配或预定义内存就成了一个问题。例如可以采取平均分配,也就是给每个程序上下文都分配尽可能多的内存并大小相等。如果按照系统中所需内存最大的程序所需的内存量来为所有的程序预留物理内存,那么系统就需要准备更大的物理内存。这样做虽然可以保证系统的正常运行,结果会是浪费一些物理内存。或者根据每个程序所需要的实际内存大小而为每个程序预分配。这样做的一个问题就是有的程序在运行之前,并不知道所需要内存的最大量,因为有些内存需要在程序运行中才能确定,比如要浏览一个网页,程序在运行之前并不知道要浏览网页的大小。综合上面的问题:1,在一个32位的CPU系统中,尽可能地为每个程序分配足够多的空间2,按分配方式,把程序所用的内存分为两类,一类是静态的,一类是动态的。静态的内存在程序设计和编译时就可以确定的,比如程序的代码,使用的全局变量等。这些内存在程序运行的生命周期中是一直存在。动态的内存是在程序运行过程中根据需要而向系统动态地申请的内存。解决上面的问题,系统引入了虚拟内存的概念。虚拟内存是如何解决这个问题的呢?基于ARMCPU的Linux物理内存管理Page5刘永生微信:eternalvita邮箱:yongshliu@sina.cn如下图在上图中,1,程序执行过程中所看到的不在是物理内存,而是虚拟内存。就是说程序执行的代码地址和所要访问的数据地址都是使用虚拟地址。虚拟地址不是物理地址,它是CPU能看到的所有寻址空间,换句话说虚拟地址是始终存在的。所以就可以在CPU所支持的4G空间里,为每个程序预先分配最大可能的虚拟地址空间。如上图,程序A,程序B,程序C和系统代码分别得到了1G的虚拟地址空间。2,虚拟地址可以和物理内存绑定,也可以不和物理内存绑定。这种绑定也就做映射,默认状态下虚拟地址是没有映射物理内存的。只有被映射之后的虚拟地址才能被CPU访问,否则系统会产生异常。3,在程序被加载的时候,可以把程序所需要的静态内存部分进行映射。如上图中的代码段,数据段和一个堆栈。堆栈大小虽然不是在编译时候就知道,但程序运行必须要有栈,所以这里可以为每个程序预分配一个合适大小的栈,比如4K。如果程序运行中,所需要的栈超过4K了,那么就动态地向系统申请和映射一块更大的栈。这个过程也就做栈扩展。4,连续的虚拟内存映射的物理内存上不一定或不需要是连续的。5,CPU在运行中所关心的是它所能访问的指令和数据是否是连续的,通过虚拟内存就能保证CPU所看到的地址始终是连续的,比如对一个数组进行操作,这个数组所占据的虚拟内存对应的物理地址是否连续,CPU并不关心也看不到,因为它只需要连续地访问虚拟地址就可以实现数据的读写操作。这也是为什么虚拟地址又被成为线性地址。CPU为了达到这个目的,也就是在执行过程中直接操作虚拟地址而结果被自动地保存在物理内存中,需要额外的硬件支持。这个能够把CPU执行过程中需要的虚拟地址自动地翻译成物理内存地址的附属硬件,被称为内存管理单元(MMU)。所以映射需要CPU在硬件上支持,而存放这种映射关系的矩阵被称为地址映射表。6,在运行过程中,如果函数调用的层次变多或局部变量累积变大而导致堆栈空间不断变大并超过了预分配的栈空间(比如超过了预分配的4K栈),那么就需要动态地增加堆栈的空间。而系统中虚拟空间是早就分配好只是没有映射实际的物理内存,所以问题就简化成,如果堆栈空间不够,只要继续映射更多的物理内存就可以了。从上图可以看到,每个程序都有一些可使用但没被映射的虚拟地址,在物理内存上也有一些能被使用但未被映射的物理内存。这些未被使用的物理内存在运行过程中被动态分配给所需要的程序,也就是按照实际把更多的物理内存映射到某个程序所需要的虚拟空间上的过程,就是动态分配。除了堆栈,动态分配也适用于运行中处理预留的数据段不足的场景,如前面所说的动态下载网页的例子。这个动态分配的数据区域,也叫做堆(heap)用以区分静态分配的数据段。既然,数据和堆栈区都需要动态的伸缩,上图的布局在动态改变数据和堆栈区时就会产生互相间隔(interleave)。为了使各功能区间连续,可以把布局做点改基于ARMCPU的Linux物理内存管理Page6刘永生微信:eternalvita邮箱:yongshliu@sina.cn动,如下图。这样数据区间就可以在运行时向高地址扩展形成堆(heap),而堆栈的扩展也可以连续地从高地址向低地址移动。堆(heap)和堆栈(stack)区始终是分开的,不会互相间隔。这也是为什么大多数系统的‘栈’都是从上到下(高地址到低地址)增长的原因(也系统的栈是向上增长的例外)。这样就解决了动态需要内存的问题,同时在没有增加实际物理内存总量的情况下,增加了系统中物理内存的使用效率。所以就需要在系统代码中增加对动态申请和释放内存的支持,因此操作系统引入了另一个重要的功能—‘内存管理’。一个完整的操作系统应该具有如下功能,a)调度模块以使不同的cpu上下文都能得到CPU的时间来运行b)同步机制来协调不同CPU上下文同时访问同一资源的竞争问题c)驱动和具有服务性质的软件协议栈,例如网卡和TCP/IP协议栈,硬盘和文件系统等d)静态和动态内存管理e)加载和运行应用程序此时,每个程序在操作系统的调度下运行在系统划分好的虚拟空间内,互相不干扰相安无事。但程序是人编写的,总会由于失误而出现这样或那样的问题。随着程序复杂性的增高,程序出现问题的概率也变得越来越大。例如,程序A运行中出现了问题,它破坏了自己代码区的内容,但此时操作系统并知道程序A的代码被破坏了,还在继续调度执行A的代码,那么执行的结果就是错误的和不可预期的,甚至是破坏整个系统。比如继续执行被修改的A代码破坏了程序B的代码或数据,那么程序B就不能正常工作了。或者更严重一点,程序A的代码破坏了操作系统的代码和数据,那么整个系统都不能正常工作,系统就会崩溃。这并不是我们想要看到的,而我们希望系统应该是足够健壮的。理想的系统应该是:1,某个区间应该是只读的,比如代码区间,那么它就不会在运行过程中被修改2,某个程序只能访问被限制的或指定的区间,比如程序A不能访问程序B的区间,不但包括B的代码,还包括程序B的数据和堆栈段都不能被程序A访问。3,任何程序代码不能破坏操作系统的代码和数据。为了解决这个问题,引入了内存保护的概念,如下图基于ARMCPU的Linux物理内存管理Page7刘永生微信:eternalvita邮箱:yongshliu@sina.cn同内存映射一样,内存保护也需要在硬件上支持。如上图所示,这个保护机制应该能:1,设定任一内存区域的读写属性,这样就可以设置任何程序的代码区为只读;2,设定程序的访问权限这样可以防止程序越界破坏其他程序管理的空间。一旦发生越界的事情,能被捕获并通知CPU。3,在操作系统中支持因破坏保护而产生的异常。异常被捕获之后,操作系统需要执行进一步的处理,比如终止程序A的调度,结束