学习本章之前要先复习以下2篇文章:
之前讲到page cache是一种可插入式的管理方式,在sqlite3GlobalConfig.pcache2里定义了对page cache管理的一系列方法接口,并且介绍了最简单的一种接口testpcache,现在我们来分析一下默认的接口pache1,这个要比testpcache复杂很多。
1.内存结构
一个page cache在内存中按如下格式存储,由数据内容和头部组成:
其中PgHdr1在pcache1.c里定义,PgHdr在pcache.c里定义,MemPage在btree.c里定义,在新建一个页时,上述内容由sqlite3_pcache_page结构体表示
struct sqlite3_pcache_page {
void *pBuf; /* The content of the page */
void *pExtra; /* Extra information associated with the page */
};
其中pBuf指向database page content和PgHdr1,pExtra指向PgHdr和MemPage,另外在PgHdr1结构体里定义了一个sqlite3_pcache_page对象。
struct PgHdr1 {
sqlite3_pcache_page page; /* Base class. Must be first. pBuf & pExtra */
……
};
新建一个page cache时代码如下:
pPg = pcache1Alloc(pCache->szAlloc);
p = (PgHdr1 *)&((u8 *)pPg)[pCache->szPage];
p->page.pBuf = pPg;
p->page.pExtra = &p[1];
2.结构关系
hash表
每一次调用pcache1的接口时,需要传入一个sqlite3_pcache*类型的对象作为连接句柄,在pcache1中被转换为PCache1*类型。
有些时候还需要传入页面对象作为参数,传入时的类型是sqlite3_pcache_page*,这是一个基类对象,如上一节所说,在pcahce1中会被扩展成PgHdr1*类型的对象。
在PCache1*类型的对象中有一张hash表,所有的page cache都存放在这张hash表里,如果page cache的key值对应的hash表的索引相同,那么相同地址的元素再建立一个链表。
在Pcache1中与hash表相关变量如下:
struct PCache1 {
……
int szPage; /* Size of database content section */
int szExtra; /* sizeof(MemPage)+sizeof(PgHdr) */
//即szPage+szExtra+sizeof(PgHdr1)
int szAlloc; /* Total size of one pcache line */
……
//hash表中最大的关键字,即最大的页面序号
unsigned int iMaxKey; /* Largest key seen since xTruncate() */
//包括hash表元素和所有链表元素的总个数
unsigned int nPage; /* Total number of pages in apHash */
//即apHash数组的长度
unsigned int nHash; /* Number of slots in apHash[] */
PgHdr1 **apHash; /* Hash table for fast lookup by key
};
PGroup
我们把存放page cache的地址称作slot,那么上节讲到的hash表就把这些slot很好地组织在了一起,从而更容易查找对应的缓存页。
这些需要经常用到的页缓存我们把它标记为pinned,不常用的缓存页我们把它标记为unpinned。我们还可以通过一种叫做PGroup的方式把这些unpinned slot组织在一起,这个是LRU算法的基础。也就是说当缓存页数量已经达到最大时,需要清理掉一些不常用的缓存页来增加新的缓存页。
PGroup的实现有2种模式:
模式1:
每一个连接的PCache拥有自己独立的PGroup,这个时候不需要加锁,访问速度更快,但是占用的内存空间更大。
模式2:
所有连接的PCache共有一个PGroup,也就是说所有PCache的unppined page组成一个PGroup,这时候PGroup属于多线程中的共享资源,需要加锁,所以速度慢一点,但是这种模式可以回收利用更多的内存空间。
PGroup的构建方式如下图所示:
所有的unpinned page组成一个双向的循环链表,pGroup->lru作为这个链表的表头。其实这相当于一个队列,新插入的page加入到队列头部,在队列尾部的page是最早的,所以回收时先回收队列尾部的page。用循环列表就不用查找操作,只要知道了pGroup->lru,就能定位到队列的头部和尾部。
pGroup相关数据结构如下:
struct PCache1 {
/* Cache configuration parameters. Page size (szPage) and the purgeable
** flag (bPurgeable) are set when the cache is created. nMax may be
** modified at any time by a call to the pcache1Cachesize() method.
** The PGroup mutex must be held when accessing nMax.
*/
PGroup *pGroup; /* PGroup this cache belongs to */
……
/* 如果该值为0,那么该PCache的所有page都不可回收利用 */
int bPurgeable; /* True if cache is purgeable */
//每个PCache预留的slot数量,当前为10
unsigned int nMin; /* Minimum number of pages reserved */
//每个PCache配置的最大slot数量
unsigned int nMax; /* Configured "cache_size" value */
unsigned int n90pct; /* nMax*9/10 */
unsigned int nRecyclable; /* Number of pages in the LRU list */
……
};
struct PGroup {
//在模式1时锁为空
sqlite3_mutex *mutex; /* MUTEX_STATIC_LRU or NULL */
//所有pCache.nMax之和
unsigned int nMaxPage; /* Sum of nMax for purgeable caches */
//所有pCache.nMin之和
unsigned int nMinPage; /* Sum of nMin for purgeable caches */
//在createFlag==1时,最大使用的slot数量
//预留nMaxpage- mxPinned= nMinPage-10数量的slot
unsigned int mxPinned; /* nMaxpage + 10 - nMinPage */
unsigned int nCurrentPage; /* Number of purgeable pages allocated */
PgHdr1 lru; /* The beginning and end of the LRU list */
};
3.内存申请
在page cache中,申请内存主要由以下3种方式:
1.PCache-local bulk分配器
这个针对pGroup的模式1,也就是先申请一个大的zBulk空间,然后将其分割成一个个slot,每个slot按照内存结构关系定义好,再把这些slot组成一个链表,使用时只要从头部摘下即可,不用了放回头部。
2.页缓存内存分配器
这个针对pGroup的模式2,缺省时是关闭的,需要调用 sqlite3_config(SQLITE_CONFIG_PAGECACHE, pBuf, sz, N)接口来配置
其中pBuf是申请的空间地址,sz是slot大小,N是slot个数,申请的空间再通过sqlite3PcacheBufferSetup()函数配置,这里也是把一大块地址分割成许多个slot再组成链表,放到pcache1.pFree,但是slot的格式并没有定义,这是因为针对不同的PCache,每个页缓存的szAlloc可能会有所不同。
3.普通内存分配器
当以上2种方式都没有申请到内存时,调用sqlite3Malloc()
4.Page的读取
如果pGroup是模式1,那么调用pcache1FetchWithMutex()加锁,如果pGroup是模式2,那么直接调用pcache1FetchNoMutex()。
读取一个page按照以下流程:
1.根据页号(iKey)搜索hash表
static PgHdr1 *pcache1FetchNoMutex(
sqlite3_pcache *p,
unsigned int iKey,
int createFlag
){
……
PCache1 *pCache = (PCache1 *)p;
PgHdr1 *pPage = 0;
pPage = pCache->apHash[iKey % pCache->nHash];
while( pPage && pPage->iKey!=iKey ){ pPage = pPage->pNext; }
……
}
2.如果页面找到了,那么返回这个页面;如果没找到,并且createFlag是0,那么返回异常;如果没找到,但是createFlag不为0,继续以下步骤。
3.如果createFlag==1,并且使用的page已经超过最大限制,或者内存紧缺,那么直接返回0。
unsigned int nPinned;
PGroup *pGroup = pCache->pGroup;
if( createFlag==1 && (
nPinned>=pGroup->mxPinned
|| nPinned>=pCache->n90pct
|| (pcache1UnderMemoryPressure(pCache) && pCache->nRecyclable<nPinned)
)){
return 0;
}
因为通常步骤3以后是不需要的,所以把以后的步骤单独放在pcache1FetchStage2()函数里,并且设置强制不内联,以减少函数堆栈的初始化,加快读取速度。
4.如果满足条件,回收利用unpinned page
if( pCache->bPurgeable//可回收
//循环链表的表头不能被回收
&& !pGroup->lru.pLruPrev->isAnchor
//当使用的page超过了设置的最大值或者内存不足才回收
&& ((pCache->nPage+1>=pCache->nMax) || pcache1UnderMemoryPressure(pCache))
){
PCache1 *pOther;
pPage = pGroup->lru.pLruPrev;//回收队列尾部的page
assert( pPage->isPinned==0 );
pcache1RemoveFromHash(pPage, 0);//把它从原来的hash表中移除
pcache1PinPage(pPage);//标记为pinned
pOther = pPage->pCache;
if( pOther->szAlloc != pCache->szAlloc ){
//回收的slot长度不符合要求
pcache1FreePage(pPage);
pPage = 0;
}else{
//其实相当于bPurgeable为0,那么nCurrentPage++
pGroup->nCurrentPage -= (pOther->bPurgeable - pCache->bPurgeable);
}
}
5.经过上面步骤还没找到page,那么重新申请一个page cache
5.函数说明
Ÿ void sqlite3PCacheBufferSetup(void *pBuf, int sz, int n)
配置页缓存内存分配器。
Ÿ static int pcache1InitBulk(PCache1 *pCache)
初始化bulk内存分配器
Ÿ static void *pcache1Alloc(int nByte)
为一个缓存页申请nByte大小的空间,先使用页缓存内存分配器,如果内存不够分配,再使用通用内存分配器。
Ÿ static void pcache1Free(void *p)
释放缓存页
Ÿ static int pcache1MemSize(void *p)
获取申请内存的长度
Ÿ static PgHdr1 *pcache1AllocPage(PCache1 *pCache, int benignMalloc)
创建一个新的缓存页,如果不是通过bulk内存分配器获得内存,那么需要对申请的slot定义缓存页的内存结构
Ÿ void *sqlite3PageMalloc(int sz)
pcache1Alloc()的一个对外接口
Ÿ static void pcache1FreePage(PgHdr1 *p)
pcache1Free()的一个对外接口
Ÿ static void pcache1ResizeHash(PCache1 *p)
当pCache->nPage>=pCache->nHash时,把hash表长度扩大1倍,并重新调整hash表结构
Ÿ static PgHdr1 *pcache1PinPage(PgHdr1 *pPage)
把刚创建或刚回收的缓存页标记为pinned
Ÿ static void pcache1RemoveFromHash(PgHdr1 *pPage, int freeFlag)
将pPage从hash表中移除,如果freeFlag置1,那么释放内存
Ÿ static void pcache1EnforceMaxPage(PCache1 *pCache)
如果pGroup->nCurrentPage>pGroup->nMaxPage,那么移除LRU队列中多余的page
Ÿ static void pcache1TruncateUnsafe(
PCache1 *pCache, /* The cache to truncate */
unsigned int iLimit /* Drop pages with this pgno or larger */
)
释放页号大于iLimit的页,如果pCache->iMaxKey - iLimit < pCache->nHash,那么不用扫描整个hash表,否则从pCache->nHash/2处开始扫描整个hash表。
Ÿ static int pcache1Init(void *NotUsed)
设置PGroup模式,如果是模式2,初始化锁。
Ÿ static sqlite3_pcache *pcache1Create(int szPage, int szExtra, int bPurgeable)
创建一个pCache,并初始化相关参数
Ÿ static void pcache1Cachesize(sqlite3_pcache *p, int nMax)
设置pCache->nMax
Ÿ static void pcache1Shrink(sqlite3_pcache *p)
把所有unpinned page都释放掉
Ÿ static int pcache1Pagecount(sqlite3_pcache *p)
获得当前缓存页的数量
Ÿ static SQLITE_NOINLINE PgHdr1 *pcache1FetchStage2(
PCache1 *pCache,
unsigned int iKey,
int createFlag
)
读取缓存页,见上节分析
Ÿ static PgHdr1 *pcache1FetchNoMutex(
sqlite3_pcache *p,
unsigned int iKey,
int createFlag
)
读取缓存页,见上节分析
Ÿ static PgHdr1 *pcache1FetchWithMutex(
sqlite3_pcache *p,
unsigned int iKey,
int createFlag
)
读取缓存页,需要先加锁
Ÿ static sqlite3_pcache_page *pcache1Fetch(
sqlite3_pcache *p,
unsigned int iKey,
int createFlag
)
读取缓存页的对外接口
Ÿ static void pcache1Unpin(
sqlite3_pcache *p,
sqlite3_pcache_page *pPg,
int reuseUnlikely
)
把page插入到LRU队列里
Ÿ static void pcache1Rekey(
sqlite3_pcache *p,
sqlite3_pcache_page *pPg,
unsigned int iOld,
unsigned int iNew
)
重新设置page的页号
Ÿ static void pcache1Truncate(sqlite3_pcache *p, unsigned int iLimit)
加锁后再调用pcache1TruncateUnsafe()
Ÿ static void pcache1Destroy(sqlite3_pcache *p)
释放pCache中的所有页
Ÿ void sqlite3PCacheSetDefault(void)
设置pcache1的对外接口到sqlite3GlobalConfig.pcache2
Ÿ int sqlite3PcacheReleaseMemory(int nReq)
从PGroup里释放nReq大小的空间