文章目录
1. 概述
本文翻译自 design/chunk_header.md
在 iceoryx 中,块(Chunks)是传输的封装体。它们存储发布者的数据作为有效负载,并被发送给一个或多个订阅者。此外,还有一些与有效负载一起存储的元信息,例如块的大小和来源。这些数据组合在“ChunkHeader”中,并位于块的前端。可以添加自定义的元信息来扩展“ChunkHeader”的数据,并使块适应特定的用例。虽然这使得块的布局更加复杂,但这种复杂性否则会分布在代码库的不同位置,例如在请求/响应功能中。此外,对用户头的调整使得用户有效负载的任意对齐易于实现。
2. 术语
名称 | 描述 |
---|---|
块(Chunk) | 一块内存 |
块头(Chunk-Header) | 包含与块相关的元信息 |
块有效负载(Chunk-Payload) | 块中用于有效负载的部分,分为用户头和用户有效负载 |
用户头(User-Header) | 包含自定义元信息,例如时间戳 |
用户有效负载(User-Payload) | 具有自定义对齐方式的用户数据 |
反向偏移(Back-Offset) | 存储在用户有效负载前面的偏移量,用于计算回到块头(对于最简单的情况,它将与存储在块头中的用户有效负载偏移量重叠) |
用术语来框定
+=============================================================================+
| 块 |
+===================+=========================================================+
| | 块有效负载 |
| 块头 +===============+=======+====================+============+
| | 用户头 | ¦ ᶺ | 用户有效负载 | 填充 |
+===================+===============+=====|=+====================+============+
└ 反向偏移
3 设计
3.1 考虑因素
- 记录块以供以后重放并非罕见 -> 在重放时检测不兼容性
- iceoryx 在多个平台上运行 -> 记录的块的字节序可能不同
- 为了跟踪,块应该是唯一可识别的 -> 存储来源和序列号
- 块位于共享内存中,这将被映射到各种进程的地址空间中的任意位置 -> 不允许使用绝对指针
- 为了降低复杂性,用户头的对齐方式不得超过“ChunkHeader”的对齐方式
3.2 解决方案
块头定义
class ChunkHeader
{
uint32_t userHeaderSize{0U};
uint8_t chunkHeaderVersion;
uint8_t reserved{0};
uint16_t userHeaderId;
popo::UniquePortId originId; // 底层类型 = uint64_t
uint64_t sequenceNumber;
uint64_t chunkSize;
uint32_t userPayloadSize{0U};
uint32_t userPayloadAlignment{1U};
UserPayloadOffset_t userPayloadOffset; // 别名为 uint32_t
};
- userHeaderSize 是用户头占用的块的大小
- chunkHeaderVersion 用于检测记录和重放功能的不兼容性
- reserved 目前未使用,设置为
0
- userHeaderId 目前未使用,设置为
NO_USER_HEADER
- originId 是发送块的发布者的唯一标识符
- sequenceNumber 是发送的块的序列号
- chunkSize 是整个块的大小
- userPayloadSize 是用户有效负载占用的块的大小
- userPayloadAlignment 是用户有效负载占用的块的对齐方式
- userPayloadOffset 是用户有效负载相对于块开头的偏移量
框定
为了从用户有效负载指针反向计算到“ChunkHeader”指针,必须能够从相对于用户有效负载的定义位置访问用户有效负载偏移量。我们称之为“反向偏移”。这通过在用户有效负载前面的 4 个字节中存储偏移量来解决。在“ChunkHeader”与用户有效负载相邻的简单布局中,这与“userPayloadOffset”的位置很好地重叠,不会浪费内存。在更复杂的情况下,偏移量必须存储第二次。如果用户有效负载的对齐需要从头部扩展进行一些填充,则使用此内存来存储偏移量。
- 没有用户头且用户有效负载的对齐不超过“ChunkHeader”的对齐
sizeof(ChunkHeader) userPayloadSize
|------------------>|--------------------->|
| | |
+===================+======================+==================================+
| 块头 ¦ * | 用户有效负载 | 填充 |
+===================+======================+==================================+
| | |
| 用户有效负载偏移量 | |
|------------------>| |
| 块大小 |
|---------------------------------------------------------------------------->|
*) 来自块头的用户有效负载偏移量和反向偏移量重叠
- 没有用户头且用户有效负载的对齐超过“ChunkHeader”的对齐
sizeof(ChunkHeader) 反向偏移 userPayloadSize
|------------------>| |<---|------------------->|
| | | | |
+===================+=======================+====================+============+
| 块头 | ¦ | 用户有效负载 | 填充 |
+===================+=======================+====================+============+
| | |
| 用户有效负载偏移量 | |
|------------------------------------------>| |
| |
| 块大小 |
|---------------------------------------------------------------------------->|
根据块的地址,有可能“ChunkHeader”仍然与用户有效负载相邻。在这种情况下,框定看起来与情况 1 完全相同。
- 使用了用户头
sizeof(ChunkHeader) 反向偏移 userPayloadSize
|------------------>| |<---|------------------->|
| | | | |
+===================+===============+=======+====================+============+
| 块头 | 用户头 | ¦ | 用户有效负载 | 填充 |
+===================+===============+=======+====================+============+
| | |
| 用户有效负载偏移量 | |
|------------------------------------------>| |
| |
| 块大小 |
|---------------------------------------------------------------------------->|
用户有效负载偏移量计算
- 没有用户头且用户有效负载的对齐不超过“ChunkHeader”的对齐
userPayloadOffset = sizeof(ChunkHeader);
- 没有用户头且用户有效负载的对齐超过“ChunkHeader”的对齐,这意味着用户有效负载要么与“ChunkHeader”相邻,要么在用户有效负载前面有至少“ChunkHeader”对齐大小的填充,因此有足够的空间来存储“反向偏移”
chunkHeaderEndAddress = addressof(chunkHeader) + sizeof(chunkHeader);
alignedUserPayloadAddress = align(chunkHeaderEndAddress, userPayloadAlignment);
userPayloadOffset = alignedUserPayloadAddress - addressof(chunkHeader);
- 使用了用户头
chunkHeaderEndAddress = addressof(chunkHeader) + sizeof(chunkHeader) + sizeof(userHeader);
anticipatedBackOffsetAddress = align(chunkHeaderEndAddress, alignof(userPayloadOffset));
unalignedUserPayloadAddress = anticipatedBackOffsetAddress + sizeof(userPayloadOffset);
alignedUserPayloadAddress = align(unalignedUserPayloadAddress, userPayloadAlignment);
userPayloadOffset = alignedUserPayloadAddress - addressof(chunkHeader);
所需块大小计算
为了将用户头和用户有效负载适配到块中,必须进行最坏情况的计算。我们可以假设块对于“ChunkHeader”有足够的对齐。
- 没有用户头且用户有效负载的对齐不超过“ChunkHeader”的对齐
chunkSize = sizeof(chunkHeader) + userPayloadSize;
- 没有用户头且用户有效负载的对齐超过“ChunkHeader”的对齐
最坏的情况是当一部分“ChunkHeader”跨越用户有效负载的对齐边界,因此用户有效负载必须对齐到下一个边界。以下图形演示了这种情况。
┌ 反向偏移
+===============+==========|+===============================+
| 块头 | ¦ᵛ| 用户有效负载 |
+===============+===========+===============================+
⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ <- “ChunkHeader”的对齐边界
⊥ ⊥ ⊥ ⊥ <- 用户有效负载的对齐边界
<-----------|--------------->
前置 用户有效负载 |------------------------------->
用户有效负载 对齐 用户有效负载大小
对齐
悬垂
以下公式用于计算所需的块大小。
preUserPayloadAlignmentOverhang = sizeof(chunkHeader) - alignof(chunkHeader);
chunkSize = preUserPayloadAlignmentOverhang + userPayloadAlignment + userPayloadSize;
- 使用了用户头
与情况 2 类似,但在这种情况下,可能是“反向偏移”跨越了用户有效负载的对齐边界。
┌ 反向偏移,具有与用户有效负载偏移量相同的对齐
|
+===============+===========+==============|+===============================+
| 块头 | 用户头 | ¦ᵛ| 用户有效负载 |
+===============+===========+===============+===============================+
⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ <- 用户有效负载偏移量的对齐边界
⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ ⊥ <- “ChunkHeader”的对齐边界
⊥ ⊥ ⊥ ⊥ <- 用户有效负载的对齐边界
<---------------------------|--------------->
前置用户有效负载对齐 用户有效负载 |------------------------------->
悬垂 对齐 用户有效负载大小
以下公式用于计算所需的块大小。
chunkHeaderSize = sizeof(chunkHeader) + sizeof(userHeader)
preUserPayloadAlignmentOverhang = align(chunkHeaderSize, alignof(userPayloadOffset));
maxPadding = max(sizeof(userPayloadOffset), userPayloadAlignment);
chunkSize = preUserPayloadAlignmentOverhang + maxPadding + userPayloadSize;
访问块头扩展
“ChunkHeader”有一个模板方法来获取用户头。不小心错误使用它存在风险,但目前对此没有更好的解决方案。
由于用户头总是与“ChunkHeader”相邻,获取用户头的公式为:
userHeader = addressOf(chunkHeader) + sizeof(chunkHeader);
块头方法
void* userPayload()
返回指向用户有效负载的指针template <typename T> T* userHeader()
返回指向用户头的指针static ChunkHeader* fromUserPayload(const void* const userPayload)
返回与用户有效负载相关联的“ChunkHeader”的指针
集成到发布者/订阅者 API
“Publisher”对于用户头和用户有效负载的对齐有额外的模板参数。或者,这也可以通过“allocate”方法完成,但这增加了意外错误使用的风险。
此外,为了将用户头完全集成到常规控制流中,可以将指向“ChunkHeaderHook”的指针传递给“Publisher”的构造函数。
“ChunkHeaderHook”不在共享内存中,并具有以下虚方法:
virtual void allocateHook(ChunkHeader& chunkHeader)
可用于初始化用户头virtual void deliveryHook(ChunkHeader& chunkHeader)
可用于设置用户头的成员,例如时间戳
此外,“Publisher”和“Subscriber”可以访问“ChunkHeader”,并可以使用“userHeader()”方法访问用户头。
陷阱与测试
- 当用户有效负载与“ChunkHeader”相邻时,必须确保“userPayloadOffset”与“反向偏移”重叠,即在用户有效负载前面“sizeof(userPayloadOffset)”处
- 为了简化计算,假设用户头的对齐不超过“ChunkHeader”的对齐。这必须用“assert”来强制执行
4. 未解决的问题
- 可以在发布者端使用“ChunkHeaderHook”
template <typename UserHeader>
class MyChunkHeaderHook : public ChunkHeaderHook
{
public:
void deliveryHook(ChunkHeader& chunkHeader) override
{
chunkHeader.userHeader<UserHeader>().timestamp = myTimeProvider::now();
}
};
auto userHeaderHook = MyChunkHeaderHook<MyUserHeader>();
auto pub = iox::popo::Publisher<MyPayload>(serviceDescription, userHeaderHook);
- 或者,代替“ChunkHeaderHook”类,发布者可以有一个方法“registerDeliveryHook(std::function<void(ChunkHeader&)>)”
- 只允许用户对“ChunkHeader”有只读访问权限,对“UserHeader”有写访问权限
- 用户定义的序列号
- 这可能通过“ChunkHeaderHook”完成
- 或者,可以在“ChunkHeader”中引入一个标志
- 用户定义的时间戳
- 这可能应该在用户头中
- 内存池配置
- 目前我们指定块有效负载的大小,“ChunkHeader”的大小会自动添加
- 采用新方法后,指定的块有效负载大小的一部分可能用于用户头
- 指定的块有效负载的一部分也可能用作用户有效负载对齐的填充
- 用户将继续指定块有效负载;如果使用了用户头或自定义的用户有效负载对齐,用户需要考虑到这一点
- 如果使用了用户扩展,是否有必要在“ChunkHeader”中存储一个标志?
- 我们可以维护一个已知用户头 ID 或 ID 范围的列表,类似于“ IANA ” https://datatracker.ietf.org/doc/id/draft-cotton-tsvwg-iana-ports-00.html#privateports
- 这些 ID 可以存储在“ChunkHeader”中,任何大于
0xC000
的 ID 都可以自由使用 - 为了使其更安全,“ChunkHeaderHook”必须是强制性的,例如具有
virtual uint16_t getId() = 0;
方法,该方法将在“ChunkSender”中被调用- 或者,用户头结构体必须具有
constexpr uint16_t USER_HEADER_ID
;如果不存在,我们可以将 ID 设置为0xC000
,这是第一个可自由使用的 ID
- 或者,用户头结构体必须具有
- 我们是否希望像“chunkHeaderVersion”一样在“ChunkHeader”中存储用户扩展的版本
- 对于记录和重放,用户头是完全不透明的,如果例如在用户头中存储了时间戳并且需要为重放进行更新,这可能会导致问题
- 如果我们维护已知用户头 ID 的列表并且还存储用户头版本,记录和重放框架可以实现所需的转换
- 对于记录和重放,有必要存储用户有效负载的对齐方式;决定是否应该使用
uint16_t
或者是否只应在uint8_t
中存储对齐的幂
5. 开源问题
- 对于发布者而言,“ChunkHeaderHook” 可以被使用。
template <typename UserHeader>
class MyChunkHeaderHook : public ChunkHeaderHook
{
public:
void deliveryHook(ChunkHeader& chunkHeader) override
{
chunkHeader.userHeader<UserHeader>().timestamp = myTimeProvider::now();
}
};
auto userHeaderHook = MyChunkHeaderHook<MyUserHeader>();
auto pub = iox::popo::Publisher<MyPayload>(serviceDescription, userHeaderHook);
- 另外,取代“ChunkHeaderHook”类,发布者可以拥有一个方法“registerDeliveryHook(std::function<void(ChunkHeader&)>)”。
- 允许用户对“ChunkHeader”仅拥有只读访问权限,对“UserHeader”拥有写访问权限。
- 用户自定义的序列号
- 这或许能够通过“ChunkHeaderHook”来实现。
- 或者,在“ChunkHeader”中引入一个标志位。
- 用户自定义的时间戳
- 这应该可能放在用户头中。
- 内存池配置
- 目前我们指定块有效负载的大小,“ChunkHeader”的大小会自动添加。
- 采用新方法后,指定的块有效负载大小的一部分可能会被用于用户头。
- 指定的块有效负载的一部分也可能用作为用户有效负载对齐的填充。
- 用户将继续指定块有效负载;如果使用了用户头或自定义的用户有效负载对齐,用户需要将此情况考虑在内。
- 如果使用了用户扩展,是否有必要在“ChunkHeader”中存储一个标志?
- 我们可以维护一个已知用户头 ID 或 ID 范围的列表,类似于 “IANA” https://datatracker.ietf.org/doc/id/draft-cotton-tsvwg-iana-ports-00.html#privateports 。
- 这些 ID 可以存储在“ChunkHeader”中,任何大于
0xC000
的 ID 都可以自由使用。 - 为了使其更安全,“ChunkHeaderHook”必须是强制使用的,例如具有
virtual uint16_t getId() = 0;
方法,该方法将在“ChunkSender”中被调用。- 或者,用户头结构体必须具有
constexpr uint16_t USER_HEADER_ID
;如果不存在,我们可以将 ID 设置为0xC000
,这是第一个可自由使用的 ID。
- 或者,用户头结构体必须具有
- 我们是否希望像“chunkHeaderVersion”一样在“ChunkHeader”中存储用户扩展的版本。
- 对于记录和重放,用户头是完全不透明的,如果例如在用户头中存储了时间戳并且需要为重放进行更新,这可能会导致问题。
- 如果我们维护已知用户头 ID 的列表并且还存储用户头版本,记录和重放框架可以实现所需的转换。
- 对于记录和重放,有必要存储用户有效负载的对齐方式;决定是否应该使用
uint16_t
或者是否只应在uint8_t
中存储对齐的幂。