• C基础 内存统一入口


    引言  - malloc 引述

       C标准中堆上内存入口就只有 malloc, calloc, realloc . 内存回收口是 free. 常见的一种写法是

    struct person * per = malloc(sizoef(struct person));
    if(NULL == ptr) {
          fprintf(stderr, "malloc struct person is error!");
          // to do error thing ...
          ...
    }
    
    // 处理正常逻辑
    ...
    
    // 回收
    free(per);

    特别是 if NULL == ptr 那些操作实在让人繁琐. 有点不爽, 构建了一组接口, 尝试一种方式来简便一下.

    借鉴思路是 上层语言 new 的套路. 简单粗暴, 失败直接崩溃. 大体思路是

        struct header * ptr = malloc(sz + sizeof(struct header));
        // 检查内存分配的结果
        if(NULL == ptr) {
            fprintf(stderr, "_header_get >%s:%d:%s< alloc error not enough memory start fail!
    ", file, line, func);
            exit(EXIT_FAILURE);
        }

    利用 exit 结束分配为NULL情况. 毕竟计算机一级内存不足, 一切运行对于软件层都已经是接近"未定义的边缘了"

    参照资料 : 云大大的skynet2 demo  https://github.com/cloudwu/skynet2/tree/master/skynet-src

    前言 - 定义接口统一处理

       处理的思路很简单, 主要是 提供一个内存申请的入口像new一样, 返回初始化的内存, 并且内存不足直接崩溃. 首先接口设计如下

    scalloc.h 

    #ifndef _H_SIMPLEC_SCALLOC
    #define _H_SIMPLEC_SCALLOC
    
    #include <stdlib.h>
    
    // 释放sm_malloc_和sm_realloc_申请的内存, 必须配套使用
    void sm_free_(void * ptr, const char * file, int line, const char * func);
    // 返回申请的一段干净的内存
    void * sm_malloc_(size_t sz, const char * file, int line, const char * func);
    // 返回重新申请的内存, 只能和sm_malloc_配套使用
    void * sm_realloc_(void * ptr, size_t sz, const char * file, int line, const char * func);
    
    /*
     * 释放申请的内存
     * ptr  : 申请的内存
     */
    #define sm_free(ptr)        sm_free_(ptr, __FILE__, __LINE__, __func__)
    /*
     * 返回申请的内存, 并且是填充''
     * sz   : 申请内存的长度
     */
    #define sm_malloc(sz)       sm_malloc_(sz, __FILE__, __LINE__, __func__)
    /*
     * 返回申请到num*sz长度内存, 并且是填充''
     * num  : 申请的数量
     * sz   : 申请内存的长度
     */
    #define sm_calloc(num, sz)  sm_malloc_(num*sz, __FILE__, __LINE__, __func__)
    /*
     * 返回重新申请的内存
     * ptr  : 申请的内存
     * sz   : 申请内存的长度
     */
    #define sm_realloc(ptr, sz) sm_realloc_(ptr, sz, __FILE__, __LINE__, __func__)
    
    // 定义全局内存使用宏, 替换原有的malloc系列函数
    #ifndef _SIMPLEC_SCALLOC_CLOSE
    #   define free         sm_free
    #   define malloc       sm_malloc
    #   define calloc       sm_calloc
    #   define realloc      sm_realloc
    #endif #endif // !_H_SIMPLEC_SCALLOC

     上面 sm_malloc sm_calloc sm_realloc sm_free 宏相比原先的四个函数, 多了几个编译宏参数, 方便以后查找问题.

    _SIMPLEC_SCALLOC_CLOSE 头文件表示 当前是否替代老的 内存相关操作的入口.
    这里扯一点, calloc 感觉是设计的失败.
    #include <stdlib.h>
    
    void * calloc(size_t nmemb, size_t size);
    
    calloc() allocates memory for an array of nmemb elements of size bytes each and returns a pointer to the allocated memory.  The memory is set to zero.

    上面描述 相当于 calloc(nmemb, size) <=> malloc(nmemb*size) ; memset(ptr, 0, nmemb*size);  感觉好傻. 

    我么看看源码 在malloc.c 文件中实现 .

    void *
    __libc_calloc (size_t n, size_t elem_size)
    {
      mstate av;
      mchunkptr oldtop, p;
      INTERNAL_SIZE_T bytes, sz, csz, oldtopsize;
      void *mem;
      unsigned long clearsize;
      unsigned long nclears;
      INTERNAL_SIZE_T *d;
    
      /* size_t is unsigned so the behavior on overflow is defined.  */
      bytes = n * elem_size;
    #define HALF_INTERNAL_SIZE_T 
      (((INTERNAL_SIZE_T) 1) << (8 * sizeof (INTERNAL_SIZE_T) / 2))
      if (__builtin_expect ((n | elem_size) >= HALF_INTERNAL_SIZE_T, 0))
        {
          if (elem_size != 0 && bytes / elem_size != n)
            {
              __set_errno (ENOMEM);
              return 0;
            }
        }
    
      void *(*hook) (size_t, const void *) =
        atomic_forced_read (__malloc_hook);
      if (__builtin_expect (hook != NULL, 0))
        {
          sz = bytes;
          mem = (*hook)(sz, RETURN_ADDRESS (0));
          if (mem == 0)
            return 0;
    
          return memset (mem, 0, sz);
        }
    
      sz = bytes;
    
      arena_get (av, sz);
      if (av)
        {
          /* Check if we hand out the top chunk, in which case there may be no
         need to clear. */
    #if MORECORE_CLEARS
          oldtop = top (av);
          oldtopsize = chunksize (top (av));
    # if MORECORE_CLEARS < 2
          /* Only newly allocated memory is guaranteed to be cleared.  */
          if (av == &main_arena &&
          oldtopsize < mp_.sbrk_base + av->max_system_mem - (char *) oldtop)
        oldtopsize = (mp_.sbrk_base + av->max_system_mem - (char *) oldtop);
    # endif
          if (av != &main_arena)
        {
          heap_info *heap = heap_for_ptr (oldtop);
          if (oldtopsize < (char *) heap + heap->mprotect_size - (char *) oldtop)
            oldtopsize = (char *) heap + heap->mprotect_size - (char *) oldtop;
        }
    #endif
        }
      else
        {
          /* No usable arenas.  */
          oldtop = 0;
          oldtopsize = 0;
        }
      mem = _int_malloc (av, sz);
    
    
      assert (!mem || chunk_is_mmapped (mem2chunk (mem)) ||
              av == arena_for_chunk (mem2chunk (mem)));
    
      if (mem == 0 && av != NULL)
        {
          LIBC_PROBE (memory_calloc_retry, 1, sz);
          av = arena_get_retry (av, sz);
          mem = _int_malloc (av, sz);
        }
    
      if (av != NULL)
        (void) mutex_unlock (&av->mutex);
    
      /* Allocation failed even after a retry.  */
      if (mem == 0)
        return 0;
    
      p = mem2chunk (mem);
    
      /* Two optional cases in which clearing not necessary */
      if (chunk_is_mmapped (p))
        {
          if (__builtin_expect (perturb_byte, 0))
            return memset (mem, 0, sz);
    
          return mem;
        }
    
      csz = chunksize (p);
    
    #if MORECORE_CLEARS
      if (perturb_byte == 0 && (p == oldtop && csz > oldtopsize))
        {
          /* clear only the bytes from non-freshly-sbrked memory */
          csz = oldtopsize;
        }
    #endif
    
      /* Unroll clear of <= 36 bytes (72 if 8byte sizes).  We know that
         contents have an odd number of INTERNAL_SIZE_T-sized words;
         minimally 3.  */
      d = (INTERNAL_SIZE_T *) mem;
      clearsize = csz - SIZE_SZ;
      nclears = clearsize / sizeof (INTERNAL_SIZE_T);
      assert (nclears >= 3);
    
      if (nclears > 9)
        return memset (d, 0, clearsize);
    
      else
        {
          *(d + 0) = 0;
          *(d + 1) = 0;
          *(d + 2) = 0;
          if (nclears > 4)
            {
              *(d + 3) = 0;
              *(d + 4) = 0;
              if (nclears > 6)
                {
                  *(d + 5) = 0;
                  *(d + 6) = 0;
                  if (nclears > 8)
                    {
                      *(d + 7) = 0;
                      *(d + 8) = 0;
                    }
                }
            }
        }
    
      return mem;
    }
    View Code

    比较复杂, 从中就摘录下面 几行帮助理解

     ...
    
      /* size_t is unsigned so the behavior on overflow is defined.  */
      bytes = n * elem_size;
    
    ...
    
          return memset (mem, 0, sz);
    ...
    
      if (av != NULL)
        (void) mutex_unlock (&av->mutex);
    
    ...

    实现起来很复杂, 主要围绕性能考虑,  重新套了一份内存申请的思路. 上面摘录的三点, 能够表明, 从功能上malloc 可以替代 calloc.

    最后表明 glibc(gcc) 源码上是线程安全的.后面会分析上面接口的具体实现, 并测试个demo.

    正文 - 开始实现, 运行demo

      首先看具体实现, scalloc.c 

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    // 标识枚举
    typedef enum {
        HF_Alloc,
        HF_Free
    } header_e;
    
    // 每次申请内存的[16-24]字节额外消耗, 用于记录内存申请情况
    struct header {
        header_e flag;        // 当前内存使用的标识
        int line;            // 申请的文件行
        const char * file;    // 申请的文件名
        const char * func;    // 申请的函数名
    };
    
    // 内部使用的malloc, 返回内存会用''初始化
    void * 
    sm_malloc_(size_t sz, const char * file, int line, const char * func) {
        struct header * ptr = malloc(sz + sizeof(struct header));
        // 检查内存分配的结果
        if(NULL == ptr) {
            fprintf(stderr, "_header_get >%s:%d:%s< alloc error not enough memory start fail!
    ", file, line, func);
            exit(EXIT_FAILURE);
        }
    
        ptr->flag = HF_Alloc;
        ptr->line = line;
        ptr->file = file;
        ptr->func = func;
        memset(++ptr, 0, sz);
        return ptr;
    }
    
    // 得到申请内存的开头部分, 并检查
    static struct header * _header_get(void * ptr, const char * file, int line, const char * func) {
        struct header * node = (struct header *)ptr - 1;
        // 正常情况直接返回
        if(HF_Alloc != node->flag) {    
            // 异常情况, 内存多次释放, 和内存无效释放
            fprintf(stderr, "_header_get free invalid memony flag %d by >%s:%d:%s<
    ", node->flag, file, line, func);
            exit(EXIT_FAILURE);
        }
        return node;
    }
    
    // 内部使用的realloc
    void * 
    sm_realloc_(void * ptr, size_t sz, const char * file, int line, const char * func) {
        struct header * node , * buf;
        if(NULL == ptr)
            return sm_malloc_(sz, file, line, func);
        
        // 合理内存分割
        node = _header_get(ptr, file, line, func);
        node->flag = HF_Free;
        // 构造返回内存信息
        buf = realloc(node, sz + sizeof(struct header));
        buf->flag = HF_Alloc;
        buf->line = line;
        buf->file = file;
        buf->func = func;
    
        return buf + 1;
    }
    
    // 内部使用的free, 每次释放都会打印日志信息
    void 
    sm_free_(void * ptr, const char * file, int line, const char * func) {
        if(NULL !=  ptr) {
            // 得到内存地址, 并且标识一下, 开始释放
            struct header * node = _header_get(ptr, file, line, func);
            node->flag = HF_Free;
            free(node);
        }
    }

    这里主要围绕 1 插入申请内存头

    // 每次申请内存的[16-24]字节额外消耗, 用于记录内存申请情况
    struct header {
        header_e flag;        // 当前内存使用的标识
        int line;            // 申请的文件行
        const char * file;    // 申请的文件名
        const char * func;    // 申请的函数名
    };

    围绕2 在 malloc 时候 和 _header_get 得到头检查 时候, 直接exit.

    思路很清晰基础, 假如这代码跑在64位机器上,  线上一个服务器, 运行时创建100000万个malloc对象 .

    100000 * (4 + 4 + 8 +8)B / 1024 / 1024 = 2.288818359375 MB 的内存损耗. 还有一次取内存检查的性能损耗.

    这些是可以接受的, 特殊时候可以通过打印的信息, 判断出内存调用出错的位置.

    扯一点 这里用了枚举 方便和宏区分

    // 标识枚举
    typedef enum {
        HF_Alloc,
        HF_Free
    } header_e;

    其实宏和枚举在C中基本一样, 只能人为的添加特殊规范, 约定二者区别. 宏用的太多, 复杂度会越来越大. 双刃剑. 

    下面我们测试一下 演示demo main.c

    #include <stdio.h>
    #include "scalloc.h"
    
    /*
     * 测试内存管理, 得到内存注册信息
     */
    int main(int argc, char * argv[]) {
        int * piyo = malloc(10);
        free(piyo);
        
        puts("start testing...");
    
        // 简单测试一下
        free(piyo);
    
        getchar();
        return 0;
    }

    演示结果

    到这里 基本思路都已经介绍完毕了. 主要核心就是偷梁换柱.

    后记 - ~○~

      错误是难免的, 有问题再打补丁修复. 欢迎将这思路用在自己的项目构建中.

  • 相关阅读:
    【006期】JavaSE面试题(六):泛型
    【005期】JavaSE面试题(五):String类
    【004期】JavaSE面试题(四):JavaSE语法(3)
    【003期】JavaSE面试题(三):JavaSE语法(1)
    【002期】JavaSE面试题(二):基本数据类型与访问修饰符
    【001期】 | JavaSE面试题(一):面向对象
    【000期】Java最全面试题库思维导图
    LeetCode 1. 两数之和(python3)实现
    虚拟网络学习笔记一:Linux虚拟网络
    快速排序(python实现)
  • 原文地址:https://www.cnblogs.com/life2refuel/p/5744370.html
Copyright © 2020-2023  润新知