这里直接对sqlite3源码中的缓存那一部分的源码进行分析
SQLite 数据库文件由固定大小的“页(page)”组成。页的默认大小为 1024 个字节(1KB)。页 是数据库读写和在内存中进行管理的基本单位。
数据从文件读到内存以后,总得有个地方存吧,但无论从 PgHdr1 结构还是从 PgHdr 结构中, 都找不到这样的指向页缓存的指针。 实际的实现是这样的:当为PgHdr1 结构变量分配内存时,不是按照 PgHdr1 结构的大小来 分配,而是分配一块更大的内存,包括 4 个部分,如下图:
1:数据库页面内容在前面。
2:PgHdr1: pcache1.c层添加的,PgHdr1的头是sqlite3_pcache_page的子类pghdr1包含需要查找的页面的页号信息。sqlite3_pcache_page中的pBuf指向数据库页内容的开始地方,pExtra指向PgHdr
3:MemPage:这一部分是在btree.c中添加的包含了数据库页的一些信息可以理解为MemPage结构体
4:PgHdr:在pcache.c 中添加的这部分保存了那些页面是脏页
(MemPage+PgHdr+PgHdr1)的大小可以通过sqlite3_config(SQLITE_CONFIG_PCACHE_HDRSZ,&size)来定义。
如何从缓存中获取一个page页缓存
根据createFlags的值的三种情况对应三种不同的获取方法createFlag值对应0,1,2
外加一种情况是不管createFlag的值,在缓存中请求到的页面的副本会被返回
(1):createFlag==0返回空
(2):createFlag==1时对应下面几个情况
在下面的任何一种情况下,即使createFlag的值为1,缓存中如果没有这个页面,那么也会返回空
(a) 被缓存钉住的页数比PCache1.nMax更大
(b) 被缓存钉住的页数比所有purgeable缓存中的nMax还要大,比nMin还要小
如果前面的三种情况都没有并且是purgeable缓存,那么看下面的情况
(a)分配的缓存页数已经达到 PCache1.nMax
(b) 分配的所有purgeable缓存页数已经等于或大于页数所有purgeable缓存的nMax
(c) 这个系统迫于内存的压力,避免不必要的缓存页被分配
然后就企图在LRU链表中替换一个page,如果大小合适就回收利用LRU链表链表中的桶,否则就释放这个桶执行第五步
(3):createFlag==2时的情况
分配一个新页。
sqlite3PcacheFetch函数
SQLITE_PRIVATEsqlite3_pcache_page*sqlite3PcacheFetch(
PCache *pCache, /* Obtain the pagefrom this cache(从这个缓存区中获取page) */
Pgnopgno, /* Page number toobtain */
intcreateFlag /* If true, createpage if it does not exist already (测试时为3表明创建一个新的page)*/
){
int eCreate; //如果可填充缓冲区(sqlite3_pcache*)还没有分配,则现在分配。
sqlite3_pcache_page *pRes;
assert( pCache!=0 );//必须存在一个缓存对象
assert( pCache->pCache!=0 );
assert( createFlag==3 || createFlag==0 );//要么返回空,要么创建新的
assert( pCache->eCreate==((pCache->bPurgeable && pCache->pDirty) ? 1 : 2) );//如果是脏页pCache->eCreate为1,否则为2
/* eCreate defines what to do if the page does not exist.
** 0 Do not allocate a newpage. (createFlag==0) 0代表不需要分配一个新的页
** 1 Allocate a new page if doingso is inexpensive.
** (createFlag==1 AND bPurgeable AND pDirty)不溢出脏页且不超过缓存大小限制分配一个新页 因为这样代价小
** 2 Allocate a new page even itdoing so is difficult.
** (createFlag==1 AND !(bPurgeable AND pDirty)就算不在备份的缓存中也会分配,虽然代价比较大
*/
eCreate = createFlag & pCache->eCreate;//0,1,2 (测试时为2)
assert( eCreate==0 || eCreate==1 || eCreate==2 );
assert( createFlag==0 || pCache->eCreate==eCreate );
assert( createFlag==0 || eCreate==1+(!pCache->bPurgeable||!pCache->pDirty) );
pRes = sqlite3GlobalConfig.pcache2.xFetch(pCache->pCache, pgno, eCreate);
pcacheTrace(("%p.FETCH %d%s (result: %p)\n",pCache,pgno,
createFlag?" create":"",pRes));
return pRes;
}
sqlite3PcacheFetch函数是一个入口具体调用哪个方法是在初始化的时候确定的,在这个函数中做了这么几个事情:
(1):确定eCreate的值 对应页面获取的三种情况
(2):调用具体的方法 代码中加粗的地方
pcache1Fetch方法
该方法的调用就是sqlite3PcacheFetch映射找到的
staticsqlite3_pcache_page *pcache1Fetch(
sqlite3_pcache *p,
unsignedintiKey,
intcreateFlag
){
#ifPCACHE1_MIGHT_USE_GROUP_MUTEX || defined(SQLITE_DEBUG)
PCache1 *pCache = (PCache1 *)p;
#endif
assert( offsetof(PgHdr1,page)==0 );
assert( pCache->bPurgeable || createFlag!=1 );
assert( pCache->bPurgeable || pCache->nMin==0 );
assert( pCache->bPurgeable==0 || pCache->nMin==10 );
assert( pCache->nMin==0 || pCache->bPurgeable );
assert( pCache->nHash>0 );
#ifPCACHE1_MIGHT_USE_GROUP_MUTEX
if( pCache->pGroup->mutex ){
return (sqlite3_pcache_page*)pcache1FetchWithMutex(p, iKey,createFlag);//需要互斥锁
}else
#endif
{
return (sqlite3_pcache_page*)pcache1FetchNoMutex(p, iKey, createFlag);//不需要互斥
}
}
这个方法对应了两个下一层入口:
互斥的和非互斥的。我在测试是因为打开的是单个数据库所以是非互斥的
pcache1FetchNoMutex方法
staticPgHdr1 *pcache1FetchNoMutex( sqlite3_pcache *p, unsignedintiKey, //测试时值为1 intcreateFlag //测试时值为2(分配一个新的) ){ PCache1 *pCache = (PCache1 *)p; PgHdr1 *pPage = 0;
/* Step 1: Search the hash table for an existing entry.(第一步:为现有条目搜索哈希表) */ pPage = pCache->apHash[iKey % pCache->nHash]; while( pPage && pPage->iKey!=iKey ){ pPage = pPage->pNext; }
/* Step 2: If the page was found in the hash table, then return it. ** If the page was not in the hash table and createFlag is 0, abort. ** Otherwise (page not in hash and createFlag!=0) continue with ** subsequent steps to try to create the page. (第二步:如果这个page在哈希表中被发现就返回。如果没有发现并且createFlag为0就终止。否则就是创建一个新的page)*/ if( pPage ){ //如果在哈希表中 if( PAGE_IS_UNPINNED(pPage) ){//如果该页在LRU链表中,需要执行下面的操作 return pcache1PinPage(pPage);//将pPage从PGroup LRU链表中移除 }else{ return pPage;//返回这个页 分配成功 } }elseif( createFlag ){ //如果createFlag不为0 且没有hash表中找到就创建新的 /* Steps 3, 4, and 5 implemented by this subroutine */ return pcache1FetchStage2(pCache, iKey, createFlag); }else{ return 0; } }
|
在获取缓存页面的时候一共需要5步,在这个方法里面有两步:
第一步:通过iKey 从现有哈希表里面找是否有对应的缓存页。
第二步:在第二步里面对应了三种情况:
1:第一种情况(createFlag==0)
如果是没有钉住的页也就是在LRU链表中的页,通过调用pcache1PinPage方法将这个页从LRU链表中删除。
这个函数就是将page从这个LRU链表表中移除并且返回这个page,前提是PGroup LRU链表的一部分,如果不是就不操作
staticPgHdr1 *pcache1PinPage(PgHdr1 *pPage){ assert( pPage!=0 ); assert( PAGE_IS_UNPINNED(pPage) ); assert( pPage->pLruNext ); assert( pPage->pLruPrev ); assert( sqlite3_mutex_held(pPage->pCache->pGroup->mutex) );//必须持有互斥 pPage->pLruPrev->pLruNext = pPage->pLruNext;//开始移除操作也就是从双向链表中移除 pPage->pLruNext->pLruPrev = pPage->pLruPrev; pPage->pLruNext = 0; pPage->pLruPrev = 0; assert( pPage->isAnchor==0 ); assert( pPage->pCache->pGroup->lru.isAnchor==1 ); pPage->pCache->nRecyclable--;//LRU链表的总页数- - returnpPage; } |
为什么要从LRU链表链表中删除呢,我的猜测是将LRU链表中的页提升为钉住的页也就是不能被替换的页。
2:第二种情况(createFlag==0)
这个页就在钉住的缓存中(在哈希表中但不在LRU链表中)就直接返回
3:第三种情况(createFlag!==0)
就是createFlag不为0,创建一个新的页面,执行下面的3,4,5步
pcache1FetchStage2函数
这个函数执行了3,4,5步,分别对应createFlag==1,和2的情况
staticSQLITE_NOINLINEPgHdr1 *pcache1FetchStage2( PCache1 *pCache, unsignedintiKey, intcreateFlag ){ unsignedint nPinned; PGroup *pGroup = pCache->pGroup; PgHdr1 *pPage = 0;
/* Step 3: Abort if createFlag is 1 but the cache is nearly full (如果缓存满了createFlag为1时就终止)*/ assert( pCache->nPage >= pCache->nRecyclable );//哈希表中的总页数>=LRU链表的总页数 nPinned = pCache->nPage - pCache->nRecyclable;//那么被钉住的页就是 哈希表中的总页数-LRU链表的总页数 assert( pGroup->mxPinned == pGroup->nMaxPage + 10 - pGroup->nMinPage );//被钉住页的最大值就是pGroup中可清除缓存的最大值+10-可清除缓存的最小值 assert( pCache->n90pct == pCache->nMax*9/10 );//配置的换成区大小*9/10 if( createFlag==1 && ( nPinned>=pGroup->mxPinned || nPinned>=pCache->n90pct || (pcache1UnderMemoryPressure(pCache) && pCache->nRecyclable<nPinned) )){ return 0; }
if( pCache->nPage>=pCache->nHash ) pcache1ResizeHash(pCache); assert( pCache->nHash>0 && pCache->apHash );
/* Step 4. Try to recycle a page.(第四步:试着回收一页) */ if( pCache->bPurgeable //可清除标志 && !pGroup->lru.pLruPrev->isAnchor//并且不在PGroup LRU链表中 && ((pCache->nPage+1>=pCache->nMax) || pcache1UnderMemoryPressure(pCache)) ){ PCache1 *pOther; pPage = pGroup->lru.pLruPrev; assert( PAGE_IS_UNPINNED(pPage) ); pcache1RemoveFromHash(pPage, 0); pcache1PinPage(pPage); pOther = pPage->pCache; if( pOther->szAlloc != pCache->szAlloc ){ pcache1FreePage(pPage); pPage = 0; }else{ pGroup->nPurgeable -= (pOther->bPurgeable - pCache->bPurgeable); } }
/* Step 5. If a usable page buffer has still not been found, ** attempt to allocate a new one. (第五步:创建一个新页) */ if( !pPage ){ pPage = pcache1AllocPage(pCache, createFlag==1); }
if( pPage ){ unsignedint h = iKey % pCache->nHash; pCache->nPage++;//总页数++ pPage->iKey = iKey; pPage->pNext = pCache->apHash[h];//添加到哈希桶中 pPage->pCache = pCache; pPage->pLruPrev = 0; pPage->pLruNext = 0; *(void **)pPage->page.pExtra = 0; pCache->apHash[h] = pPage;//该page 在哈希桶中的位置 if( iKey>pCache->iMaxKey ){ pCache->iMaxKey = iKey; } } return pPage; } |
第三步:如果缓存满了即使createFlag==1也会终止返回0
其中 nPinned =pCache->nPage - pCache->nRecyclable;被钉住的页就是 哈希表中的总页数-LRU链表的总页数
第四步:试着将LRU链表中的页回收利用
这是也对应了下面几种情况:
这里用到了bPurgeable(可清除标志)如果这个标志为真 表面这是一个非内存文件。
还用到了一个判定函数pcache1UnderMemoryPressure
pcache1UnderMemoryPressure函数
如果禁止分配一个新的页面缓存就返回true
如果内存是专门分配给页面缓存的 但是已经全部被用了,然后最好能避免分配一个新的页面缓存条目。大概是因为sqlite_config_pagecache应该足够所有页面缓存的需求,我们不应该再溢出分配
或者该堆用于所有页面缓存内存,但堆内存不足,因此希望避免分配新的页缓存条目
为了避免进一步加重堆的压力。
staticint pcache1UnderMemoryPressure(PCache1 *pCache){ if( pcache1.nSlot && (pCache->szPage+pCache->szExtra)<=pcache1.szSlot ){//数据库内容区大小+数据库额外大小<=每一个自由槽大小 returnpcache1.bUnderPressure; }else{ return sqlite3HeapNearlyFull(); } } |
第五步:创建一个新页:
pcache1AllocPage函数
staticPgHdr1 *pcache1AllocPage(PCache1 *pCache, intbenignMalloc){ PgHdr1 *p = 0; void *pPg;
assert( sqlite3_mutex_held(pCache->pGroup->mutex) ); if( pCache->pFree || (pCache->nPage==0 && pcache1InitBulk(pCache)) ){//要么存在未使用的未使用的PCACHE局部页面列表 要么哈希表apHash中的总页数为0并且初始化 p = pCache->pFree; pCache->pFree = p->pNext; p->pNext = 0; }else{ #ifdef SQLITE_ENABLE_MEMORY_MANAGEMENT /* The group mutex must be released before pcache1Alloc() is called. This ** is because it might call sqlite3_release_memory(), which assumes that ** this mutex is not held. */ assert( pcache1.separateCache==0 ); assert( pCache->pGroup==&pcache1.grp ); pcache1LeaveMutex(pCache->pGroup); #endif if( benignMalloc ){ sqlite3BeginBenignMalloc(); } #ifdef SQLITE_PCACHE_SEPARATE_HEADER pPg = pcache1Alloc(pCache->szPage); p = sqlite3Malloc(sizeof(PgHdr1) + pCache->szExtra); if( !pPg || !p ){ pcache1Free(pPg); sqlite3_free(p); pPg = 0; } #else pPg = pcache1Alloc(pCache->szAlloc); p = (PgHdr1 *)&((u8 *)pPg)[pCache->szPage]; #endif if( benignMalloc ){ sqlite3EndBenignMalloc(); } #ifdef SQLITE_ENABLE_MEMORY_MANAGEMENT pcache1EnterMutex(pCache->pGroup); #endif if( pPg==0 ) return 0; p->page.pBuf = pPg; p->page.pExtra = &p[1]; p->isBulkLocal = 0; p->isAnchor = 0; } (*pCache->pnPurgeable)++; return p; } |
在这个函数里面即使分配新页 也对应了几种不同的情况
第一种:pCache->pFree存在 即未使用的PCACHE局部页面列表存在就让p = pCache->pFree;
然后返回这个p
第二种:试图初始化PCACHE -> pfree和PCACHE -> pbulk字段,如果pCache->pFree后面还有free pages就返回true,通过调用pcache1InitBulk函数来
staticint pcache1InitBulk(PCache1 *pCache){ i64 szBulk; char *zBulk; if( pcache1.nInitPage==0 ) return 0; /* Do not bother with a bulk allocation if the cache size very small (如果缓存太小就不管了)*/ if( pCache->nMax<3 ) return 0; sqlite3BeginBenignMalloc(); if( pcache1.nInitPage>0 ){ szBulk = pCache->szAlloc * (i64)pcache1.nInitPage;//szBulk=初始批量分配大小*缓存链表的总大小 }else{ szBulk = -1024 * (i64)pcache1.nInitPage; } if( szBulk > pCache->szAlloc*(i64)pCache->nMax ){ szBulk = pCache->szAlloc*(i64)pCache->nMax; } zBulk = pCache->pBulk = sqlite3Malloc( szBulk ); sqlite3EndBenignMalloc(); if( zBulk ){ int nBulk = sqlite3MallocSize(zBulk)/pCache->szAlloc; do{ PgHdr1 *pX = (PgHdr1*)&zBulk[pCache->szPage]; pX->page.pBuf = zBulk;//页内容区 pX->page.pExtra = &pX[1];//页额外区 pX->isBulkLocal = 1; pX->isAnchor = 0;//不在 PGroup.lru中 pX->pNext = pCache->pFree; pCache->pFree = pX; //pFree就是PgHdr1 zBulk += pCache->szAlloc; }while( --nBulk ); } returnpCache->pFree!=0; } |
这里分析的时候如果不分析pcache1Create函数有点弄不清楚,所以我决定先分析下
pcache1Create函数。然后回来解释里面的具体实现。
现在就可以理解 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 链表中(现在就可以理解上面程序段中红色两句的含义了)。