SQLite3源程序分析 - v100 联系客服

发布时间 : 星期一 文章SQLite3源程序分析 - v100更新完毕开始阅读b31e841614791711cc791709

*/

#define pcache1 (GLOBAL(struct PCacheGlobal, pcache1_g))

该全局变量管理一个全局的、很大的静态内存缓冲区。应用程序在SQLite启动时为它提供几个大的缓冲区,这样,在SQLite的运行过程中,所有的内存申请都尽量使用这些缓冲区,而不需要调用malloc()和free()(参本文附录一)。该全局变量在下面的函数中初始化: /*

此函数用于初始化静态缓冲区。

如果页缓冲区是通过将SQLITE_CONFIG_PAGECACHE传递给sqlite3_config()来建立,会调用此函数。

参数pBuf指向一个已分配的足够大的内存空间,包含'n'个页缓冲,每个页缓冲为'sz'字节。 */

void sqlite3PCacheBufferSetup(void *pBuf, int sz, int n){ if( pcache1.isInit ){ PgFreeslot *p;

sz = ROUNDDOWN8(sz); pcache1.szSlot = sz; pcache1.pStart = pBuf; pcache1.pFree = 0; while( n-- ){

p = (PgFreeslot*)pBuf; p->pNext = pcache1.pFree; pcache1.pFree = p;

pBuf = (void*)&((char*)pBuf)[sz]; }

pcache1.pEnd = pBuf; } }

可见,SQLite对静态内存的管理用的就是数据结构中静态链表的方法。在初始化时将大缓冲区分成若干个大小为pcache1.szSlot字节的单元,并将所有这些空闲单元链接起来构成空闲单元链表(数据结构中也叫自由队列)。初始化的结果如下图:

全局变量pcache1 pFree pStart pEnd NULL

图2-5 SQLite的静态缓冲区和pcache1全局变量

其中,pcache1的pFree为空闲单元链表首指针。另外两个域pStart和pEnd分别指向第一个

和最后一个单元,将来用于边界检查。

了解这些内容之后,空间的分配与回收就好理解了。 /*

在静态缓冲区中分配nByte大小的空间。

如果没有合适大小的空间或这样的空间已经用完,调用sqlite3Malloc()。 */

static void *pcache1Alloc(int nByte){ void *p;

if( nByte<=pcache1.szSlot && pcache1.pFree ){ /* 从空闲单元链表中删除头结点 */ p = (PgHdr1 *)pcache1.pFree;

pcache1.pFree = pcache1.pFree->pNext; }else{

/* 使用sqlite3Malloc分配一个新的缓冲区。 */ p = sqlite3Malloc(nByte); }

return p; } /*

释放一个由pcache1Alloc()分配的缓冲空间。 */

static void pcache1Free(void *p){ if( p==0 ) return;

if( p>=pcache1.pStart && p

/* 在静态缓冲区范围内,加入到空闲单元链表的头部 */ PgFreeslot *pSlot;

pSlot = (PgFreeslot*)p;

pSlot->pNext = pcache1.pFree; pcache1.pFree = pSlot; }else{

/* 不在静态缓冲区范围内,释放空间 */ sqlite3_free(p); } }

现在,可以再来深入讨论PCache1结构的组织了(前面仅介绍了该结构的定义,见2.2.9节,还很不够)。

每个SQLite进程只有一个pcache1全局变量,也就是说只有一个静态缓冲区,但会为每个打开的数据库文件创建一个PCache1结构的页缓冲区,如下图:

PCache1结构 PCache1结构 … PCache1结构 静态缓冲区 图2-6 页缓冲区和静态缓冲区

当页缓冲区需要一个页空间时,就从静态缓冲区中申请;当页缓冲区中的页不再使用时,就释放回静态缓冲区。

下面是内存分配子系统中为页缓冲区分配一个页的程序: /*

为缓冲区pCache分配一个新页。 */

static PgHdr1 *pcache1AllocPage(PCache1 *pCache){ /* 计算需要分配的字节数:PgHdr1结构大小+页大小 */ int nByte = sizeof(PgHdr1) + pCache->szPage; void *pPg = pcache1Alloc(nByte); PgHdr1 *p; if( pPg ){

p = PAGE_TO_PGHDR1(pCache, pPg); if( pCache->bPurgeable ){ pcache1.nCurrentPage++; } }else{ p = 0; }

return p; }

每个页缓冲区中维护一个Hash表,见PCache1结构的下面3个域:

unsigned int nPage; /* HASH表apHash中总的页数 */ unsigned int nHash; /* apHash[]的槽位数 */

PgHdr1 **apHash; /* Hash表,用于按照键值快速查找 */

页缓冲区中所有的页都存储在Hash表apHash中,有关该Hash表的定义与算法见2.2.9节。 下面是在静态缓冲区中申请一个新页的程序段:

static void *pcache1Fetch(sqlite3_pcache *p, unsigned int iKey, int createFlag){ PCache1 *pCache = (PCache1 *)p; PgHdr1 *pPage = 0;

/* 第5步 */ if( !pPage ){ /* 申请新页 */

pPage = pcache1AllocPage(pCache); }

if( pPage ){

/*将新页插入到页缓冲区Hash表相应槽链的头部 */ unsigned int h = iKey % pCache->nHash; pCache->nPage++; pPage->iKey = iKey;

pPage->pNext = pCache->apHash[h]; pPage->pCache = pCache; pPage->pLruPrev = 0; pPage->pLruNext = 0;

pCache->apHash[h] = pPage; }

return (pPage ? PGHDR1_TO_PAGE(pPage) : 0); }

现在就可以理解PgHdr1结构中pNext域的含义了,它是指向Hash表相应槽链中下一个页的指针。

上述程序段中红色的两句是什么意思呢?下面来解释。

SQLite加载到页缓冲区中的页,有些随时可以释放,如仅为读数据库操作服务,读完了就可以释放了。有些不能释放,如已经加锁的页或已经被修改的页。对于不能释放的页,SQLite会把它“钉住(pin)”。对于可以释放的页,SQLite不到必要(这里“必要”包括多种情况,不详细讨论)时也不会释放,这样,下次如果再使用该页时就不需要再次从磁盘读取了。 如果静态缓冲区中已无空闲单元,当再要向静态缓冲区申请空间时,SQLite会释放一些“可以释放”的页,以满足新的空间申请要求。SQLite的这种空间使用机制是通过LRU链表来实现的。

在pcache1全局变量中维护着一个LRU(最近最少使用算法)双向链表,SQLite将所有未钉住的页都加入到此链表中,pLruHead和pLruTail域分别指向该双向链表的表头和表尾。当需要释放一个页时从表尾删除,某个页使用了之后就会移到表头处,新解除“钉住”的页也加入到表头处,这样就实现了页缓冲区的LRU功能。 如果一个页是不钉住的(即该页在LRU链表中),则PgHdr1结构中的两个域pLruNext和pLruPrev分别指向LRU链表中的前一个页和后一个页。如果一个页是钉住的,这两个域的值为0。

理解了这个机制,好多功能的实现就好理解了。比如,所谓“钉住”一个页,其实就是把它从LRU表中删除。所谓“不钉住”一个页,就是把它加到LRU表中去。刚分配的页都是钉住的,不加到LRU链表中(现在就可以理解上面程序段中红色两句的含义了)。

将一个页钉住的程序如下,典型的双向链表的删除算法: /*

此函数将pPage页从全局LRU链表中移除。如果pPage不在LRU链表中,此函数什么也不做。 */

static void pcache1PinPage(PgHdr1 *pPage){