SQLite3原始碼學習(33) Pager模組中的相關問題和細節

偏飛123發表於2018-06-11


1. getPageMMap

getPageMMap()函式是一個根據頁號來獲取檔案資料頁的函式,與之對應的是getPageNormal()函式。getPageNormal()需要通過read介面來向磁碟讀資料頁,而使用getPageMMap之前,需要呼叫CreateFileMappingW()讓檔案對映到記憶體,此時會返回一個控制程式碼,再把控制程式碼傳入MapViewOfFile()從而取出記憶體地址放到pFd->pMapRegion指標裡,讀取頁資料時只需要根據偏移地址從指標裡取出資料即可

if( pFd->mmapSize >= iOff+nAmt ){
      *pp = &((u8 *)pFd->pMapRegion)[iOff];
      pFd->nFetchOut++;
    }

使用前需要配置pPager->szMmap大於0,配置命令為:

PRAGMA[schema.]mmap_size(N)

2. winShmMap

此函式是win平臺下sqlite3OsShmMap()介面的實現,根據傳入的頁號用來獲取共享記憶體的對應頁。pDbFd是當前資料庫連線控制程式碼,pDbFd->pShm是當前共享記憶體,每一個資料庫都會有一個共享記憶體,所有共享記憶體組成一個連結串列, pShm->pShmNode是當前節點,winShmNodeList是該連結串列的表頭,pShmNode->hFile是當前共享記憶體檔案的winOpen開啟的連線控制程式碼,pShmNode->hFile.h是對應的作業系統連線控制程式碼,該引數用來獲取共享記憶體地址,pShmNode->aRegion[iRegion].pMap是第iRegion頁的共享快取,每一頁長度為szRegion,iRegion由函式的引數傳入。

3. MasterJournal

主日誌應用於一個事務需要對多個資料庫檔案操作,每個資料庫對應一個子日誌,子日誌的記錄了主日誌的名字用來和主日誌關聯,主日誌不包含任何資料庫的內容,只存放子日誌檔名。

主日誌是為了保證一個事務在多檔案操作的原子性,考慮有3個資料庫,完成寫資料庫後,在刪掉其中一個資料庫的日誌後斷電,剩餘2個資料庫的日誌還在,在下一次事務開始時,保留日誌的2個資料庫將被還原,而日誌被刪掉後的資料庫將不能還原,這就不能保證事務的原子性。

而引入主日誌後,提交事務時是先刪除主日誌,再刪除子日誌,如果主日誌被刪除,子日誌還在,這說明事務已經結束不再對子日誌進行回滾。

4. PgHdr.flags

該標誌位用來表示頁面快取的狀態,主要有以下幾種:

#define PGHDR_CLEAN           0x001  /* Page not on the PCache.pDirty list */
#define PGHDR_DIRTY           0x002  /* Page is on the PCache.pDirty list */
#define PGHDR_WRITEABLE       0x004  /* Journaled and ready to modify */
#define PGHDR_NEED_SYNC       0x008  /* Fsync the rollback journal before

如果需要修改頁面,需要將頁面新增到髒頁連結串列裡,此時flag被置為PGHDR_DIRTY,如果頁面不在髒頁連結串列裡,flag被置為PGHDR_CLEAN。PGHDR_WRITEABLE表示可以對頁面進行修改,即頁面可寫。如果頁面可寫那麼一定在髒頁連結串列裡,而反之在髒頁連結串列的頁面並不一定可寫。

PGHDR_NEED_SYNC表示原始頁面被寫入到了日誌裡面,但是還沒有刷盤,在日誌刷盤後,該標誌位會被清除,緊接著就會把髒頁寫入到資料庫裡。

一般在寫事務提交時會對日誌刷盤,但是有時候由於快取空間不夠,pagerStress()來釋放頁快取的時候,需要把髒頁寫到資料庫,如果PGHDR_NEED_SYNC被置位,則需要對日誌先刷盤,此後在日誌結尾新增日誌頭重新開啟一個日誌段。

5. sqlite3PagerMovepage

當資料庫使用很久之後,有些頁沒有使用變成空閒頁,一般SQLite在新增新的頁之前都會重新利用這些空閒頁。但是當資料庫檔案長度太大時,就需要刪除空閒頁,調整資料頁的位置,此時需要把後面在使用的頁移到前面空閒的頁。

在資料庫完成更新後,此時還要重新調整快取,通過sqlite3PagerMovepage()函式把要移動的資料庫的頁號改為移動之後的頁號,而原來的空閒頁中的快取需要先釋放掉。

雖然相同的頁號在移動前後資料內容已經變更,但是如果原來頁的資料已經寫入日誌,則還原時會被還原成原來的資料。所以移動前後頁快取中PGHDR_NEED_SYNC標誌位應該保持不變,即這一頁已經寫入日誌需要刷盤,不能應移動後而沒有刷盤,否則不能保證事務的原子性。

6. pager_playback_one_page

該函式用來回滾日誌中的一個記錄到資料庫,isMainJrnl引數決定回滾的主日誌還是子日誌。

jfd = isMainJrnl? pPager->jfd : pPager->sjfd;

只有資料頁的日誌刷盤並寫入到資料庫後才將日誌裡的內容回滾到資料庫,否則資料還在記憶體中沒有寫入資料庫,沒有必要回滾,只要頁快取裡的內容還原即可。

pPg = sqlite3PagerLookup(pPager, pgno);
  if( isMainJrnl ){
    //日誌刷盤後都會把pPager->journalHdr修改為下一個塊的偏移地址
    isSynced = pPager->noSync || (*pOffset <= pPager->journalHdr);
  }else{
    //假設日誌有10條記錄,現在需要釋放一頁的cache
    //此時日誌裡包含的頁都寫入到資料庫, PGHDR_NEED_SYNC
    //會被清掉,釋放的這一頁已經不在頁快取裡了
    isSynced = (pPg==0 || 0==(pPg->flags & PGHDR_NEED_SYNC));
  }
      if( isOpen(pPager->fd)
       //此條件代表日誌已經刷盤
   && (pPager->eState>=PAGER_WRITER_DBMOD || pPager->eState==PAGER_OPEN)
   && isSynced
  ){
    //把日誌的內容寫回到資料庫
} else if( !isMainJrnl && pPg==0 ){
  //進入這個條件說明日誌還沒有刷盤,但是這一頁卻在快取裡找不到了
  //這說明這一頁已經通過sqlite3PagerMovepage()移動到新的空閒頁了
  //此時需要獲取該頁的頁快取,把這一頁的內容還原,否則這一頁是從
  //資料庫讀取的,就不是最新的了,如果頁快取不夠,用pPager->doNotSpill 
  //標誌位控制強制不寫入資料庫 
  //--------------------------------------------------
  //如果是wal日誌模式始終進這個條件,不用考慮日誌刷盤問題   
}
if( pPg ){
   //把這一頁的頁快取還原為日誌裡的記錄頁
}

7. WAL日誌的savepoint

在回滾日誌模式下,每個資料庫都對應一個主日誌(main journal),注意這和多資料庫事務的主日誌(master journal)不同。在儲存點模式下有一個子日誌(sub journal),所有儲存點共用一個子日誌,每新建一個儲存點記下當前子日誌的記錄數。WAL模式沒有主日誌,只有子日誌,這個子日誌記錄了每個儲存點所在頁的原始狀態,而且新增了4個標記變數,用來記錄下當前WAL日誌的最大幀,上一次寫事務提交後的校驗值和檢查點序列號:

void sqlite3WalSavepoint(Wal *pWal, u32 *aWalData){
  assert( pWal->writeLock );
  aWalData[0] = pWal->hdr.mxFrame;
  aWalData[1] = pWal->hdr.aFrameCksum[0];
  aWalData[2] = pWal->hdr.aFrameCksum[1];
  aWalData[3] = pWal->nCkpt;
}

所有的儲存點都在一個事務中存在,如果事務提交了,那麼儲存點就會被釋放掉。寫事務開始後,會獲取獨佔寫鎖,所以不可能有其他程式的事務去變更WAL-index檔案的mxFrame欄位。由於WAL日誌是往後追加的模式,回滾時只需把頁快取裡的內容恢復成日誌裡的內容,並把pWal->hdr.mxFrame還原成子日誌裡的記錄值即可。

如果在寫事務的開始就新建一個儲存點,此後還原時發現pWal->nCkpt變更,說明WAL日誌已經開始重寫,此時把pWal->hdr.mxFrame恢復成0即可。

int sqlite3WalSavepointUndo(Wal *pWal, u32 *aWalData){
  int rc = SQLITE_OK;

  assert( pWal->writeLock );
  assert( aWalData[3]!=pWal->nCkpt || aWalData[0]<=pWal->hdr.mxFrame );

  if( aWalData[3]!=pWal->nCkpt ){
    /* This savepoint was opened immediately after the write-transaction
    ** was started. Right after that, the writer decided to wrap around
    ** to the start of the log. Update the savepoint values to match.
    */
    aWalData[0] = 0;
    aWalData[3] = pWal->nCkpt;
  }

  if( aWalData[0]<pWal->hdr.mxFrame ){
    pWal->hdr.mxFrame = aWalData[0];
    pWal->hdr.aFrameCksum[0] = aWalData[1];
    pWal->hdr.aFrameCksum[1] = aWalData[2];
    walCleanupHash(pWal);
  }

  return rc;
}

如果要把一個事務回退到開始時狀態,通過呼叫sqlite3WalUndo()函式實現,主要做的工作是把髒頁連結串列裡的頁恢復到初始狀態,從上次事務提交的mxFrame到當前pWal->hdr.mxFrame的頁也要恢復成初始狀態,因為事務提交前有些髒頁會通過pagerStress()函式寫入到WAL日誌後被釋放掉。

8. 定時阻塞等待

獲取鎖的時候如果需要等待一段時間,可以通過回撥函式控制等待的時間,還可在回撥函式裡做一些其他事情。

do {
    rc = walLockExclusive(pWal, lockIdx, n);
  }while( xBusy && rc==SQLITE_BUSY && xBusy(pBusyArg) );

xBusy和pBusyArg分別為傳入的回撥函式和引數,其定義如下:

int (*xBusyHandler)(void*);
void *pBusyHandlerArg;

根據傳入的引數,又可以巢狀上一層的回撥函式,如b-tree層傳入的是btreeInvokeBusyHandler,引數是pBt->db->busyHandler

static int btreeInvokeBusyHandler(void *pArg){
  BtShared *pBt = (BtShared*)pArg;
  assert( pBt->db );
  assert( sqlite3_mutex_held(pBt->db->mutex) );
  return sqlite3InvokeBusyHandler(&pBt->db->busyHandler);
}

這個控制程式碼裡又包含了新的回撥函式和2個引數

int sqlite3InvokeBusyHandler(BusyHandler *p){
  int rc;
  if( NEVER(p==0) || p->xFunc==0 || p->nBusy<0 ) return 0;
  rc = p->xFunc(p->pArg, p->nBusy);
  if( rc==0 ){
    p->nBusy = -1;
  }else{
    p->nBusy++;
  }
  return rc; 
}

p->xFunc是定時回撥函式,預設傳入的是sqliteDefaultBusyCallback,p->nBusy是時間戳,定時時間到則返回0,否則返回1,其定義如下

sqlite3_busy_handler(db, sqliteDefaultBusyCallback, (void*)db);
int sqlite3_busy_handler(
  sqlite3 *db,
  int (*xBusy)(void*,int),
  void *pArg
){
  sqlite3_mutex_enter(db->mutex);
  db->busyHandler.xFunc = xBusy;
  db->busyHandler.pArg = pArg;
  db->busyHandler.nBusy = 0;
  db->busyTimeout = 0;
  sqlite3_mutex_leave(db->mutex);
  return SQLITE_OK;
}


相關文章