V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
monkeyNik
V2EX  ›  程序员

C 语言级联内存池之轻松零拷贝 IPC

  •  1
     
  •   monkeyNik ·
    Water-Melon · 2022-08-20 12:40:28 +08:00 · 884 次点击
    这是一个创建于 630 天前的主题,其中的信息可能已经有所发展或是发生改变。

    以前的文章中,笔者介绍过利用内存池有哪些优点,我们列举如下:

    1. 集中释放,便于编码逻辑,集中释放减少空洞
    2. 特定的分配释放算法及池结构,可以借助指令预取及 cache 命中来提升性能
    3. 延迟释放闲置内存块,通过提升复用率来提升分配效率

    因此,本文不再赘述上面的部分。

    这篇文章我们介绍一种级联结构内存池,该内存池的实现可以参考:Github: Melon 库

    池结构的模型大致如下:

                   -------------
                   |    父池    |
                   -------------
                     |     |
             -----------  ---------
            | 子池 1     | | 子池 2   |
             -----------  ---------
              |      |     ....
         --------   --------
        |  孙池 1  | | 孙池 2  |
         --------   --------
    

    这种结构是什么意思呢?

    子池所使用的内存及其所能分配的内存均来自于父池。故此,孙池的内存也是由其依赖的子池而来的。

    在 Melon 库的内存池组件中,内存的来源有三处:

    1. 堆内存(或者匿名映射区),即 malloc 库所提供
    2. 共享内存(主要是用于主子进程间的共享,因此是 mmap 的匿名映射区共享)
    3. 其他内存池管理的内存

    我们先来看一个简单的内存池使用的例子:

    示例一,Melon 常规内存池使用举例

    #include <stdio.h>
    #include <stdlib.h>
    #include "mln_core.h"
    #include "mln_log.h"
    #include "mln_alloc.h"
    
    int main(int argc, char *argv[])
    {
        char *p;
        mln_alloc_t *pool;
        struct mln_core_attr cattr;
    
        /* libmelon init begin*/
        cattr.argc = argc;
        cattr.argv = argv;
        cattr.global_init = NULL;
        cattr.master_process = NULL;
        cattr.worker_process = NULL;
        if (mln_core_init(&cattr) < 0) {
            fprintf(stderr, "init failed\n");
            return -1;
        }
        /* libmelon init end */
    
        pool = mln_alloc_init(NULL);
        if (pool == NULL) {
            mln_log(error, "pool init failed\n");
            return -1;
        }
    
        p = (char *)mln_alloc_m(pool, 6);
        if (p == NULL) {
            mln_log(error, "alloc failed\n");
            return -1;
        }
    
        memcpy(p, "hello", 5);
        p[5] = 0;
        mln_log(debug, "%s\n", p);
    
        mln_alloc_destroy(pool);
    
        return 0;
    }
    

    在这个例子中,我们创建了一个堆内存池,并且利用该内存池分配了 6 个字节的内存区用于写入"hello"字符串。

    这个例子很常规,与很多常见开源软件中的用法类似(例如 nginx )。

    下面看一个级联使用的例子:

    示例二,堆级联内存池

    #include <stdio.h>
    #include <stdlib.h>
    #include "mln_core.h"
    #include "mln_log.h"
    #include "mln_alloc.h"
    
    int main(int argc, char *argv[])
    {
        char *p;
        mln_alloc_t *pool, *parent;
        struct mln_core_attr cattr;
    
        /* libmelon init begin*/
        cattr.argc = argc;
        cattr.argv = argv;
        cattr.global_init = NULL;
        cattr.master_process = NULL;
        cattr.worker_process = NULL;
        if (mln_core_init(&cattr) < 0) {
            fprintf(stderr, "init failed\n");
            return -1;
        }
        /* libmelon init end */
    
        parent = mln_alloc_init(NULL);
        if (parent == NULL) {
            mln_log(error, "parent pool init failed\n");
            return -1;
        }
    
        pool = mln_alloc_init(parent);
        if (pool == NULL) {
            mln_log(error, "pool init failed\n");
            return -1;
        }
    
        p = (char *)mln_alloc_m(pool, 6);
        if (p == NULL) {
            mln_log(error, "alloc failed\n");
            return -1;
        }
    
        memcpy(p, "hello", 5);
        p[5] = 0;
        mln_log(debug, "%s\n", p);
    
        mln_alloc_destroy(parent);
    
        return 0;
    }
    

    可以看到,我们先从堆内存上创建了一个内存池名为 parent ,然后将其作为内存池 pool 的上层父池。在 pool 池创建后,我们从 pool 中分配一个 6 字节内存区,并写入 hello 字符串。

    此时,内存区实际上是由父池 parent 分配而来,并在子池 pool 中被管理使用。换言之,这块内存区既被子池 pool 管理,也被父池 parent 管理。

    最后,我们直接将父池 parent 进行了销毁,那么连带子池 pool 也就一同销毁了。

    到这里,可能有的读者会问,这么多此一举的意义是什么?

    我们可以通过下面一个例子来寻找答案:

    示例三,共享内存级联池

    #include <stdio.h>
    #include <stdlib.h>
    #include "mln_core.h"
    #include "mln_log.h"
    #include "mln_alloc.h"
    #include "mln_defs.h"
    
    int func_lock(void *locker)
    {
        printf("lock\n");
        MLN_LOCK((mln_lock_t *)locker);
        return 0;
    }
    
    int func_unlock(void *locker)
    {
        printf("unlock\n");
        MLN_UNLOCK((mln_lock_t *)locker);
        return 0;
    }
    
    int main(int argc, char *argv[])
    {
        char *p;
        mln_lock_t lock;
        mln_alloc_t *pool, *parent;
        struct mln_core_attr cattr;
        struct mln_alloc_shm_attr_s sattr;
    
        /* libmelon init begin*/
        cattr.argc = argc;
        cattr.argv = argv;
        cattr.global_init = NULL;
        cattr.master_process = NULL;
        cattr.worker_process = NULL;
        if (mln_core_init(&cattr) < 0) {
            fprintf(stderr, "init failed\n");
            return -1;
        }
        /* libmelon init begin*/
    
        /* create a shared memory pool*/
        MLN_LOCK_INIT(&lock);
        sattr.size = 10 * 1024 * 1024;
        sattr.locker = &lock;
        sattr.lock = func_lock;
        sattr.unlock = func_unlock;
        parent = mln_alloc_shm_init(&sattr);
        if (parent == NULL) {
            mln_log(error, "parent pool init failed\n");
            return -1;
        }
    
        pool = mln_alloc_init(parent);
        if (pool == NULL) {
            mln_log(error, "pool init failed\n");
            return -1;
        }
    
        p = (char *)mln_alloc_m(pool, 6);
        if (p == NULL) {
            mln_log(error, "alloc failed\n");
            return -1;
        }
    
        memcpy(p, "hello", 5);
        p[5] = 0;
        mln_log(debug, "%s\n", p);
    
        mln_alloc_destroy(parent);
    
        return 0;
    }
    

    读者可以对比示例二和示例三的差异。

    在这个例子中,我们将 parent 初始化成一个基于共享内存的内存池。因为涉及进程间资源争抢,因此需要给出内存池所使用的锁资源及其操作原语(即加解锁回调)。此处额外说一句,Melon 的共享内存使用的锁是由使用者自行定义的,而不是强制配备互斥量或者读写锁之类的。

    随后,由子池 pool 分配了一个 6 字节内存区,这个内存区实际上是由共享内存中而来。

    到此,不知道读者是否明白级联内存池的一部分用意呢?

    即:如果我们的整个程序的动态内存分配完全依赖于内存池的分配的话,那么只需要简单地将父池改为基于共享内存的内存池,就可以完成程序从堆到共享内存的迁移了。

    事实上,在 Melon 中,使用级联结构操作共享内存有如下好处:

    1. 子池仍保持了集中释放的优势
    2. 共享内存与堆内存的管理和分配策略不同,因此可以结合两者策略来提升共享内存的使用效率
    3. 将父池作为隔离层,可以让程序轻松在堆与共享内存之间做切换,而不必修改其余内存分配的代码

    最后一个问题,我们将内存全部迁移到共享内存的意义是什么呢?

    进程间零拷贝 IPC

    当一个进程 A 中维护的信息需要与另一个进程 B 进行交换和共享的时候,我们只需要将这些信息由 A 进程写入共享内存中一次,B 进程就可以直接访问。而不需要将数据从 A 的地址空间拷贝到内核缓冲区,再由内和缓冲区拷贝到进程 B 的用户态缓冲区,这样的频繁复制。

    甚至,如果进程 A 和 B 的可执行程序在同一 CPU 、操作系统、编译器版本和头文件下编译生成,那么我们甚至不需要对传递的消息做任何序列化就可以直接访问。

    感谢阅读!

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5505 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 05:53 · PVG 13:53 · LAX 22:53 · JFK 01:53
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.