Nginx 采用多进程架构,在实现访问控制、限流等功能时,进程间需要共享数据。例如,一个 URI 的多笔请求可能由多个进程处理,此时进程需要判断该 URI 是否达到了限制次数。因此,访问控制模块和限流模块需要使用共享内存进行进程间通信。
Nginx 实现了 ngx_shm_t 共享内存,但如果要共享一些复杂的数据结构,ngx_shm_t 很难满足这种需求。因此,在 ngx_shm_t 的基础上实现了 slab 共享内存,提供了更灵活的内存管理能力。
1、初始化共享内存
模块在配置初始化时,会申请一块 slab 内存池。开发者可以通过 ngx_slab_alloc 向这个内存池申请内存,当内存池用尽时,该函数会返回 NULL。
API 函数
1 2
| 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 个不同模块定义的内存池具有相同的名字,一般传入本模块结构体的地址
tag 的作用:
- 本模块结构体的地址通常为全局变量,因此在 reload、Nginx 重读配置时,因为 tag 没有变化,所以不会重新申请内存
- 还有一个好处是,如果之前共享内存是有数据的,这样不会丢掉之前共享内存中的数据
- 使用的思想是:尽可能使用旧的共享内存,当然前提是旧的存在
2、操作 slab 共享内存
API 函数列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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)
|
为什么需要 _locked 版本?
Nginx 采用多进程结构,需要使用同步锁才能操作共享数据。那为什么还有 ngx_slab_alloc_locked?
原因:Nginx 的代码可能存在多层锁的嵌套。如果外层已经加锁,那么内存操作就没有必要再次上锁,毕竟上锁会增加开销,降低效率。因此提供了 _locked 版本,供已持有锁的代码路径使用。
注意事项
当 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) { 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);
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 = page->slab; type = ngx_slab_page_type(page);
switch (type) { case NGX_SLAB_SMALL: shift = slab & NGX_SLAB_SHIFT_MASK; size = (size_t) 1 << shift;
if ((uintptr_t) p & (size - 1)) { goto wrong_chunk; }
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));
if (bitmap[n] & m) { }
goto chunk_already_free;
case NGX_SLAB_EXACT:
case NGX_SLAB_BIG:
case NGX_SLAB_PAGE:
default: 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 仍然持有原来的内存地址值。