三言两语聊kernel:内存管理入门

前言

最近稍有闲暇将自己的一些所学做了整理, 只是浅显的说个大概,没有往细了说,观题目就可知,仅作入门:)

内存管理的目的

由于计算机系统所含的实际物理内存大小是有限的, 所以CPU中通常都提供了内存管理机制来对系统中的内存进行有效的管理。
在intel CPU中, 提供了两种内存管理机制:

  • 分段机制
  • 分页机制
    分页管理系统是可以选择的, 它有menuconfig的配置来决定是否需要分页。

对于linux内核而言,它并没有使用intel CPU的分段机制, 而只是采用了它的分页机制。

内存管理的一个目的是地址变换,另外一个目的是寻址保护。

分页机制主要明白以下概念

  • 虚拟地址
  • 物理地址
  • page结构体

对于进程而言, 它看到的都是虚拟地址空间, 在32bit CPU上,它的地址空间是4G;
对于CPU而言,它访问的是具体的物理内存,即物理地址空间。

如果由进程的虚拟机制空间来转换为CPU的物理地址空间,就有了MMU这个单元,它的作用是将虚拟地址空间映射为物理地址空间。映射方式就是采用的分页机制,首先将实际物理内存按照4K大小为单位来将物理内存划分为一个个物理页框,这些物理页框通过page结构体来索引, 所有的物理page都是通过一个字节数组mem_map[]来进行检索,该数组的每一个元素代表一个物理page的状态。 mem_map[]对于numa和uma,sparse memory和flat memroy这些内存管理模型上具有不同的实现。 numa是基于节点的, sparse memory是基于section的。

在地址变换的过程中,首先由虚拟地址的前12bit得出起PGD,结合PGD的内容和CP15寄存器计算出其PMD的基址,虚拟地址的中间8bit为其偏移,然后通过PMD里面的内容计算出PTE的基址(即物理页框的地址),虚拟地址的最后12bit是PTE的偏移,这个地址就是该虚拟地址对应的物理内存的地址。

虚拟地址空间的划分

进程的地址空间分为内核空间和用户空间两部分。 这两部分的比例可以通过menuconfig来进行配置, 一般都是配置为1:3,即用户空间占3G, 内核空间占1G。

用户空间是0~3G这部分地址, 内核空间是3G~4G这部分地址。 对于用户空间而言,每个进程都有自己的页表,而内核空间则只有一份页表(中断/内核线程这些只运行在内核空间的都是没有mm_struct的)。

内核空间是如下划分: 内核空间起始于PAGE_OFFSET(即3G), 在开始的区域是nomal memory区域,这部分区域包括内核镜像/mem_map[]数组,normal memory区域是线性映射, 它映射到物理内存的起始部分。

然后从high–memory这个地方开始就是所谓的高端内存, 在高端内存和normal memory之间有一个4K的保护页,它的主要作用是起到保护作用。

高端内存

高端内存主要是为了解决线性地址不够用的问题。
高端内存有几种映射方式:

  • 固定映射
  • 永久映射
  • 临时映射
  • 以及非连续物理地址映射。

非连续物理地址映射即vmalloc区域,它从VMALLOC_START到VMALLOC_END 这个区域,它的意思是线性地址是连续的,但是映射的物理地址未必是连续的,这部分空间是通过vmalloc()来申请。

再往后是永久映射,就是所谓的pmap区域,它通过pmap()来申请, 通过punmap()来释放。

再往后是固定映射区, 它从fixed_addr_start开始,到fixed_addr_top结束,固定映射一般都是在编译阶段就确定好一些外设的映射关系,在系统启动阶段建立好它的映射关系后便不会在改变, 它的目的主要是解决一些外设在boot阶段就需要建立好映射的情况,一般都是用一个数学公式来表示它的映射。 都有哪些设备会用到固定映射?? 例如中断控制器就是采用固定映射,中断入口地址就是在编译阶段确定的,对于arm而言是0xffff0000。

在固定映射区的后面有一个4K的保护页面    。

不同的映射方式之间都会有一个空隙来起到保护作用。

写时拷贝机制

写时拷贝机制的目的:

  1. 为了节约物理内存
  2. 为了加快创建进程的速度

在使用fork()生成新的进程时,新进程与原进程会共享同一内存区,这部分内存区是只读的, 当其中一个进程进行写操作时,系统才会为其另外分配内存页面。 这就是copy-on-write的概念。

当进程A使用系统调用fork来创建一个子进程B时, 由于子进程B实际上是父进程A的一个拷贝,因此会拥有与父进程相同的物理页面。也即为了达到节约物理内存和快速创建的目的,fork()函数会让子进程B以只读方式共享父进程A的物理页面,同时父进程A对这些物理页面的访问权限也设置为只读(这是通过copy_page_tables()来实现的)。 这样以来,当父进程A或者子进程B,其中任何一方对这些共享页面执行写操作的时候, 都会产生page-fault,然后cpu会执行异常处理函数do_wp_page()来试图解决这个异常。

首先,会对这个物理页面取消共享, 然后为写进程复制一新的物理页面, 是父进程和子进程各自拥有一块内容相同的物理页面,这时才真正的进行了复制操作(当然是只复制这一个物理页面。),然后,将要执行写操作的这个物理页面置为可写的。 最后,从异常处理函数返回,cpu会重新执行刚才导致异常的写入操作指令,使进程能够执行下去。

伙伴分配算法

这个没有深入研究过, 也没有在实际工作中遇到这方面的问题, 所以只知道一个大概。

Comments