SQLite的动态内存分配系统提供一个可选的底层内存分配器,在代码里是一个全局变量:
sqlite3Config.m
其声明如下:
sqlite3_mem_methods m;
typedef struct sqlite3_mem_methods sqlite3_mem_methods;
struct sqlite3_mem_methods {
void *(*xMalloc)(int); /* Memory allocation function */
void (*xFree)(void*); /* Free a prior allocation */
void *(*xRealloc)(void*,int); /* Resize an allocation */
int (*xSize)(void*); /* Return the size of an allocation */
int (*xRoundup)(int); /* Round up request size to allocation size */
int (*xInit)(void*); /* Initialize the memory allocator */
void (*xShutdown)(void*); /* Deinitialize the memory allocator */
void *pAppData; /* Argument to xInit() and xShutdown() */
};
可以看到其结构体里是一系列函数指针,定义时并不知道具体实现方式,初始化时需要和相应的内存分配器实体绑定在一起才能工作。
1.默认内存分配器
如果用户没有对内存分配器进行配置,那么使用默认内存分配器,实现在mem1.c里。
这个默认内存分配器使用C标准库的malloc(), realloc()和free()来分配内存,实现中还做了一层薄的包装以提供一个memsize()函数返回一个现存分配的大小。
memsize()实现是在每个malloc()请求上多分配额外8个字节作为头部,并把分配的大小保存到这个8字节的头部。
注册:
void sqlite3MemSetDefault(void){
static const sqlite3_mem_methods defaultMethods = {
sqlite3MemMalloc,
sqlite3MemFree,
sqlite3MemRealloc,
sqlite3MemSize,
sqlite3MemRoundup,
sqlite3MemInit,
sqlite3MemShutdown,
0
};
sqlite3_config(SQLITE_CONFIG_MALLOC, &defaultMethods);
}
实现如下:
#define SQLITE_MALLOC(x) malloc(x)
#define SQLITE_FREE(x) free(x)
#define SQLITE_REALLOC(x,y) realloc((x),(y))
static void *sqlite3MemMalloc(int nByte){
sqlite3_int64 *p;
assert( nByte>0 );
testcase( ROUND8(nByte)!=nByte );
p = SQLITE_MALLOC( nByte+8 );//8个字节的头部来保存长度
if( p ){
p[0] = nByte;
p++;
}else{
testcase( sqlite3GlobalConfig.xLog!=0 );
sqlite3_log(SQLITE_NOMEM, "failed to allocate %u bytes of memory", nByte);
}
return (void *)p;
}
static void sqlite3MemFree(void *pPrior){
sqlite3_int64 *p = (sqlite3_int64*)pPrior;
assert( pPrior!=0 );
p--;
SQLITE_FREE(p);
}
static int sqlite3MemSize(void *pPrior){
sqlite3_int64 *p;
assert( pPrior!=0 );
p = (sqlite3_int64*)pPrior;
p--;
return (int)p[0];
}
2.调试内存分配器
2.1内存结构
如果SQLite使用SQLITE_MEMDEBUG编译时选项来编译,那么将会使用一个重型包装的调试内存分配器来代替默认的,代码实现在mem2.c里。
每一次分配格式如下
Title | backtrace pointers | MemBlockHdr | allocation | EndGuard |
包含的内容分别为标题、分配时调用堆栈、内存块头部、分配的内存空间,尾部哨兵值。
title用来记录测试用例的名称,可以通过以下语句来设置:
void sqlite3MemdebugSettitle(const char *zTitle);
MemBlockHdr的作用是把所有还没有释放的内存连接成一个链表,记录malloc调用时的函数堆栈深度,其定义如下:
struct MemBlockHdr {
i64 iSize; /* Size of this allocation */
struct MemBlockHdr *pNext, *pPrev; /* Linked list of all unfreed memory */
char nBacktrace; /* Number of backtraces on this alloc */
char nBacktraceSlots; /* Available backtrace slots */
u8 nTitle; /* Bytes of title; includes '\0' */
u8 eType; /* Allocation type code */
int iForeGuard; /* Guard word for sanity */
};
backtrace pointers是用来保存追踪的函数调用堆栈地址。
allocation是实现分配的内存空间。
EndGuard为末尾哨兵值,用来做边界检查。
2.2内存分配实现
首先分配总的内存空间:
totalSize = nReserve + sizeof(*pHdr) + sizeof(int) +
mem.nBacktrace*sizeof(void*) + mem.nTitle;
p = malloc(totalSize);
把MemBlockHdr插入到双向链表:
pBt = (void**)&z[mem.nTitle];
pHdr = (struct MemBlockHdr*)&pBt[mem.nBacktrace];
pHdr->pNext = 0;
pHdr->pPrev = mem.pLast;//前驱结点为原链表最后一个结点
if( mem.pLast ){ //如果链表不为空,插入新的结点
mem.pLast->pNext = pHdr;
}else{//如果链表为空,新增头结点
mem.pFirst = pHdr;
}
mem.pLast = pHdr;//更新链表尾结点
追踪函数分配时函数的堆栈,tcl中需要sqlite3_memdebug_backtrace命令设置mem.nBacktrace的值来打开追踪功能:
if( mem.nBacktrace ){
void *aAddr[40];
pHdr->nBacktrace = backtrace(aAddr, mem.nBacktrace+1)-1;//这里的+1、-1主要是排除__libc_start_main
memcpy(pBt, &aAddr[1], pHdr->nBacktrace*sizeof(void*));
assert(pBt[0]);
if( mem.xBacktrace ){//这里的回调函数给test_memdebug_log使用
mem.xBacktrace(nByte, pHdr->nBacktrace-1, &aAddr[1]);
}
}else{
pHdr->nBacktrace = 0;
}
释放时会检查iForeGuard和EndGuard的哨兵值来判断有无内存越界,然后把MemBlockHdr从链表中移除,再把释放的内存填充成一些随机值确保释放后不会留下之前使用过的痕迹。
3.追踪记录
TCL里打开追踪记录的命令如下:
sqlite3_memdebug_backtrace 30//打开追踪,最大追踪深度30
sqlite3_memdebug_log start//使能回调函数
……//动态内存申请操作
这里有2种方式输出追踪方式
sqlite3_memdebug_log dump//这条命令只记录堆栈地址
sqlite3_memdebug_dump//这条命令记录函数调用堆栈的函数名
显然第2种方式要比第1种方式好,但是第1种方式用到了回调函数、hash表等知识,还是值得学习的。
3.1 sqlite3_memdebug_log的实现
sqlite3_memdebug_log命令对应test_maloc.c里的函数test_memdebug_log(),首先注册回调函数并新建hash表:
#define MALLOC_LOG_KEYINTS ( \
10 * ((sizeof(int)>=sizeof(void*)) ? 1 : sizeof(void*)/sizeof(int)) \
)
if( !isInit ){
#ifdef SQLITE_MEMDEBUG
extern void sqlite3MemdebugBacktraceCallback(
void (*xBacktrace)(int, int, void **));
sqlite3MemdebugBacktraceCallback(test_memdebug_callback);
#endif
Tcl_InitHashTable(&aMallocLog, MALLOC_LOG_KEYINTS);//建立一个以数组为关键字的hash表,这个数组长度是MALLOC_LOG_KEYINTS
isInit = 1;
}
在回调函数中为关键字新建一个条目,并设定这个条目的值
static void test_memdebug_callback(int nByte, int nFrame, void **aFrame){
if( mallocLogEnabled ){
MallocLog *pLog;
Tcl_HashEntry *pEntry;
int isNew;
int aKey[MALLOC_LOG_KEYINTS];
unsigned int nKey = sizeof(int)*MALLOC_LOG_KEYINTS;
memset(aKey, 0, nKey);
if( (sizeof(void*)*nFrame)<nKey ){
nKey = nFrame*sizeof(void*);
}
memcpy(aKey, aFrame, nKey);// aFrame是调用的堆栈地址
pEntry = Tcl_CreateHashEntry(&aMallocLog, (const char *)aKey, &isNew);
if( isNew ){
pLog = (MallocLog *)Tcl_Alloc(sizeof(MallocLog));
memset(pLog, 0, sizeof(MallocLog));
Tcl_SetHashValue(pEntry, (ClientData)pLog);//设定hash值
}else{
pLog = (MallocLog *)Tcl_GetHashValue(pEntry);
}
pLog->nCall++;//记录调用次数
pLog->nByte += nByte;//该调用总的分配内存
}
轮询所有hash表条目,输出该条目的值和关键字
case MB_LOG_DUMP: {
Tcl_HashSearch search;
Tcl_HashEntry *pEntry;
Tcl_Obj *pRet = Tcl_NewObj();
assert(sizeof(Tcl_WideInt)>=sizeof(void*));
for(
pEntry=Tcl_FirstHashEntry(&aMallocLog, &search);
pEntry;
pEntry=Tcl_NextHashEntry(&search)
){
Tcl_Obj *apElem[MALLOC_LOG_FRAMES+2];
MallocLog *pLog = (MallocLog *)Tcl_GetHashValue(pEntry);
Tcl_WideInt *aKey = (Tcl_WideInt *)Tcl_GetHashKey(&aMallocLog, pEntry);
int ii;
/*前2个是条目的值,后面是对于的关键字*/
apElem[0] = Tcl_NewIntObj(pLog->nCall);
apElem[1] = Tcl_NewIntObj(pLog->nByte);
for(ii=0; ii<MALLOC_LOG_FRAMES; ii++){
apElem[ii+2] = Tcl_NewWideIntObj(aKey[ii]); }
/*根据apElem创建一个新的列表对象,并添加到pRet*/
Tcl_ListObjAppendElement(interp, pRet,
Tcl_NewListObj(MALLOC_LOG_FRAMES+2, apElem)
);
}
Tcl_SetObjResult(interp, pRet);//输出列表元素
break;
释放时轮询所有条目,释放每个条目申请的内存空间,然后再删除所有条目,释放相关的内存空间。
static void test_memdebug_log_clear(void){
Tcl_HashSearch search;
Tcl_HashEntry *pEntry;
for(
pEntry=Tcl_FirstHashEntry(&aMallocLog, &search);
pEntry;
pEntry=Tcl_NextHashEntry(&search)
){
MallocLog *pLog = (MallocLog *)Tcl_GetHashValue(pEntry);
Tcl_Free((char *)pLog);
}
Tcl_DeleteHashTable(&aMallocLog);
Tcl_InitHashTable(&aMallocLog, MALLOC_LOG_KEYINTS);
}
3.2 sqlite3_memdebug_dump的实现
这个命令由sqlite3_memdebug_dump()函数来实现,将所有未释放的内存申请的函数调用追踪记录输出到文件。
上面讲到对MemBlockHdr建了一个双向链表,所以只要遍历每个节点,并把指针定位到backtrace pointers,再由backtrace_symbols_fd()函数输出结果即可:
void sqlite3MemdebugDump(const char *zFilename){
FILE *out;
struct MemBlockHdr *pHdr;
void **pBt;
int i;
out = fopen(zFilename, "w");
if( out==0 ){
fprintf(stderr, "** Unable to output memory debug output log: %s **\n",
zFilename);
return;
}
for(pHdr=mem.pFirst; pHdr; pHdr=pHdr->pNext){
char *z = (char*)pHdr;
z -= pHdr->nBacktraceSlots*sizeof(void*) + pHdr->nTitle;
//输出标题
fprintf(out, "**** %lld bytes at %p from %s ****\n",
pHdr->iSize, &pHdr[1], pHdr->nTitle ? z : "???");
if( pHdr->nBacktrace ){
fflush(out);
pBt = (void**)pHdr;
pBt -= pHdr->nBacktraceSlots;//定位函数调用堆栈地址
backtrace_symbols_fd(pBt, pHdr->nBacktrace, fileno(out));
fprintf(out, "\n");
}
}
……//这里省略内存分配的统计输出
fclose(out);
}
4.内存分配失败模拟器
在SQLite内核和底层内存分配器之间插入覆盖层,使用覆盖层可以模拟内存分配失败的情形。
在test_malloc.c里实现了一个错误模拟器的覆盖层,首先定义一个全局变量memfault:
static struct MemFault {
int iCountdown; /* Number of pending successes before a failure */
int nRepeat; /* Number of times to repeat the failure */
int nBenign; /* Number of benign failures seen since last config */
int nFail; /* Number of failures seen since last config */
u8 enable; /* True if enabled */
int isInstalled; /* True if the fault simulation layer is installed */
int isBenignMode; /* True if malloc failures are considered benign */
sqlite3_mem_methods m; /* 'Real' malloc implementation */
} memfault;
获取原来的内存分配器给memfault.m,再把内存分配器替换为内存分配失败模拟器
static struct sqlite3_mem_methods m = {
faultsimMalloc, /* xMalloc */
faultsimFree, /* xFree */
faultsimRealloc, /* xRealloc */
faultsimSize, /* xSize */
faultsimRoundup, /* xRoundup */
faultsimInit, /* xInit */
faultsimShutdown, /* xShutdown */
0 /* pAppData */
};
int rc;
rc = sqlite3_config(SQLITE_CONFIG_GETMALLOC, &memfault.m);//获取原来的内存分配器
assert(memfault.m.xMalloc);
if( rc==SQLITE_OK ){
rc = sqlite3_config(SQLITE_CONFIG_MALLOC, &m);//替换为现在的
}
sqlite3_test_control(SQLITE_TESTCTRL_BENIGN_MALLOC_HOOKS,
faultsimBeginBenign, faultsimEndBenign
);
测试时可以设定过多少次内存申请后开始模拟失败,还可以设置模拟的失败的次数:
static void *faultsimMalloc(int n){
void *p = 0;
if( !faultsimStep() ){
p = memfault.m.xMalloc(n);
}
return p;
}
static int faultsimStep(void){
if( likely(!memfault.enable) ){
return 0;//模拟器没使能,正常分配
}
if( memfault.iCountdown>0 ){
memfault.iCountdown--;//在开始模拟失败前需要成功分配的次数
return 0;
}
sqlite3Fault();
memfault.nFail++;
if( memfault.isBenignMode>0 ){
memfault.nBenign++;//某些特定地方的内存申请
}
memfault.nRepeat--;//模拟失败的次数
if( memfault.nRepeat<=0 ){
memfault.enable = 0;
}
return 1;
}