iceoryx_hoofs 基础库

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::vectornewstd::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
2
3
4
5
6
7
8
9
10
11
12
13
14
template <typename... T>
class IOX_NO_DISCARD expected;

/// @brief expected implementation from the C++20 proposal with C++11. The interface
/// is inspired by the proposal but it has changes since we are not allowed to
/// throw an exception.
/// @param ErrorType type of the error which can be stored in the expected
///
/// @code
/// cxx::expected<int, float> callMe() {
/// bool l_errorOccured;
/// // ... do stuff
/// if ( l_errorOccured ) {
/// return cxx::error<float>(55.1f);

用法约定:成功返回 cxx::success<T>(value),失败返回 cxx::error<E>(err)IOX_NO_DISCARD 强制调用方检查结果。hoofs/posh 中几乎所有可失败 API(共享内存创建、UDS 收发、端口创建)都以它为返回类型,形成贯穿全库的错误管道。

2.3 vector / string:定容 + 内联

1
2
template <typename T, uint64_t Capacity>
class vector
1
2
template <uint64_t Capacity>
class string

容量进模板参数意味着:sizeof 编译期已知、无堆、memcpy 语义友好(配合 relocatable 设计),代价是超容 push_back 返回 false 而不是扩容——上层必须处理”满”这个正常状态,这正是 iceoryx 全库”资源上限显式化”哲学的缩影。


3. concurrent 并发原语

3.1 LockFreeQueue:MPMC 无锁定容队列

concurrent/lockfree_queue.hpp,多生产者多消费者(MPMC)、FIFO、定容、lock-free。内部结构是”两条索引队列 + 一个定长缓冲区”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected:
using Queue = IndexQueue<Capacity>;
using BufferIndex = typename Queue::value_t;

// remark: actually m_freeIndices do not have to be in a queue, it could be another
// multi-push multi-pop capable lockfree container (e.g. a stack or a list)
Queue m_freeIndices;

// required to be a queue for LockFreeQueue to exhibit FIFO behaviour
Queue m_usedIndices;

Buffer<ElementType, Capacity, BufferIndex> m_buffer;

std::atomic<uint64_t> m_size{0u};

工作原理:push = 从 m_freeIndices 弹出一个空闲槽位索引 → 写 m_buffer[index] → 索引压入 m_usedIndicespop 反向。数据搬运与索引流转解耦,索引流转由底层 IndexQueueinternal/concurrent/lockfree_queue/index_queue.hpp)完成——一个基于 CyclicIndex(带循环计数防 ABA)+ std::atomic CAS 的 MPMC 索引环:

1
2
3
4
/// @brief lockfree queue capable of storing indices 0,1,... Capacity-1
template <uint64_t Capacity, typename ValueType = uint64_t>
class IndexQueue
{

特色 API:push() 满时必定成功,返回被挤掉的最旧元素(cxx::optional<T>)——这是订阅者队列”丢最旧”溢出策略的实现基础。另有 ResizeableLockFreeQueueconcurrent/resizeable_lockfree_queue.hpp)支持运行时在编译期上限内调整容量(订阅者 queue capacity 可配)。

3.2 SoFi:单生产者单消费者安全溢出 FIFO

Softer Fifo(internal/concurrent/sofi.hpp),SPSC、仅限 trivially copyable 类型、无锁,满时覆写最旧数据并把旧值交还给 push 方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/// @brief
/// Thread safe producer and consumer queue with a safe overflowing behavior.
/// SoFi is designed in a FIFO Manner but prevents data loss when pushing into
/// a full SoFi. When SoFi is full and a Sender tries to push, the data at the
/// current read position will be returned. SoFi is a Thread safe without using
/// locks. When the buffer is filled, new data is written starting at the
/// beginning of the buffer and overwriting the old.The SoFi is especially
/// designed to provide fixed capacity storage. When its capacity is exhausted,
/// newly inserted elements will cause elements either at the beginning
/// to be overwritten.The SoFi only allocates memory when
/// created , capacity can be is adjusted explicitly.
///
/// @param[in] ValueType DataType to be stored, must be trivially copyable
/// @param[in] CapacityValue Capacity of the SoFi
template <class ValueType, uint64_t CapacityValue>
class SoFi
{
static_assert(std::is_trivially_copyable<ValueType>::value, "SoFi can handle only trivially copyable data types");
/// @brief Check if Atomic integer is lockfree on platform
/// ATOMIC_INT_LOCK_FREE = 2 - is always lockfree
/// ATOMIC_INT_LOCK_FREE = 1 - is sometimes lockfree
/// ATOMIC_INT_LOCK_FREE = 0 - is never lockfree
static_assert(2 <= ATOMIC_INT_LOCK_FREE, "SoFi is not able to run lock free on this data type");

实现只用两个原子读写位置(读者只动 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
2
3
4
5
6
7
8
9
10
11
12
13
14
class LoFFLi
{
public:
using Index_t = uint32_t;

private:
struct alignas(8) Node
{
Index_t indexToNextFreeIndex;
uint32_t abaCounter;
};

static_assert(sizeof(Node) <= 8U,
"The size of 'Node' must not exceed 8 bytes in order to be lock-free on 64 bit systems!");

注意其索引数组指针是 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// @brief The smart_lock class is a wrapping class which can be used to make
/// an arbitrary class threadsafe by wrapping it with the help of the
/// arrow operator.
/// IMPORTANT: If you generate a threadsafe container with smart_lock,
/// only the container is threadsafe not the containing
/// elements!
/// @code
/// #include <algorithm>
/// #include <vector>
/// #include "smart_lock.hpp"
///
/// int main() {
/// concurrent::smart_lock<std::vector<int>> threadSafeVector;
/// threadSafeVector->push_back(123);
/// threadSafeVector->push_back(456);
/// threadSafeVector->push_back(789);
/// size_t vectorSize = threadSafeVector->size();
///
/// {
/// auto guardedVector = threadSafeVector.getScopeGuard();
/// auto iter = std::find(guardVector->begin(), guardVector->end(), 456);
/// if (iter != guardVector->end()) guardVector->erase(iter);
/// }
/// }
/// @endcode
template <typename T, typename MutexType = ::std::mutex>
class smart_lock

跨多次调用需要原子性时用 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.hpptaco.hpp 主动对象模式 / 线程间单槽交换

4. posix 封装

设计模式统一:类不可直接构造,经 DesignPattern::Creationdesign_pattern/creation.hpp)的 create() 工厂返回 cxx::expected,把”构造可能失败”显式化;所有系统调用经 posix::posixCallposix_wrapper/posix_call.hpp)包装,统一处理 errno、EINTR 重试与返回值检查。

4.1 SharedMemoryObject

internal/posix_wrapper/shared_memory_object.hpp = shm_open + mmap + 线性分配器三合一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class SharedMemoryObject : public DesignPattern::Creation<SharedMemoryObject, SharedMemoryObjectError>
{
public:
static constexpr void* NO_ADDRESS_HINT = nullptr;
SharedMemoryObject(const SharedMemoryObject&) = delete;
SharedMemoryObject& operator=(const SharedMemoryObject&) = delete;
SharedMemoryObject(SharedMemoryObject&&) noexcept = default;
SharedMemoryObject& operator=(SharedMemoryObject&&) noexcept = default;
~SharedMemoryObject() noexcept = default;

void* allocate(const uint64_t size, const uint64_t alignment) noexcept;
void finalizeAllocation() noexcept;

Allocator* getAllocator() noexcept;
const void* getBaseAddress() const noexcept;
void* getBaseAddress() noexcept;

uint64_t getSizeInBytes() const noexcept;
int getFileHandle() const noexcept;
bool hasOwnership() const noexcept;

注意 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.hpppthread_mutex_t 封装,支持跨进程 robust mutex(持有者死亡可恢复)。

4.3 IPC channel:UnixDomainSocket / MessageQueue / NamedPipe

Runtime↔RouDi 的控制通道有三种可选实现,统一在 internal/posix_wrapper/ipc_channel.hpp 的接口语义下(Linux 默认 UDS):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// @brief Wrapper class for unix domain socket
class UnixDomainSocket : public DesignPattern::Creation<UnixDomainSocket, IpcChannelError>
{
public:
struct NoPathPrefix_t
{
};
static constexpr NoPathPrefix_t NoPathPrefix{};
static constexpr uint64_t NULL_TERMINATOR_SIZE = 1U;
static constexpr uint64_t MAX_MESSAGE_SIZE = platform::IOX_UDS_SOCKET_MAX_MESSAGE_SIZE - NULL_TERMINATOR_SIZE;
/// @brief The name length is limited by the size of the sockaddr_un::sun_path buffer and the IOX_SOCKET_PATH_PREFIX
static constexpr size_t LONGEST_VALID_NAME = sizeof(sockaddr_un::sun_path) - 1;

using UdsName_t = cxx::string<LONGEST_VALID_NAME>;
using Message_t = cxx::string<MAX_MESSAGE_SIZE>;

提供 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.hppsemaphore.hppsocket.hppacl.hpppthread.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// @brief pointer class to use when pointer and pointee are located in different shared memory segments
/// We can have the following scenario:
/// Pointer p is stored in segment S1 and points to object X of type T in segment S2.
///
/// Shared Memory S1: p S2: X
/// |___________________^
/// App1 a1 b1 c1 d1
/// App2 a2 b2 c2 d2
///
/// Now it is no longer true in general that both segments will be offset by the same difference in App2 and therefore
/// relocatable pointers are no longer sufficient.
/// Relative pointers solve this problem by incorporating the information from where they need to measure differences
/// (i.e. relative to the given address). This requires an additional registration mechanism to be used by all
/// applications where the start addresses and the size of all segments to be used are registered. Since these start
/// address may differ between applications, each segment is identified by a unique id, which can be provided upon
/// registration by the first application. In the figure, this means that the starting addresses of both segments(a1, a2
/// and c1, c2) would have to be registered in both applications.
/// Once this registration is done, relative pointers can be constructed from raw pointers similar to relocatable
/// pointers.
/// @note It should be noted that relocating a memory segment will invalidate relative pointers, i.e. relative pointers
/// are NOT relocatable. This is because the registration mechanism cannot be automatically informed about the copy of a
/// whole segment, such a segment would have to be registered on its own (and the original segment deregistered).

存储上只有两个 8 字节成员——段 id 与段内偏移,这就是它可以原样躺在共享内存里被任何进程读取的原因:

1
2
3
protected:
id_t m_id{NULL_POINTER_ID};
offset_t m_offset{NULL_POINTER_OFFSET};

编解码就是一次基址加减(基址查本进程的注册表):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
BaseRelativePointer::offset_t BaseRelativePointer::getOffset(const id_t id, const_ptr_t ptr) noexcept
{
if (id == NULL_POINTER_ID)
{
return NULL_POINTER_OFFSET;
}
auto* basePtr = getBasePtr(id);
return reinterpret_cast<offset_t>(ptr) - reinterpret_cast<offset_t>(basePtr);
}

BaseRelativePointer::ptr_t BaseRelativePointer::getPtr(const id_t id, const offset_t offset) noexcept
{
if (offset == NULL_POINTER_OFFSET)
{
return nullptr;
}
auto* basePtr = getBasePtr(id);
// NOLINTNEXTLINE(performance-no-int-to-ptr) reliance on integers for offset computation by design
return reinterpret_cast<ptr_t>(offset + reinterpret_cast<offset_t>(basePtr));
}

类型化外壳 RelativePointer<T>internal/relocatable_pointer/relative_pointer.hpp)在其上提供 operator* / operator-> / 隐式转 T*,用起来与裸指针无异。另有 AtomicRelativePointeratomic_relocatable_pointer.hpp)供无锁结构使用。

6.3 PointerRepository:每进程一份的段基址注册表

1
2
3
4
5
6
7
8
/// @brief Allows registration of memory segments with their start pointers and size.
/// This class is used to resolve relative pointers in the corresponding address space of the application.
/// Up to CAPACITY segments can be registered with MIN_ID = 1 to MAX_ID = CAPACITY - 1
/// id 0 is reserved and allows relative pointers to behave like normal pointers
/// (which is equivalent to measure the offset relative to 0).
template <typename id_t, typename ptr_t, uint64_t CAPACITY = 10000U>
class PointerRepository
{

要点:

  • 注册表是进程本地的静态单例(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/// @brief Smart pointer type that allows objects using it to able to be copied by memcpy
/// without invalidating the pointer.
/// This applies only if it points to memory owned by the object itself
/// (i.e. not to memory outside of the object).
/// This is useful to improve copy-efficiency and allow the types build with relocatable
/// pointers only to be stored in shared memory.
/// It is useable like a raw pointer of the corresponding type and can be implicily
/// converted to one.
///
/// @tparam T the native type wrapped by the relocatable_ptr, i.e. relocatable_ptr<T>
/// has native type T and corresponds to a raw pointer of type T*.
///
/// @note It is advisable to use relocatable_ptr only for storage (e.g. member variables),
/// not to pass them around as function arguments or as return value.
/// There should be no need for this, since as pass-around type
/// regular pointers do the job just fine and do not incur
/// the slight runtime overhead of a relocatable_ptr.
/// There should be no memory overhead on 64 bit systems.
/// @note relocatable_ptr is not trivially copyable since in general the copy constructor
/// requires additional logic. Hence obects that contain it re not trivially
/// copyable in the C++ sense. However, if the pointees of a host object containing the
/// relocatable ptr are all located inside the object and the obect is otherwise trivially
/// copyable it can be safely copied by memcpy.
/// @todo specialize for another pointer class for this use case once it is fully defined/understood
template <typename T>
class relocatable_ptr

它存的是”从 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. 内存与生命周期设计原则

  1. 分配只在初始化期:RouDi 启动时经 bump Allocator 一次性划分全部共享内存,finalizeAllocation() 后拒绝再分配;应用运行期唯一的”分配”是从 MemPool 无锁地借还 Chunk(O(1),free-list 操作)。
  2. 容量一律编译期封顶cxx::vector<T, Capacity>MAX_PUBLISHERSMAX_PROCESS_NUMBER = 300 等;”资源耗尽”是 API 层面的正常返回值(expected 错误 / push 返回 false),不是异常。
  3. 构造可失败 → 工厂化DesignPattern::Creation 把失败从构造函数挪到 create()expected 返回值,杜绝半初始化对象。
  4. RAII + 引用计数管理 Chunk:用户侧 popo::Sample/cxx::unique_ptr 析构自动归还;共享内存里 Chunk 由 ChunkManagement 引用计数(多订阅者共享一份 payload),归零回 MemPool。
  5. 崩溃回收:Chunk 的归属登记在共享内存中,RouDi 通过 KEEPALIVE 检测应用死亡后遍历回收其端口与 Chunk——生命周期的最终兜底者是 RouDi。
  6. 无锁优先,锁只留给控制面:数据面 LoFFLi/LockFreeQueue/SoFi;控制面(RouDi 内部进程表等)才用 smart_lock/mutex。

下一篇02-mepoo共享内存与内存池.md

文章互动

阅读 --

留言

0 条留言

正在加载留言…