1、 在伙伴算法之前的内存管理机制本文的所描述的内核版本是 linux-2.4.20我们知道在 linux 内核中,获取物理页面是靠着 alloc_pages 这个函数来实现的,这个函数会尽它的一切努力满足页面的分配需求,如果 free 页面的队列充足,那么就直接分配一个然后返回了,很简单。但是如果不足,那就要洗洗刷刷把不干净的页面转换成干净的,干净页面多了也可以满足分配的需求了。这个函数从某种意思上说非常以及极其的简单,那是在页面充足的情况下。系统里的页面越是缺乏,那么这个函数就越显得复杂和面目可憎,挖空心思的洗洗涮涮,克服一切困难,有一种得不到页面绝不罢休的姿态。应该说这个函数是在内核运行当中
2、内存管理部分的一个很重要的部分,很简单,没有他你就分配不到任何页面。因为这个函数里分配页面的算法被称为伙伴算法,因此这个机制也被称为伙伴内存机制。但是这个机制不是自始至终都存在的,在内核在初始化阶段,这个伙伴机制所必须的那些页面队列还没有建立,他无法正常工作,而必须引入一个过度内存管理机制,一来是完成初始化时候的内存分配需求,二来是帮助建立伙伴算法所必须的基础性设施,然后自动隐退让伙伴算法登台工作,三来就是划出伙伴算法不允许碰触的内核自己的不动产内存,这些内存常驻系统,只归内存私自占用,谁也分不到,因为伙伴算法根本就看不到这些页面的内存。本文主要就是描述这个过度的临时的内存管理机制,bootm
3、em 机制。这个机制的工作过程是这样的,初始化的时候算出物理内存的最大页面号,比如物理内存为 256*1024*1024=256M 个字节,那么这个最大的页面号就是(256*1024*1024)/4096=65535 ,因为每个页面为 4K 即 4096 字节。所以系统一共有 65535 个页面,每个 4096 字节。Bootmem 机制是用一张位图来管理这些页面的,这个位图是在 init_bootmem_core 函数里面建立的,这个位图建立在那里呢,正好是在内核映像的后面,假如内核映像(其中包括内核的代码和数据所形成的二进制的文件)占用了 0601 个页面,那么这些页面属于基础设施了是不能
4、在动的了,动他就是动了根本了,这 0601 个页面中任何一个不能被分配或覆盖。所以我们的位图只能老老实实的从 602 个页面开始,显而易见的是这个 bootmem 的位图必须能代表这 065534(一共 65535)所有的页面,如果少了那么这部分少了的就相当于浪费掉了,操作系统不能干这种事,如果多了,更麻烦,那就是吹牛了,这个后果更严重,可能你去分配那个页面但是那个页面不存在,那就完蛋了。我们看看 init_bootmem_core 这个函数是怎么干的,核心的只有这么一句memset(bdata-node_bootmem_map, 0xff, mapsize);其中 bdata-node_bo
5、otmem_map 就是我们上面说的那第 602 个页面,只不过这里经过一些转换把他变成一个虚拟地址,因为 CPU 不是按页面来进行操作的,页面只是我们自己算起来方便用的,所以一旦要交给 CPU 干活的时候就要把他搞成虚拟地址,但他们代表的意思都一样的,都是在内核映像的后头。而这个 mapsize 就是我们的位图大小,就是不能大也不能小的那个(当然为了与页面对齐进行 UP 对齐或者 DOWN 对齐的剪修掉的那一点点内存除外) ,这个mapsize 在我们这里的情况计算出来就是 65536/8=8192 个字节,也就是我们这个位图一共占掉了整整 2 个页面 8K 字节,这两个页面分别是 602
6、和 603 号页面。然后上面这条语句把这两个页面的内存都置为 1。置为 1 是什么意思呢,就是代表这 65535 个页面都被占用掉了,一个都不剩了。我们会很困惑,到目前为止只有 0603 页面被内核映像和这个位图占用了,把这 604 个页面都置为1 才对啊。事实上内核的思想与我们正好相反,我们是想一开始都置为 0,表示都空闲,那个被分配了在置为 1。可是这里有个问题这 65535 个页面中本来就有些是不能用的,他们是 ROM 而不是 RAM,你一开始就应该把他置为 1,而且还有其他一些复杂的情况,所以内核反过来,先全置为 1 表示全部占用,然后发现有空闲的在置为 0 就好了。好了我们现在有了一
7、张 8K 的两个页面的位图了,而且他里面全是 1。好下面开始把能用的 RAM 内存给空出来,这个是在函数register_bootmem_low_pages 完成的,这个函数根据 bios 提供的 RAM 分布状况,来进行置 0. 由于 RAM 可能分成好几个段,专门有一个结构来描述这个状况:struct e820map int nr_map;struct e820entry unsigned long long addr; /* start of memory segment */unsigned long long size; /* size of memory segment */uns
8、igned long type; /* type of memory segment */ mapE820MAX;addr 为起始地址,size 为大小,type 为类型,例如这个数组中有一个成员的 tpyes 是 RAM,那就是我们把位图相应位置 0 的。假如 addr 这个物理地址换算成页面号为 100,而 size 的值是 108,那么从 100207 这 108 个页面就是可用的页面,把这些页面在位图里对应的 bit 位置成 0,表示这些页面空闲可用,对应这个结构数组中所有的 RAM 类型的都这么处理,这样就达到了把系统中所有的的可用 RAM 都标出来了的效果,而其他的 ROM 等不可
9、用的内存仍然保留成 1。但是讲到这里,有人可能要提出疑问,内核的映像和以上的这两个页面的位图也是无疑在 RAM 中的,而这些这个 register_bootmem_low_pages 函数不问青红皂白把所有的 RAM 都置为可用,也包括内核的映像和这 8K 的位图,说明这部分内存也可以分配,那不就出问题了吗。是的,你说的没错,所以内核马上要把这部分内存保留起来,通过下面的代码:reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY);顾名思义,这个函数是用来保留内
10、存的,第一个参数是那个地址开始保留,第二个参数为保留的大小。这里的 HIGH_MEMORY 是定义为 1024*1024 为 1M,由于历史原因内核不能从 0 开始,01M 的内存另有他用,所以内核从 1M 开始,这里的 HIGH_MEMORY 作为开始保留的地址其实就是想内核映像的首地址开始保存,那么这个大小是多大呢,start_pfn 是内核映像的结束地址,bootmap_size 是位图大小,在我们这里就是 2 个页面(602,603) ,后面的“+PAGE_SIZE-1”是用来对齐用的,这个大小就包括了从 1M 开始的内核映像和两个位图页面在内的这一段内存,因为内核映像和位图已经被占用
11、了所以就把他们都置为 1。这是 init_bootmem_core 设置了全部为 1 的位图之后的第一次保留。为了更好的体现上面的过程,用几张图帮助理解一下:内核映像两个页面的位图第 1 M 内存这是初始化时内存的基本分布状况以下是这两个位图的页面的变化情况:(1)最开始的时候,无论怎样,两个页面的位图都被置为 1,表示都不可用。1第一个4 K第二个4 K1R A MR A MR O M 或不可用R O M 或不可用(2)经过 register_bootmem_low_pages 进行 RAM 的解放之后的位图情况0第一个4 K第二个4 K0R A MR A MR O M 或不可用R O M
12、或不可用11我们可以看到,RAM 被全部解放了,0 表示可用的内存。(3)经过 reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY);保留了内核映像和两个位图的内存之后的位图情况:0第一个4 K第二个4 K0R A MR A MR O M 或不可用R O M 或不可用1101内核映像和位图的两个页面我们发现内核映像和两个页面的位图的内存的 RAM 部分也被置为 1 了,表示不可再用。好了到目前为止,我们已经成功了实现了实现了 bootmem 位图机制的建立,下
13、面开始大规模的使用这个原理来进行页面的管理和操作。内核先用 reserve_bootmem(0, PAGE_SIZE);这条语句保留了 04096 字节的这 4K 内存,因为这块内存是又特殊用途的。接着在这一阶段内核使用内存都是用这种方式来保留的,位图的的变化情况也都是照着上面的原理来的,大家也都能想清楚,至于内核在这一阶段保留了那些资源,可以自己去看代码,但是原理都是一样的。下面介绍一下重点内容,内核页目录和页表的建立。内核在刚进入保护模式的时候曾经建立过一个临时的只有两个页表项的页表,一共只能映射前 8M的内存,这在当时已经够用了,因为整个的内核映像文件都在 8M 以内,CPU可以映射到任
14、何一个内核函数和数据。而现在是到了为整个物理内存建立映射的时候了。我们知道内核是可以访问任何一个地址的,不论是内核空间的地址还是用户空间的地址。而这些虚拟地址在内核空间都是靠着这个内核页表而进行正确映射的。这些固定的映射的页表是一经建立就不会在释放的,这个页表和物理内存有一个固定的映射,例如你访问 0xC0003000 这个虚拟地址,你就一定会访问到 0x3000 这个物理地址,0xC0003000 去页表里查到的物理地址就始终是固定的 0x3000,有人问,页表不是以映射随机的内存而著称的吗,是的,那是在用户空间,用户空间的页表是需要的时候随机建立的,那不是固定的。每个用户进程都有自己的 3
15、G 的用户空间,这些空间的地址都是小于0xC0000000 的。凡是大于 0xC0000000 的地址表示他是内核地址,统一使用那张固定的内核页表来映射,而用户进程则不是,例如两个进程都要访问0x80000000 这个地址,两个进程都有自己的页表,0x80000000 这个地址在这两个进程页表中映射到的物理地址是肯定不一样的。用户的页表的建立我们以后在讲,这里我们要介绍的是内核的这张固定页表的建立过程,这里需要注意的是,内核的这张固定页表是靠着 bootmem 机制建立起来的,跟伙伴算法没有任何关系,因为这个时候伙伴算法还没建立呢,而用户空间的页表建立跟伙伴算法是有关系的,比如要分配一个页面来
16、做页表,那么用户空间是用alloc_pages,而内核空间的页表的建立是靠着 alloc_bootmem_low_pages 函数来执行的。毫无疑问,alloc_bootmem_low_pages 正是靠着我们上面讲的 bootmem 机制和他建立的那张位图来进行的。pagetable_init 函数完成了初始化阶段内核固定映射页表的建立,在这个函数中大量用到了 alloc_bootmem_low_pages 函数来分配页目录和页表的内存。这里我们需要重点注意一下,这里通过 alloc_bootmem_low_pages 分配的内存和上面的内核映像占有的内存是一样的,分配好之后是常驻内存,不会
17、被在分配,不会换入唤出,成为内核的私有财产。也就是在 bootmem 机制退出建立伙伴算法的时候,把这些内存都扣留下了,压根没把这些内存往伙伴算法的空闲队列中放,伙伴算法从来都不曾控制过这些内存,更别谈去分配和利用他了。在 alloc_bootmem_low_pages 这个函数中他实际调用的是_alloc_bootmem_core 函数,而_alloc_bootmem_core 则是充分利用了上面说的那个位图,先是查看 bit 为 0 的(表示空闲)的页面,然后就把他分配了,分配的过程是这样的,首先根据这个 bit 位的在位图中的序号(其实也就是页面号)乘以 4096(因为一个页面代表着 4
18、096 个字节) ,得到了物理地址,然后在把这个物理地址转换成虚拟地址(加上 0xC0000000)返回就可以了,因为 CPU 是需要虚拟地址来运行的。当然这个 bit 位代表的页面已经被分配掉了,就得把位图中这个位置 1 表示已经不可用了。干完了固定映射页表的建立之后,内核又进行了管理区的建立,与此同时,做了一件非常有影响的事情,就是建立了全局的 page 结构的数组,用来管理物理内存,看下面语句:map_size = (totalpages + 1)*sizeof(struct page);lmem_map = (struct page *) alloc_bootmem_node(pgda
19、t, map_size);map_size 算出了这个数组的大小,由页面的数目乘以 page 结构的大小,页面的数目就是前面算出来的,在我们这里应该是 65535,而 page 结构的大小50 个字节左右,这样一算这套数组大概耗掉了 2M 多的内存,上面的alloc_bootmem_node 最终还是靠_alloc_bootmem_core 来实现的。应该说这个page 数组是系统里非常重要的一项固定资产。当完成了这一切之后,内核认为 bootmem 的任务已经完成了,准备建立伙伴机制,而让 bootmem 退出舞台,这一步实质是由 free_all_bootmem_core 完成了,这个函数
20、都干了什么呢,这个函数里把所有的 bit 位为 0 的页面都挂到管理区的空闲队列中,以后分配的页面都必须也只能从这些空闲队列中来分。现在我们知道那些曾经被置为 1 的那些页面,我们以后就再也都分不到了。总结一下:我们先把所有的页面都置为不可用(0) ,然后把 RAM 都置为可用(1) ,然后把 RAM 已经用掉的置为不可用,如内核映像,位图。然后做了两件比较大的事情,一个是建立了页目录也页表,用来固定映射所有的物理内存,这部分页目录和页表用到的页面也随之被置为不可用(1) ,还有一件大的事情是建立了 page 数组,这些页面也被置为不可用了(1) ,除此之外,还为内核保留了很多的资源,这些已经被保留的都被置为不可用了,然后在这个基础上建立伙伴算法体系,伙伴算法的所有页面都只能从管理区的空闲队列中去分配,而管理区里的这些页面都是在经过 bootmem 分配后没有被保留的也就是 bit 位为 0 的页面,而那些 bit 位为 1 的页面都永远的称为内核的私有财产了。