SQLite3源码学习(33) Pager模块中的相关问题和细节


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;
}


猜你喜欢

转载自blog.csdn.net/pfysw/article/details/80648242