轉自:https://zhuanlan.zhihu.com/p/73539328
前面的文章提到“什么情況下觸發direct reclaim,什么情況下又會觸發kswapd,是由內存的watermark決定的”,那這個"watermark"到底是如何發揮作用的呢?
Kswapd與Watermark
Linux中物理內存的每個zone都有自己獨立的min, low和high三個檔位的watermark值,在代碼中以struct zone中的_watermark[NR_WMARK]來表示。
在進行內存分配的時候,如果分配器(比如buddy allocator)發現當前空余內存的值低於"low"但高於"min",說明現在內存面臨一定的壓力,那么在此次內存分配完成后,kswapd將被喚醒,以執行內存回收操作。在這種情況下,內存分配雖然會觸發內存回收,但不存在被內存回收所阻塞的問題,兩者的執行關系是異步的。
這里所說的"空余內存"其實是一個zone總的空余內存減去其lowmem_reserve的值。對於kswapd來說,要回收多少內存才算完成任務呢?只要把空余內存的大小恢復到"high"對應的watermark值就可以了,當然,這取決於當前空余內存和"high"值之間的差距,差距越大,需要回收的內存也就越多。"low"可以被認為是一個警戒水位線,而"high"則是一個安全的水位線。
如果內存分配器發現空余內存的值低於了"min",說明現在內存嚴重不足。這里要分兩種情況來討論,一種是默認的操作,此時分配器將同步等待內存回收完成,再進行內存分配,也就是direct reclaim。還有一種特殊情況,如果內存分配的請求是帶了PF_MEMALLOC標志位的,並且現在空余內存的大小可以滿足本次內存分配的需求,那么也將是先分配,再回收。
使用PF_MEMALLOC("PF"表示per-process flag)相當於是忽略了watermark,因此它對應的內存分配的標志是ALLOC_NO_WATERMARK。能夠獲取"min"值以下的內存,也意味着該process有動用幾乎所有內存的權利,因此它也對應GFP的標志__GFP_MEMALLOC。
if (gfp_mask & __GFP_MEMALLOC) return ALLOC_NO_WATERMARKS; if (alloc_flags & ALLOC_NO_WATERMARKS) set_page_pfmemalloc(page);
那誰有這樣的權利,可以在內存嚴重短缺的時候,不等待回收而強行分配內存呢?其中的一個人物就是kswapd啦,因為kswapd本身就是負責回收內存的,它只需要占用一小部分內存支撐其正常運行(就像啟動資金一樣),就可以去回收更多的內存(賺更多的錢回來)。
雖然kswapd是在"low"到"min"的這段區間被喚醒加入調度隊列的,但當它真正執行的時候,空余內存的值可能已經掉到"min"以下了。可見,"min"值存在的一個意義是保證像kswapd這樣的特殊任務能夠在需要的時候立刻獲得所需內存。
Watermark的取值
那么這三個watermark值的大小又是如何確定的呢?ZONE_HIGHMEM的watermark值比較特殊,但因為現在64位系統已經不再使用ZONE_HIGHMEM了,為了簡化討論,以下將以不含ZONE_HIGHMEM,且只有一個node的64位系統為例進行講解。
在這種系統中,總的"min"值約等於所有zones可用內存的總和乘以16再開平方的大小,可通過"/proc/sys/vm/min_free_kbytes"查看和修改。假設可用內存的大小是4GiB,那么其對應的"min"值就是8MiB ( )。
int __meminit init_per_zone_wmark_min(void) { min_free_kbytes = int_sqrt(lowmem_kbytes * 16); if (min_free_kbytes < 128) min_free_kbytes = 128; if (min_free_kbytes > 65536) min_free_kbytes = 65536; ... }
這里的"min"值有個下限和上限,就是最小不能低於128KiB,最大不能超過65536KiB。在實際應用中,通常建議為不低於1024KiB。
得到總的"min"值后,我們就可以根據各個zone在總內存中的占比,通過do_div()計算出它們各自的"min"值。假設總的"min"值是8MiB,有ZONE_DMA和ZONE_NORMAL兩個zones,大小分別是128MiB和896MiB,那么ZONE_DMA和ZONE_NORMAL的"min"值就分別是1MiB和7MiB。
void __setup_per_zone_wmarks(void) { unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10); for_each_zone(zone) { tmp = (u64)pages_min * zone->managed_pages; do_div(tmp, lowmem_pages); zone->watermark[WMARK_MIN] = tmp; zone->watermark[WMARK_LOW] = min_wmark_pages(zone) + (tmp >> 2); zone->watermark[WMARK_HIGH] = min_wmark_pages(zone) + (tmp >> 1); ... }
一個zone的"low"和"high"的值都是根據它的"min"值算出來的,"low"比"min"的值大1/4左右,"high"比"min"的值大1/2左右,三者的比例關系大致是4:5:6。
使用"cat /proc/zoneinfo"可以查看這三個值的大小(注意這里是以page為單位的):
你可以把"/proc/zoneinfo"中所有zones的"min"值加起來乘以4(如果page size是4KiB的話),看一下是不是基本等於"/proc/sys/vm"中的"min_free_kbytes"的值。
Watermark的調節
為了盡量避免出現direct reclaim,我們需要空余內存的大小一直保持在"min"值之上。在網絡收發的時候,數據量可能突然增大,需要臨時申請大量的內存,這種場景被稱為"burst allocation"。此時kswapd回收內存的速度可能趕不上內存分配的速度,造成direct reclaim被觸發,影響系統性能。
在內存分配時,只有"low"與"min"之間之間的這段區域才是kswapd的活動空間,低於了"min"會觸發direct reclaim,高於了"low"又不會喚醒kswapd,而Linux中默認的"low"與"min"之間的差值確實顯得小了點。
為此,Android的設計者在Linux的內存watermark的基礎上,增加了一個"extra_free_kbytes"的變量,這個"extra"是額外加在"low"與"min"之間的,它在保持"min"值不變的情況下,讓"low"值有所增大。假設你的"burst allocation"需要100MiB(100*1024KiB)的空間,那么你就可以把"extra_free_kbytes"的值設為102400。
於是,設置各個zone的watermark的代碼變成了這樣:
void __setup_per_zone_wmarks(void) { unsigned long pages_min = min_free_kbytes >> (PAGE_SHIFT - 10); unsigned long pages_low = extra_free_kbytes >> (PAGE_SHIFT - 10); for_each_zone(zone) { min = (u64)pages_min * zone->managed_pages; do_div(min, lowmem_pages); low = (u64)pages_low * zone->managed_pages; do_div(low, vm_total_pages); zone->watermark[WMARK_MIN] = min; zone->watermark[WMARK_LOW] = min + low + (min >> 2); zone->watermark[WMARK_HIGH] = min + low + (min >> 1) ... }
和Linux中對應的代碼相比,主要就是多了這樣一個"extra_free_kbytes",該參數可通過設置"/proc/sys/vm/extra_free_kbytes"來調節。關於Andoird這個patch的詳細信息,請參考這個提交記錄。
在Android的機器(基於4.4的Linux內核)上用"cat /proc/zoneinfo"查看一下:
可見,這里"low"和"high"已經不再是"min"值的5/4和6/4了,而是多出了一大截。想要知道調節有沒有取得預期的效果,可以通過"/proc/vmstat"中的"pageoutrun"和"allocstall"來查看,兩者分別代表了kswapd和direct reclaim啟動的次數。
在Linux內核4.6版本中,誕生了一種新的調節watermark的方式。具體做法是引入一個叫做"watermark_scale_factor"的系數,其默認值為10,對應內存占比0.1%(10/10000),可通過"/proc/ sys/vm/watermark_scale_factor"設置,最大為1000。當它的值被設定為1000時,意味着"low"與"min"之間的差值,以及"high"與"low"之間的差值都將是內存大小的10%(1000/10000)。
tmp = max_t(u64, tmp >> 2, mult_frac(zone_managed_pages(zone), watermark_scale_factor, 10000)); zone->_watermark[WMARK_LOW] = min_wmark_pages(zone) + tmp; zone->_watermark[WMARK_HIGH] = min_wmark_pages(zone) + tmp * 2;
關於這個patch的詳細信息,請參考這個提交記錄。前面講到的"extra_free_kbytes"的方式只增大了"low"與"min"之間的差值,而"watermark_scale_factor"的方式同時增大了"low"與"min"之間,以及"high"與"low"之間的差值。現在的Android代碼已經合並了4.6內核的這個改動,不再單獨提供通過"extra_free_kbytes"來調節watermark的方式了。
參考:
https://lwn.net/Articles/422291/
Zoned Allocator -3- (Watermark)
原創文章,轉載請注明出處。