MySQL 原始碼分析 記憶體分配機制

2021-09-23 09:49:32 字數 3895 閱讀 7843

記憶體資源由作業系統管理,分配與**操作可能會執行系統呼叫(以 malloc 演算法為例,較大的記憶體空間分配介面是 mmap, 而較小的空間 free 之後並不歸還給作業系統 ),頻繁的系統呼叫必然會降低系統效能,但是可以最大限度的把使用完畢的記憶體讓給其它程序使用,相反長時間占有記憶體資源可以減少系統呼叫次數,但是記憶體資源不足會導致作業系統頻繁換頁,降低伺服器的整體效能。

資料庫是使用記憶體的「大戶」,合理的記憶體分配機制就尤為重要,上一期月報介紹了 postgresql 的記憶體上下文,本文將介紹在 mysql 中又是怎麼管理記憶體的。

mysql 在基本的記憶體操作介面上面封裝了一層,增加了控制引數 my_flags

void *my_malloc(size_t size, myf my_flags)

void *my_realloc(void *oldpoint, size_t size, myf my_flags)

void my_free(void *ptr)

my_flags 的值目前有:

my_fae 		/* fatal if any error */

my_wme /* write message on error */

my_zerofill /* fill array with zero */

my_fae 表示記憶體分配失敗就退出整個程序,my_wme 表示記憶體分配失敗是否需要記錄到日誌中,my_zerofill 表示分配記憶體後初始化為0。

在 mysql 的 server 層中廣泛使用 mem_root 結構來管理記憶體,避免頻繁呼叫封裝的基礎介面,也可以統一分配和管理,防止發生記憶體洩漏。不同的 mem_root 之間互相沒有影響,不像 pg 中不同的記憶體上下文之間還有關聯。這可能得益於 mysql server 層是物件導向的**,mem_root 作為類中的乙個成員變數,伴隨著物件的整個生命週期。比較典型的類有: thd,string, table, table_share, query_arena, st_transactions 等。

mem_root 分配記憶體的單元是 block,使用 used_mem 結構體來描述。結構比較簡單,block 之間相互連線形成記憶體塊鍊錶,left 和 size 表示對應 block 還有多少可分配的空間和總的空間大小。

typedef struct st_used_mem

used_mem;

而 mem_root 結構體負責管理 block 鍊錶 :

typedef struct st_mem_root

mem_root;

整體結構就是兩個 block 鍊錶,free 鍊錶管理所有的仍然存在可分配空間的 block,used 鍊錶管理已經沒有可分配空間的所有 block。pre_alloc 類似於 pg 記憶體上下文中的 keeper,在初始化 mem_root 的時候就可以預分配乙個 block 放到 free 鍊錶中,當 free 整個 mem_root 的時候可以通過引數控制,選擇保留 pre_alloc 指向的 block。min_malloc 控制乙個 block 剩餘空間還有多少的時候從 free 鍊錶移除,加入到 used 鍊錶中。block_size 表示初始化 block 的大小。block_num 表示 mem_root 管理的 block 數量。first_block_usage 表示 free 鍊錶中第乙個 block 不滿足申請空間大小的次數,是乙個調優的引數。err_handler 是錯誤處理函式。

使用 mem_root 首先需要初始化,呼叫 init_alloc_root, 通過引數可以控制初始化的 block 大小和 pre_alloc_size 的大小。其中比較有意思的點是 min_block_size 直接指定乙個值 32,個人覺得不太靈活,對於小記憶體的申請可能會有比較大的記憶體碎片。另乙個是 block_num 初始化為 4,這個和決定新分配的 block 大小策略有關。

void init_alloc_root(mem_root *mem_root, size_t block_size,

size_t pre_alloc_size __attribute__((unused)))

} dbug_void_return;

}

初始化完成就可以呼叫 alloc_root 進行記憶體申請,整個分配流程並不複雜,**也不算長,為了方便閱讀貼出來,也可以略過直接看分析。

void *alloc_root( mem_root *mem_root, size_t length )

// 找到乙個空閒空間大於申請記憶體空間的 block

for ( next = *prev; next && next->left < length; next = next->next )

prev = &next->next;

}if ( !next ) // free 鍊錶為空,或者沒有滿足可分配條件 block

mem_root->block_num++;

next->next = *prev;

next->size = get_size;

next->left = get_size - align_size( sizeof(used_mem) );

*prev = next; // 新申請的 block 放到 free 鍊錶尾部

}point = (uchar *) ( (char *) next + (next->size - next->left) );

if ( (next->left -= length) < mem_root->min_malloc ) // 分配完畢後,block 是否還能在 free 鍊錶中繼續分配

}

首先判斷 free 鍊錶是否為空,如果不為空,按邏輯應該遍歷整個鍊錶,找到乙個空閒空間足夠大的 block,但是看**是先執行了乙個判斷語句,這其實是乙個空間換時間的優化策略,因為free 鍊錶大多數情況下都是不為空的,幾乎每次分配都需要從 free 鍊錶的第乙個 block 開始判斷,我們當然希望第乙個 block 可以立刻滿足要求,不需要再掃瞄 free 鍊錶,所以根據呼叫端的申請趨勢,設定兩個變數:alloc_max_block_usage_before_drop 和 alloc_max_block_to_drop,當 free 鍊錶的第乙個 block 申請次數超過 alloc_max_block_usage_before_drop 而且剩餘的空閒空間小於 alloc_max_block_to_drop,就把這個 block 放到 used 煉表裡,因為它已經一段時間無法滿足呼叫端的需求了。

如果在 free 鍊錶中沒有找到合適的 block,就需要呼叫基礎介面申請一塊新的記憶體空間,新的記憶體空間大小當然至少要滿足這次申請的大小,同時預估的新 block 大小是 :mem_root->block_size * (mem_root->block_num >> 2)也就是初始化的 block 大小乘以當前 block 數量的 1/4,所以初始化 mem_root 的 block_num 至少是 4。

找到合適的 block 之後定位到可用空間的位置就行了,返回之前最後需要判斷 block 分配之後是否需要移動到 used 鍊錶。

歸還記憶體空間的介面有兩個:mark_blocks_free(mem_root *root)free_root(men_root *root,myf myflags),可以看到兩個函式的引數不像基礎封裝的介面,沒有直接傳需要歸還空間的指標,傳入的是 mem_root 結構體指標,說明對於 mem_root 分配的記憶體空間,是統一歸還的。mark_blocks_free不真正的歸還 block,而是放到 free 鍊錶中標記可用。free_root真正歸還空間給作業系統,myflages 可以控制是否和標記刪除的函式行為一樣,也可以控制 pre_alloc 指向的 block 是否歸還。

Libevent原始碼分析 記憶體分配

libevent的記憶體分配函式還是比較簡單的,並沒有定義記憶體池之類的東西。如同前一篇部落格 那樣,給予libevent庫的使用者充分的設定權 定製 即可以設定使用者 libevent庫的使用者 自己的記憶體分配函式。至於怎麼分配,主動權在於使用者。但在設定 定製 的時候要注意一些地方,下面會說到...

Libevent原始碼分析 記憶體分配

libevent的記憶體分配函式還是比較簡單的,並沒有定義記憶體池之類的東西。如同前一篇部落格 那樣,給予libevent庫的使用者充分的設定權 定製 即可以設定使用者 libevent庫的使用者 自己的記憶體分配函式。至於怎麼分配,主動權在於使用者。但在設定 定製 的時候要注意一些地方,下面會說到...

list的記憶體分配機制分析

該程式演示了list在記憶體分配時候的問題。裡面的備註資訊是我的想法。include include include include using namespace std class cdata cdata int i,string s cdata const cdata data cdata o...