記憶體頁不足導致程式啟動失敗:page allocation failure
現象
之前一直穩定運行了很久的核心ko模組突然功能失靈,通過dmesg命令檢視核心資訊,發現該模組提示記憶體頁分配失敗,如下圖所示
當時看到 "Failed to allocate memory for ip_entry" 字樣,第一反應就是記憶體不足,直接用命令free -h
命令檢視系統記憶體
從圖中看到空閒的記憶體有890M,按道理,空閒記憶體應該是夠用的,ip_entry這個資料結構怎麼也不至於用掉890M以上的記憶體。於是再看堆疊資訊,看到一個關鍵資訊:page allocation failure,這個資訊表示系統無法分配高階記憶體(所謂的高階記憶體,指的是大塊的連續實體記憶體,記憶體分配原理可檢視本文下面的“記憶體分配演算法”),使用命令檢視記憶體頁的分配情況:cat /proc/buddyinfo
可以看到記憶體的碎片化情況很嚴重,存在大量的低階記憶體頁,但缺少64KB以上的高階記憶體頁(紅框表示64KB以上的記憶體頁數量都為0)
分析ip_entry
既然系統缺少64KB以上的記憶體頁,那麼是否說明ip_entry這個資料結構要大於64KB呢,於是寫程式用sizeof函式來測試這個資料結構,因為這個資料而機構用到了核心的函式,所以要和系統的原始碼一起編譯成ko檔案,不能直接在使用者態呼叫sizeof函式。
- 編寫Hello.c
#include <linux/rcupdate.h> #include <linux/rbtree.h> #include <linux/init.h> #include <linux/module.h> #include <asm/thread_info.h> #include <linux/sched.h> struct interval_tree_node { struct rb_node rb; unsigned long start; unsigned long last; unsigned long __subtree_last; }; struct ip_entry { struct rcu_head rhead; struct ip_entry *next; struct ip_entry **pprev; struct interval_tree_node node; int type; __be32 saddr; __be32 mask; ktime_t timestamp; u64 nr_hits[NR_CPUS]; }; static int test_init(void) { printk("---Insmod---"); return 0; } static void test_exit(void) { struct ip_entry e; int c; printk("sizeof int: %d\n", sizeof(c)); printk("sizeof ip_entry: %d\n", sizeof(e)); printk("---Rmmod---"); } module_init(test_init); module_exit(test_exit); MODULE_LICENSE("GPL");
- 編寫Makefile
CONFIG_MODULE_SIG=n
obj-m:=Hello.o
KDIR:=/lib/modules/$(shell uname -r)/build
PWD:=$(shell pwd)
default:
$(MAKE) -C $(KDIR) M=$(PWD) modules
- 編譯:執行
make
命令(注意,在ubuntu20系統上能編譯成功,但是在往核心插入模組時會提示錯誤:insmod: ERROR: could not insert module Hello.ko: Invalid module format,所以只能用ubuntu16來編譯) - 插入核心模組:執行
insmod Hello.ko
rmmod Hello
)
從上圖可以看到,在64位的系統上,int的大小為4Byte,ip_entry的大小為65640Byte,摺合為64.1KB,而在本系統中,剛好沒有了大於等於64KB的連續記憶體頁,所以導致了記憶體頁分配失敗。
解決方法
釋放記憶體
- 釋放頁快取:
echo 1 > /proc/sys/vm/drop_caches
- 釋放目錄和索引節點快取:
echo 2 > /proc/sys/vm/drop_caches
- 同時釋放頁、目錄、索引節點快取:
echo 3 > /proc/sys/vm/drop_caches
上述的操作是無害的,因為只會釋放完全沒有使用的記憶體物件,髒物件將繼續被使用直到他們被寫入磁碟中,所以記憶體中的髒物件並不會被釋放。如果如果重複echo 3 > /proc/sys/vm/drop_caches
不能再次釋放快取,可以先嚐試echo 0 > /proc/sys/vm/drop_caches
然後再執行echo 3 > /proc/sys/vm/drop_caches
記憶體壓縮
當上面釋放的記憶體也沒有足夠的高階記憶體時,可以通過命令:echo 1 > /proc/sys/vm/compact_memory
進行記憶體壓縮,但這個步驟比較消耗CPU
可以看到經過記憶體壓縮後,釋放了大量的高階記憶體
Linux記憶體
夥伴系統
Linux系統使用了一個名為夥伴系統(buddy system)的記憶體分配演算法,將所有的空閒頁表(一個頁表的大小為4K)分別連結到包含了11個元素的陣列中,陣列中的每個元素將大小相同的連續頁表組成一個連結串列,頁表的數量為:1,2,4,8,16,32,64,128,256,512,1024,所一次性可以分配的最大連續記憶體為1024個連續的4k頁表,即4MB的記憶體。假設你想申請一個包括256個頁表的記憶體,系統會首先查詢陣列中的第9個連結串列(即大小為256的連結串列),如果該連結串列為空,就繼續查詢大小為512的連結串列,如果找到了,就將512個頁表劃分為兩個256,一個分配給程序,另一個就掛載到大小為256的連結串列上。如果大小為512的連結串列也是空,就會繼續查詢大小為1024的連結串列,仍然為空就返回一個錯誤。當一個頁表被釋放之後,相鄰的兩個頁表就會合併成一個大的頁框。
分配演算法
當申請分配頁的時候,如果無法從夥伴系統的空閒連結串列中獲得頁面,則進入慢速記憶體分配路徑,率先使用低水位線嘗試分配,若失敗,則說明記憶體稍有不足,頁分配器會喚醒 kswapd 執行緒非同步回收頁,然後再嘗試使用最低水位線分配頁。如果分配失敗,說明剩餘記憶體嚴重不足,會先執行非同步的記憶體規整,若非同步規整後仍無法分配頁面,則執行直接記憶體回收,或回收的頁面數量仍不滿足需求,則進行直接記憶體規整,若直接記憶體回收一個頁面都未收到,則呼叫 oom killer 回收記憶體。
記憶體碎片
- 內部碎片:假設一個程序需要3KB的實體記憶體,但是記憶體頁的最小顆粒度是4KB,所以就有1KB的空閒記憶體無法利用
- 外部碎片:假設系統剩下的頁表都不連續,此時系統就無法分配超過4KB的連續實體記憶體,從而導致記憶體溢位