1、C 字符串不记录自身长度,造成缓冲区溢出场景
- 例如:在程序中有两个内存相邻的 C 字符串 s1 和 s2 , 其中 s1 保存了字符串 "Redis",s2 保存了字符串 "MongoDB",如图所示
- 使用 strcat(s1,"Cluster"); 将 "Cluster" 拼接到 s1 中,由于 s1 没有分配足够的空间,s1 的数据将溢出到 s2 所在的空间中,导致 s2 保存的内容被意外修改,如图所示
#include<stdio.h>
int main() {
// 传统C字符串保存形式: 'H'、'e'、'l'、'l'、'o'、''、'a'、'a'、'a'、'a'、'a'、'\0'
char str1[12] = "Hello aaaaa";
char str2[6] = "world";
printf("Before merging: str1 = %s, str2 = %s\n", str1, str2);
// Merge str2 into str1
// strcat(str1, str2);
// printf("After merging: str1 = %s, str2 = %s\n", str1, str2);
return 0;
}
2、SDS 是如何避免缓冲区溢出的?
1. 空间预分配
SDS API 对 SDS 进行修改时,API 会先检查 SDS 的空间是否满足修改所需要的要求,如果不满足的话,API 会自动将 SDS 空间扩展至执行所需要的大小,然后再执行实际的修改操作;
分配规则:
- SDS 修改之后长度 < 1 MB:Redis 先分配数据实际需要的空间,同时还会分配和 len 属性同样大小的未使用空间(减少内存重分配次数),此时 SDS buf 数组的实际长度 = 未使用空间 + 已使用空间 +1,如下图:
修改前:
修改后:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 定义SDS结构
typedef struct sds {
// 字符串的长度
int len;
// 剩余的未使用空间
int free;
// 字符串缓冲区,使用柔性数组
char buf[];
} sds;
// 初始化 SDS 字符串
sds* sdsnew(const char* init) {
// 初始化字符长度
size_t initlen = (init == NULL) ? 0 : strlen(init);
sds* s = malloc(sizeof(sds) + initlen + 1);
if (s == NULL) return NULL;
// 给 len 赋值 = 当前字符串长度
s->len = initlen;
// 给 free 赋予初始值 0
s->free = 0;
if (initlen) memcpy(s->buf, init, initlen);
s->buf[initlen] = '\0';
return s;
}
// 拼接字符串到现有的SDS字符串
sds* sdscat(sds* s, const char* t) {
// 原字符串长度
size_t totlen, curlen = s->len;
// 拼接的字符串长度
size_t tlen = strlen(t);
if (s->free < tlen) {
// free 空间不够,需要重新分配空间
totlen = curlen + tlen;
s = realloc(s, sizeof(sds) + totlen + 1);
// 设置 free 为新的总长度
s->free = totlen;
}
memcpy(s->buf + curlen, t, tlen);
s->len = curlen + tlen;
s->buf[s->len] = '\0';
return s;
}
int main() {
// 创建一个新的SDS字符串
sds* mystring = sdsnew("Hello");
// 获取SDS字符串的长度
printf("旧字符串 len 值: %d\n", mystring->len);
// 获取SDS字符串的当前分配空间
printf("SDS 总分配空间: %zu\n", mystring->len + mystring->free + 1);
// 计算剩余未使用空间
printf("剩余未使用空间: %d\n", mystring->free);
printf("------------------- \n");
// 拼接字符串
mystring = sdscat(mystring, "World");
// 获取SDS字符串的长度
printf("拼接新字符串后 len 值: %d\n", mystring->len);
// 获取SDS字符串的当前分配空间
printf("拼接新字符串后 SDS 总分配空间: %zu\n", mystring->len + mystring->free + 1);
// 计算剩余未使用空间
printf("拼接新字符串后 SDS 剩余未使用空间: %d\n", mystring->free);
// 释放SDS字符串的内存空间
free(mystring);
return 0;
}
- SDS 修改后的长度 >= 1MB : 那么修改之后的 len 将变成 30 MB,同时分配 1 MB 的未使用空间,SDS 的实际长度 = 30 MB +1 MB + 1 byte;
2. 惰性空间释放
- 惰性空间释放用于优化 SDS 的字符串缩短操作:当缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用。