在伙伴算法之前的内存管理机制

本文的所描述的内核版本是linux-2.4.20
我们知道在linux内核中,获取物理页面是靠着alloc_pages这个函数来实现的,这个函数会尽它的一切努力满足页面的分配需求,如果free页面的队列充足,那么就直接分配一个然后返回了,很简单。但是如果不足,那就要洗洗刷刷把不干净的页面转换成干净的,干净页面多了也可以满足分配的需求了。这个函数从某种意思上说非常以及极其的简单,那是在页面充足的情况下。系统里的页面越是缺乏,那么这个函数就越显得复杂和面目可憎,挖空心思的洗洗涮涮,克服一切困难,有一种得不到页面绝不罢休的姿态。
应该说这个函数是在内核运行当中内存管理部分的一个很重要的部分,很简单,没有他你就分配不到任何页面。因为这个函数里分配页面的算法被称为伙伴算法,因此这个机制也被称为伙伴内存机制。
但是这个机制不是自始至终都存在的,在内核在初始化阶段,这个伙伴机制所必须的那些页面队列还没有建立,他无法正常工作,而必须引入一个过度内存管理机制,一来是完成初始化时候的内存分配需求,二来是帮助建立伙伴算法所必须的基础性设施,然后自动隐退让伙伴算法登台工作,三来就是划出伙伴算法不允许碰触的内核自己的不动产内存,这些内存常驻系统,只归内存私自占用,谁也分不到,因为伙伴算法根本就看不到这些页面的内存。
本文主要就是描述这个过度的临时的内存管理机制,bootmem机制。这个机制的工作过程是这样的,初始化的时候算出物理内存的最大页面号,比如物理内存为256*1024*1024=256M个字节,那么这个最大的页面号就是(256*1024*1024)/4096=65535,因为每个页面为4K即4096字节。所以系统一共有65535个页面,每个4096字节。Bootmem机制是用一张位图来管理这些页面的,这个位图是在init_bootmem_core函数里面建立的,这个位图建立在那里呢,正好是在内核映像的后面,假如内核映像(其中包括内核的代码和数据所形成的二进制的文件)占用了0~601个页面,那么这些页面属于基础设施了是不能在动的了,动他就是动了根本了,这0~601个页面中任何一个不能被分配或覆盖。所以我们的位图只能老老实实的从602个页面开始,显而易见的是这个bootmem的位图必须能代表这0~65534(一共65535)所有的页面,如果少了那么这部分少了的就相当于浪费掉了,操作系统不能干这种事,如果多了,更麻烦,那就是吹牛了,这个后果更严重,可能你去分配那个页面但是那个页面不存在,那就完蛋了。我们看看init_bootmem_core这个函数是怎么干的,核心的只有这么一句
memset(bdata->node_bootmem_map, 0xff, mapsize);
其中bdata->node_bootmem_map就是我们上面说的那第602个页面,只不过这里经过一些转换把他变成一个虚拟地址,因为CPU不是按页面来进行操作的,页面只是我们自己算起来方便用的,所以一旦要交给CPU干活的时候就要把他搞成虚拟地址,但他们代表的意思都一样的,都是在内核映像的后头。而这个mapsize就是我们的位图大小,就是不能大也不能小的那个(当然为了与页面对齐进行UP对齐或者DOWN对齐的剪修掉的那一点点内存除外),这个mapsize在我们这里的情况计算出来就是65536/8=8192个字节,也就是我们这个位图一共占掉了整整2个页面8K字节,这两个页面分别是602和603号页面。然后上面这条语句把这两个页面的内存都置为1。置为1是什么意思呢,就是代表这65535个页面都被占用掉了,一个都不剩了。我们会很困惑,到目前为止只有0~603页面被内核映像和这个位图占用了,把这604个页面都置为1才对啊。事实上内核的思想与我们正好相反,我们是想一开始都置为0,表示都空闲,那个被分配了在置为1。可是这里有个问题这65535个页面中本来就有些是不能用的,他们是ROM而不是RAM,你一开始就应该把他置为1,而且还有其他一些复杂的情况,所以内核反过来,先全置为1表示全部占用,然后发现有空闲的在置为0就好了。
好了我们现在有了一张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 / unsigned long type; / type of memory segment */ } map[E820MAX]; };
addr为起始地址,size为大小,type为类型,例如这个数组中有一个成员的tpyes是RAM,那就是我们把位图相应位置0的。假如addr这个物理地址换算成页面号为100,而size的值是108,那么从100~207这108个页面就是可用的页面,把这些页面在位图里对应的bit位置成0,表示这些页面空闲可用,对应这个结构数组中所有的RAM类型的都这么处理,这样就达到了把系统中所有的的可用RAM都标出来了的效果,而其他的ROM等不可用的内存仍然保留成1。
但是讲到这里,有人可能要提出疑问,内核的映像和以上的这两个页面的位图也是无疑在RAM中的,而这些这个register_bootmem_low_pages函数不问青红皂白把所有的RAM都置为可用,也包括内核的映像和这8K的位图,说明这部分内存也可以分配,那不就出问题了吗。是的,你说的没错,所以内核马上要把这部分内存保留起来,通过下面的代码:
reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +
bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));
顾名思义,这个函数是用来保留内存的,第一个参数是那个地址开始保留,第二个参数为保留的大小。这里的HIGH_MEMORY是定义为1024*1024为1M,由于历史原因内核不能从0开始,0~1M的内存另有他用,所以内核从1M开始,这里的HIGH_MEMORY作为开始保留的地址其实就是想内核映像的首地址开始保存,那么这个大小是多大呢,start_pfn是内核映像的结束地址,bootmap_size是位图大小,在我们这里就是2个页面(602,603),后面的“+PAGE_SIZE-1”是用来对齐用的,这个大小就包括了从1M开始的内核映像和两个位图页面在内的这一段内存,因为内核映像和位图已经被占用了所以就把他们都置为1。这是init_bootmem_core设置了全部为1的位图之后的第一次保留。
我们发现内核映像和两个页面的位图的内存的RAM部分也被置为1了,表示不可再用。

好了到目前为止,我们已经成功了实现了实现了bootmem位图机制的建立,下面开始大规模的使用这个原理来进行页面的管理和操作。
内核先用reserve_bootmem(0, PAGE_SIZE);这条语句保留了0~4096字节的这4K内存,因为这块内存是又特殊用途的。接着在这一阶段内核使用内存都是用这种方式来保留的,位图的的变化情况也都是照着上面的原理来的,大家也都能想清楚,至于内核在这一阶段保留了那些资源,可以自己去看代码,但是原理都是一样的。

下面介绍一下重点内容,内核页目录和页表的建立。内核在刚进入保护模式的时候曾经建立过一个临时的只有两个页表项的页表,一共只能映射前8M的内存,这在当时已经够用了,因为整个的内核映像文件都在8M以内,CPU可以映射到任何一个内核函数和数据。而现在是到了为整个物理内存建立映射的时候了。我们知道内核是可以访问任何一个地址的,不论是内核空间的地址还是用户空间的地址。而这些虚拟地址在内核空间都是靠着这个内核页表而进行正确映射的。这些固定的映射的页表是一经建立就不会在释放的,这个页表和物理内存有一个固定的映射,例如你访问0xC0003000这个虚拟地址,你就一定会访问到0x3000这个物理地址,0xC0003000去页表里查到的物理地址就始终是固定的0x3000,有人问,页表不是以映射随机的内存而著称的吗,是的,那是在用户空间,用户空间的页表是需要的时候随机建立的,那不是固定的。每个用户进程都有自己的3G的用户空间,这些空间的地址都是小于0xC0000000的。凡是大于0xC0000000的地址表示他是内核地址,统一使用那张固定的内核页表来映射,而用户进程则不是,例如两个进程都要访问0x80000000这个地址,两个进程都有自己的页表,0x80000000这个地址在这两个进程页表中映射到的物理地址是肯定不一样的。用户的页表的建立我们以后在讲,这里我们要介绍的是内核的这张固定页表的建立过程,这里需要注意的是,内核的这张固定页表是靠着bootmem机制建立起来的,跟伙伴算法没有任何关系,因为这个时候伙伴算法还没建立呢,而用户空间的页表建立跟伙伴算法是有关系的,比如要分配一个页面来做页表,那么用户空间是用alloc_pages,而内核空间的页表的建立是靠着alloc_bootmem_low_pages函数来执行的。
毫无疑问,alloc_bootmem_low_pages正是靠着我们上面讲的bootmem机制和他建立的那张位图来进行的。
pagetable_init函数完成了初始化阶段内核固定映射页表的建立,在这个函数中大量用到了alloc_bootmem_low_pages函数来分配页目录和页表的内存。这里我们需要重点注意一下,这里通过alloc_bootmem_low_pages分配的内存和上面的内核映像占有的内存是一样的,分配好之后是常驻内存,不会被在分配,不会换入唤出,成为内核的私有财产。也就是在bootmem机制退出建立伙伴算法的时候,把这些内存都扣留下了,压根没把这些内存往伙伴算法的空闲队列中放,伙伴算法从来都不曾控制过这些内存,更别谈去分配和利用他了。
在alloc_bootmem_low_pages这个函数中他实际调用的是__alloc_bootmem_core函数,而__alloc_bootmem_core则是充分利用了上面说的那个位图,先是查看bit为0的(表示空闲)的页面,然后就把他分配了,分配的过程是这样的,首先根据这个bit位的在位图中的序号(其实也就是页面号)乘以4096(因为一个页面代表着4096个字节),得到了物理地址,然后在把这个物理地址转换成虚拟地址(加上0xC0000000)返回就可以了,因为CPU是需要虚拟地址来运行的。当然这个bit位代表的页面已经被分配掉了,就得把位图

中这个位置1表示已经不可用了。
干完了固定映射页表的建立之后,内核又进行了管理区的建立,与此同时,做了一件非常有影响的事情,就是建立了全局的page结构的数组,用来管理物理内存,看下面语句:
map_size = (totalpages + 1)*sizeof(struct page);
lmem_map = (struct page *) alloc_bootmem_node(pgdat, map_size); map_size算出了这个数组的大小,由页面的数目乘以page结构的大小,页面的数目就是前面算出来的,在我们这里应该是65535,而page结构的大小50个字节左右,这样一算这套数组大概耗掉了2M多的内存,上面的alloc_bootmem_node最终还是靠__alloc_bootmem_core来实现的。应该说这个page数组是系统里非常重要的一项固定资产。
当完成了这一切之后,内核认为bootmem的任务已经完成了,准备建立伙伴机制,而让bootmem退出舞台,这一步实质是由free_all_bootmem_core完成了,这个函数都干了什么呢,这个函数里把所有的bit位为0的页面都挂到管理区的空闲队列中,以后分配的页面都必须也只能从这些空闲队列中来分。现在我们知道那些曾经被置为1的那些页面,我们以后就再也都分不到了。 
总结一下:我们先把所有的页面都置为不可用(0),然后把RAM都置为可用(1),然后把RAM已经用掉的置为不可用,如内核映像,位图。然后做了两件比较大的事情,一个是建立了页目录也页表,用来固定映射所有的物理内存,这部分页目录和页表用到的页面也随之被置为不可用(1),还有一件大的事情是建立了page数组,这些页面也被置为不可用了(1),除此之外,还为内核保留了很多的资源,这些已经被保留的都被置为不可用了,然后在这个基础上建立伙伴算法体系,伙伴算法的所有页面都只能从管理区的空闲队列中去分配,而管理区里的这些页面都是在经过bootmem分配后没有被保留的也就是bit位为0的页面,而那些bit位为1的页面都永远的称为内核的私有财产了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值