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) { 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 仍然持有原来的内存地址值。