http://lwn.net/Articles/658081/linux
内核的内存分配要想在大多数状况下工做良好须要不少限制。随着时间推移,这些限制给底层分配代码带来了很大的复杂性。 可是仍然有问题存在,有时候,最好的解决方法是删掉一些复杂性并使用一个更简单的方法。Mel Gorman提的patch已经通过 几轮review,到了一个成熟的阶段;它能够做为一个例子来看看要想在目前的内核工做良好须要什么。算法
在这里有问题的分配器是底层页分配器(伙伴分配器)。它处理的最小内存单位是一页(大多数系统是4KB)。slab分配器 (包括kmalloc())创建在页分配器之上;他们有他们的复杂性,在此不考虑。性能
页分配器是系统中内存的最终来源,若是这个分配器不能知足一个请求,那内存就不能得到。因此为保证在全部状况下都能 得到内存作了不少努力,尤为是不能等待从其余地方回收一些内存的高优先级调用。高阶分配(那些须要多于一页物理上连 续内存的请求)使问题更复杂化;随着时间推移,内存趋于碎片化,难以找到连续的内存。NUMA系统的内存使用平衡增长了 另外一个问题。全部的这些限制必须在分配器自己在不拖慢系统的状况下被处理。解决这个问题须要引入不少复杂的代码,可 怕的经验算法,还有其余;因此内存管理变更很难进入主线一点都不奇怪。spa
zone cache .net
页分配器把物理内存分红“zone”,每一个zone跟有特定特性的内存关联。ZONE_DMA包含在地址区域底部被紧要设备使用的内存, 而ZONE_NORMAL可能包含系统中大多数内存。32位系统有个包含不能直接映射到内核地址空间内存的ZONE_HIGHMEM。每一个NUMA 节点也有他们本身的zone集。基于分配请求的特性,页分配器按照特定优先级顺序搜索可用的zone。对于好奇的人,/proc/zoneinfo 提供了系统中使用的zone的不少信息。设计
检查一个zone是否有内存知足请求须要比你想象的要多的工做。除了最高优先级请求,一个zone不该该低于特定的watermarks, 一个zone的可用内存与watermark做比较须要大量计算。若是zone回收特性被使能了,检查一个将要空的zone会让内存管理系统 回收zone的内存。由于这个和其余的缘由,zonelist cache在2006年加入到2.6.20。这个cache用来记住最近被发现快满的zone, 可让分配请求避免检查满的zone。orm
zonelist cache的做用随着时间被减弱了,Mel Gorman的patch经过下降watermark检查的代价进一步减弱了它。zone回收被指出是 不少负载的性能问题,如今默认是关掉的。可是最大的问题是,若是一个zone不能知足一个高阶分配,它就被标记为满即便有可用 的单个页。接下来的单页分配会跳过这个zone,即便它有足够的能力知足这些分配。这会引发分配没必要要的在远端NUMA节点执行, 让性能更坏。进程
Mel指出这个问题能够经过增长复杂性解决,可是zonelist cache的好处是有疑问的,因此删掉它会更好。patch删掉了近300行复杂 的内存管理代码并提升了一些benchmark。zone老是被检查也有其余问题;很明显最值得注意的是检查原本避免的zone致使更多的直 接回收工做(执行分配的进程尝试回收内存)。内存
原子高阶预留开发
在zone里,内存被分为page block,每一个能够用描述block怎么被分配的migration type标记。目前的内核其中一个type是MIGRATE_RESERVE ,它标记的内存不能被分配除非请求分配而后会失败。因为标记的是一个物理连续的block区域,因此这个策略的影响是在系统中维护一个 最小数量的高阶页。这也意味着高阶请求(合理的)能够被知足即便内存被碎片化了。
Mel在2007年的2.6.24开发周期中加入了migration reserve。预留改善了当时的情况,可是最终它依赖于多年前在内核实现的最小watermark 的特性。预留不会主动保留高阶页,它只是简单地阻止在特定内存区域的请求除非没有其它选项,它这样作是想让这个区域保持连续。 预留的方法也早于如今的在避免碎片化和在碎片化发生的时候执行紧缩方面作得更好的内存管理代码。Mel的patch代表这种预留 已通过时,应该删掉它。
可是为高阶分配预留内存块仍然有价值,碎片化在当前的内核里还是个问题。因此Mel另外一个patch为了这个用不一样的方法建立了一个 新的MIGRATE_HIGHATOMIC预留。一开始,这个预留不包含任何页块。若是一个高阶分配在不拆分一整个页块的状况下不能被知足,这 个页块就会被标记为高阶原子预留的一部分,此后,只有高阶分配(只有高优先级的)能够用这个页块来知足。
内核会限制这个预留的大小约为内存的1%,因此它不会变的过大。页块留在预留里直到内存压力达到单个页分配会失败的程度,在这种状况下 内核会从预留里取出一个页块来知足请求。最终高阶页面预留会更灵活,根据目前的负载来增长或减小。因为高阶页面的需求在不一样 系统以前变化很大,根据实际运行调节预留是合理的,结果是更灵活的分配和更高可靠的访问高阶页面。
可是以避免未来内核开发者认为他们对高阶分配能够更放松,Mel提醒说,预留大小受限致使的一个结果是,为长期的高阶分配去访问 预留而投机的滥用原子分配的调用很很快失败。可是他没有给出指示他认为的这些调用是谁。须要记住这种预留的另外一个潜在的缺陷是 :因为直到执行一个高阶分配才有页块进入预留,预留可能系统运行了很长时间仍是空的。到那时(系统运行很长时间后),内存可能 碎片化很严重了而不能分给预留。若是这种状况在实际使用中出现,能够经过在启动时先把最小数量的内存放到预留来解决。
高阶预留也让删除高阶页的watermark变得可行。这些watermark为了保证每一个zone对每一个order都有最少可用的页,分配器会让致使 低于watermark的分配失败,除了最高优先级的分配。这些watermark实现起来相对困难,也可能引发正常优先级分配在即便有足够适配 页的状况下失败。打上Mel的补丁后,代码仍强制单页的watermark,可是对于高阶分配,它仅仅检查一个适配页可用,计算高阶预留来 保证页面为高优先级分配可用。
Flag day
内核中的内存分配请求老是分为一组GFP标志,这些标志描述了为知足请求什么能够作很什么不能作。最经常使用的标志是GFP_ATOMIC和 GFP_KERNEL,但实际上他们基于底层的标志。GFP_ATOMIC是最高优先级请求,它可使用预留并不容许睡眠。GFP_ATOMIC被定义 为一个位__GFP_HIGH,标记为一个高优先级请求。GFP_KERNEL不能使用预留但能够睡眠,它是__GFP_WAIT(可能睡眠), __GFP_IO(可能启动底层IO),__GFP_FS(可能调用文件系统操做)的组合。整个标志集合很大,能够再include/linux/gfp.h找到。
有趣的是,最高优先级请求不是标记为__GFP_HIGH,而是经过没有__GFP_WAIT标记。带有__GFP_HIGH标记的请求可能使内存低于watermark, 可是只有非__GFP_WAIT请求能够访问原子预留。这种机制在当前内核中工做的并很差,由于不少子系统可能发起不想等待的分配(常常是 由于他们有分配失败的回调机制),可是这些子系统不须要访问最后的预留。可是,由于没有__GFP_WAIT,这些代码老是会访问这些 预留。
这个问题,同时想更明确的控制内存分配请求被知足的方式,让Mel从新设计GFP标志集。他增长了一些新的标志:
用这些标志,代码能够表示绝对不能睡眠和不想睡眠之间的区别。一个请求的“必须成功”的特色从“不睡眠”中区分开来,减小了 没必要要的访问原子预留的状况。对GFP_ATOMIC和GFP_KERNEL的用户来讲没有改变,但Mel的patch针对不少使用底层GFP标志的调用场合 作了改动。
总的来讲,这个patch涉及到101个文件,删掉了240行代码。幸运的是,不少核心的内存管理算法被简化的同事提升了性能并让系统 更可靠。Mel强烈的基于benchmark的方法为这些工做增长了信心,但这对于一个复杂的内核子系统来讲是很大的改动,因此这些patch 会通过不少review。看起来这个进程已经接近尾声,可能会在下一个或两个开发周期进入主线。