用显式长度保存包含零字节的字符串
Redis SDS 把字节长度与可用容量放进头部,追加按长度复制,正文允许包含零字节。
工程问题#
以零字节作为结束标记的 C 字符串无法单独表达任意二进制载荷;重复求长度和频繁扩容也会增加解析、拼接路径的成本。
实际实现#
sdshdr8 等头部区分 len 和 alloc;sdscatlen 先取已用长度,再申请空间,按传入的字节数 memcpy,随后更新长度并补终止符。终止符用于兼容部分 C API,不决定 SDS 的逻辑长度。
处理流程#
- 从头部读取当前字节长度
- 按新增字节数确保剩余容量,接收可能变化的新地址
- 复制指定数量的字节,不用 strlen 推断载荷大小
- 更新 len,并在逻辑内容之后写入终止符
源码片段#
固定版本 · 原文摘录479/* Append the specified binary-safe string pointed by 't' of 'len' bytes to the480 * end of the specified sds string 's'.481 *482 * After the call, the passed sds string is no longer valid and all the483 * references must be substituted with the new pointer returned by the call. */484sds sdscatlen(sds s, const void *t, size_t len) {485 size_t curlen = sdslen(s);486487 s = sdsMakeRoomFor(s,len);488 if (s == NULL) return NULL;489 memcpy(s+curlen, t, len);490 sdssetlen(s, curlen+len);491 s[curlen+len] = '\0';492 return s;493}完整追加函数按 len 复制并显式维护长度;原始注释要求调用后改用返回的新指针。
51struct __attribute__ ((__packed__)) sdshdr8 {52 uint8_t len; /* used */53 uint8_t alloc; /* excluding the header and null terminator */54 unsigned char flags; /* 3 lsb of type, 5 unused bits */55 char buf[];56};57struct __attribute__ ((__packed__)) sdshdr16 {58 uint16_t len; /* used */59 uint16_t alloc; /* excluding the header and null terminator */60 unsigned char flags; /* 3 lsb of type, 5 unused bits */61 char buf[];62};两种实际头部都分开存储已用字节数 len 与不含头部、终止符的容量 alloc。
SDS 的长度不是字符数,也不靠最后一个零字节推导。此例不把所有大小的字符串追加笼统宣称为摊还 O(1)。
收益与取舍#
工程分析带来的收益
- 显式长度支持包含零字节的二进制内容
- 取长度无需扫描正文
- 分离容量与逻辑长度,允许复用预留空间
付出的代价
- 头部元数据和预留容量增加空间占用
- 扩容可能改变地址,旧别名不能继续使用
- 调用方必须检查失败并正确更新句柄,不能把任意 char 指针当成 SDS
可比较的选择
以零结尾的 C 字符串接口普遍,但 strlen 需要扫描,且正文中零字节会截断字符串语义。
指针与长度的只读视图适合不拥有数据的切片,不负责动态扩容或储存生命周期。
版本与适用范围#
对应 Redis 7.2.0 的 SDS。sdshdr5 把短长度放在 flags 中且不记录空闲容量;这里展示的 len/alloc 分离来自其他头类型。二进制安全要求使用带长度的 API,sdscat 等 C 字符串接口仍使用 strlen。
参考来源#
源码、官方文档与原始论文相关实现与比较#
4 个相关案例nginx 解析 upstream 块时,用 ngx_array_t 连续保存服务器条目;每遇到一条 server 指令便追加一个元素。
Linux IIO 的 kfifo 缓冲在采样进入内核与用户态读取之间暂存数据,按先进先出顺序交付采样记录。
Linux BPF 验证器的 check_cfg 使用显式指令栈执行非递归 DFS,检查跳转目标、可达性及控制流结构。
Git 把内存中的 index 条目指针保存在有序、可增长的数组中,按路径和合并 stage 二分查找位置。