iceoryx_hoofs 基础库
源码根:
ros2_humble/src/eclipse-iceoryx/iceoryx/iceoryx_hoofs· 版本 2.0.6
hoofs = Handy Objects For Utilizing Files and Streams
上一篇:00-整体框架与模块划分.md
1. 定位:无异常、无堆分配的 C++ 基础库
iceoryx_posh 的一切数据结构都要放进共享内存并满足实时约束,因此不能用 STL(std::vector 会 new、std::string 有 SSO 之外的堆分配、容器内部存裸指针在别的进程地址空间无效)。hoofs 提供一套替代设施,遵循三条铁律:
| 铁律 | 落地方式 |
|---|---|
| 不抛异常 | 所有函数 noexcept;错误用 cxx::expected<T, E> 返回 |
| 不做堆分配 | 容器容量是模板参数(vector<T, Capacity>),内存全部内联在对象里 |
| 可放入共享内存 | 对象自包含(self-contained),指向自身内部用 relocatable_ptr,跨段引用用 RelativePointer |
子模块一览(include/iceoryx_hoofs/):
| 子模块 | 路径 | 内容 |
|---|---|---|
| cxx | cxx/、internal/cxx/ |
词汇类型与容器(expected/optional/variant/vector/string/function_ref/list/stack …) |
| concurrent | concurrent/、internal/concurrent/ |
LockFreeQueue、IndexQueue、LoFFLi、SoFi、smart_lock、TriggerQueue … |
| posix_wrapper | posix_wrapper/、internal/posix_wrapper/ |
共享内存、信号量、互斥量、UDS、消息队列、ACL、posixCall |
| relocatable_pointer | internal/relocatable_pointer/ |
RelativePointer / relocatable_ptr / PointerRepository |
| platform | platform/{linux,mac,qnx,unix,win}/ |
OS 差异抹平层 |
| 其他 | log/、error_handling/、design_pattern/、internal/units/ |
日志、错误处理、Creation 模式、时间单位 |
2. cxx 容器与词汇类型:为何不用 STL
2.1 一览表
| 类型 | 文件(相对 iceoryx_hoofs/include/iceoryx_hoofs/) | 对标 STL | 差异关键点 |
|---|---|---|---|
cxx::expected<T, E> |
cxx/expected.hpp |
C++23 std::expected |
无异常的错误传递主干;提供 and_then/or_else 链式调用 |
cxx::optional<T> |
cxx/optional.hpp |
std::optional |
内联存储,无异常(访问空值走错误处理器而非 throw) |
cxx::variant<...> |
cxx/variant.hpp |
std::variant |
无异常、无 valueless_by_exception 状态 |
cxx::vector<T, Capacity> |
cxx/vector.hpp |
std::vector |
容量编译期固定,内存内联,永不 realloc |
cxx::string<Capacity> |
cxx/string.hpp |
std::string |
定容字符数组,可安全放入共享内存 |
cxx::function_ref<Sig> |
cxx/function_ref.hpp |
C++26 function_ref |
非拥有的可调用引用,无堆分配(std::function 可能堆分配) |
cxx::function<Sig, Bytes> |
cxx/function.hpp |
std::function |
定长内联存储版 function |
cxx::list/forward_list/stack |
cxx/list.hpp 等 |
对应 STL | 节点全部内联,定容 |
cxx::unique_ptr<T> |
cxx/unique_ptr.hpp |
std::unique_ptr |
删除器为运行时 function_ref,用于 Chunk 归还等场景 |
不用 STL 的原因归纳:① STL 容器在增长时动态分配(违反实时/确定性);② 异常是 STL 的错误通道(违反无异常);③ STL 容器内部保存绝对指针,映射到另一个进程后失效(违反共享内存可用性);④ 各家 libc++/libstdc++ 的 ABI 与行为差异不可控(车规认证需要可控实现)。
2.2 expected:错误处理主干
1 | template <typename... T> |
用法约定:成功返回 cxx::success<T>(value),失败返回 cxx::error<E>(err);IOX_NO_DISCARD 强制调用方检查结果。hoofs/posh 中几乎所有可失败 API(共享内存创建、UDS 收发、端口创建)都以它为返回类型,形成贯穿全库的错误管道。
2.3 vector / string:定容 + 内联
1 | template <typename T, uint64_t Capacity> |
1 | template <uint64_t Capacity> |
容量进模板参数意味着:sizeof 编译期已知、无堆、memcpy 语义友好(配合 relocatable 设计),代价是超容 push_back 返回 false 而不是扩容——上层必须处理”满”这个正常状态,这正是 iceoryx 全库”资源上限显式化”哲学的缩影。
3. concurrent 并发原语
3.1 LockFreeQueue:MPMC 无锁定容队列
concurrent/lockfree_queue.hpp,多生产者多消费者(MPMC)、FIFO、定容、lock-free。内部结构是”两条索引队列 + 一个定长缓冲区”:
1 | protected: |
工作原理:push = 从 m_freeIndices 弹出一个空闲槽位索引 → 写 m_buffer[index] → 索引压入 m_usedIndices;pop 反向。数据搬运与索引流转解耦,索引流转由底层 IndexQueue(internal/concurrent/lockfree_queue/index_queue.hpp)完成——一个基于 CyclicIndex(带循环计数防 ABA)+ std::atomic CAS 的 MPMC 索引环:
1 | /// @brief lockfree queue capable of storing indices 0,1,... Capacity-1 |
特色 API:push() 满时必定成功,返回被挤掉的最旧元素(cxx::optional<T>)——这是订阅者队列”丢最旧”溢出策略的实现基础。另有 ResizeableLockFreeQueue(concurrent/resizeable_lockfree_queue.hpp)支持运行时在编译期上限内调整容量(订阅者 queue capacity 可配)。
3.2 SoFi:单生产者单消费者安全溢出 FIFO
Softer Fifo(internal/concurrent/sofi.hpp),SPSC、仅限 trivially copyable 类型、无锁,满时覆写最旧数据并把旧值交还给 push 方:
1 | /// @brief |
实现只用两个原子读写位置(读者只动 m_readPosition,写者只动 m_writePosition),内部容量比名义容量多 1 个槽位用于空/满判别。posh 中它经 cxx::VariantQueue 作为订阅者 Chunk 队列的一种可选实现(SoFi_SingleProducerSingleConsumer 策略)。
3.3 LoFFLi:无锁 free-list(MemPool 的心脏)
Lock-Free Free-List(internal/concurrent/loffli.hpp),管理”哪些 Chunk 索引空闲”。核心是把 next 索引和 ABA 计数打包进一个 8 字节原子量做 CAS:
1 | class LoFFLi |
注意其索引数组指针是 rp::RelativePointer<Index_t> m_nextFreeIndex(同文件第 71 行)——因为 LoFFLi 本体和索引数组都活在共享内存里,必须用相对指针(见第 6 节)。这也解释了 MemPool 里”chunk 数只能 32 位”的注释:64 位 CAS 里要留 32 位给 ABA 计数。
3.4 smart_lock:给任意类型加锁的包装器
internal/concurrent/smart_lock.hpp。控制面(非实时路径)用它把任意对象变成线程安全对象:operator-> 返回一个持锁 Proxy,实现”每次调用自动加解锁”:
1 | /// @brief The smart_lock class is a wrapping class which can be used to make |
跨多次调用需要原子性时用 getScopeGuard()。RouDi 的进程表、服务注册表等控制面结构大量使用它。
3.5 其他并发设施
| 设施 | 文件 | 用途 |
|---|---|---|
FiFo |
internal/concurrent/fifo.hpp |
最简 SPSC FIFO |
TriggerQueue |
internal/concurrent/trigger_queue.hpp |
可阻塞唤醒的队列 |
PeriodicTask |
internal/concurrent/periodic_task.hpp |
周期任务线程(RouDi KEEPALIVE 监控等) |
ActiveObject/TACO |
internal/concurrent/active_object.hpp、taco.hpp |
主动对象模式 / 线程间单槽交换 |
4. posix 封装
设计模式统一:类不可直接构造,经 DesignPattern::Creation(design_pattern/creation.hpp)的 create() 工厂返回 cxx::expected,把”构造可能失败”显式化;所有系统调用经 posix::posixCall(posix_wrapper/posix_call.hpp)包装,统一处理 errno、EINTR 重试与返回值检查。
4.1 SharedMemoryObject
internal/posix_wrapper/shared_memory_object.hpp = shm_open + mmap + 线性分配器三合一:
1 | class SharedMemoryObject : public DesignPattern::Creation<SharedMemoryObject, SharedMemoryObjectError> |
注意 allocate()/finalizeAllocation():这是一个只进不出的 bump allocator(shared_memory_object/allocator.hpp)——RouDi 启动阶段在段内依次摆放管理结构和 MemPool,finalizeAllocation() 之后不允许再分配。这就是”无动态内存分配”在共享内存侧的执行机制:分配只发生在初始化阶段。
4.2 semaphore / mutex
posix_wrapper/semaphore.hpp:命名/匿名 POSIX 信号量;匿名版可放共享内存(pshared),是 WaitSet 的ConditionVariableData跨进程唤醒的底层。internal/posix_wrapper/mutex.hpp:pthread_mutex_t封装,支持跨进程 robust mutex(持有者死亡可恢复)。
4.3 IPC channel:UnixDomainSocket / MessageQueue / NamedPipe
Runtime↔RouDi 的控制通道有三种可选实现,统一在 internal/posix_wrapper/ipc_channel.hpp 的接口语义下(Linux 默认 UDS):
1 | /// @brief Wrapper class for unix domain socket |
提供 send/timedSend/receive/timedReceive,全部返回 cxx::expected。
4.4 access_control:按用户组授权共享内存段
internal/posix_wrapper/access_control.hpp 封装 POSIX ACL(libacl)。posh 的每个用户 payload 段配置 reader/writer 用户组(mepoo/segment_config.hpp),RouDi 创建段时用 ACL 落实——这是 iceoryx 的安全模型:操作系统级用户组决定谁能读写哪个段。
5. platform 抽象层
iceoryx_hoofs/platform/ 下每个 OS 一套同名头文件(linux/ mac/ qnx/ unix/ win/),如 platform/linux/include/iceoryx_hoofs/platform/ 里有 mman.hpp、semaphore.hpp、socket.hpp、acl.hpp、pthread.hpp 等 24 个头。上层代码一律 #include "iceoryx_hoofs/platform/xxx.hpp",由 CMake 选择平台目录;Windows 等缺失的 POSIX API 在对应目录里给出模拟实现,QNX 等 RTOS 只需薄转发。platform_settings.hpp 集中平台常量(UDS 路径前缀、最大消息长度等)。
6. relocatable pointer:共享内存中指针为何必须相对化(关键)
6.1 问题:同一块物理内存,各进程虚拟地址不同
mmap 同一个共享内存对象,进程 A 可能映射到 0x7f11...,进程 B 映射到 0x7f42...。若 A 在共享内存里存了一个裸指针(绝对虚拟地址),B 解引用它就是未定义行为。而 iceoryx 的端口结构、队列、free-list、Chunk 引用全部活在共享内存里且互相指来指去——所以”指针”必须改存与某个已知基址的偏移,读取时在本进程重算绝对地址。
iceoryx 给出两级方案:
| 方案 | 适用场景 | 额外依赖 |
|---|---|---|
relocatable_ptr<T> |
指针与被指对象在同一内存块内(自包含对象) | 无(存自身到目标的偏移) |
RelativePointer<T> |
指针与被指对象可在不同共享内存段 | 每进程的段注册表 PointerRepository |
6.2 RelativePointer:(segment id, offset) 二元组
源码注释把场景画得很清楚:
1 | /// @brief pointer class to use when pointer and pointee are located in different shared memory segments |
存储上只有两个 8 字节成员——段 id 与段内偏移,这就是它可以原样躺在共享内存里被任何进程读取的原因:
1 | protected: |
编解码就是一次基址加减(基址查本进程的注册表):
1 | BaseRelativePointer::offset_t BaseRelativePointer::getOffset(const id_t id, const_ptr_t ptr) noexcept |
类型化外壳 RelativePointer<T>(internal/relocatable_pointer/relative_pointer.hpp)在其上提供 operator* / operator-> / 隐式转 T*,用起来与裸指针无异。另有 AtomicRelativePointer(atomic_relocatable_pointer.hpp)供无锁结构使用。
6.3 PointerRepository:每进程一份的段基址注册表
1 | /// @brief Allows registration of memory segments with their start pointers and size. |
要点:
- 注册表是进程本地的静态单例(
BaseRelativePointer::getRepository()内的函数静态变量),不在共享内存里;每个进程mmap完各段后各自调用registerPtr(id, 本进程基址, size)。段 id 全局一致,基址各进程各异——这正是跨进程解引用成立的机制。 - id 0 保留:偏移直接当裸指针用(相当于基址为 0),使同一套代码可同时处理进程内/共享内存两种情形。
- 在 posh 中,应用向 RouDi 注册(
REG/REG_ACK)后拿到各段的 id 与名字,mmap后完成注册;此后端口结构里的RelativePointer(如 MemPool 的m_rawMemory、LoFFLi 的索引数组指针)在任何进程中都可正确解引用。 - 注意注释的告警:RelativePointer 本身不可搬移(段整体 memcpy 到别处后失效),因为注册表无从得知搬移。
6.4 relocatable_ptr:自包含对象的可搬移指针
1 | /// @brief Smart pointer type that allows objects using it to able to be copied by memcpy |
它存的是”从 this(指针自身地址)到目标的偏移”,因此整个对象连同指针一起 memcpy/重新映射后依然有效,且不需要任何注册表;限制是只能指向对象自身拥有的内存。
6.4.1 两者对比小结
relocatable_ptr<T> |
RelativePointer<T> |
|
|---|---|---|
| 偏移基准 | 指针自身地址(this) |
段基址(查 PointerRepository) |
| 可跨段指 | 否(仅对象内部) | 是 |
| 需要注册表 | 否 | 是(每进程注册段基址) |
| 段整体搬移后 | 仍有效 | 失效 |
| 典型用户 | 自包含容器 | MemPool、LoFFLi、端口、Chunk 引用 |
6.5 为什么说这是理解 iceoryx 的关键
回看 00 篇的数据流:publish() 推给订阅者的”Chunk 指针”、订阅者队列本身、MemPool 的 free-list——全是建立在 RelativePointer 之上的结构(posh 中 ShmSafeUnmanagedChunk 等类型即是对”chunk 的相对引用”的封装,见 iceoryx_posh/include/iceoryx_posh/internal/mepoo/shm_safe_unmanaged_chunk.hpp)。零拷贝 = 只传偏移;没有指针相对化,跨进程共享数据结构无从谈起。
7. 内存与生命周期设计原则
- 分配只在初始化期:RouDi 启动时经 bump
Allocator一次性划分全部共享内存,finalizeAllocation()后拒绝再分配;应用运行期唯一的”分配”是从 MemPool 无锁地借还 Chunk(O(1),free-list 操作)。 - 容量一律编译期封顶:
cxx::vector<T, Capacity>、MAX_PUBLISHERS、MAX_PROCESS_NUMBER = 300等;”资源耗尽”是 API 层面的正常返回值(expected错误 /push返回 false),不是异常。 - 构造可失败 → 工厂化:
DesignPattern::Creation把失败从构造函数挪到create()的expected返回值,杜绝半初始化对象。 - RAII + 引用计数管理 Chunk:用户侧
popo::Sample/cxx::unique_ptr析构自动归还;共享内存里 Chunk 由ChunkManagement引用计数(多订阅者共享一份 payload),归零回 MemPool。 - 崩溃回收:Chunk 的归属登记在共享内存中,RouDi 通过 KEEPALIVE 检测应用死亡后遍历回收其端口与 Chunk——生命周期的最终兜底者是 RouDi。
- 无锁优先,锁只留给控制面:数据面 LoFFLi/LockFreeQueue/SoFi;控制面(RouDi 内部进程表等)才用
smart_lock/mutex。
正在加载留言…