自动驾驶中间件iceoryx - 内存与 Chunk 管理(一)

本章深入讲解 iceoryx 在实现零拷贝进程间通信时的内存管理机制。内容涵盖共享内存的架构与布局、MePoo(内存池集合)、Chunk(数据块)头与生命周期、分配策略(包括 BumpAllocator)、以及 RouDi 与参与进程之间如何协调内存访问与通知。由于内容较多,分为三次介绍。

学习目标:

  • 掌握 MePoo 的结构以及如何计算 MePoo 的总占用(requiredFullMemorySize())。
  • 跟踪创建并映射共享内存的代码路径(shm_openftruncatemmap)。
  • 理解 Chunk 的生命周期:分配(allocate)、借出(loan)、发布(publish)与释放(free)。
  • 学会使用 C/C++ API 创建内存池、分配 Chunk 并处理运行时错误。

4.1 内存角色与设计目标

iceoryx 以高性能与确定性为设计目标,使用共享内存作为数据平面以实现零拷贝通信。为支持这一点,系统分为三个逻辑平面(详见第三章):

  • 控制平面:用于 RouDi 与参与者之间的元数据与管理消息交换(在 Linux 上通常通过 Unix Domain Sockets 实现)。控制平面不承载数据块本身。
  • 通知平面:用于唤醒或通知订阅者(通常使用 unnamed semaphore 或条件通知器)。
  • 数据平面:存放实际数据的共享内存区域(MePoo)。

设计约束包括:

  • 启动时要预分配内存并完成分区,避免运行时进行不可预测的动态分配。
  • Chunk 的分配与释放需要尽量无锁或使用轻量原语,以满足实时系统的需求。
  • 共享内存内不能使用进程间的绝对指针,使用相对指针(RelativePointer)以保证跨进程地址可重定位。

4.2 Segment 与 MemPool(内存池)

4.2.0 术语说明

iceoryx 的内存管理采用两层结构。在深入细节之前,需要先理解几个容易混淆的术3语。

在代码和文档中经常出现 "MePoo" 这个词,它是 Mem ory Pool 的缩写,而不是一个独立的架构层级。相关的类和结构体包括:

  • MePooConfig:配置结构,用于定义多个内存池的参数(大小和数量)
  • MePooSegment:共享内存段类,封装了共享内存对象和内存管理器
  • MemoryManager:内存管理器,负责创建和管理多个 MemPool 实例
  • MemPool:内存池类,管理固定大小的 Chunk

类关系图
封装
封装
使用配置
管理
读取配置
1 0..* MePooConfig
+vector<Entry> m_mempoolConfig
+addMemPool(Entry entry)
MePooSegment
-PosixSharedMemoryObject m_sharedMemoryObject
-MemoryManager m_memoryManager
-PosixGroup m_readerGroup
-PosixGroup m_writerGroup
+getMemoryManager() : MemoryManager
+getSegmentSize() : uint64_t
MemoryManager
-vector<MemPool> m_memPoolVector
+configureMemoryManager(MePooConfig)
+addMemPool(...)
+getChunk(ChunkSettings) : SharedChunk
MemPool
-uint32_t m_chunkSize
-uint32_t m_chunkCount
-MpmcLoFFLi m_freeIndices
-Atomic<uint32_t> m_usedChunks
+getChunk() : void
+freeChunk(void*)
PosixSharedMemoryObject
配置层:定义内存池参数
容器层:封装共享内存和管理器
管理层:统一管理多个内存池
执行层:管理固定大小的 Chunk

关系说明

  • 组合关系(*--)MePooSegment 拥有 PosixSharedMemoryObjectMemoryManager,它们的生命周期绑定
  • 依赖关系(...>)MePooSegmentMemoryManager 在初始化时读取 MePooConfig 的配置
  • 聚合关系(*--)MemoryManager 管理多个 MemPool 实例

4.2.1 两层架构

iceoryx 的内存管理实际上只有两层:Segment(共享内存段)和 MemPool(内存池)。

Segment(共享内存段)

Segment 是物理容器,对应一个独立的 POSIX 共享内存对象。在 TOML 配置文件中,每个 [[segment]] 块定义一个 Segment。

创建过程

Segment 通过 shm_open() 系统调用创建,生成一个共享内存文件(如 /dev/shm/iceoryx_segment_0)。每个 Segment 可以设置独立的访问权限,包括 reader 组和 writer 组,这为多租户场景下的安全隔离提供了基础。

封装实现

在代码中,Segment 由 MePooSegment 模板类实例化。所谓"封装"是指这个类将底层的共享内存对象和内存管理器组合在一起,作为成员变量:

cpp 复制代码
template <typename SharedMemoryObjectType, typename MemoryManagerType>
class MePooSegment {
    SharedMemoryObjectType m_sharedMemoryObject;  // 封装的共享内存对象
    MemoryManagerType m_memoryManager;            // 封装的内存管理器
    PosixGroup m_readerGroup;                     // reader 访问组
    PosixGroup m_writerGroup;                     // writer 访问组
};

这里的 m_sharedMemoryObject 成员封装了 PosixSharedMemoryObject,它负责:

  1. 调用 shm_open() 创建共享内存
  2. 调用 ftruncate() 设置大小
  3. 调用 mmap() 映射到进程地址空间
  4. 提供 getBaseAddress() 方法获取映射地址
  5. 在析构时自动 munmap()shm_unlink()

MePooSegment 则在更高层次上:

  • 根据配置计算所需的共享内存大小
  • 设置 POSIX ACL 访问权限(reader/writer 组)
  • 在共享内存中初始化 MemoryManager
  • 注册相对指针基址,使跨进程引用成为可能

一个 Segment 内部包含多个 MemPool,这些 MemPool 由 MemoryManager 统一管理。

MemPool(内存池)

MemPool 是逻辑管理单元,每个池负责管理固定大小的 Chunk。在 TOML 配置中,每个 [[segment.mempool]] 块定义一个 MemPool,指定 Chunk 的大小(size)和数量(count)。

例如,一个典型的配置可能包含三个池:256B 池、2KB 池和 8KB 池。当应用请求分配 Chunk 时,MemoryManager 会选择能容纳该大小的最小池进行分配,这种策略可以有效减少内存碎片。

层级关系
复制代码
Segment(共享内存段)
  ├─ MemoryManager(管理多个 MemPool)
  │   ├─ MemPool 0(256B × 128 个 Chunk)
  │   ├─ MemPool 1(2KB × 64 个 Chunk)
  │   └─ MemPool 2(8KB × 16 个 Chunk)
  └─ 共享内存数据区

4.2.2 配置示例

以下是一个完整的 TOML 配置示例,展示了如何定义 Segment 和 MemPool:

toml 复制代码
[[segment]]              # 定义一个共享内存段
[[segment.mempool]]      # 该段内的第一个内存池
size = 256               # Chunk 大小 256 字节
count = 128              # 数量 128 个

[[segment.mempool]]      # 该段内的第二个内存池
size = 2048              # Chunk 大小 2 KiB
count = 64               # 数量 64 个

[[segment.mempool]]      # 该段内的第三个内存池
size = 8192              # Chunk 大小 8 KiB
count = 16               # 数量 16 个

这个配置创建了一个 Segment,其中 MemoryManager 管理三个不同大小的 MemPool。

4.2.3 代码结构

iceoryx 中与内存池相关的主要类结构如下:

cpp 复制代码
// MePooConfig:配置结构,定义多个内存池的参数
struct MePooConfig {
    vector<Entry, MAX_NUMBER_OF_MEMPOOLS> m_mempoolConfig;
    void addMemPool(Entry entry);
};

// MePooSegment:共享内存段类
template <typename SharedMemoryObjectType, typename MemoryManagerType>
class MePooSegment {
    SharedMemoryObjectType m_sharedMemoryObject;  // POSIX 共享内存对象
    MemoryManagerType m_memoryManager;            // 内存管理器
};

// MemoryManager:管理多个 MemPool 的分配和回收
class MemoryManager {
    vector<MemPool, MAX_NUMBER_OF_MEMPOOLS> m_memPoolVector;
    void addMemPool(...);
    expected<SharedChunk, Error> getChunk(const ChunkSettings& settings);
};

4.2.4 完整分层视图

从配置文件到实际内存布局的完整流程包含四个层次:
💾 存储层(Chunk 数据)
🎛️ MemPool 层(内存池管理)
🗂️ Segment 层(共享内存段)
📝 配置层(TOML)
内存池实例
解析配置
初始化
映射到
映射到
映射到
创建池
roudi_config.toml

────────────────

\[segment\]

\[segment.mempool\]

size = 256

count = 128

\[segment.mempool\]

size = 2048

count = 64
MePooSegment

────────────────

/dev/shm/iceoryx_segment_0
PosixSharedMemoryObject

────────────────

• shm_open()

• ftruncate()

• mmap()
访问控制

────────────────

• Reader Group

• Writer Group

• POSIX ACL
MemoryManager

────────────────

统一管理多个 MemPool
MemPool 0

────────────

256 B × 128

────────────

✓ LoFFLi 无锁链表

✓ 原子计数器

✓ CAS 操作
MemPool 1

────────────

2 KiB × 64

────────────

✓ LoFFLi 无锁链表

✓ 原子计数器

✓ CAS 操作
MemPool 2

────────────

8 KiB × 16

────────────

✓ LoFFLi 无锁链表

✓ 原子计数器

✓ CAS 操作
共享内存物理布局

总大小:约 295 KiB
📦 Pool 0 区域 32,768 字节
元数据 128 B
Chunk 0

256 B
Chunk 1

256 B
... 126 个 Chunk
📦 Pool 1 区域 131,072 字节
元数据 256 B
Chunk 0

2 KiB
Chunk 1

2 KiB
... 62 个 Chunk
📦 Pool 2 区域 131,072 字节
元数据 128 B
Chunk 0

8 KiB
... 15 个 Chunk

层次说明

  1. 配置层:TOML 文件定义 Segment 和 MemPool 的参数
  2. Segment 层MePooSegment 封装共享内存对象和访问控制
  3. MemPool 层MemoryManager 管理多个不同大小的 MemPool
  4. 存储层:共享内存中的实际物理布局,包含所有 Chunk 数据

数据流

  • 配置解析 → Segment 创建 → MemoryManager 初始化 → MemPool 实例化 → 物理内存映射

4.2.5 MemPool 的内部组成

每个 MemPool 在物理上包含两个主要部分:

管理元数据

MemPool 负责维护自己的状态信息:

  • LoFFLi (Lock-Free Free List) :使用无锁空闲链表(MpmcLoFFLi)记录每个 Chunk 的空闲/占用状态
    • 每个节点只占 8 字节(索引 + ABA 计数器)
    • 支持多生产者多消费者(MPMC)的无锁并发访问
    • 使用 CAS (Compare-And-Swap) 原子操作保证线程安全
  • 统计计数器:原子计数器追踪已用 Chunk 数和历史最小空闲数
  • 元数据结构紧凑,通常只占用几百字节
数据存储区

存储区包含所有 Chunk 的实际数据:

  • Chunk 按固定大小连续排列,便于通过索引快速定位
  • 每个 Chunk 包含头部(ChunkHeader)和用户数据区(Payload)
  • 使用相对指针(RelativePointer)进行跨进程引用,避免绝对地址失效

4.2.6 Segment 大小计算

Segment 的总大小等于其内部所有 MemPool 的大小之和。每个 MemPool 的大小由 MemoryManager::requiredFullMemorySize() 方法计算,包括以下部分:

  1. 管理元数据开销:包括位图、free-list 头节点、控制结构等,按平台对齐要求对齐
  2. Chunk 数据区大小number_of_chunks × (aligned_chunk_size + chunk_header_size)
设计考虑

每个 Segment (MePooSegment) 配置一个 MemoryManager 实例,该 MemoryManager 统一管理该 Segment 内的所有 MemPool。这种设计具有以下优势:

  • 统一管理 :MemoryManager 维护一个 vector<MemPool> (m_memPoolVector),集中管理多个不同大小的内存池
  • 智能分配:根据请求的 Chunk 大小,MemoryManager 自动选择最合适的 MemPool 进行分配
  • 有序配置:MemPool 必须按 Chunk 大小递增顺序添加,便于二分查找最优池
  • 元数据分离:每个 MemPool 独立管理自己的 LoFFLi 空闲链表,避免跨大小类的碎片问题
计算示例

假设配置一个包含 100 个 Chunk 的 MemPool,参数如下:

  • Payload 大小:1024 字节
  • Chunk 头部:32 字节
  • 对齐要求:8 字节

计算过程:

  1. 单个 Chunk 原始大小:32 + 1024 = 1056 字节
  2. 对齐后大小:roundUp(1056, 8) = 1056 字节
  3. 所有 Chunk 总大小:100 × 1056 = 105,600 字节
  4. 加上 MemoryManager 元数据(假设 256 字节)
  5. 最终大小:105,856 字节(对齐后约 106 KB)

实际实现中,requiredFullMemorySize() 会自动处理所有对齐和填充计算。

4.3 共享内存创建流程

RouDi 在系统启动时负责创建和初始化共享内存段。整个流程基于 POSIX 共享内存机制,通过系统调用序列 shm_open → ftruncate → mmap 完成。

4.3.1 创建流程图

失败
成功
失败
成功
失败
成功
RouDi 启动
步骤 1:计算总大小
遍历所有 MemPool 配置
调用 requiredFullMemorySize

计算每个池所需空间
汇总:元数据 + Chunk 数据区

  • 对齐填充
    得到 totalSize
    步骤 2:创建共享内存对象
    shm_open('/iceoryx_segment_0',

O_CREAT | O_RDWR, 0600)
fd 有效?
错误:名称冲突

或权限不足
获得文件描述符 fd
步骤 3:设置大小
ftruncate(fd, totalSize)
设置成功?
错误:空间不足

或配额限制
共享内存大小已设置
步骤 4:映射到进程地址空间
mmap(nullptr, totalSize,

PROT_READ | PROT_WRITE,

MAP_SHARED, fd, 0)
映射成功?
错误:地址空间不足

或权限问题
获得映射地址 addr
步骤 5:初始化数据结构
使用 BumpAllocator

划分内存区域
placement new 创建

MemoryManager 实例
初始化每个 MemPool:

• LoFFLi 空闲链表

• 原子计数器

• ChunkHeader
注册相对指针基址
共享内存创建完成
创建失败

流程说明

  1. 计算阶段:遍历配置,累加所有 MemPool 的空间需求
  2. 创建阶段:通过 POSIX API 创建共享内存对象
  3. 配置阶段:设置共享内存大小并映射到当前进程
  4. 初始化阶段:在共享内存中构建数据结构
  5. 错误处理:每个关键步骤都有失败检查和错误分支

4.3.2 创建步骤详解

步骤 1:计算总大小

首先汇总所有 MemPool 的 requiredFullMemorySize,加上全局头部和对齐填充,得到 Segment 的总大小 totalSize

步骤 2:创建共享内存对象

调用 shm_open() 创建或打开共享内存对象,返回文件描述符。在 Linux 系统上,这会在 /dev/shm/ 目录下创建一个文件。

cpp 复制代码
int fd = shm_open("/iceoryx_segment_0", O_CREAT | O_RDWR, 0600);
步骤 3:设置大小

使用 ftruncate() 将共享内存对象的大小设置为计算出的 totalSize

cpp 复制代码
ftruncate(fd, totalSize);
步骤 4:映射到进程地址空间

通过 mmap() 将共享内存映射到当前进程的地址空间:

cpp 复制代码
void* addr = mmap(nullptr, totalSize, 
                  PROT_READ | PROT_WRITE, 
                  MAP_SHARED, fd, 0);
步骤 5:初始化数据结构

在映射的内存区域上使用 placement new 初始化各种控制结构:

  • MemoryManager 实例
  • 每个 MemPool 的 LoFFLi (Lock-Free Free List)
  • Chunk 头部

4.3.3 实现位置

相关代码分布在以下文件中:

  • posix_memory_map.cpp:封装了 mmap()shm_open() 等底层系统调用
  • memory_provider.cpp:提供内存分配和初始化的高层接口
  • mempool_collection_memory_block.cpp:实现 size() 方法,计算所需总大小

(待续)

相关推荐
橘颂TA14 小时前
【剑斩OFFER】算法的暴力美学——面试题 01.02 :判定是否互为字符串重排
c++·算法·leetcode·职场和发展·结构与算法
HABuo14 小时前
【Linux进程(二)】操作系统&Linux的进程状态深入剖析
linux·运维·服务器·c语言·c++·ubuntu·centos
糯诺诺米团14 小时前
C++多线程打包成so给JAVA后端(Ubuntu)<2>
java·开发语言·c++
-西门吹雪14 小时前
c++线程之再研究研究多线程
开发语言·c++
Tisfy14 小时前
LeetCode 1390.四因数:因数分解+缓存
算法·leetcode·缓存
EnigmaCoder14 小时前
【C++期末大作业】图书管理系统(面向对象+STL+数据持久化)
开发语言·c++·课程设计
折翅嘀皇虫14 小时前
Epoch / QSBR 内存回收解决ABA问题
c++
云雾J视界14 小时前
告别重复编码:Boost.Optional、Variant和Assign如何提升C++工程可维护性?
c++·自动驾驶·分布式系统·variant·工具链·boost.optional·assign
漫随流水1 天前
leetcode算法(151.反转字符串中的单词)
数据结构·算法·leetcode