让程序轻松逾越堆与共享内存的阻碍,轻松实现零拷贝IPC

什么是级联内存池?
如何让程序轻松逾越堆与共享内存的阻碍?
如何轻松实现零拷贝IPC?
本文给你带来不一样的程序设计视线

以前的文章中,码哥介绍过利用内存池有哪些长处,咱们列举如下:

  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、操作系统、编译器版本和头文件下编译生成,那么咱们甚至不须要对传递的音讯做任何序列化就能够间接拜访。

感激浏览!