Talloc記憶體池介紹
介紹
Talloc是一個層次結構的,包含引用計數和析構機制的記憶體池系統。它構建於標準C庫上,定義了一組介面用以簡化資料的申請和回收,尤其是對於那些包含了許多動態申請的元素(比如陣列和字串)的複雜資料結構尤為有效。
Talloc庫的主要目標是:不必再為每一個複雜的資料結構都單獨編寫記憶體釋放函式;為申請的記憶體塊提供一個邏輯組織架構;減少長時間執行的應用程式中出現記憶體洩露的機率。所有這些都依靠在層次結構的talloc context中申請記憶體而實現:當釋放一個context時,它所有的子context都會被釋放。
1、什麼是talloc context
Talloc context是talloc庫中最重要的部分,負責著這個記憶體分配器的每一個特性。它是talloc管理的記憶體區域的邏輯單位。
從開發者的視角來看,talloc context完全可以看作使用標準C庫申請記憶體時返回的指標,每個使用talloc庫返回的context都可以直接被不使用talloc的函式呼叫,比如像下面這樣:
char *str1 = strdup("I am NOT a talloc context"); char *str2 = talloc_strdup(NULL, "I AM a talloc context"); printf("%d\n", strcmp(str1, str2) == 0); free(str1); talloc_free(str2); /* we can not use free() on str2 */
原因:talloc context內部維護了一個特殊的固定大小的資料結構,叫做talloc chunk。每個chunk儲存了記憶體的元資料,當talloc函式返回一個context(指標)時,它實際上指向的是talloc trunk的使用者資料區域,而在使用talloc庫函式處理context時,talloc庫會將指標向前偏移到talloc chunk的起始地址,如下圖所示:
talloc.h中定義了TALLOC_CTX型別,其用來在函式中宣告context。它是void型別的一個別名,存在的意義是為了語義上的原因——這樣我們可以分辨出void * (任意資料)與TALLOC_CTX *(talloc context)。
1.1、Context元資料
每個context元資料包含了與申請這塊記憶體相關的幾部分資訊:
a) 名稱——用來報告context的層次結構,以及模擬動態型別系統。
b) 申請的記憶體位元組數——這個可以用來判斷陣列中的元素個數。
c) 附加的解構函式——當此塊記憶體被釋放前,它會被執行
d) context的引用
e) 子context以及父context——建立記憶體的分層機制。
1.2、Talloc context的層級
每個talloc context都儲存著它的子節點和父節點的資訊。Talloc依靠這些資訊建立了一個層次化的記憶體模型。更明確的說,它建立了一個N叉樹,每個節點代表著一個talloc context。這個樹的根節點被稱為頂級context——一個沒有任何父節點的context。
這種方法有幾個優點:
1、在釋放talloc context結構時,它包含的所有子節點都會被自動釋放。
2、可以改變talloc context的父節點,即:將整棵子樹移動到另一個節點下方。
3、建立了一種更加自然的資料結構管理方式。
1.3、示例
有一個儲存使用者基本資訊的資料結構——他/她的名字,身份證號以及他/她所屬於的所有的組:
struct user {
uid_t uid;
char *username;
size_t num_groups;
char **groups;
};
用talloc申請這個資料結構,其結果是如下所示的context樹:
user指標的申請過程如下:
/* create new top level context */
struct user *user = talloc(NULL, struct user);
user->uid = 1000;
user->num_groups = N;
/* make user the parent of following contexts */
user->username = talloc_strdup(user, "Test user");
user->groups = talloc_array(user, char*, user->num_groups);
for (i = 0; i < user->num_groups; i++) {
/* make user->groups the parent of following context */
user->groups[i] = talloc_asprintf(user->groups,
"Test group %d", i);
}
user指標的釋放如下:
talloc_free(user);
由此可見,talloc和malloc的區別如下:
在釋放指標式,如果使用標準C庫,則需要先遍歷group陣列釋放每一個元素,然後再釋放儲存元素的陣列和使用者名稱字串,最後釋放整個結構體。而使用talloc,僅僅需要釋放結構體context,它的子節點都會被自動釋放。
2、建立一個talloc context
最重要的函式,用來建立talloc context。有兩種方式:一是型別安全的建立context;二是建立0長度的context。
2.1、型別安全的建立context(推薦)
它將申請指定型別的大小的記憶體,並返回一個新的,已經被轉換過型別的指標,Context的名稱將會被自動設定為資料型別的名稱,用來模擬動態型別系統。
示例:
struct user *user = talloc(ctx, struct user);
/* initialize to default values */
user->uid = 0;
user->name = NULL;
user->num_groups = 0;
user->groups = NULL;
/* or we can achieve the same result with */
struct user *user_zero = talloc_zero(ctx, struct user);
2.2、零長度的context
零長度的context是一個沒有任何語義含義的context,它只由context的元資料構成,型別是TALLOC_CTX*。
它一般用來聚合幾個資料結構到一個(零長度的)父context,比如一個臨時的context用來儲存這個函式裡的所有記憶體,而函式的呼叫者對這些記憶體並不關心。申請一個臨時的context可以讓函式業務執行後的清理工作變得更簡單。
舉例:
TALLOC_CTX *tmp_ctx = NULL;
struct foo *foo = NULL;
struct bar *bar = NULL;
/* new zero-length top level context */
tmp_ctx = talloc_new(NULL);
if (tmp_ctx == NULL) {
return ENOMEM;
}
foo = talloc(tmp_ctx, struct foo);
bar = talloc(tmp_ctx, struct bar);
/* free everything at once */
talloc_free(tmp_ctx);
3、過繼talloc context
Talloc可以變更一個context的父節點,這種操作被稱為“過繼”(譯者注:原文為stealing,為更加易懂翻譯為過繼),它是最重要的talloc context操作之一。
如果我們需要讓一個context的生命週期比它的父節點更長時,那麼可以將其過繼給其他父節點,比如:將資料庫的搜尋結果過繼給記憶體中的快取,或者將父節點從一個通用結構更改為一個具體的結構,反之亦然。最常見的場景(至少對Samba來說),是將資料從某一個函式內專用的context過繼給輸出context,作為輸出函式的引數。
舉例:
struct foo {
char *a1;
char *a2;
char *a3;
};
struct bar {
char *wurst;
struct foo *foo;
};
struct foo *foo = talloc_zero(ctx, struct foo);
foo->a1 = talloc_strdup(foo, "a1");
foo->a2 = talloc_strdup(foo, "a2");
foo->a3 = talloc_strdup(foo, "a3");
struct bar *bar = talloc_zero(NULL, struct bar);
/* change parent of foo from ctx to bar */
bar->foo = talloc_steal(bar, foo);
/* or do the same but assign foo = NULL */
bar->foo = talloc_move(bar, &foo);
talloc_move()函式與talloc_steal函式類似,不同點是它進一步將源指標設為了NULL。
一般來說,源指標自身是不會被改變的(talloc只改變了它的元資料中的父節點)。但一個常見的用法是將函式呼叫結果賦值給一個另外的變數,這樣的話通過原來的指標訪問變數應當被避免,除非必須如此。在這種情況下推薦使用talloc_move()來過繼context。由於它將源指標設為NULL,這樣避免了指標被意外釋放,也避免了舊變數在父節點被更改的情況下被錯誤的使用。
4、動態型別
使用C語言進行泛型程式設計是非常困難的,這裡沒有像面嚮物件語言一樣的模板和繼承關係,也沒有動態型別系統。因此,使用這種語言進行泛型程式設計的方法一般是將一個變數轉換為void*型別,將其通過一個泛型函式傳遞給具體的回撥函式。
void generic_function(callback_fn cb, void *pvt)
{
/* do some stuff and call the callback */
cb(pvt);
}
void specific_callback(void *pvt)
{
struct specific_struct *data;
data = (struct specific_struct*)pvt;
/* ... */
}
void specific_function()
{
struct specific_struct data;
generic_function(callback, &data);
}
無論在編譯器編譯還是程式碼執行時,系統都無法檢查傳遞的引數型別是否正確。將一個錯誤型別的資料傳遞給回撥函式將會造成不可預知的結果(不一定導致程式崩潰)。
每一個talloc context都包含一個名字,並且此名字在任何時候都是可用的,因此,在變數的型別經過轉換而無法分辨的時候,它可以用來幫助我們分辨context的型別。
雖然context的名稱可以被設定為任意字串,建議將其設定為變數型別的名字。
推薦使用talloc()或者talloc_array(或者它的變體)中的一個函式用來建立context,它們會自動將context名稱設定為變數型別。
可以通過使用兩個相似的函式來同時做到型別檢查和型別轉換:
talloc_get_type()
talloc_get_type_abort()
talloc的動態型別的一種典型的應用就是:用於判斷向回撥函式傳遞的引數是否非法,若是,則程式將會中止。如下:
void foo_callback(void *pvt)
{
struct foo *data = talloc_get_type_abort(pvt, struct foo);
/* ... */
}
int do_foo()
{
struct foo *data = talloc_zero(NULL, struct foo);
/* ... */
return generic_function(foo_callback, data);
}
假如,在編寫服務端程式時,我們可能希望在開發環境中出現此種情況時中止程式(來確保錯誤不被忽視),而在生產環境下去嘗試從錯誤中恢復。可以通過向talloc註冊一個自定義的abort函式:
void my_abort(const char *reason)
{
fprintf(stderr, "talloc abort: %s\n", reason);
#ifdef ABORT_ON_TYPE_MISMATCH
abort();
#endif
}
此時talloc_get_type_abort()函式的效果將會變成這樣:
talloc_set_abort_fn(my_abort);
TALLOC_CTX *ctx = talloc_new(NULL);
char *str = talloc_get_type_abort(ctx, char);
if (str == NULL) {
/* recovery code */
}
/* talloc abort: ../src/main.c:25: Type mismatch:
name[talloc_new: ../src/main.c:24] expected[char] */
5、解構函式
開發者可以為talloc context新增一個解構函式,用於在釋放talloc context時,解構函式可以做一些相關聯的事情。
示例:
比如,建立一個動態連結串列,在釋放元素前,需要先確認它已經從連結串列中被移除了。一般來說,這需要兩個先後完成的動作:將其從連結串列中移除,然後釋放記憶體。但是,使用talloc時,可以通過設定解構函式將元素從連結串列中移除,而talloc_free()完成記憶體的釋放。
解構函式如下:
int list_remove(struct list_el *el)
{
/* remove element from the list */
}
設定解構函式:
struct list_el* list_insert(TALLOC_CTX *mem_ctx,
struct list_el *where,
void *ptr)
{
struct list_el *el = talloc(mem_ctx, struct list_el);
el->data = ptr;
/* insert into list */
talloc_set_destructor(el, list_remove);
return el;
}
釋放記憶體:
struct list_el* list_insert_free(TALLOC_CTX *mem_ctx,
struct list_el *where,
void *ptr)
{
struct list_el *el = NULL;
el = list_insert(mem_ctx, where, ptr);
talloc_steal(el, ptr);
return el;
}
6、記憶體池
記憶體池是一塊固定大小的預申請好的記憶體空間,在需要申請新記憶體時,將從記憶體池中分配,而不是從系統中分配新的記憶體。記憶體池是通過建立一個指向預申請記憶體空間區域內的指標來實現的,這使得記憶體池無法擴容,否則會改變指標的位置——原來指向它內部的指標將變為無效指標。因此,在使用記憶體池時,需要評估好它所需要的記憶體空間。
Talloc庫包含了自行實現的記憶體池,在初始化一個記憶體池context時,使用talloc_pool()函式。
talloc記憶體池具有以下幾個屬性:
如果從一個記憶體池中申請記憶體,則將從記憶體池中分配所需的記憶體;
如果一個context是記憶體池的子context,則它將使用記憶體池的空間。
如果記憶體池的剩餘空間不夠,則將建立一個新的非記憶體池context,獨立於記憶體池之外。
/* 為記憶體池申請1KiB記憶體 */
TALLOC_CTX *pool_ctx = talloc_pool(NULL, 1024);
/* 從記憶體池中取走512B, 記憶體池中還剩下512B */
void *ptr = talloc_size(pool_ctx, 512);
/* 1024B > 512B, 這將在記憶體池之外建立一個新的talloc chunk */
void *ptr2 = talloc_size(ptr, 1024);
/* 記憶體池中還有512可用位元組,這將從中再取走200B. */
void *ptr3 = talloc_size(ptr, 200);
/* 這將銷燬context 'ptr3' 但是記憶體並沒有被釋放, 記憶體池中可用的記憶體大小將會增加到 512B. */
talloc_free(ptr3);
/* 這將同時釋放 'pool_ctx' 和 'ptr2' 的記憶體. */
talloc_free(pool_ctx);
如果一個talloc記憶體池的子節點更改了它的父節點,則整塊記憶體池都不能釋放,直到此子節點被釋放。
TALLOC_CTX *mem_ctx = talloc_new(NULL);
TALLOC_CTX *pool_ctx = talloc_pool(NULL, 1024);
struct foo *foo = talloc(pool_ctx, struct foo);
/* mem_ctx 不在記憶體池中 */
talloc_steal(mem_ctx, foo);
/* pool_ctx 被標記已經被釋放, 但這塊記憶體並沒有被釋放, 再次訪問pool_ctx這塊記憶體將產生一個錯誤 */
talloc_free(pool_ctx);
/* 這時才會釋放pool_ctx的記憶體. */
talloc_free(mem_ctx);
原文:https://blog.csdn.net/yuyeanci/article/details/53314089
Talloc下載地址:https://www.samba.org/ftp/talloc/
Linux man page:https://linux.die.net/man/3/talloc
https://talloc.samba.org/talloc/doc/html/index.html