Redis源码剖析——内存分配

内存分配是程序需要解决的一个大问题,也应该是剖析一个程序的源码最基础最关键的部分之一。

Redis的内存分配主要是在C语言中对内存处理的函数:malloc、realloc、free的基础加了一些封装和异常的处理。

其特点主要有以下三点:

  1. 添加对使用内存的统计,在分配和释放内存的时候都会更新记录使用内存的量
  2. 支持线程安全模式,通过锁的机制对use_memory进行控制,避免其出现脏数据的可能
  3. 增添对内存溢出的处理

首先Redis源码中与内存分配相关的以下全局变量:

1
2
3
static size_t used_memory = 0;    // 使用的内存大小
static int zmalloc_thread_safe = 0; // 线程安全模式状态
pthread_mutex_t used_memory_mutex = PTHREAD_MUTEX_INITIALIZER; // 为此服务器

内存分配相关功能函数

Redis中和内存分配相关的功能函数主要有以下这些:

1
2
3
4
5
6
7
8
9
10
11
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); // 获取rss信息
size_t zmalloc_get_private_dirty(void); // 获取实际物理分配的内存

内存申请与调整

Redis中和内存申请相关的函数主要是zmalloc、zcalloc和zrealloc,分别是对C语言中的malloc、calloc和realloc的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// 对malloc进行封装,加上异常处理和内存统计
void *zmalloc(size_t size) {
// 多申请的内存用于存储次块空间大小
void *ptr = malloc(size+PREFIX_SIZE);

// 内存溢出
if (!ptr) zmalloc_oom_handler(size);
// 进行内存统计
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
#else
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
return (char*)ptr+PREFIX_SIZE;
#endif
}

void *zcalloc(size_t size) {
void *ptr = calloc(1, size+PREFIX_SIZE);

if (!ptr) zmalloc_oom_handler(size);
#ifdef HAVE_MALLOC_SIZE
update_zmalloc_stat_alloc(zmalloc_size(ptr));
return ptr;
#else
*((size_t*)ptr) = size;
update_zmalloc_stat_alloc(size+PREFIX_SIZE);
return (char*)ptr+PREFIX_SIZE;
#endif
}

// 封装重新分配内存函数
void *zrealloc(void *ptr, size_t size) {
#ifndef HAVE_MALLOC_SIZE
void *realptr;
#endif
size_t oldsize;
void *newptr;

if (ptr == NULL) return zmalloc(size);
#ifdef HAVE_MALLOC_SIZE
// 原来所使用空间的大小
oldsize = zmalloc_size(ptr);
// 重新分配新的空间
newptr = realloc(ptr,size);
if (!newptr) zmalloc_oom_handler(size);

// 更新保存的使用内存值
update_zmalloc_stat_free(oldsize);
update_zmalloc_stat_alloc(zmalloc_size(newptr));
return newptr;
#else
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
newptr = realloc(realptr,size+PREFIX_SIZE);
if (!newptr) zmalloc_oom_handler(size);

*((size_t*)newptr) = size;
update_zmalloc_stat_free(oldsize);
update_zmalloc_stat_alloc(size);
return (char*)newptr+PREFIX_SIZE;
#endif
}

首先要注意的是对内存溢出的处理,可以自定义处理函数,其默认的处理方式如下:

1
2
3
4
5
6
7
8
9
10
// 默认溢出处理方法
static void zmalloc_default_oom(size_t size) {
fprintf(stderr, "zmalloc: Out of memory trying to allocate %zu bytes\n",
size);
fflush(stderr);
abort();
}

// 内存溢出处理方法
static void (*zmalloc_oom_handler)(size_t) = zmalloc_default_oom;

然后我们可以看到每次在调整内存的时候,都会对全局变量use_memory进行改变,调整是以sizeof(long)的整数倍进行调整的。在改变use_memory的时候会判断是否在线程安全模式下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 更新使用的内存数量值
#define update_zmalloc_stat_alloc(__n) do { \
size_t _n = (__n); \
// _n调整为size(long)的整数倍
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_add(__n) do { \
pthread_mutex_lock(&used_memory_mutex); \
used_memory += (__n); \
pthread_mutex_unlock(&used_memory_mutex); \
} while(0)

内存释放

内存释放部分和申请部分类似,主要是对free函数的封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 获取内存占用空间大小
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. */
// 调整为sizeof(long)的整数倍,方便对齐
if (size&(sizeof(long)-1)) size += sizeof(long)-(size&(sizeof(long)-1));
return size+PREFIX_SIZE;
}
#endif

// 封装对系统的free调用
void zfree(void *ptr) {
#ifndef HAVE_MALLOC_SIZE
void *realptr;
size_t oldsize;
#endif

if (ptr == NULL) return;
#ifdef HAVE_MALLOC_SIZE
// 减少对内存的计数
update_zmalloc_stat_free(zmalloc_size(ptr));
free(ptr);
#else
realptr = (char*)ptr-PREFIX_SIZE;
oldsize = *((size_t*)realptr);
update_zmalloc_stat_free(oldsize+PREFIX_SIZE);
free(realptr);
#endif
}

对use_memory的操作:

1
2
3
4
5
6
7
8
9
10
11
12
// 释放内存以后对内存空间统计变量的改变
#define update_zmalloc_stat_free(__n) do { \
size_t _n = (__n); \
// 以sizeof(long)的整数倍进行调整
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)

辅助函数

除了最基本的一些内存分配的函数以外,还实现了一些辅助函数(如复制字符串、获取已经使用内存的大小):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 复制字符串操作
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;
}