1. 程式人生 > >SQLite3原始碼學習(21) pcache1分析

SQLite3原始碼學習(21) pcache1分析

學習本章之前要先複習以下2篇文章:

之前講到page cache是一種可插入式的管理方式,在sqlite3GlobalConfig.pcache2裡定義了對page cache管理的一系列方法介面,並且介紹了最簡單的一種介面testpcache,現在我們來分析一下預設的介面pache1,這個要比testpcache複雜很多。

1.記憶體結構

一個page cache在記憶體中按如下格式儲存,由資料內容和頭部組成:


其中PgHdr1pcache1.c裡定義,PgHdrpcache.c裡定義,MemPagebtree.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 contentPgHdr1pExtra指向PgHdrMemPage,另外在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 cachekey值對應的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,也就是說所有PCacheunppined 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是申請的空間地址,szslot大小,Nslot個數,申請的空間再通過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.如果頁面找到了,那麼返回這個頁面;如果沒找到,並且createFlag0,那麼返回異常;如果沒找到,但是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)

pPagehash表中移除,如果freeFlag1,那麼釋放記憶體

Ÿ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大小的空間