現象
之前一直穩定運行了很久的內核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的連續內存頁,所以導致了內存頁分配失敗。
解決方法
釋放內存
在釋放內存之前先手動執行sync命令,將所有未寫的系統緩沖區寫到磁盤中,包含已修改的 i-node、已延遲的塊 I/O 和讀寫映射文件。
- 釋放頁緩存:
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的連續物理內存,從而導致內存溢出