2012年(10)
分类: LINUX
2012-11-06 17:09:51
1. Squid MemPool机制 1.1. MemPool原理
关于MemPool,最权威的说法来自于cf.data.pre
NAME: memory_pools COMMENT: on|off TYPE: onoff DEFAULT: on LOC: Config.onoff.mem_pools DOC_START If set, Squid will keep pools of allocated (but unused) memory available for future use. If memory is a premium on your system and you believe your malloc library outperforms Squid routines, disable this. DOC_END
|
Squid会维护一个内存池,其中保留着一些已经分配但还没有使用的内存以供未来使用。如果内存资源在你的系统中十分紧张或者你确信你的malloc 库比squid的强,那么可以关掉这个配置项。
整个MemPool体系中最核心的存储结构是一个Stack类型的全局变量Pools。Pools中的每一个item存放着一种MemPool的指针,结构如图1.1所示
图 SEQ 图 \* ARABIC 1.1
主要的MemPool相关方法有:
void memConfigure()
根据Config.MemPools.limit等配置值来调整MemPool的大小 |
void memInitModule(void)
为全局结构pools分配内存 |
void memCleanModule (void)
对于pools中所有的MemPool都执行一遍memPoolDestroy,然后clean掉pools自身 |
static void memShrink(size_t new_limit)
缩小所有的MemPool的大小直到达到new_limit为止 |
MemPool * memPoolCreate(const char *label, size_t obj_size) 重要
增加一种MemPool的类型label是其名称,obj_size通常都传一个sizeof,并初始化这个MemPool的pstack
|
void memPoolDestroy(MemPool * pool) 重要
删除一种MemPool。注意:删除操作只是将pools.items中的某一项置为NULL。因此我们写代码遍历pools.items时要注意跳过NULL的项。 |
void * memPoolAlloc(MemPool * pool) 非常重要
从指定的pool中分配一块内存。 如果该pool中还有空闲内存(pool->pstack.count > 0),则直接从pool->pstack中取一块返回。 如果该pool中已经没有空闲内存了,则直接分配一块内存返回。(此时并不会增加pstack的大小,而是要等到free的时候,才把这次分配的内存放到pstack中去!) 这里有2个细节: 第一, 分配内存时,会修改该pool的已分配内存的计数。从pstack中取的时候,减少了pool->meter.idle,而直接分配的时候,增加了pool->meter.alloc。 第二, 分配内存时,会根据配置项zero_buffers来决定是用xmalloc还是xcalloc
注意,每个pool的pstack最开始都是空的,系统运行起来之后的一段时间内,都是使用新分配的内存,这段时间之后,通常都是使用旧的内存。 |
void memPoolFree(MemPool * pool, void *obj) 非常重要
将obj所占的内存放到pool中。 如果将obj的大小加上所有pool的空闲块大小超过了某一限制mem_idle_limit,则直接free掉obj 如果没有超过这一限制,将obj push到pool中。 |
1.2. MemPool在squid中的使用分析
Squid中,MemPool的使用主要是在mem.c中。它自己维护着一个MemPool指针数组
static MemPool *MemPools[MEM_MAX]; |
数组中的每一个元素对应着一个mem_type。mem_type的值包括MEM_2K_BUF、MEM_4K_BUF、MEM_REQUEST_T、MEM_HTTP_REPLY等我们熟悉的值。
系统初始化时,会初始化每一个类型的mem_type,例如
memDataInit(MEM_2K_BUF, "2K Buffer", 2048, 10); |
在代码中经常用于分配内存的方法是memAllocate,这个方法是对memPoolAlloc的一个封装。
void * memAllocate(mem_type type) { return memPoolAlloc(MemPools[type]); } |
内存释放的方法也是类似的。
void memFree(void *p, int type) { memPoolFree(MemPools[type], p); } |
另外一种内存分配的方法是memAllocBuf。
void * memAllocBuf(size_t net_size, size_t * gross_size) |
该方法是不需要传进mem_type参数的。系统会自动找到一个匹配net_size的mem_type的pool来进行内存分配。查找的方法是,逐个判断net_size是否小于等于2K,4K,8K…64K。这段代码应该可以修改,增加一些小于2K的大小,以减少内存的浪费;但是这样做可能会造成一些其他问题,建议谨慎试验后再用。
memAllocBuf方法应用于一些读写buffer的分配,例如connState->in.buf、AddVaryState->buf等等。
与之相对的是memFreeBuf
void memFreeBuf(size_t size, void *buf) |
使用了mem.c中提供的方法来管理内存的主要类型有:
MEM_NONE, MEM_2K_BUF, MEM_4K_BUF, MEM_8K_BUF, MEM_16K_BUF, MEM_32K_BUF, MEM_64K_BUF, MEM_ACL, MEM_ACL_DENY_INFO_LIST, MEM_ACL_IP_DATA, MEM_ACL_LIST, MEM_ACL_NAME_LIST, MEM_ACL_REQUEST_TYPE, MEM_AUTH_USER_T, MEM_AUTH_USER_HASH, MEM_ACL_PROXY_AUTH_MATCH, MEM_ACL_USER_DATA, MEM_ACL_TIME_DATA, MEM_CACHE_DIGEST, MEM_CLIENT_INFO, MEM_STORE_CLIENT_BUF, MEM_LINK_LIST, MEM_DLINK_NODE, MEM_DONTFREE, MEM_DREAD_CTRL, MEM_DWRITE_Q, MEM_FQDNCACHE_ENTRY, MEM_FWD_SERVER, MEM_HELPER_REQUEST, MEM_HELPER_STATEFUL_REQUEST, MEM_HTTP_HDR_CC, MEM_HTTP_HDR_CONTENT_RANGE, MEM_HTTP_HDR_ENTRY, MEM_HTTP_HDR_RANGE, MEM_HTTP_HDR_RANGE_SPEC, MEM_HTTP_REPLY, MEM_INTLIST, MEM_IPCACHE_ENTRY, MEM_MD5_DIGEST, MEM_MEMOBJECT, MEM_MEM_NODE, MEM_NETDBENTRY, MEM_NET_DB_NAME, MEM_RELIST, MEM_REQUEST_T, MEM_STOREENTRY, MEM_WORDLIST, MEM_IDNS_QUERY, MEM_EVENT, MEM_TLV, MEM_SWAP_LOG_DATA, MEM_ACL_CERT_DATA, |
1.3. MemPool上手指南
MemPool的使用分为以下几种:
1.3.1 特定类型的MemPool特定类型的MemPool是Squid中最常见的MemPool用法,MEM_2K_BUF、MEM_4K_BUF、MEM_REQUEST_T、MEM_HTTP_REPLY等类型的数据都是使用这种方式管理内存的。当然,我们自己定义的类型也可以使用这种方式来管理。
要加入一种特定类型的MemPool,名为my_type,需要做以下几项工作:
1. 修改mem_type枚举型,在MEM_MAX前面加入我们自己的类型,如MEM_MYTYPE。
2. 修改memInit函数,在后面加上
memDataInit(MEM_MYTYPE, "my data type", sizeof(my_type), 0);
|
3. 分配内存时,传入MEM_MYTYPE
my_type *mt = memAllocate(MEM_MYTYPE)
|
4. 释放内存时,同样传入MEM_MYTYPE
memFree(mt, MEM_TYPE)
|
1.3.2通过cbdata使用MemPool以及内存读写buffer
这种类型的MemPool应用主要是在一些常驻内存的结构中的读写buffer。例如我们自己有一种结构,这种结构是一种State,这种State需要在函数回调的过程中被反复传递,并且这种State结构中需要一些读写buffer的话,这个State就应该被定义为一种cbdata,它的buffer就需要用memAllocBuf/memFreeBuf来管理。(cbdata的具体细节会在第2章中讨论)
这里有一个实例:如果squid开启了vary机制,对于每一个url会在磁盘上留下一个vary的索引文件,在这个索引文件中存着这个url的各种变化(压缩、非压缩)的object的key。因此在刷新的时候就需要遍历这个索引文件找到所有的key,并用这个key刷掉相应的文件。如图1.2所示
HTTP/1.1 200 OK Date: Tue, 16 Jun 2009 08:58:59 GMT Server: Apache/2.0.52 (Red Hat) X-Powered-By: PHP/4.3.9 Cache-Control: max-age=600, max-age=120 Last-Modified: Wed, 10 Jun 2009 05:19:27 GMT Expires: Tue, 16 Jun 2009 09:00:59 GMT Content-Length: 7221 Connection: close Content-Type: x-squid-internal/vary
Key: 1234567890123456 Vary: Accept-Encoding …… Key: 0987654321098765 Vary: ……. |
图 1.2
这种情况的遍历需要读取整个Object的内容,而Object的读取不能一次完成,而是需要异步进行的。因此我们需要创建一个适合于在回调过程中来回传递的State类型TraverseVaryState,其中的buf就是一个读写buffer。
typedef struct { StoreEntry *e; store_client *sc; char *buf; size_t buf_size; size_t buf_offset; squid_off_t seen_offset; int action; int method; } TraverseVaryState;
CBDATA_TYPE(TraverseVaryState); |
TraverseVaryState和它的buf的分配过程很简单:
CBDATA_INIT_TYPE(TraverseVaryState); state = cbdataAlloc(TraverseVaryState);
state->buf = memAllocBuf(4096, &state->buf_size);
|
使用这种方式分配的内存就具有了cbdata或memPool内存的一切优点,包括可管理、可统计等。
这两种结构的使用和其他内存结构没有什么区别:
static void storeTraverseVaryRead(void *data, char *buf, ssize_t size) { TraverseVaryState *state = data; //……使用State…… storeClientCopy(state->sc, state->e, state->seen_offset, state->seen_offset, state->buf_size - state->buf_offset, state->buf + state->buf_offset, storeTraverseVaryRead, state); } |
这两种内存的释放过程同样简单:
memFreeBuf(state->buf_size, state->buf);
cbdataFree(state); |
2. cbdata机制
对于cbdata机制,Squid官方也有自己的解释,在cbdata.c中。大概意思是说,一些方法中要使用回调数据的指针,但是如果管理不好(例如反复释放),就很容易引起squid挂掉。对于这种情况,我们可以把这些数据的指针放到cbdata中,在注册回调函数之前锁定它,然后在调用回调函数之前对其进行校验,并在完成时将它释放掉。
这样做的好处是,即使free发生在unlock之前,这个数据也会被标示为invalid,因此(cbdataValid)会保证回调不会被执行。在Unlock时,lock count会减少,如果减少为0,这个数据就是invalid的。
/* * These routines manage a set of registered callback data pointers. * One of the easiest ways to make Squid coredump is to issue a * callback to for some data structure which has previously been * freed. With these routines, we register (add) callback data * pointers, lock them just before registering the callback function, * validate them before issuing the callback, and then free them * when finished. * * In terms of time, the sequence goes something like this: * * foo = cbdataAlloc(sizeof(foo),NULL); * ... * some_blocking_operation(..., callback_func, foo); * cbdataLock(foo); * ... * some_blocking_operation_completes() * if (cbdataValid(foo)) * callback_func(..., foo) * cbdataUnlock(foo); * ... * cbdataFree(foo); * * The nice thing is that, we do not need to require that Unlock * occurs before Free. If the Free happens first, then the * callback data is marked invalid and the callback will never * be made. When we Unlock and the lock count reaches zero, * we free the memory if it is marked invalid. */ |
2.1 cbdata原理
cbdata的结构如下(删掉了无用的成员)
typedef struct _cbdata { int valid; int locks; int type; void *y; /* cookie used while debugging */ union { void *pointer; double double_float; int integer; } data; } cbdata; |
valid是记录这个数据是否有效的标志位,locks是锁的个数,type是记录这个cbdata的data成员究竟是哪种类型的(type虽然定义成为了int类型,但实际上存的是cbdata_type这个枚举型的数据),data是实际存放数据的地方。
另外,squid为了管理cbdata,还创建了两个全局变量cbdata_index和cbdata_types
struct { MemPool *pool; FREE *free_func; } *cbdata_index = NULL; int cbdata_types = 0; |
其中cbdata_index是存放每一种cbdata的MemPool和释放函数的一个动态开辟空间的数组,通过cbdata_type枚举型的值作为下标来访问。每增加一种类型的cbdata(程序中可以随时增加cbdata),这个数组就realloc一次。ctdata_types是cbdata_index的长度。
关于cbdata的一系列重要方法如下:
CBDATA_TYPE
#define CBDATA_TYPE(type) static cbdata_type CBDATA_##type = 0
|
在要使用cbdata的地方,首先需要调用CBDATA_TYPE这个宏,它的作用是声明一个cbdata_type类型的全局静态变量。例如CBDATA_TYPE(my_type),实际上就是声明了一个变量
static cbdata_type CBDATA_my_type = 0;
|
CBDATA_INIT_TYPE
#define CBDATA_INIT_TYPE(type) (CBDATA_##type ? 0 : (CBDATA_##type = cbdataAddType(CBDATA_##type, #type, sizeof(type), NULL))) |
CBDATA_INIT_TYPE看起来稍微复杂一点,但是也不难理解,它实际上是对cbdataAddType的一个封装,只是加上了一个判断CBDATA_##type是否等于0。
a) 如果CBDATA_TYPE没有执行过,那么CBDATA_INIT_TYPE也是不可能编译通过的。
b) 如果CBDATA_INIT_TYPE没有执行过,那么CBDATA_##type肯定等于0,因此会执行到cbdataAddType。
c) 否则不进行任何操作
CBDATA_INIT_TYPE_FREECB与CBDATA_INIT_TYPE非常类似,只是多传入了一个free_func。
而CREATE_CBDATA 与CREATE_CBDATA_FREE 则是分别对应于CBDATA_INIT_TYPE和CBDATA_INIT_TYPE_FREECB的,只不过CREATE系列的函数主要是用于系统预定义的cbdata类型,而INIT系列的函数是用于程序中自定义的cbdata类型的。
cbdataAddType
cbdata_type cbdataAddType(cbdata_type type, const char *name, int size, FREE * free_func)
|
增加一种cbdata,实际上的操作就是扩展cbdata_index,增加cbdata_types,并初始化这种类型所对应的MemPool。注意,初始化MemPool时所用的size是size + OFFSET_OF(cbdata, data),这样分配出来的内存除了包括type的大小,还包括了cbdata的valid、locks、type等其他成员的大小。
一个有意思的细节,传给MemPool的label值为:cbdata %s (%d)
这也是为什么我们用squidclient mgr:mem所看到的列表中,如果包括了我们自己声明的cbdata类型(例如TraverseVaryState),就会出现一个”cbdata TraverseVaryState (34)”的标记。
cbdataAlloc
#define cbdataAlloc(type) ((type *)cbdataInternalAlloc(CBDATA_##type)) void * cbdataInternalAlloc(cbdata_type type) |
cbdataInternalAlloc就是分配cbdata内存的函数了。但是里面有一点不太容易理解的地方:MemPool分配出来的内存是type的大小加上cbdata其他成员的大小,返回的值是memPoolAlloc返回的结果看作cbdata的指针所对应的data成员。
这样实际上是一个较小的cbdata的指针指向了一片较大的内存,cbdata的data成员指向了程序中真正使用的内存的首地址。data成员实际上只是一个内存地址标识的作用,并没有被真正当做union来使用!(这一段欢迎高手批判)
另外,还有一个细节,在cbdataAlloc中将cbdata->y 设置成为了 CBDATA_COOKIE(cbdata->data),并在后面的代码中会经常断言二者相等。这样做是为了保护cbdata的内存不会被意想不到的代码篡改。之前FC5出现过cbdata断言错的bug,就是这个机制帮助我们提早发现了代码中的错误。
cbdataValid
int cbdataValid(const void *p)
|
判断p是否还是一个有效的cbdata。前面提到过,valid的定义是:p为NULL或者p没有被free过。这个函数通常被用来终止回调过程,例如:一个cbdata->data为valid,才继续进行回调过程,否则退出。
cbdataLock
void cbdataLock(const void *p) |
为cbdata加锁,实际上就是c->locks++
cbdataUnlock
void cbdataUnlock(const void *p) |
为cbdata解锁,实际上就是c->locks--,当locks减到0,并且valid==0,会调用free_func,并调用MemPoolFree来释放p指向的内存。
cbdataFree
void * cbdataInternalFree(void *p) |
释放cbdata,但是如果locks>0,就不会真正释放,而是将valid置为0,然后return掉,等待下次有人调用cbdataUnlock时释放。当然,valid置为0并不影响data的使用,data仍然是一个合法的指针,只要程序中不去判断cbdataValid就可以继续使用data,取决于程序的具体逻辑。
简单总结一下,由于cbdata可能是被多个回调过程共享的,cbdataLock实际上表示“我要用这个cbdata,你们不要真正把它释放掉了!”通常是一个过程进入异步过程(例如epoll)之前调用一下。
cbdataUnlock实际上表示“我用完了,有没有人想把它释放掉?”并且检查大家是否都用完了(locks)以及是否有人想释放掉它(valid),如果都符合就释放掉。
cbdataFree表示“这个东西也该释放掉了,你们要是都用完了,就释放掉它吧!”,如果locks为0就自己释放,否则就等待cbdataUnlock来释放。
2.2 cbdata在squid中的使用
在squid中使用cbdata机制的内存类型主要有:
CBDATA_UNKNOWN = 0, CBDATA_UNDEF = 0, CBDATA_acl_access, CBDATA_aclCheck_t, CBDATA_clientHttpRequest, CBDATA_ConnStateData, CBDATA_ErrorState, CBDATA_FwdState, CBDATA_generic_cbdata, CBDATA_helper, CBDATA_helper_server, CBDATA_statefulhelper, CBDATA_helper_stateful_server, CBDATA_HttpStateData, CBDATA_peer, CBDATA_ps_state, CBDATA_RemovalPolicy, CBDATA_RemovalPolicyWalker, CBDATA_RemovalPurgeWalker, CBDATA_store_client, CBDATA_FIRST_CUSTOM_TYPE = 1000 |
另外我们也可以在程序代码中自定义cbdata类型,方法见1.3.2章。
2.3 cbdata上手指南cbdata的使用在1.3.2章中已经描述了一部分,包括cbdata的声明类型、分配与释放等。这里换一个场景来讲解cbdataLock、cbdataUnlock等方法的应用。
选取大家比较熟悉的aclCheck函数为例:
static void aclCheck(aclCheck_t * checklist) { while ((A = checklist->access_list) != NULL) { ……. if (match) { aclCheckCallback(checklist, allow); return; } ……. checklist->access_list = A->next; if (A->next) cbdataLock(A->next); cbdataUnlock(A); } aclCheckCallback(checklist, allow); } |
这个函数在循环中会拿出checklist的access_list中的每一个acl_access的acl_list与checklist去做匹配,如果匹配上则执行回调。
在每一次循环的末尾,会调用一次cbdataLock锁住access_list中的下一个acl_access,即宣布将要使用那个acl_access,请别人不要free掉它。然后调用cbdataUnlock来放弃对本次的acl_access的使用权。
3. Squid String机制
字符串(char数组)机制是一种比较特殊的内存管理方式,由于其长度的不定性,为它的内存管理引入了一些复杂度。而我们的代码中之所以要用到malloc/calloc等内存分配方式,很大一部分也是处理字符串用的。mem.c中对String也提供了一种特殊的方式来管理字符串内存。
3.1 mem.c中对字符串内存的管理mem.c对字符串类型使用了一种与众不同的管理方式:
首先,字符串类型所对应的MemPool是特殊定义的。
#define mem_str_pool_count 3 static const struct { const char *name; size_t obj_size; } StrPoolsAttrs[mem_str_pool_count] = { { "Short Strings", 36, }, /* to fit rfc1123 and similar */ { "Medium Strings", 128, }, /* to fit most urls */ { "Long Strings", 512 } /* other */ }; |
然后,提供了对字符串的分配与释放的专门方法
void * memAllocString(size_t net_size, size_t * gross_size) void memFreeString(size_t size, void *buf) |
注意:memAllocString返回的还只是“一块”内存,只是这块内存的大小是向36、128和512“取整”了。这块内存可以被当作char *来使用,这样就成了字符串。
3.2 String.c中对字符串的封装当然,只是提供“一块”内存的分配与释放方法,对于应用来说还是不够方便的,因此Squid中还提供了String.c来封装字符串。其中的主要定以及方法包括:
struct _String { /* never reference these directly! */ unsigned short int size; /* buffer size; 64K limit */ unsigned short int len; /* current length */ char *buf; }; |
void stringInit(String * s, const char *str) void stringClean(String * s) void stringAppend(String * s, const char *str, int len) String stringDup(const String * s) |
注意一个细节,stringDup中是返回了栈上的一个struct的copy。看起来如果结构体比较小的话,为了程序的简单性,偶尔整体copy一下也无妨。
我们在处理字符串的时候,如果拿不准是否会有内存泄露问题的话,就应该使用Squid自身提供的内存机制。不推荐直接使用memAllocString等mem.c中的函数,而是应该使用String.c中的函数!
String结构在嵌入其他结构体中的时候,推荐直接嵌入一个String类型的变量,而非String指针,这样就省了free 掉String的过程,但是一定要记得调用stringClean!
4. 总结squid的内存管理机制是围绕着MemPool展开的。而MemPool仅是提供了一种与具体业务无关的,内存分配与释放的管理机制。
而建立在MemPool之上的,更加方便使用的机制有两个:一个是mem.c提供的MemPool封装,另一个就是cbdata。这两种机制都有各自的适用范围。
一般来说,程序对内存使用方法分为3类,
a) 全局变量,包括挂在全局变量下面的成员变量,例如StoreEntry、fd等。
b) 局部变量,即在一个函数调用过程中声明的变量,它可能会被传递给子函数等。
c) 回调过程中用到的变量,它介于全局变量和局部变量之间,在一个函数中产生,但这个函数因为某种原因(如等待epoll等外部条件)而半途中止运行,并将自己或其他函数注册到外部条件的回调中去,而且还需要用到之前声明的变量。例如LocateVaryState这类的状态机,还包括clientHttpRequest等类型。
不同的变量存在着不同的问题,解决方法入表4.1所示
类型 |
问题 |
解决方法 |
全局变量 |
数量难以统计,一旦出现bug导致泄露,难以追查 |
使用mem.c提供的memAllocate等方法,或者直接用MemPool |
局部变量 |
无 |
可以直接使用xmalloc,xcalloc等 |
回调变量 |
数量难以统计,可能出现多处代码释放同一变量的情况 |
使用cbdata机制 |
表 4.1
使用squid自身带来的各种机制,肯定能够获得事半功倍的效果,但是也可能会引入一些问题。我个人认为,
第一, 不能想当然,看别人用了自己也用,一定要在充分了解其原理的前提下使用!
第二, 并且不要为了使用而使用,而是要充分了解自己想要什么,以及squid机制能提供什么的前提下使用!