介紹
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