logo资料库

基于ARM CPU的Linux物理内存管理.pdf

第1页 / 共56页
第2页 / 共56页
第3页 / 共56页
第4页 / 共56页
第5页 / 共56页
第6页 / 共56页
第7页 / 共56页
第8页 / 共56页
资料共56页,剩余部分请下载后查看
基于 ARM CPU 的 Linux 物理内存管理 基于 ARM CPU 的 Linux 物理内存管理 刘永生 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn Page 1
基于 ARM CPU 的 Linux 物理内存管理 本文一共分为四个部分 第一部分介绍内存布局的演进。这样方便理解为什么内存管理中需要虚拟地址,物理内存和 访问保护。 第二部分介绍在 ARMC CPU 上是如何支持内存管理的。操作系统对内存的管理的目的就是满 足应用程序(当然也有部分内核代码)的内存申请和释放,而内存的申请和释放都是围绕 CPU 硬件上的内存管理单元(MMU)而进行的。所以不了解 ARM MMU 对地址映射的一些 概念和要求,就没办法理解内核中的某些数据结构和执行操作。如果对这部分比较了解,可 以越过。 第三部分介绍 Linux 内核对物理内存管理的思想和原理。如果能在原理和框架上理解内核对 物理内存如何管理的,那么就能更快和深入地理解内核代码是如何实现内核管理的。 第四部分在源代码中介绍 Linux 内核是如何实现物理内存管理的。 注,本文只介绍了内核是如何管理物理内存的,并不包括内存管理的其他部分。本文的介绍 内容到 buddy 系统建立为止,而建立在 buddy 系统之上的 cache-cache 机制,如 Slob,Slab 和 Slub 等则不在本文范围。 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn Page 2
基于 ARM CPU 的 Linux 物理内存管理 第一部分,内存布局的演进 首先在一个设备上,CPU 执行所需要的代码可以存放在 Rom 中,也可以存放在 Ram 里 存放在 ROM 中的代码,虽然可以被直接执行,但因为 Rom 不能被改变(或需要复杂的命令 序列来改变),所以系统还需要一些 Ram 来存放用于程序执行所需要的数据,例如全局变量 和堆栈。因为 RAM 在断电的情况下是不能继续保持其中内容的,所以在 RAM 中的程序代码 需要系统上电的时候从外面的存储介质中加载,可以是从一个 Rom 中把代码读入到 Ram, 也可以其他的存储介质,比如 Nand Flash 或 SD/MMC 卡。而在 PC 上典型的是从硬盘(SATA 接口)读入。无论哪种启动执行方式,CPU 都需要一块能够可读写的 RAM。我们的问题是, 系统中 RAM 是如何使用的呢? 最直接和简单就是把一块内存划分成不同的区域,用来存放不同目的数据,例如下图所 示,直接把 Ram 划分成代码,数据和堆栈,也是程序执行必须的三要素。 这是最直接的内存使用方式,没有什么技巧和也不需要管理,直接简单粗暴的把内存分 为三部分。每部分有各自的使用目的和大小,在使用期间也不需要改变大小和使用目的。 在某些单片机或 DSP 上就是这么使用物理内存的,直接高效。它的缺点就是只能运行 一个程序,所解决问题的也比较单一和固定。 随着使用需求的变化,这样的布局就出现了局限。如果我们需要运行多个程序,每 个程序以时间片的方式共享 CPU 时间。 如上图所示,系统中需要有三个程序来完成不同的任务,CPU 会轮流地执行不同程 序的代码。CPU 在切换到不同程序前,要把当前程序的状态保留下来,以便之后再轮到 当前程序运行时能从当前的状态开始继续执行。这个需要被保存下来的状态,也就做程 序的 CPU 上下文(cpu context)。每个程序都需要自己的运行数据,那么相应地,根据 运行程序的数量也要把内存分成三部分。每部分为一个程序服务,又把内存划分成更小 的代码, 数据和堆栈区。如下图 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn Page 3
基于 ARM CPU 的 Linux 物理内存管理 系统中既然有了多于一个的 CPU 上下文,就需要 CPU 能不断地在不同的上下文之 中切换。例如 CPU 可以先执行 A,然后执行 B,然后有要切换去执行 C。那么 CPU 什么 时候需要切换和如何进行切换?解决它,系统就要有一个独立于所运行程序之外的模块 来仲裁和调度不同的程序,执行 CPU 上下文的切换。这样系统还需要一个管理 CPU 上 下文切换的模块。如下图所示,这个模块我们先叫它为系统代码。 开 始 地 址 址 地 束 结 代码A 全局数据 堆栈 代码B 全局数据 堆栈 代码C 全局数据 堆栈 系统代码 系统数据 系统堆栈 系统代码除了管理 CPU 上下文切换之外,还要管理一些外围设备并提供统一的使用接 口。这样不同的应用程序就能直接调用这些通用接口来直接使用外设,而不需要每个程 序各自编写自己的程序来控制外设。这些外设管理程序也被叫做去驱动。这样做的好处 是 - 外设管理的代码统一集中,不需要需要每个程序自己都编写设备使用代码 - 易于定义统一通用的接口,这样在理想情况下,应用程序就可以在不改面代码的情 况在其他操作上运行。 - 简化了应用程序使用外设的设计和编码 - 便于协调和同步不同程序使用同一外设,驱动程序可以决定应用程序是共享式的使 用当前设备还是排他式的使用。 此时,上图中的系统代码就有了管理 CPU 上下文切换和各种外部设备的功能,具有这 么多功能的系统代码,也可以被称为操作系统(Operating System)。 这样每个程序都有了属于自己的,预定义的代码,数据和堆栈区间。但问题是不同 的程序在运行的过程中,所需要的数据和堆栈空间是不一样的。如何给系统中的程序分 配或预定义内存就成了一个问题。例如可以采取平均分配,也就是给每个程序上下文都 分配尽可能多的内存并大小相等。如果按照系统中所需内存最大的程序所需的内存量来 为所有的程序预留物理内存,那么系统就需要准备更大的物理内存。这样做虽然可以保 证系统的正常运行,结果会是浪费一些物理内存。或者根据每个程序所需要的实际内存 大小而为每个程序预分配。这样做的一个问题就是有的程序在运行之前,并不知道所需 要内存的最大量,因为有些内存需要在程序运行中才能确定,比如要浏览一个网页,程 序在运行之前并不知道要浏览网页的大小。综合上面的问题: 1, 在一个 32 位的 CPU 系统中,尽可能地为每个程序分配足够多的空间 2, 按分配方式,把程序所用的内存分为两类,一类是静态的,一类是动态的。静态的 内存在程序设计和编译时就可以确定的,比如程序的代码,使用的全局变量等。这 些内存在程序运行的生命周期中是一直存在。动态的内存是在程序运行过程中根据 需要而向系统动态地申请的内存。 解决上面的问题,系统引入了虚拟内存的概念。虚拟内存是如何解决这个问题的呢? Page 4 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn
基于 ARM CPU 的 Linux 物理内存管理 如下图 在上图中, 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)。为了使各功能区间连续,可以把布局做点改 Page 5 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn
基于 ARM CPU 的 Linux 物理内存管理 动,如下图。这样数据区间就可以在运行时向高地址扩展形成堆(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, 任何程序代码不能破坏操作系统的代码和数据。 为了解决这个问题,引入了内存保护的概念,如下图 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn Page 6
基于 ARM CPU 的 Linux 物理内存管理 同内存映射一样,内存保护也需要在硬件上支持。如上图所示,这个保护机制应该能: 1, 设定任一内存区域的读写属性,这样就可以设置任何程序的代码区为只读; 2, 设定程序的访问权限这样可以防止程序越界破坏其他程序管理的空间。一旦发生越 界的事情,能被捕获并通知 CPU。 3, 在操作系统中支持因破坏保护而产生的异常。异常被捕获之后,操作系统需要执行 进一步的处理,比如终止程序 A 的调度,结束 A 程序的运行,释放程序 A 占据的外 设使用权等。 在上图中可以看到,内存的保护是作用在虚拟地址上的,所以就在硬件的内存管理单元 中加入了内存的权限管理。因此内存管理单元就有了如下功能: a) 设置内存访问权限和捕获异常 b) 虚拟地址的转换 从此,系统可以在线性的虚拟地址上运行并得到了来自硬件上的保护。每个程序都在自 己的相对大的空间里执行,享受着操作系统提供的各种服务。 有一天程序 A 需要升级,升级后的程序 A 需要 2G 的空间。此时程序 B 和 C 也需要至少 1G 的内存地址空间。还有,系统需要动态地执行一个新的程序 D,因为空间都已经被分配 出去了,并没有可用的空间给程序 D 使用。但运行新程序是一个普遍的使用场景和需求。 为解决这些问题,继续对上面讨论过的布局进行优化。现在先分析下上图中虚拟地址使用的 特点: 1, 不同程序之间是分时执行的,也就是程序 A 执行的时候,程序 B 是没有运行的。而程序 A 又被限制不能访问程序 B 的空间,所以在程序 A 运行时,程序 B 和程序 C 的空间是完 全无效的。那么假如此时程序 A 能利用程序 B 或 C 管理的虚拟空间,那么程序 A 所覆盖 的虚拟空间就会变的更大,最大到 3G。然后在程序 B 开始运行时,程序 A‘归还’属于 程序 B 的虚拟空间,这样程序 B 就可以正常地运行,并且还可以‘借用‘程序 A 和程序 C 的虚拟空间。 2, 在程序 A‘借用’程序 B 的虚拟空间时,不能使用程序 B 所映射的物理内存。那么程序 A 虽然借用了 B 的虚拟空间,但不会破坏程序 B 使用的物理内存中的内容。 3, 无论哪个程序运行,都需要使用和享受操作系统作提供的基本功能和服务。 所以,系统的内存布局就变成了下面的样子 1, 这样每个程序所能看到的虚拟空间就变成了 3G, 这个 3G 的空间也被称为应用空间, 2, 操作系统代码和数据以及栈的空间映射保持不变,这样对于每个程序,操作系统都是统 Page 7 也叫做用户空间。 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn
基于 ARM CPU 的 Linux 物理内存管理 一的地址布局和调用接口。作操作系统所占的空间叫做内核空间。 3, 系统中任一时刻 CPU 上只有一个程序在运行,运行的程序占据了整个 3G 的虚拟空间。 每个程序所使用的物理内存都是独立的。虽然程序间可以共享物理内存,但并没有改变 进程独占物理内存的本质。进程需要有自己的内存映射表,这样进程所有的物理内存就 映射在自己独享的内存映射表中。当程序 A,B 进行切换时,不仅要变更 CPU 的上下文 (通用寄存器的内容), 还有变更内存上下文(虚拟程序映射表)。 4, 程序间的通信变得了困难,列如在程序 A 运行时不能访问也看不到程序 B 的地址空间, 无法直接访问程序 B 的内容。解决这个问题,引入了 IPC(跨进程通信)的概念。IPC 的 实现方法有很多,例如在不同进程共享一块物理内存,或者通过操作系统代码和操作系 统数据段来实现 IPC 等。IPC 虽然能实现通信,但效率比较低下。 演进到此,上图的样子是几乎所有现代流行操作系统所采用的内存布局了。操作系统的 内核空间的划分并不是固定的,也可以划分在 2G 处。但这只是分界不同,主要思想没变。 这只是个原理图,实际的系统在内存管理上会复杂一点,但其思想都是源于的,没有脱离上 图的范围。现在的操作系统管理内存看似很复杂,但都是为了解决某些问题而一步一步演进 而来的。所以,不是内存的管理复杂,而是使用内存的场景变得更复杂,从而导致系统增加 了各种方式方法来满足这些复杂的需求 。如果我们把复杂的内存管理去掉一些容错和某 些特殊的操作后,就会发现其实它挺简单的,如上图一样清晰明了。 内存布局的演进就是不断地解决所遇到的问题,不断地满足新的需求。 而为了满足新 的需求而出现的方法和程序更新本身就是一种创新或发明。问题不会终结,解决问题也不会 停止,就需要有更多的创新和发明出现。 上面的演进只是列子,不是全部,需求永无止境,演进也不会停止。内存使用过程中, 还有很多其他的问题。比如从硬件角度,随着 CPU 的频率越来越快,内存的读写速度已经 严重的制约了 CPU 的执行速度。为解决这个问题,而发明了 Cache;随着 Cache 的应用,发 早期 VIVT 型的 cache 虽然查找和比对的速度快,但在运行中需要不断的同步 cache 的内容, 为解决这个问题,增加了 VIPT 和 PIPI 类型的 Cache。同时为加快地址翻译速度,又引入 TLB, 用来缓存地址映射。为了增加物理内存管理的灵活和颗粒度,又把虚拟内存映射表分成几级。 为了支持多个操作系统,又引入了 Hypervisor。 而软件上的演进更是一日千里,除了要支 持上述的硬件演进外,还要解决系统面临的其他问题,比如内存管理方法和策略,任务调度 的方法和策略等。 在内存管理中,另一个重要的需求就是尽量地降低内存使用过程中产生的碎片,这也是 提高内存使用效率的一个要求。 刘永生 微信: eternalvita 邮箱:yongshliu@sina.cn Page 8
分享到:
收藏