sqlite3源码分析之缓存

  这里直接对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 链表中(现在就可以理解上面程序段中红色两句的含义了)。





  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值