1. 程式人生 > 其它 >記憶體頁不足導致程式啟動失敗:page allocation failure

記憶體頁不足導致程式啟動失敗: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的連續實體記憶體,從而導致記憶體溢位

參考文件