ngx_slab共享内存

nginx是多进程,实现访问控制、限流功能时,进程需要共享数据,比如一个uri的多笔请求,可能由多个进程处理,此时进程需要判断这个uri是否达到了限制次数,因此访问控制模块和限流模块使用共享内存进行进程间的通信,nginx实现了ngx_shm_t共享内存,但是如果要共享一些复杂的数据结构,ngx_shm_t很难满足这种需求,因此在这个基础上实现了slab共享内存。

1、初始化共享内存

模块在配置初始化时,将会申请一块slab内存池,开发者可以通过ngx_slab_alloc向这个内存池申请内存,当内存池用尽时,这个函数就会返回NULL。

1
2
3
ngx_shm_zone_t *

ngx_shared_memory_add(ngx_conf_t *cf, ngx_str_t *name, size_t size, void* *tag)
  • ngx_conf_t *cf //全局配置文件
  • ngx_str_t *name //这块slab共享内存的名字
  • size_t size //这块共享内存的大小
  • void *tag //防止2个不同模块定义的内存池具有相同的名字,一般传入本模块结构体的地址

本模块结构体的地址通常为全局变量,因此在reload,nginx重读配置时,因为tag没有变化,所以不会重新申请内存。还有一个好处是,如果之前共享内存是有数据的,这样不会丢掉之前共享内存中的数据,因此使用的思想是,尽可能使用旧的共享内存,当然前提是旧的存在。

2、操作slab共享内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//初始化共享内存
void
ngx_slab_init(ngx_slab_pool_t *pool)

//加锁的内存申请方法
void *
ngx_slab_alloc(ngx_slab_pool_t *pool, size_t size)

//不加锁的内存申请方法
void *
ngx_slab_alloc_locked(ngx_slab_pool_t *pool, size_t size)

//加锁的内存释放方法
void
ngx_slab_free(ngx_slab_pool_t *pool, void *p)

//不加锁的内存释放方法
void
ngx_slab_free_locked(ngx_slab_pool_t *pool, void *p)


nginx多进程结构,需要使用同步锁才能操作共享数据。那为什么还有ngx_slab_alloc_locked?事实上,nginx的代码可能存在多层锁的嵌套,如果外层已经加锁,那么内存是没有必要上锁的,毕竟上锁会增加开销,降低效率。

需要注意的是,当slab内存池的内存用完时,ngx_slab_alloc会直接返回NULL,因此需要合理评估模块使用的内存大小,如果slab共享内存设置的太小会导致异常。

以ssl模块为例,共享内存

1
2
sscf->shm_zone = ngx_shared_memory_add(cf, &name, n,
&ngx_http_ssl_module);

3、API详解

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
void ngx_slab_free_locked(ngx_slab_pool_t *pool, void *p)
{
// 定义局部变量,用于计算和存储内存页和slab信息。
size_t size;
uintptr_t slab, m, *bitmap;
ngx_uint_t i, n, type, slot, shift, map;
ngx_slab_page_t *slots, *page;

// 记录调试信息,显示正在释放的内存地址。
ngx_log_debug1(NGX_LOG_DEBUG_ALLOC, ngx_cycle->log, 0, "slab free: %p", p);

// 检查释放的内存地址是否在slab pool的范围内。
if ((u_char *) p < pool->start || (u_char *) p > pool->end) {
// 如果不在范围内,记录错误日志并退出函数。
ngx_slab_error(pool, NGX_LOG_ALERT, "ngx_slab_free(): outside of pool");
goto fail;
}

// 计算内存页的索引。
n = ((u_char *) p - pool->start) >> ngx_pagesize_shift;
// 获取内存页的指针。
page = &pool->pages[n];
// 获取slab的地址。
slab = page->slab;
// 获取内存页的类型。
type = ngx_slab_page_type(page);

// 根据内存页类型进行不同的处理。
switch (type) {
// 小对象内存页的处理。
case NGX_SLAB_SMALL:
// 计算slab的大小和位移。
shift = slab & NGX_SLAB_SHIFT_MASK;
size = (size_t) 1 << shift;

// 检查p是否是size的整数倍。
if ((uintptr_t) p & (size - 1)) {
goto wrong_chunk;
}

// 计算在bitmap中的位置。
n = ((uintptr_t) p & (ngx_pagesize - 1)) >> shift;
m = (uintptr_t) 1 << (n % (8 * sizeof(uintptr_t)));
n /= 8 * sizeof(uintptr_t);
bitmap = (uintptr_t *)((uintptr_t) p & ~((uintptr_t) ngx_pagesize - 1));

// 检查bitmap对应的位是否被设置,即内存块是否已被分配。
if (bitmap[n] & m) {
// 释放内存块,更新slab的bitmap和内存页的链表。
// ...
// 省略了释放内存块的代码。
}

// 如果内存块已经被释放,则报错。
goto chunk_already_free;

// 精确大小内存页的处理。
case NGX_SLAB_EXACT:
// ...
// 省略了精确大小内存页的处理代码。

// 大对象内存页的处理。
case NGX_SLAB_BIG:
// ...
// 省略了大对象内存页的处理代码。

// 特殊内存页的处理,用于存储大于slab可以分配的最大块大小的对象。
case NGX_SLAB_PAGE:
// ...
// 省略了特殊内存页的处理代码。

default:
// 未处理的case,不应该到达这里。
break;
}

// 函数结束,正常释放内存后会执行到这里。
return;

fail:
// 释放失败,记录错误日志。
return;

done:
// 正常释放内存后更新使用的统计信息,并填充释放的内存以避免重复使用。
pool->stats[slot].used--;
ngx_slab_junk(p, size);
return;

wrong_chunk:
// 释放的内存块地址不正确,记录错误日志。
ngx_slab_error(pool, NGX_LOG_ALERT, "ngx_slab_free(): pointer to wrong chunk");
goto fail;

chunk_already_free:
// 尝试释放一个已经被释放的内存块,记录错误日志。
ngx_slab_error(pool, NGX_LOG_ALERT, "ngx_slab_free(): chunk is already free");
goto fail;
}

4、释放问题

ngx_slab_free_locked 函数释放通过 slab 分配器分配的内存时,不会改变指针本身的值,而是将指针指向的内存块标记为可用。这个很关键,因此当一个指针是否持有合理内存时,不能判断是否为NULL。

内存分配器(如 slab 分配器)负责管理内存块的分配和释放。当一个内存块被分配时,内存分配器会记录该内存块的状态(已分配)。当这个内存块被释放时,内存分配器会更新该内存块的状态(可用)。

在 C 语言中,指针是用来存储内存地址的变量。指针本身只是一个变量,存储了一个内存地址。在调用 ngx_slab_free_locked 函数时,传递的是指针的值(即内存地址),而不是指针本身。因此,函数内部对内存的操作不会改变传入指针的值。

指针传递: 当调用 ngx_slab_free_locked(shpool, h) 时,传递的是 h 的值(内存地址)。
内存释放: 函数 ngx_slab_free_locked 使用 h 指向的内存地址,在内存池中找到对应的内存块,并将其标记为可用。这涉及到更新 slab 分配器内部的数据结构,但不改变 h 本身的值。

指针保持不变: 函数调用结束后,h 仍然持有原来的内存地址值。


ngx_slab共享内存
https://zjfans.github.io/2024/07/09/ngx_slab/
作者
张三疯
发布于
2024年7月9日
许可协议