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。看起來這個進程已經接近尾聲,可能會在下一個或兩個開發週期進入主線。