一种进程间共享内存的实现

  • 一、进程间共享内存的必要

进程间共享内存的作用是用于传递实时的数据,尤其是复杂多变的数据。整数型、字符型等简单数据能够很方便的在进程间传递,并且很方便的使用,例如windows上的PostMessage等函数。但复杂数据传递时比较麻烦,尤其是数据类型中含有指针的时候。

  • 二、进程间共享内存使用相同的基地址

传统的共享内存,使用时都是申请一段内存空间,然后放入各自的地址空间中。这种方式比较适用于传递二进制数据,例如一段声音或者视频或者其他数据。但如果你传递的数据是带有格式信息的一个结构体,这种方式使用起来就很麻烦了。例如你传递的结构体如下:

struct _data {
    int member_size;
    char* member_name;
    char* member_data;
};

当你将该结构体放入传统共享内存空间时,由于member_name和member_data是指针,数据存放在另外的地址中,你同时需要将两个指针指向的数据也存储到共享内存区域。但别的进程读取member_name和member_data时,它们指向的数据区域在该进程中并不存在或者不可访问。所以需要在存入共享内存时将member_name和member_data的地址数据转换为与基地址的差值。同时在使用时将该地址转换回去。当多个进程使用该结构体时,还需要先将数据复制到自己的堆栈空间,然后再使用。

所以如果所有使用该共享内存的进程,映射共享内存的基地址都是相同的,则不需要该转换步骤,可以直接使用。

庆幸的是,MapViewOfFileEx和shmat函数提供该功能。

LPVOID MapViewOfFileEx(
  [in]           HANDLE hFileMappingObject,
  [in]           DWORD  dwDesiredAccess,
  [in]           DWORD  dwFileOffsetHigh,
  [in]           DWORD  dwFileOffsetLow,
  [in]           SIZE_T dwNumberOfBytesToMap,
  [in, optional] LPVOID lpBaseAddress
);

或者

void *shmat(int shmid, const void *shmaddr, int shmflg)
  • 三、共享内存的管理

进程间共享内存,如果管理数据放在各自的进程空间,则无法达到进程间共享的目的,只能用于同一进程的不同线程间共享。所以我们需要将管理数据也放在共享内存区域中。可以在共享内存区域内开辟一块空间,用于存放管理数据。由于是存放在共享空间中,不同进程间申请内存时,可以有效的同步。只需要用一个信号量(互斥量),控制不同进程对共享内存的访问即可。由于基地址一样,共享内存管理数据也存储在共享内存区域,所以我们需要一种方便的管理方法,直接访问内存数据即可确定使用的共享内存空间。

  • 四、一种简单表示的内存池

我们设计一种简单表示的内存池,一个内存池至少需要占用4096字节的空间。内存池中的每个节点大小为2的幂数,但节点占用的空间超过4096字节时,占用空间为4096的整数倍。虽然这样申请的内存空间存在浪费的现象,但计算方便。

下面结构体是内存池节点管理数据,空间大小为24字节。

struct memory_pool_node_t {
    int32_t memory_block_size;            // 内存池中的内存块节点大小
    int16_t max_memory_block_count;       // 剩余内存池中的内存块节点数量
    int16_t left_memory_block_count;      // 剩余内存池中的内存块节点数量
    union {
        memory_block_t *p_head;           // 内存块节点首地址
        int8_t reserved1[8];              // 用于内存对齐
    };
    union {
        memory_block_t *p_idle;           // 下一个空闲内存块节点
        int8_t reserved2[8];              // 用于内存对齐
    };
};

下面结构体是内存块节点管理数据,空间大小同样占用24字节

struct memory_block_t {
    union {
        int8_t reserved[24];             // 对齐到24字节
        struct {
            memory_block_t *p_pre;       // 前一个节点指针
            uint8_t used;                // 所属内存池节点是否使用
            memory_block_t *p_next;      // 下一个节点指针
            uint32_t memory_pool_index;  // 所属内存池节点索引
        };
    };
    //    int8_t data[0];
};

同时,我们还需要一个结构体,用来表示空闲的内存池节点

struct idle_memory_pool_node_t {
    int32_t count;       // 负数表示 空闲内存池节点数量,包含当前节点和后面的节点
    int32_t index;       // 当前内存池所属节点索引
    union {
        idle_memory_pool_node_t *p_next;   // 下一个空闲内存池节点
        int8_t reserved1[8];               // 用于内存对齐
    };
    union {
        idle_memory_pool_node_t *p_pre;    // 上一个空闲内存池节点
        int8_t reserved2[8];               // 用于内存对齐
    };
};
  • 五、内存池与对应的内存空间

我们先定义需要申请0x100000(1048576)字节的共享内存,基地址我们选择0x50000000。

首先,我们需要计算下一个4K空间中能够容纳多少个内存池节点。

int32_t pool_count_per_page = 4K/ sizeof(memory_pool_node_t);

计算得到一个4K页面中能够容纳170个内存池节点。

然后计算下该共享空间中有多少个4K。

0x100000 / 0x1000(4K) = 0x100(256)

所以,如果要容纳256个内存池节点,我们需要占用多少个4K。

256 + (170 -1) / 170 = 2

这样,我们只需要2个4K空间,就能将全部的内存池节点保存起来。

为了便于处理与数据隔离我们多保留一个4K空间,即需要3个4K空间。从共享内存空间中画出前面3个4K(即12288字节)用作管理空间,其余的的作为数据空间。

另外,我们用角标的方式将内存池与内存池对应的内存空间做一个关联。

即第1个内存池节点表示从基地址开始的第2个4K空间,第2个内存池节点表示从基地吃开始的第2个4K空间。若要查找对应的空间,只需要知道内存池节点是第几个就可以了。

由于前3个4K空间用于了内存管理,所以内存池从第4个开始使用。这样我们就建立了内存池和对应存储空间的关联关系。

  • 六、内存池与内存块

一个内存池要管理1到n个内存块。内存块的相关定义见章节4。一个内存块的前24个字节代表了该内存块的信息,同时一个内存块的大小需要符合2的最小幂数倍或者4K的整数倍。内存块的最小大小是(24 + 1)的2的最小幂数倍,即32(2的5次幂)字节。所以存在以下两种情况:

  • 内存块大小不大于4K

        此时用一个内存池可以表示该内存块,一共可能有1到128个内存块。

  • 内存块大小大于4K

        此时用2到n个内存池表示该内存块。如上图所示,内存池4表示该内存块,但是内存池5和6被占用掉了,不能使用,因为他们所指向的空间被内存池4使用了。

  • 七、内存块

一个内存块的前24字节被内存块信息占用,内存块信息包含

  1. 前一个内存块节点指针
  2. 内存块是否被使用
  3. 下一个内存块节点指针
  4. 所属内存池的节点索引。

内存块的两个指针用于形成一个内存链表,例如章节2中所示的结构体,当结构体的指针释放时,结构体中的两个指针同时释放,将内存块还给内存池。

  • 八、内存池

一个内存池包含如下信息:

  1. 该池中内存块的节点大小,至少32
  2. 该池中内存块的总数
  3. 该池中剩余的内存块节点数
  4. 该池的内存首地址
  5. 该池的空闲块地址

当进程申请空间时,先查找内存池列表中是否有空间大小符合的内存池,并检查该池是否有剩余空间。如果有,将该池的空闲块返回,并更新空闲块地址,同时根据传入的参数,将内存块链表更新。如果无可用空间,将该指针置为null。

当进程申请的空间找不到符合大小的时候,需要生成新的内存池。有两种情况:从未使用的空间生成或者从中间被释放的空间生成。

  • 九、内存块的释放

当一个内存块被释放时,需要检查其有没有附属的内存块,如果有也需要释放。

内存块释放时同时需要更新对应的内存池信息,如果内存池中所有的内存块都释放了,需要将空间还给内存池列表中。这时我们建立一个空闲内存池节点链表。空闲池节点结构体同样为24字节,与一个内存池结构体大小相当。

空闲池结构体包含如下信息

  1. 当前空闲池和之后空闲池数量的总和(连续空间内),用负数表示,以便和内存池区分。
  2. 当前空闲池所属节点索引
  3. 下一个空闲池地址
  4. 上一个空闲池地址。

由于第一个内存池(空闲池)不会被使用,所以我们用该池作为空闲池的链表头。建立新的内存池前,先从该链表查找可用的空间,如果找不到可用空间或者该链表为空时,才从未使用的内存池节点中建立一个新的。

  • 十、相关代码

https://gitee.com/CalmStorm/mempool

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值