libuv分配了内存缓冲区重用技术

时间:2015-02-14 01:44:57

标签: c memory memory-management libuv

我正在使用libuv用于我的广泛网络交互应用程序,我担心重新使用已分配内存的哪些技术可以同时高效且安全地使用libuv回调执行。

在基本层,暴露给libuv用户,需要在设置句柄阅读器的同时指定缓冲区分配回调:

UV_EXTERN int uv_read_start(uv_stream_t*, uv_alloc_cb alloc_cb, uv_read_cb read_cb);

其中uv_alloc_cb

typedef void (*uv_alloc_cb)(uv_handle_t* handle, size_t suggested_size, uv_buf_t* buf);

但问题在于:每次通过句柄传递新消息时(例如,收到来自uv_udp_t句柄的每个UDP数据报)都会调用此内存分配回调,并为每个句柄直接分配新缓冲区传入的UDP数据报看起来非常非内存。

所以我要求在可能的情况下重新使用相同的已分配内存的公共C技术(可能在libuv回调系统引入的延迟执行上下文中)。

另外,如果可能的话,我想保持便携式窗户。

注意:

  • 我知道这个问题:Does libuv provide any facilities to attach a buffer to a connection and re use it;它接受的答案并没有回答如何使用libuv实际进行内存分配,除了说明静态分配的缓冲区是不行的。特别是,它没有覆盖安全性(在同一缓冲区上使用延迟写入回调,可以与libuv主循环的多次迭代中的另一个读取回调调用重叠)连接到句柄的缓冲区方面(通过包装器结构或句柄 - > ;数据上下文)。
  • 阅读http://nikhilm.github.io/uvbook/filesystem.html,我注意到了uvtee/main.c - Write to pipe下的以下短语:

      

    我们制作一个副本,这样我们就可以将两个缓冲区中的两个缓冲区彼此独立地释放到write_data中。虽然这样的演示程序可以接受,但您可能需要更智能的内存管理,例如引用计数缓冲区或任何主要应用程序中的缓冲池。

    但我无法找到任何涉及libuv缓冲区引用计数的解决方案(如何正确执行?)或libuv环境中缓冲池的显式示例(是否有任何库?)。

2 个答案:

答案 0 :(得分:17)

我想分享我自己解决这个问题的经验。我能感受到你的痛苦和困惑,但实际上,如果你知道自己在做什么,考虑到你拥有的各种选择,实施一个有效的解决方案并不是一件难事。

目的

  1. 实施一个能够执行两项操作的缓冲池 - 获取发布

  2. 基本汇集策略:

    • 获取从池中撤回缓冲区,有效地将缓冲区的可用数量减少1;
    • 如果没有可用的缓冲区,则会出现两个选项:
      • 扩展池并返回一个新创建的缓冲区;或
      • 创建并返回虚拟缓冲区(如下所述)。
    • 发布会将缓冲区返回池中。
  3. 池可以是固定的或可变的。 “变量”表示最初有M个预先分配的缓冲区(例如零),并且池可以按需增长到N. “Fixed”表示所有缓冲区在创建池时预先分配(M = N)。

  4. 实现一个获取libuv缓冲区的回调。

  5. 在内存不足的情况下,除了内存不足之外,不允许无限池增长仍然使池功能正常。

  6. 实施

    现在,让我们详细了解所有这些。

    池结构:

    #define BUFPOOL_CAPACITY 100
    
    typedef struct bufpool_s bufpool_t;
    
    struct bufpool_s {
        void *bufs[BUFPOOL_CAPACITY];
        int size;
    };
    

    size是当前的池大小。

    缓冲区本身是一个前缀为以下结构的内存块:

    #define bufbase(ptr) ((bufbase_t *)((char *)(ptr) - sizeof(bufbase_t)))
    #define buflen(ptr) (bufbase(ptr)->len)
    
    typedef struct bufbase_s bufbase_t;
    
    struct bufbase_s {
        bufpool_t *pool;
        int len;
    };
    

    len是缓冲区的长度,以字节为单位。

    新缓冲区的分配如下所示:

    void *bufpool_alloc(bufpool_t *pool, int len) {
        bufbase_t *base = malloc(sizeof(bufbase_t) + len);
        if (!base) return 0;
        base->pool = pool;
        base->len = len;
        return (char *)base + sizeof(bufbase_t);
    }
    

    请注意,返回的指针指向标题之后的下一个字节 - 数据区域。这允许具有缓冲区指针,就好像它们是通过标准调用malloc分配的。

    解除分配恰恰相反:

    void bufpool_free(void *ptr) {
        if (!ptr) return;
        free(bufbase(ptr));
    }
    

    libuv的分配回调如下所示:

    void alloc_cb(uv_handle_t *handle, size_t size, uv_buf_t *buf) {
        int len;
        void *ptr = bufpool_acquire(handle->loop->data, &len);
        *buf = uv_buf_init(ptr, len);
    }
    

    您可以在此处看到alloc_cb从循环上的用户数据指针获取缓冲池的指针。这意味着缓冲池应该在使用之前附加到事件循环。换句话说,您应该在创建循环时初始化池并将其指针分配给data字段。如果您已在该字段中保留其他用户数据,则只需扩展您的结构。

    虚拟缓冲区是一个假缓冲区,这意味着它不是源自池,但仍然完全正常。虚拟缓冲区的目的是使整个事物在极端的池饥饿情况下工作,即当获得所有缓冲区并且需要另一个缓冲区时。根据我的研究,在所有现代操作系统上快速完成大约8Kb的小块内存分配 - 这非常适合虚拟缓冲区的大小。

    #define DUMMY_BUF_SIZE 8000
    
    void *bufpool_dummy() {
        return bufpool_alloc(0, DUMMY_BUF_SIZE);
    }
    

    获取操作:

    void *bufpool_acquire(bufpool_t *pool, int *len) {
        void *buf = bufpool_dequeue(pool);
        if (!buf) buf = bufpool_dummy();
        *len = buf ? buflen(buf) : 0;
        return buf;
    }
    

    发布操作:

    void bufpool_release(void *ptr) {
        bufbase_t *base;
        if (!ptr) return;
        base = bufbase(ptr);
        if (base->pool) bufpool_enqueue(base->pool, ptr);
        else free(base);
    }
    

    这里有两个功能 - bufpool_enqueuebufpool_dequeue。基本上,他们执行所有池的工作。

    在我的例子中,在上述情况之上有一个O(1)缓冲区索引队列,它允许我更快地跟踪缓冲区的索引来跟踪池的状态。没有必要像我一样极端,因为池的最大大小是有限的,因此任何数组搜索也将在时间上保持不变。

    在最简单的情况下,您可以在bufs结构中的bufpool_s数组中将这些函数实现为纯线性搜索器。例如,如果获取缓冲区,则搜索第一个非NULL点,保存指针并在该点中放置NULL。下次释放缓冲区时,搜索第一个NULL点并将其指针保存在那里。

    池内部如下:

    #define BUF_SIZE 64000
    
    void *bufpool_grow(bufpool_t *pool) {
        int idx = pool->size;
        void *buf;
        if (idx == BUFPOOL_CAPACITY) return 0;
        buf = bufpool_alloc(pool, BUF_SIZE);
        if (!buf) return 0;
        pool->bufs[idx] = 0;
        pool->size = idx + 1;
        return buf;
    }
    
    void bufpool_enqueue(bufpool_t *pool, void *ptr) {
        int idx;
        for (idx = 0; idx < pool->size; ++idx) {
            if (!pool->bufs[idx]) break;
        }
        assert(idx < pool->size);
        pool->bufs[idx] = ptr;
    }
    
    void *bufpool_dequeue(bufpool_t *pool) {
        int idx;
        void *ptr;
        for (idx = 0; idx < pool->size; ++idx) {
            ptr = pool->bufs[idx];
            if (ptr) {
                pool->bufs[idx] = 0;
                return ptr;
            }
        }
        return bufpool_grow(pool);
    }
    

    正常缓冲区大小为64000字节,因为我希望它能够轻松地适应64Kb块及其标题。

    最后,初始化和去初始化例程:

    void bufpool_init(bufpool_t *pool) {
        pool->size = 0;
    }
    
    void bufpool_done(bufpool_t *pool) {
        int idx;
        for (idx = 0; idx < pool->size; ++idx) bufpool_free(pool->bufs[idx]);
    }
    

    请注意,此实现仅为说明目的而简化。这里没有池缩减政策,而在现实世界的情况下,它很可能是必需的。

    用法

    您现在应该可以编写libuv回调:

    void read_cb(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) {
        /* ... */
        bufpool_release(buf->base); /* Release the buffer */
    }
    

    循环初始化:

    uv_loop_t *loop = malloc(sizeof(*loop));
    bufpool_t *pool = malloc(sizeof(*pool));
    uv_loop_init(loop);
    bufpool_init(pool);
    loop->data = pool;
    

    操作:

    uv_tcp_t *tcp = malloc(sizeof(*tcp));
    uv_tcp_init(tcp);
    /* ... */
    uv_read_start((uv_handle_t *)tcp, alloc_cb, read_cb);
    

    更新(2016年8月2日)

    在根据请求的大小获取缓冲区时使用自适应策略也是一个好主意,并且仅在请求大块数据时返回池化缓冲区(例如,所有读取和长写入) 。对于其他情况(例如大多数写入),返回虚拟缓冲区。这将有助于避免浪费池缓冲区,同时保持可接受的分配速度。例如:

    void alloc_cb(uv_handle_t *handle, size_t size, uv_buf_t *buf) {
        int len = size; /* Requested buffer size */
        void *ptr = bufpool_acquire(handle->loop->data, &len);
        *buf = uv_buf_init(ptr, len);
    }
    
    void *bufpool_acquire(bufpool_t *pool, int *len) {
        int size = *len;
        if (size > DUMMY_BUF_SIZE) {
            buf = bufpool_dequeue(pool);
            if (buf) {
                if (size > BUF_SIZE) *len = BUF_SIZE;
                return buf;
            }
            size = DUMMY_BUF_SIZE;
        }
        buf = bufpool_alloc(0, size);
        *len = buf ? size : 0;
        return buf;
    }
    

    P.S。 buflen不需要此代码段。

答案 1 :(得分:1)

如果你在Linux上,那么你很幸运。 Linux内核通常默认使用所谓的SLAB Allocator。这个分配器的优点是它通过维护可循环块的池来减少实际的内存分配。对您来说意味着,只要您始终分配相同大小的缓冲区(理想情况下为PAGE_SIZE的pow2大小),您就可以在Linux上使用malloc()

如果您不在Linux(或FreeBSD或Solaris)上或开发跨平台应用程序,您可以考虑使用glib及其Memory Slices作为SLAB分配器的跨平台实现。它在支持它的平台上使用本机实现,因此在Linux上使用它将不会带来任何好处(我自己运行了一些测试)。我确定还有其他库可以做同样的事情,或者你可以自己实现它。