多生产者单消费者包缓冲区
多生产者单消费者包缓冲区(MPSC_PBUF) 是一种环形缓冲区,按先进先出的顺序存储变长数据包。它假定只有一个上下文消费数据。不过,在 panic 场景下,另一个上下文可能介入,将数据全部输出,并且不再返回。数据包的生产分为两步:先分配所需大小的空间,再由生产者填入数据并提交。消费数据包也分为两步:消费者先认领数据包,获取其指针和长度,随后再释放。这种方式减少了内存复制。
MPSC 包缓冲区 具有以下主要属性:
生产数据包采用先分配、再提交的方式。
消费数据包采用先认领、再释放的方式。
分配器确保分配所需长度的连续内存。
无法分配所需空间时,可以采用以下策略:
覆盖 —— 丢弃最早的数据包,直到能够分配所需的内存。每丢弃一个数据包,都会调用用户回调。
不覆盖 —— 无法分配所需空间时,分配失败。
提供专门优化的 API,用于存储短数据包。
支持带超时的分配。
内部实现
缓冲区中的每个数据包都包含一个 MPSC_PBUF 专用头部,用于内部管理。头部包含两个标志位。为优化内存占用,可以使用 MPSC_PBUF_HDR 将此头部嵌入用户头部,第一个字的其余位可供应用使用。头部包含以下标志:
valid —— 数据包包含有效用户数据包时,此位置一
busy —— 数据包正在被消费(已认领但尚未释放)时,此位置一
头部状态:
valid |
busy |
说明 |
0 |
0 |
空间空闲 |
1 |
0 |
有效数据包 |
1 |
1 |
已认领的有效数据包 |
0 |
1 |
内部跳过包 |
包缓冲区空间包含空闲空间、有效用户数据包以及内部跳过包。内部跳过包用于标识填充区域,例如缓冲区末尾的填充。
分配
使用成对的读写索引确定可用空间。如果可以分配空间,则移动临时写索引,返回指向缓冲区内该空间的指针,并重置数据包头部。如果分配需要让写索引回绕,则在缓冲区末尾添加一个跳过包。如果无法分配空间且未启用覆盖,则返回 NULL;若分配请求带有超时,则阻塞当前上下文。
覆盖式分配
启用覆盖时,会丢弃最早的数据包,直到能够分配所需空间。丢弃时会检查头部中的 busy 标志,确保不覆盖正在消费的数据包。遇到这种数据包时,会在它之前添加跳过包,并丢弃它之后的数据包。释放这个忙数据包时会检测到这种情况,并将它转为跳过包,以免重复处理。
用法
数据包头部定义
数据包头部的详细定义见 include/zephyr/sys/mpsc_packet.h,API 函数见 include/zephyr/sys/mpsc_pbuf.h。将头文件分开,是为了避免在声明数据包时引入过多头文件。
用户头部结构必须以内部头部开头:
#include <zephyr/sys/mpsc_packet.h>
struct foo_header {
MPSC_PBUF_HDR;
uint32_t length: 32 - MPSC_PBUF_HDR_BITS;
};
包缓冲区配置
配置结构包含缓冲区信息、配置标志和回调。包缓冲区使用以下回调:
丢弃通知 —— 每当因覆盖而丢弃数据包时调用。
获取数据包长度 —— 用于确定数据包长度。
生产数据包
标准的两步方式:
foo_packet *packet = mpsc_pbuf_alloc(buffer, len, K_NO_WAIT);
fill_data(packet);
mpsc_pbuf_commit(buffer, packet);
针对性能优化的小数据包存储方式:
32 位字数据包
32 位字加指针的数据包
注意,数据包按值写入,因此其头部的 valid 位应事先置位。
mpsc_pbuf_put_word(buffer, data);
mpsc_pbuf_put_word_ext(buffer, data, ptr);
消费数据包
两步方式:
foo_packet *packet = mpsc_pbuf_claim(buffer);
process(packet);
mpsc_pbuf_free(buffer, packet);