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

猜你喜欢

转载自blog.csdn.net/qq_41688455/article/details/84661553
今日推荐