redis源码阅读之内存管理
作者:
| 更新日期:早就想阅读一下redis的源码了, 但是迟迟没有赋予行动, 现在开始记录下阅读笔记。
本文首发于公众号:天空的代码世界,微信号:tiankonguse
背景
redis是一个很不错的NOSQL数据库。
关于redis的使用文档, 可以参考这里.
关于redis内存管理的源码可以参考这里
功能
redis对内存管理函数进行了封装, 好处有下面几个。
- 可以根据自己的需要, 适当的改造内存管理函数。
- 可以方便的切换到另一种内存管理库上面。
- 可以监控内存的分配和回收, 防止内存泄露。
redis封装了下面一系列的内存相关的函数, 我们只需要重点阅读通用的内存管理函数即可。
void *zmalloc(size_t size);
void *zcalloc(size_t size);
void *zrealloc(void *ptr, size_t size);
void zfree(void *ptr);
char *zstrdup(const char *s);
size_t zmalloc_used_memory(void);
void zmalloc_enable_thread_safeness(void);
void zmalloc_set_oom_handler(void (*oom_handler)(size_t));
float zmalloc_get_fragmentation_ratio(size_t rss);
size_t zmalloc_get_rss(void);
size_t zmalloc_get_private_dirty(void);
size_t zmalloc_get_smap_bytes_by_field(char *field);
size_t zmalloc_get_memory_size(void);
void zlibc_free(void *ptr);
size_t zmalloc_size(void *ptr);
辅助函数
阅读重点函数前, 需要了解几个辅助函数, 这样阅读重点函数的时候,就可以忽略这些辅助函数了。
错误处理函数
oom
的含义是 Out of memory
.
所以函数zmalloc_default_oom
的功能就是当申请内存失败的时候, 调用改函数进行失败处理。
zmalloc_oom_handler
是一个内存申请失败时, 调用的函数指针, 默认指向函数zmalloc_default_oom
。
zmalloc_default_oom
的实现也很简单, 输出错误日志, 然后退出程序。
fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n", size);
fflush(stderr);
abort();
原子加减操作
__atomic_add_fetch
和__atomic_sub_fetch
是c++11
支持的内置函数。
#define update_zmalloc_stat_add(__n) __atomic_add_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
#define update_zmalloc_stat_sub(__n) __atomic_sub_fetch(&used_memory, (__n), __ATOMIC_RELAXED)
如果编译器不支持这两个函数的话, 就需要显示的使用锁了。
#define update_zmalloc_stat_add(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory += (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
#define update_zmalloc_stat_sub(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory -= (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)
内存统计函数
内存必须是sizeof(long)
的整数倍, 当不是的话, 需要调整为整数倍。
不要看下面使用了位操作, 其实它这个算法是最初级的算法, 优化空间还很大(后面具体分析怎么优化)。
下面直接使用sizeof(long)
来对齐, 其实是不好的写法, 至少应该使用一个宏来代替。
#define update_zmalloc_stat_alloc(__n) do { \
size_t _n = (__n); \
if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
if (zmalloc_thread_safe) { \
update_zmalloc_stat_add(_n); \
} else { \
used_memory += _n; \
} \
} while(0)
#define update_zmalloc_stat_free(__n) do { \
size_t _n = (__n); \
if (_n&(sizeof(long)-1)) _n += sizeof(long)-(_n&(sizeof(long)-1)); \
if (zmalloc_thread_safe) { \
update_zmalloc_stat_sub(_n); \
} else { \
used_memory -= _n; \
} \
} while(0)
假设sizeof(long)
为4, 我们的目标是对内存按4字节对齐, 不足的向上对齐。
则最原始的算法是这个样子:
当需要对齐的时候, 把多余的部分删除, 然后加上一个sizeof(long)
即可。
if(n%4){
//需要对齐
n = (n - n%4) + 4;
}
大家看到了取模操作, 知道很慢, 于是可以使用位操作代替。
代替后, 就会发现redis源码使用的就是只优化取模操作的算法啦。
if(n & (4-1)){
//需要对齐
n = (n - (n & (4-1))) + 4;
}
上面向上对齐的算法, 我们也可以使用位运算优化的。
对齐位全部至1, 然后再加1即可。
if(n & (4-1)){
//需要对齐
n = (n | (4-1)) + 1;
}
当然, 由于对齐数是2的幂数, 假设我们知道有多少位的话, 可以通过左移右移来实现这个对齐操作。
先右移, 及处理对齐数字, 然后加1, 再左移回来(假设左移时使用0补齐)。
if(n & (4-1)){
//需要对齐
n = ((n >> 2) + 1) << 2;
}
设置线程安全模式
线程安全模式, 会操作去加锁。
void zmalloc_enable_thread_safeness(void) {
zmalloc_thread_safe = 1;
}
设置内存错误处理函数
void zmalloc_set_oom_handler(void (*oom_handler)(size_t)) {
zmalloc_oom_handler = oom_handler;
}
内存管理函数
申请内存
zmalloc/zcalloc/zrealloc
这三个函数和内置函数功能类似, 只不过增加了下面几个功能。
- 记录申请的内存大小
- 申请失败时错误处理
- 内存申请统计
//申请指定大小的内存
void *zmalloc(size_t size);
//与zmalloc完全等价, 申请指定大小的内存
void *zcalloc(size_t size);
//调整内存
void *zrealloc(void *ptr, size_t size);
void *ptr = malloc(size+PREFIX_SIZE);
if (!ptr) zmalloc_oom_handler(size);
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
return (char*)ptr+PREFIX_SIZE;
这里大家会对PREFIX_SIZE
产生疑问, 而我上面也说了, 这个函数增加了记录内存大小的功能。
我们知道内置的函数不能得到申请内存的大小, 我们自己来记录的话, 就需要内存来储存这个大小啦。
多出来的PREFIX_SIZE
这个内存, 就是用来储存应用级别申请的大小的(应用级别代表PREFIX_SIZE
的内存对用户不可见)。
内存大小
我们记录了内存的大小, 当然既可以获得内存的大小了。
当然, 这里还是对内存大小进行了对齐。
size_t zmalloc_size(void *ptr) {
void *realptr = (char*)ptr-PREFIX_SIZE;
size_t size = *((size_t*)realptr);
/* Assume at least that all the allocations are padded at sizeof(long) by
* the underlying allocator. */
if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));
return size+PREFIX_SIZE;
}
释放内存
void zfree(void *ptr) {
void *realptr;
size_t oldsize;
if (ptr == NULL) return;
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
free(realptr);
}
深拷贝字符串
char *zstrdup(const char *s) {
size_t l = strlen(s)+1;
char *p = zmalloc(l);
memcpy(p,s,l);
return p;
}
查询使用内存大小
代码中, 这个是否打开安全模式和是否支持原子操作在函数中实现, 又是不好的代码风格。
因为很多地方都需要这个原子操作, 不能让使用方频繁的去判断, 应该封装为一个统一的函数或者宏, 然后使用者直接调用宏或者函数即可。
当然, 只所以没有封装成宏, 原因大概是还需要对变量赋值。 函数的话又怕效率低吧。
size_t zmalloc_used_memory(void) {
size_t um;
if (zmalloc_thread_safe) {
#if defined(__ATOMIC_RELAXED) || defined(HAVE_ATOMIC)
um = update_zmalloc_stat_add(0);
#else
pthread_mutex_lock(&used_memory_mutex);
um = used_memory;
pthread_mutex_unlock(&used_memory_mutex);
#endif
}
else {
um = used_memory;
}
return um;
}
实际内存大小
一个进程占占用的实际内存等于一页大小乘以实际页个数。
一页大小可以通过sysconf(_SC_PAGESIZE)
获得, 而实际页个数可以在/proc/[pid]/stat
中获得。
rss
是 Resident Set Size
的简称。
size_t zmalloc_get_rss(void);
内存中断率
当我们的数据全部加载到内存中的话, 直接运行就OK了。
但是当内存不足的时候, 系统就会创建虚拟内存, 把不常用的内存放到磁盘上, 需要的时候再加载到内存中。
如果存在从磁盘上加载数据, 性能必然就会低下了。
/* Fragmentation = RSS / allocated-bytes */
float zmalloc_get_fragmentation_ratio(size_t rss) {
return (float)rss/zmalloc_used_memory();
}
内存信息
内存的各种信息, 在/proc/[pid]/smaps
里面都可以查到, 比如上面的rss
.
/proc/self/smaps
和/proc/[pid]/smaps
完全等价。
需要注意的一点是这里的单位是kb
。
size_t zmalloc_get_smap_bytes_by_field(char *field) {
char line[1024];
size_t bytes = 0;
FILE *fp = fopen("/proc/self/smaps","r");
int flen = strlen(field);
if (!fp) return 0;
while(fgets(line,sizeof(line),fp) != NULL) {
if (strncmp(line,field,flen) == 0) {
char *p = strchr(line,'k');
if (p) {
*p = '\0';
bytes += strtol(line+flen,NULL,10) * 1024;
}
}
}
fclose(fp);
return bytes;
}
实际内存大小
size_t zmalloc_get_private_dirty(void) {
return zmalloc_get_smap_bytes_by_field("Private_Dirty:");
}
得到物理内存大小
直接读页数以及页大小, 相乘即可。
size_t zmalloc_get_memory_size(void) {
return (size_t)sysconf(_SC_PHYS_PAGES) * (size_t)sysconf(_SC_PAGESIZE);
}
本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。