写点什么

Glibc-scratch_buffer 的源码分析

作者:桑榆
  • 2022-10-20
    广东
  • 本文字数:6276 字

    阅读完需:约 1 分钟

Glibc-scratch_buffer的源码分析

背景

scratch_buffer 是 Glibc 中的一个实用工具,提供一块默认大小的栈空间,并可以动态扩展到空间大小(实用堆空间),使用 scratch_buffer 不用考虑 buffer 申请/扩容/释放,只需按照顺序调用相关函数操作 scratch_buffer 即可。在很多需要内存块,但又不必细究大小,需要动态扩容的场景很好用。

接下来,我们就来一起看一下 scratch_buffer 的具体实现。

scratch_buffer 的结构体定义

  • void* data:指向数据域起始位置的指针

  • size_t length:分配的数据域长度(以字节为单位)

  • union {...}__space:数据域的联合体

    max_align_t __align:对齐参数,当使用堆内存时生效,记录对齐参数

    char __c[1024]:预先在栈上面分配的 1024 字节内存

// glibc/include/scratch_buffer.h
/* Scratch buffer. Must be initialized with scratch_buffer_init before its use. */struct scratch_buffer { void *data; /* Pointer to the beginning of the scratch area. */ size_t length; /* Allocated space at the data pointer, in bytes. */ union { max_align_t __align; char __c[1024]; } __space;};
复制代码

scratch_buffer 相关的函数

1.scratch_buffer_init---初始化 scratch_buffer

  • 入参:scratch_buffer 结构体指针

在使用 scratch_buffer 时,先进行定义,然后必须先调用 scratch_buffer_init 才可以进行使用,该函数主要是初始化最重要的 data 和 length 参数。

  • data 初始化为预先分配的 1024 字节的首地址;

  • length 初始化为sizeof (buffer->__space),取联合体中最大变量的 size。

/* Initializes *BUFFER so that BUFFER->data points to BUFFER->__space   and BUFFER->length reflects the available space.  */static inline voidscratch_buffer_init (struct scratch_buffer *buffer){  buffer->data = buffer->__space.__c;  buffer->length = sizeof (buffer->__space);}
复制代码

2.scratch_buffer_free---释放 scratch_buffer

  • 入参:scratch_buffer 结构体指针

如果此时的 data 指针不等于预先分配的 1024 字节的首地址,说明进行了扩容,此时分配了堆空间,需要调用 free 进行内存释放。

/* Deallocates *BUFFER (if it was heap-allocated).  */static inline voidscratch_buffer_free (struct scratch_buffer *buffer){                                                                                                                                                          if (buffer->data != buffer->__space.__c)    free (buffer->data);}
复制代码

3.scratch_buffer_grow---scratch_buffer 扩容(不保留原有内容)

  • 入参:scratch_buffer 结构体指针

  • 出参:返回 true 或 false

    true 代表扩容成功,获得一块更大的 buffer,但是原有 buffer 的内容不会保留

    fail 代表扩容失败,原有 buffer 被释放,但当前的结构体还是可以继续调用释放函数

/* Grow *BUFFER by some arbitrary amount.  The buffer contents is NOT   preserved.  Return true on success, false on allocation failure (in   which case the old buffer is freed).  On success, the new buffer is   larger than the previous size.  On failure, *BUFFER is deallocated,   but remains in a free-able state, and errno is set.  */bool __libc_scratch_buffer_grow (struct scratch_buffer *buffer);libc_hidden_proto (__libc_scratch_buffer_grow)
/* Alias for __libc_scratch_buffer_grow. */static __always_inline boolscratch_buffer_grow (struct scratch_buffer *buffer){ return __glibc_likely (__libc_scratch_buffer_grow (buffer));}
复制代码

__libc_scratch_buffer_grow 的具体实现在 glibc/malloc/scratch_buffer_grow.c 中

按照如下流程进行扩容操作:

(1).计算扩容后的新长度---扩容因子为 2

新长度为原有长度的两倍

bool__libc_scratch_buffer_grow (struct scratch_buffer *buffer){  void *new_ptr;  size_t new_length = buffer->length * 2;
复制代码

(2).调用 scratch_buffer_free 释放原有 buffer

  /* Discard old buffer.  */  scratch_buffer_free (buffer);
复制代码

(3).检查 new_length 是否溢出

  • 如果 new_length 大于等于 buffer->length,说明没有溢出,调用 malloc 进行内存分配;

  • 如果 new_length 小于 buffer->length,说明出现了溢出,需要设置 errno 为 ENOMEM(Cannot allocate memory),并将 new_ptr 置为 NULL

  /* Check for overflow.  */  if (__glibc_likely (new_length >= buffer->length))    new_ptr = malloc (new_length);  else    {         __set_errno (ENOMEM);      new_ptr = NULL;    }   
复制代码

(4).兼容 new_ptr == NULL 的情况

如果出现这种情况,说明上一步中分配内存失败,那么我们需要调用 scratch_buffer_init 来保证该块 scratch_buffer 是可以被 free 的,这时返回 false,表示扩容失败。

  if (__glibc_unlikely (new_ptr == NULL))    {         /* Buffer must remain valid to free.  */      scratch_buffer_init (buffer);      return false;    }   
复制代码

(5).对 scratch_buffer 的 data 域和 length 域赋值

分别赋值为最新分配的内存首地址和长度。

  /* Install new heap-based buffer.  */  buffer->data = new_ptr;  buffer->length = new_length;  return true;}libc_hidden_def (__libc_scratch_buffer_grow)
复制代码

4.scratch_buffer_grow_preserve---scratch_buffer 扩容(保留原有内容)

  • 入参:scratch_buffer 结构体指针

  • 出参:返回 true 或 false

    true 代表扩容成功,获得一块更大的 buffer,保留原有内容作为新 buffer 的前面部分内容

    fail 代表扩容失败,原有 buffer 被释放,但当前的结构体还是可以继续调用释放函数

/* Like __libc_scratch_buffer_grow, but preserve the old buffer   contents on success, as a prefix of the new buffer.  */bool __libc_scratch_buffer_grow_preserve (struct scratch_buffer *buffer);libc_hidden_proto (__libc_scratch_buffer_grow_preserve)
/* Alias for __libc_scratch_buffer_grow_preserve. */static __always_inline boolscratch_buffer_grow_preserve (struct scratch_buffer *buffer){ return __glibc_likely (__libc_scratch_buffer_grow_preserve (buffer));}
复制代码

__libc_scratch_buffer_grow_preserve 的具体实现在 glibc/malloc/scratch_buffer_grow_preserve.c 中

按照如下流程进行扩容操作:

(1).计算扩容后的新长度---扩容因子为 2

新长度为原有长度的两倍

bool__libc_scratch_buffer_grow_preserve (struct scratch_buffer *buffer){  size_t new_length = 2 * buffer->length;  void *new_ptr;
复制代码

(2).根据 data 域的指向进行不同的操作

  • 如果 data 域与预先分配的 1024 字节栈空间首地址一致,说明是第一次扩容,那么我们需要调用 malloc 分配内存,并做内存分配检查,如果分配失败则直接返回 false,然后重要的一点是,我们还需要将栈空间中的数据拷贝到分配出的堆空间上,保留原有数据;

  • 如果 data 域已经是堆空间上面的内存,那么我们检查是否出现越界异常

    若无越界异常,则调用 realloc 函数在原有地址基础上分配 new_length 大小的内存(这里 realloc 函数内部会在空间不够时进行拷贝操作,若空间足够则不需要拷贝);

    若出现越界异常,则处理方式同 scratch_buffer_grow

  • 然后检查 new_ptr 是否等于 NULL,以便做兼容,保证扩容失败后,仍可正常 free scratch_buffer,注意,这里先 free(buffer->data)即原有数据,因为这种情况原有数据将要扩容越界,无需再进行保存。

  if (buffer->data == buffer->__space.__c)    {         /* Move buffer to the heap.  No overflow is possible because     buffer->length describes a small buffer on the stack.  */      new_ptr = malloc (new_length);      if (new_ptr == NULL)    return false;      memcpy (new_ptr, buffer->__space.__c, buffer->length);    }     else    {         /* Buffer was already on the heap.  Check for overflow.  */      if (__glibc_likely (new_length >= buffer->length))    new_ptr = realloc (buffer->data, new_length);      else    {         __set_errno (ENOMEM);      new_ptr = NULL;    }       if (__glibc_unlikely (new_ptr == NULL))    {      /* Deallocate, but buffer must remain valid to free.  */      free (buffer->data);      scratch_buffer_init (buffer);      return false;    }    }
复制代码

(3).对 scratch_buffer 的 data 域和 length 域赋值

分别赋值为最新分配的内存首地址和长度。

  /* Install new heap-based buffer.  */  buffer->data = new_ptr;  buffer->length = new_length;  return true;}libc_hidden_def (__libc_scratch_buffer_grow_preserve)
复制代码

5.scratch_buffer_set_array_size---scratch_buffer 扩容(大小至少可以容纳 nelem 个大小为 size 的元素)

  • 入参:

    scratch_buffer 结构体指针

    size_t nelem:元素个数,可以为 0

    size_t size:每个元素的 size 大小,可以为 0

  • 出参:返回 true 或 false

    true 代表扩容成功,获得一块更大的 buffer,不保留原始内容

    fail 代表扩容失败,原有 buffer 被释放,但当前的结构体还是可以继续调用释放函数

注意:这个函数是是否可以减少 buffer size 大小是不确定的

/* Grow *BUFFER so that it can store at least NELEM elements of SIZE   bytes.  The buffer contents are NOT preserved.  Both NELEM and SIZE   can be zero.  Return true on success, false on allocation failure   (in which case the old buffer is freed, but *BUFFER remains in a   free-able state, and errno is set).  It is unspecified whether this   function can reduce the array size.  */bool __libc_scratch_buffer_set_array_size (struct scratch_buffer *buffer,                       size_t nelem, size_t size);libc_hidden_proto (__libc_scratch_buffer_set_array_size)
/* Alias for __libc_scratch_set_array_size. */static __always_inline boolscratch_buffer_set_array_size (struct scratch_buffer *buffer, size_t nelem, size_t size){ return __glibc_likely (__libc_scratch_buffer_set_array_size (buffer, nelem, size));}
复制代码

__libc_scratch_buffer_set_array_size 的具体实现在 glibc/malloc/scratch_buffer_set_array_size.c 中

按照如下流程进行 size 切换操作:

(1).计算切换后的 size 大小

size 大小就等于元素个数*每个元素的 size 大小

bool__libc_scratch_buffer_set_array_size (struct scratch_buffer *buffer,                      size_t nelem, size_t size){  size_t new_length = nelem * size;
复制代码

(2).查看是否有 nelem 和 size 数值过大,导致的越界异常

  • sizeof (size_t)表示 size_t 的字节数,CHAR_BIT 是 8,一个字节 8 位,除 2 是针对两个参数,如果(nelem | size)右移这么多位数,得到的结果!=0,说明在两者中有一个在高位还有 1,即有一个值很大;

  • nelem != 0 && size != new_length / nelem 是检查前面的乘法计算有没有导致 new_length 越界变为负数;

如果满足上面两个条件,说明是出现了越界的情况,需要释放掉原有 buffer,之后重新初始化,保证后续可以调用 free 函数。

  /* Avoid overflow check if both values are small. */  if ((nelem | size) >> (sizeof (size_t) * CHAR_BIT / 2) != 0      && nelem != 0 && size != new_length / nelem)    {      /* Overflow.  Discard the old buffer, but it must remain valid     to free.  */      scratch_buffer_free (buffer);      scratch_buffer_init (buffer);      __set_errno (ENOMEM);      return false;    }
复制代码

(3).如果需要的 size 大小小于现在已分配的 size 大小,则直接返回 true,无需分配

这里也说明了,__libc_scratch_buffer_set_array_size 操作可能并不能减小 buffersize

  if (new_length <= buffer->length)    return true;
复制代码

(4).后续操作与之前基本一致

  • 销毁旧的 buffer

  • 申请新的 buffer

  • 判断 malloc 是否成功,失败需要重新 init

  • 对 scratch_buffer 的 data 域和 length 域赋值

  /* Discard old buffer.  */  scratch_buffer_free (buffer);
char *new_ptr = malloc (new_length); if (new_ptr == NULL) { /* Buffer must remain valid to free. */ scratch_buffer_init (buffer); return false; }
/* Install new heap-based buffer. */ buffer->data = new_ptr; buffer->length = new_length; return true;}libc_hidden_def (__libc_scratch_buffer_set_array_size)
复制代码

6.scratch_buffer_dupfree---返回 scratch_buffer 的 size 字节数据的拷贝(堆空间)

  • 入参:

    scratch_buffer 结构体指针

    size_t size:拷贝的 size 大小,最大只能与输入的 buffer->length 一致

  • 出参:返回 buffer 指针,成功的话返回拷贝的 buffer 指针,失败返回 NULL 指针

/* Return a copy of *BUFFER's first SIZE bytes as a heap-allocated block,   deallocating *BUFFER if it was heap-allocated.  SIZE must be at   most *BUFFER's size.  Return NULL (setting errno) on memory   exhaustion.  */void *__libc_scratch_buffer_dupfree (struct scratch_buffer *buffer,                                     size_t size);libc_hidden_proto (__libc_scratch_buffer_dupfree)
/* Alias for __libc_scratch_dupfree. */static __always_inline void *scratch_buffer_dupfree (struct scratch_buffer *buffer, size_t size){ void *r = __libc_scratch_buffer_dupfree (buffer, size); return __glibc_likely (r != NULL) ? r : NULL;}
复制代码

__libc_scratch_buffer_dupfree 的具体实现在 glibc/malloc/scratch_buffer_dupfree.c 中

按照如下流程进行 dupfree 操作:

  • 如果 data 域指针等于预先分配的栈空间 buffer:调用 malloc 分配对应 size 的 buffer,然后使用 memcpy 操作拷贝对应大小的数据即可,中间考虑空指针处理即可;

  • 如果 data 域指针是堆空间指针,那么直接调用 realloc 重分配 size 大小的 buffer,注意,这里如果重分配失败,是返回原有的 data 域指针。

void *__libc_scratch_buffer_dupfree (struct scratch_buffer *buffer, size_t size){  void *data = buffer->data;  if (data == buffer->__space.__c)    {         void *copy = malloc (size);      return copy != NULL ? memcpy (copy, data, size) : NULL;    }     else    {         void *copy = realloc (data, size);      return copy != NULL ? copy : data;    }   }libc_hidden_def (__libc_scratch_buffer_dupfree)
复制代码

scratch_buffer 的使用流程

通过上面函数的解析,我们大致了解了 scratch_buffer 的操作流程,实际上,它的使用流程如下所示,特别要注意 init 和 free 的流程。

     struct scratch_buffer tmpbuf;     scratch_buffer_init (&tmpbuf);
while (!function_that_uses_buffer (tmpbuf.data, tmpbuf.length)) if (!scratch_buffer_grow (&tmpbuf)) return -1;
scratch_buffer_free (&tmpbuf); return 0;
复制代码

总结

scratch_buffer 对原有的 malloc/realloc/free 进行了封装,使得可以方便地使用一块栈或堆上的 buffer,能够自由地进行 buffer 扩容,也提供了一些方便的操作函数,其原理也相对简单,容易理解,是一个很实用的工具。

发布于: 刚刚阅读数: 3
用户头像

桑榆

关注

北海虽赊,扶摇可接;东隅已逝,桑榆非晚! 2020-02-29 加入

Android手机厂商-相机软件系统工程师 爬山/徒步/Coding

评论

发布
暂无评论
Glibc-scratch_buffer的源码分析_源码刨析_桑榆_InfoQ写作社区