1.概述
SQLite开发者称它为"memsys5",这个内存分配器的实现不依赖于malloc,实现在mem5.c里,使用时需要打开SQLITE_ENABLE_MEMSYS5编译选项,应用程序在启动时还要调用以下接口:
sqlite3_config(SQLITE_CONFIG_HEAP, pBuf, szBuf, mnReq);
其中pBuf为初始内存,szBuf为初始内存大小,mnReq为最小的内存申请请求。
对于动态内存分配问题、内存分配失败问题,J.M.Robson进行过系统的研究,其结果发表在如下论文中:J. M. Robson. "Bounds for Some Functions Concerning Dynamic Storage Allocation". Journal of the Association for Computing Machinery, Volume 21, Number 8, July 1974, pages 491-499.
我们使用下面的记号(与Robson的记号类似,但不完全等同):
N:内存分配系统为了保证不会出现分配失败而需要的原始内存数量。
M: 应用程序曾经在任何时间点取出的最大内存数量。
n: 最大内存分配与最小分配的比值。我们假设每个内存分配大小都是最小分配大小的整数倍。
Robson证明了下面的结果:
N = M*(1 + (log2 n)/2) - n + 1
通俗地讲,Robson证明表明,为了防止内存分配失败,任何内存分配器必须使用一个大小为N的内存池,它超过曾经使用的最大内存M乘以一个取决于n的倍数。也就是说,除非所有的内存分配都是同样的大小,否则系统需要访问比曾经使用的更大的内存。此外我们看到,需要的剩余内存随着比值n的增加会迅速的增长,因此我们应该保持所有的内存分配尽可能大小相同。
Robson证明是构造性的。他提出一个算法来计算一个分配和释放操作系列由于内存碎片(可用内存大于1字节但小N字节)将导致分配失败。Robson还证明如果可用内存为N或更多的字节,一个“2的幂,首次命中”的内存分配器绝不会出现内存分配失败。
上面介绍的内容取自官网的文章“Dynamic Memory Allocation In SQLite”,memsys5使用的buddy算法正是一个“2的幂,首次命中”的内存分配器,所以不会出现内存分配失败。
2.初始化
本文以szBuf为100000,mnReq为10来说明memsys5的实现,TCL脚本可以使用如下语句来测试:
sqlite3_shutdown
sqlite3_config_heap 100000 10
初始化时调用memsys5Init(),在memsys5中每次申请的内存必须是2的幂次方大小,所以最小的分配值mem5.szAtom应该是16而不是10:
static int memsys5Log(int iValue){
int iLog;
for(iLog=0; (iLog<(int)((sizeof(int)*8)-1)) && (1<<iLog)<iValue; iLog++);
return iLog;
}
nMinLog = memsys5Log(sqlite3GlobalConfig.mnReq);// mnReq为10,那么nMinLog算出来是4
mem5.szAtom = (1<<nMinLog);// mem5.szAtom是16
然后再把总的内存大小分割成n个block
nByte = sqlite3GlobalConfig.nHeap;
zByte = (u8*)sqlite3GlobalConfig.pHeap;
mem5.nBlock = (nByte / (mem5.szAtom+sizeof(u8)));
mem5.zPool = zByte;
mem5.aCtrl = (u8 *)&mem5.zPool[mem5.nBlock*mem5.szAtom];
这里假设nByte是100000,那么mem5.nBlock就是5882,这里5882*16大小的内存用作内存池,还有5882个大小的内存放在mem5.aCtrl用作分配和释放的管理。
接着对这5882个block再按2的最大幂次方进行分割,如5882分割出4096,剩余1784,1784再分割出1024,如此循环往复:
#define LOGMAX 30
for(ii=0; ii<=LOGMAX; ii++){
mem5.aiFreelist[ii] = -1;
}
iOffset = 0;
for(ii=LOGMAX; ii>=0; ii--){
int nAlloc = (1<<ii);
if( (iOffset+nAlloc)<=mem5.nBlock ){
mem5.aCtrl[iOffset] = ii | CTRL_FREE;//记录分割后该地址的块大小并标记为未使用
memsys5Link(iOffset, ii);//把地址记录到aiFreelist
iOffset += nAlloc;
}
assert((iOffset+nAlloc)>mem5.nBlock);
}
分割示意图如下
这样分割后,就可以用2的幂来代表内存大小了,如4096是12,1024是10,然后我们用aiFreelist来表示某个大小的块在内存中的位置,如aiFreelist[12]=0, aiFreelist[1]=5880,如果有多个内存块的大小相同把相同大小的块用链表连接起来。
typedef struct Mem5Link Mem5Link;
struct Mem5Link {
int next; /* Index of next free chunk */
int prev; /* Index of previous free chunk */
};
#define MEM5LINK(idx) ((Mem5Link *)(&mem5.zPool[(idx)*mem5.szAtom]))
/*
** Link the chunk at mem5.aPool[i] so that is on the iLogsize
** free list.
*/
static void memsys5Link(int i, int iLogsize){
int x;
assert( sqlite3_mutex_held(mem5.mutex) );
assert( i>=0 && i<mem5.nBlock );
assert( iLogsize>=0 && iLogsize<=LOGMAX );
assert( (mem5.aCtrl[i] & CTRL_LOGSIZE)==iLogsize );
//i为块在内存中的地址,iLogsize为块大小的2次幂
x = MEM5LINK(i)->next = mem5.aiFreelist[iLogsize];
MEM5LINK(i)->prev = -1;//新块作为头结点
if( x>=0 ){
assert( x<mem5.nBlock );
MEM5LINK(x)->prev = i;//该大小的块已经存在,加入链表
}
mem5.aiFreelist[iLogsize] = i;//记录块大小为2^Logsize的地址
}
3.内存申请
假设申请内存大小为64,首先定位满足64字节大小的内存块:
for(iFullSz=mem5.szAtom,iLogsize=0; iFullSz<nByte; iFullSz*=2,iLogsize++){}
得到iFullSz为64,即4个mem5.szAtom,所以幂iLogsize为2,由上面的分割可知存在2^2个block大小的块,那么继续搜索2^3大小的块,直到找到为止
/* Make sure mem5.aiFreelist[iLogsize] contains at least one free
** block. If not, then split a block of the next larger power of
** two in order to create a new free block of size iLogsize.
*/
for(iBin=iLogsize; iBin<=LOGMAX && mem5.aiFreelist[iBin]<0; iBin++){}
找到后把这个块标记为未使用,并分裂这个块,一直分裂到最小的块大小为2^ iLogsize
static void memsys5Unlink(int i, int iLogsize){
int next, prev;
assert( i>=0 && i<mem5.nBlock );
assert( iLogsize>=0 && iLogsize<=LOGMAX );
assert( (mem5.aCtrl[i] & CTRL_LOGSIZE)==iLogsize );
next = MEM5LINK(i)->next;
prev = MEM5LINK(i)->prev;
if( prev<0 ){
mem5.aiFreelist[iLogsize] = next;
}else{
MEM5LINK(prev)->next = next;
}
if( next>=0 ){
MEM5LINK(next)->prev = prev;
}
}
i = mem5.aiFreelist[iBin];
memsys5Unlink(i, iBin);//把这个块从mem5.aiFreelist[iBin]链表中移除,然后再开始分裂
while( iBin>iLogsize ){
int newSize;
/*以下为分裂操作*/
iBin--;
newSize = 1 << iBin;//分成2半
mem5.aCtrl[i+newSize] = CTRL_FREE | iBin;
memsys5Link(i+newSize, iBin);//后一半标记为未使用
}
要记住把一个块分裂成2块后,这2个块叫做buddy块,释放时如果2个buddy块都为空闲,那么会合并成一块。
4.内存释放
这部分是最复杂的,用到了buddy算法,当内存多次分配后,每个原始的块已经被分裂过很多次,释放时要找到对应的buddy块进行合并。合并完成的块再插入到mem5.aiFreelist链表里。
释放的块根据一定的特征来判断自己在分裂的前端还是分裂的后端来寻找自己的buddy。
/*
** Free an outstanding memory allocation.
*/
static void memsys5FreeUnsafe(void *pOld){
u32 size, iLogsize;
int iBlock;
/* Set iBlock to the index of the block pointed to by pOld in
** the array of mem5.szAtom byte blocks pointed to by mem5.zPool.
*/
iBlock = (int)(((u8 *)pOld-mem5.zPool)/mem5.szAtom);//获取释放块的地址
/* Check that the pointer pOld points to a valid, non-free block. */
assert( iBlock>=0 && iBlock<mem5.nBlock );
assert( ((u8 *)pOld-mem5.zPool)%mem5.szAtom==0 );
assert( (mem5.aCtrl[iBlock] & CTRL_FREE)==0 );
iLogsize = mem5.aCtrl[iBlock] & CTRL_LOGSIZE;//获取2的幂次
size = 1<<iLogsize;//获取块的大小
assert( iBlock+size-1<(u32)mem5.nBlock );
mem5.aCtrl[iBlock] |= CTRL_FREE;
mem5.aCtrl[iBlock+size-1] |= CTRL_FREE;
mem5.aCtrl[iBlock] = CTRL_FREE | iLogsize;//标记块为未使用
while( ALWAYS(iLogsize<LOGMAX) ){
int iBuddy;
if( (iBlock>>iLogsize) & 1 ){//释放的块在后端
iBuddy = iBlock - size;//获取前端buddy
assert( iBuddy>=0 );
}else{//释放的块在前端
iBuddy = iBlock + size;//获取后端buddy
if( iBuddy>=mem5.nBlock ) break;//到内存池的边界
}
if( mem5.aCtrl[iBuddy]!=(CTRL_FREE | iLogsize) ) break;//buddy仍未释放
memsys5Unlink(iBuddy, iLogsize);//把buddy从aiFreelist链表移除
iLogsize++;//合并后大小为原来2倍
if( iBuddy<iBlock ){// iBuddy为前端
mem5.aCtrl[iBuddy] = CTRL_FREE | iLogsize;
mem5.aCtrl[iBlock] = 0;
iBlock = iBuddy;
}else{// iBlock为前端
mem5.aCtrl[iBlock] = CTRL_FREE | iLogsize;
mem5.aCtrl[iBuddy] = 0;
}
size *= 2;//合并后再继续下一次合并
}
参考资料:
【1】SQLite剖析(9):动态内存分配
http://blog.csdn.net/zhoudaxia/article/details/8257784