linux内存管理系统后期的内核对zonelist的简化

struct pglist_data {
    struct zone node_zones[MAX_NR_ZONES];
    struct zonelist node_zonelists[GFP_ZONETYPES];
...
}
GFP_ZONETYPES是一个宏,在2.6.8的时候它如下定义:
#define GFP_ZONEMASK    0x07
/* #define GFP_ZONETYPES       (GFP_ZONEMASK + 1) */      
#define GFP_ZONETYPES  ((GFP_ZONEMASK + 1) / 2 + 1)
也就是说每一个numa的node拥有算了一下5条zone链表,就这还算比较少了。
可是在高一点版本的内核中,这个本来已经很低的zonelists数量变成了2(支持numa)或者1(不支持numa),也许很多人会说,通过GFP标志已经无法控制在哪个zone中以及之下分配内存了,然而看一下get_page_from_freelist这个函数多了很多参数,其中一个high_zoneidx所起到的作用就是原来那么多zonelist的作用,由此可见,高版本的内核丝毫没有降低功能,倒是少维护很多链表。以下首先说一下早期的内核中的多条zonelist链表的起因以及为何那么设计。
     在早期的内核中,每一个node拥有好几条zonelist,每一条代表一个起始zone开始的以及其下的所有的zone,但是这只是实际上的做法,而理论上这个zonelist的数量会更多,这些zonelist代表了一个优先级序列,说明了内存分配在zone中尝试的顺序,在实现中,linux是用位掩码来实现的。如果我们有三个zone,那么就应该用3位掩码中的每一个位来表示不同的zone掩码,设如下:
0x01表示dma
0x02表示normal
0x04表示highmem
这样3个位就有8*2...个顺序,分别是(o为空):
1.o;
2.dma;
3.normal->o;
4.normal->dma;
5.highmem->o;
6.highmem->o->dma;
7.highmem->normal->o;
8.highmem->normal->dma;
9-16.前面8个方向反过来。
17-xx.两两反向,保持一个正向...
可是为何内核仅仅保留了3个左右的顺序呢?首先是三个原则在起作用,第一个就是一致的顺序管理起来更高效而且更不容易冲突,类似单行道的作用,虽然灵活性不够!第二个原则就是内核的内存管理是一个管理机构,但凡内核的管理机构,采取的原则都是平等至上的!第三个原则就是连续性管理原则,按照一定的顺序依次编排,这个顺序一般都是从易到难,从受影响最小的地方开始,不到万不得已不会惊动其它。有了这三个原则的话,首先我们看一下为何没有反向顺序的zonelist,如果有的话就增加了管理负担,因为自动内存置换程序(kswapd)和手动置换程序(try_to_free...)就都要在两个方向进行扫描和管理,由于存在多条路径扫描和分配,这就很不容易了解到各个zone的内存使用情况,从而不知道要将扫描主力放在哪个zone。那么为何不存在跳跃的zonelist,比如跳过normal而仅仅在highmem和dma中分配,这是因为之所以存在一个zonelist而不是一个zone是因为在该zone中分配失败之后有一个后备的可分配zone,由于分配使用的zonelist使用的顺序必然是从容易分配的zone到难分配的zone排列的,那么dma中分配内存的代价会比normal中分配的代价更大。在linux操作系统的内存管理中,主要就是管理内核内存(包括驱动的内存)和进程内存,在大内存的机器上,大量的物理内存无法一一映射进内核地址空间的前HIGH兆,因此实际上对于大多数内核内存管理来讲,这些大量的内存对之意义不大,因此它们更多的被用于进程内存,进程内存是可以随意映射进自己的地址空间的,不要求映射的方式--比如线性映射,也不要求连续性。因此进程内存优先从highmem这个zone中分配,如果不行的话,则最好先在normal中看看,然后再往dma走。对于内核内存管理而言,在系统初始化的时候,一些核心的数据和代码已经映射完毕了,需要内存的大户都是驱动或者模块或者就是诸如进程管理和网络协议栈的动态部分,比如新申请了一个task_t结构体,或者新分配了一个sk_buff结构体等等,这些内存一般从normal区开始分配,这是因为使用normal区的内存可以线性映射进系统内核,操作起来更高效。由于没有谁拥有特权,所以大家都遵守一样的原则,从它所可以触及的最高zone(gfp_flags决定)依次尝试到最低的zone,如果不行则手工调用try_to_free...从该zonelist的开始zone开始释放一些内存,这样做真的是很简单很高效!内核之所以不通知用户进程和驱动就直接在某个zone为之分配了内存是因为zone是操作系统内存管理模块最低层的概念,进程或者驱动甚至都不应该知道有zone的存在,因此内核才可以采取遵循以上三个原则的策略将物理内存分为若干个zone,然后按照从高到低的顺序依次尝试分配内存,直到成功。
     后期的内核将上述机制的实现改变了,每一个pglist_data在没有numa的情况下只有一条zonelist,按照上述的三个原则,其实没有必要搞那么多链表的,由于不存在跳跃,不存在逆向,因此只需要一个链表和一个上限值即可,每次分配内存的时候从这个上限值代表的zone往下开始尝试即可,这样就可以省去一些空间和管理费用,将事先设置好N条zonelist转为直到实际上分配内存的时候再确定上限:
struct page *__alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order,
            struct zonelist *zonelist, ...)
{ //参数中的zonelist其实就是那唯一的一条zonelist(没有numa的情况下)
    enum zone_type high_zoneidx = gfp_zone(gfp_mask);
...
}
如此一来,high_zoneidx这个变量就可以作为一个限制值,在get_page_from_freelist的时候设置一个阀值,虽然__alloc_pages_nodemask函数增加了一些分量,但是时间一点也不损耗多少,取消了几个链表,运行时增加了几个指令周期,这也许(很可能)是值得的。

你可能感兴趣的:(linux内存管理系统后期的内核对zonelist的简化)