SQLite3源码学习之test_vfs框架实例讲解
test_vfs主要是用来做测试的,用来模拟文件系统出错的,代码实现在test_vfs.c里面。对于一些诸如机械故障、文件系统空间耗尽的错误是很难实际测出来的,test_vfs在OS接口层和VFS之间插入一层测试的VFS用来模拟文件系统出错,进而验证SQLite对错误处理的稳定性。由Tcl脚本来控制模拟,在正常情况下还是调用原来的VFS。
1.注册VFS
在Tcl中使用格式如下:
testvfs VFSNAME SWITCHES
testvfs的实现函数为testvfs_cmd(),在这个函数里创建了一个新的命令VFSNAME用来设置一些测试场景,SWITCHES为testvfs命令的一些选项,如:
-noshm:置1禁止共享缓存
-default:把tvfs_vfs设置为首选VFS
-fullshm:使用原VFS的共享缓存
注册时首先声明VFS接口函数,在tvfs_vfs结构体里
static sqlite3_vfs tvfs_vfs
在pAppData记录原来默认的VFS
Testvfs *p; /* New object */ sqlite3_vfs *pVfs; /* New VFS */ zVfs = Tcl_GetString(objv[1]);//取得VFSNAME的名称 nByte = sizeof(Testvfs) + (int)strlen(zVfs)+1; p = (Testvfs *)ckalloc(nByte); memset(p, 0, nByte); p->pParent = sqlite3_vfs_find(0);//获取默认VFS p->zName = (char *)&p[1]; memcpy(p->zName, zVfs, strlen(zVfs)+1);//设置当前vfs名称 pVfs->pAppData = (void *)p;//把信息存到pVfs->pAppData里 sqlite3_vfs_register(pVfs, isDefault);//注册VFS
在创建命令时,还把pVfs->pAppData的信息(即Testvfs *p的地址)传到了命令里供调用:
//声明 EXTERN Tcl_Command Tcl_CreateObjCommand(Tcl_Interp *interp, const char *cmdName, Tcl_ObjCmdProc *proc, ClientData clientData, Tcl_CmdDeleteProc *deleteProc); //创建新命令 Tcl_CreateObjCommand(interp, zVfs, testvfs_obj_cmd, p, testvfs_obj_del); //命令函数,第1个参数即为上面传入的p static int SQLITE_TCLAPI testvfs_obj_cmd( ClientData cd, Tcl_Interp *interp, int objc, Tcl_Obj *CONST objv[] )
2. Tcl命令参数
命令的回调函数是testvfs_obj_cmd,该命令中有多个执行参数如下:
shm
假设注册VFS时新建的命令是tvfs,使用格式为:
tvfs shm FILE VALUE
上述命令中,打开文件连接FILE对应的缓存,将其加入到一个数组对象中如果第4个参数存在,那么把VALUE对象数组的值拷贝到缓存,举例:
set blob [tvfs shm $file]
上述命令中新建一个数组对象,数组存放的是对应连接文件的缓存
Testvfs *p = (Testvfs *)cd; /*搜索对应缓存*/ for(pBuffer=p->pBuffer; pBuffer; pBuffer=pBuffer->pNext){ if( 0==strcmp(pBuffer->zFile, zName) ) break; }
把缓存添加到数组对象,然后返回这个数组对象
pObj = Tcl_NewObj(); for(i=0; pBuffer->aPage[i]; i++){ int pgsz = pBuffer->pgsz; if( pgsz==0 ) pgsz = 65536; Tcl_AppendObjToObj(pObj, Tcl_NewByteArrayObj(pBuffer->aPage[i], pgsz));//依次添加数组缓存 } Tcl_SetObjResult(interp, pObj);//该命令返回的Tcl对象
假如blob对象的数组中的内容在Tcl中被修改,然后blob对象在该命令中以第4个参数被传入时,会把对象里的内容拷贝到缓存:
if( objc==4 ){ int n; u8 *a = Tcl_GetByteArrayFromObj(objv[3], &n);//数组长度 int pgsz = pBuffer->pgsz; if( pgsz==0 ) pgsz = 65536; /*拷贝对象数组到缓存页*/ for(i=0; i*pgszaPage[i], &a[i*pgsz], nByte); } }
script
注册脚本的回调函数,保存在p->pScript里,注册如下
tvfs script tvfs_cb 其中tvfs_cb是回调函数
filter
在VFS中有很多方法如:
static struct VfsMethod { char *zName; int mask; } vfsmethod [] = { { "xShmOpen", TESTVFS_SHMOPEN_MASK }, { "xShmLock", TESTVFS_SHMLOCK_MASK }, { "xShmBarrier", TESTVFS_SHMBARRIER_MASK }, { "xShmUnmap", TESTVFS_SHMCLOSE_MASK }, { "xShmMap", TESTVFS_SHMMAP_MASK }, …… }
该命令对特定的方法设置过滤掩码
/*获取过滤的方法参数列表*/ if( Tcl_ListObjGetElements(interp, objv[2], &nElem, &apElem) ){ return TCL_ERROR; } …… /*获取过滤方法的掩码*/ for(i=0; imask = mask;//最后保存掩码
比如设置xShmOpen和xShmLock方法的过滤掩码
tvfs filter xShmOpen xShmOpen
那么只有这2个方法才能执行回调函数,如
if( p->pScript && p->mask&TESTVFS_SHMOPEN_MASK ){ /*判断xShmOpen掩码有没有设置,设置了才执行脚本的回调函数*/ tvfsExecTcl(p, "xShmOpen", Tcl_NewStringObj(pFd->zFilename, -1), 0, 0, 0); if( tvfsResultCode(p, &rc) ){ if( rc!=SQLITE_OK ) return rc; } }
ioerr、fullerr、cantopenerr
这3个命名用来设置模拟出错次数等
case CMD_CANTOPENERR: case CMD_IOERR: case CMD_FULLERR: { TestFaultInject *pTest = 0; int iRet; switch( aSubcmd[i].eCmd ){ case CMD_IOERR: pTest = &p->ioerr_err; break; case CMD_FULLERR: pTest = &p->full_err; break; case CMD_CANTOPENERR: pTest = &p->cantopen_err; break; default: assert(0); } iRet = pTest->nFail;//用来设置对象返回结果 pTest->nFail = 0;//失败次数 pTest->eFault = 0;//是否持续模拟出错 pTest->iCnt = 0;//在出错前正常的次数 /*接下来在命令中取得相关参数,代码从略*/ ……
3. open
vfs打开的接口是tvfsOpen()函数,最主要的2个参数:
sqlite3_vfs *pVfs:
用来获取与vfs关联的应用数据,从而调用脚本回调函数
Tcl_Obj *pId = 0; Testvfs *p = (Testvfs *)pVfs->pAppData; if( p->pScript && p->mask&TESTVFS_OPEN_MASK ){ Tcl_Obj *pArg = Tcl_NewObj(); Tcl_IncrRefCount(pArg); …… tvfsExecTcl(p, "xOpen", Tcl_NewStringObj(pFd->zFilename, -1), pArg, 0, 0); Tcl_DecrRefCount(pArg); if( tvfsResultCode(p, &rc) ){ if( rc!=SQLITE_OK ) return rc; }else{ pId = Tcl_GetObjResult(p->interp);//此处返回的对象一般作为脚本回调函数的传入参数 } }
sqlite3_file *pFile:
这是一个连接句柄,由PagerOpen分配空间,传入时将其转换为Testvfs类型,从而设置io方法等信息
#define PARENTVFS(x) (((Testvfs *)((x)->pAppData))->pParent) TestvfsFile *pTestfile = (TestvfsFile *)pFile; TestvfsFd *pFd; pFd = (TestvfsFd *)ckalloc(sizeof(TestvfsFd) + PARENTVFS(pVfs)->szOsFile);//为parent vfs分配连接句柄 memset(pFd, 0, sizeof(TestvfsFd) + PARENTVFS(pVfs)->szOsFile); pFd->pShm = 0; pFd->pShmId = 0; pFd->zFilename = zName; pFd->pVfs = pVfs; pFd->pReal = (sqlite3_file *)&pFd[1];//此处跳过一个sizeof(TestvfsFd)长度的偏移,用来获得parent vfs的连接句柄 memset(pTestfile, 0, sizeof(TestvfsFile)); pTestfile->pFd = pFd;//绑定到连接句柄pFile上 rc = sqlite3OsOpen(PARENTVFS(pVfs), zName, pFd->pReal, flags, pOutFlags);//获取parent vfsd的句柄pFd->pReal if( pFd->pReal->pMethods ){ sqlite3_io_methods *pMethods; int nByte; if( pVfs->iVersion>1 ){ nByte = sizeof(sqlite3_io_methods); }else{ nByte = offsetof(sqlite3_io_methods, xShmMap);// 这个是求结构体成员的相对便宜地址的,宏展开为((size_t) &((sqlite3_io_methods *)0)->xShmMap) } pMethods = (sqlite3_io_methods *)ckalloc(nByte); memcpy(pMethods, &tvfs_io_methods, nByte); …… pFile->pMethods = pMethods;//绑定io_methods为tvfs_io_methods }
4. 错误模拟
如果Tcl里配置了错误信息,那么返回错误,否则正常调用parent vfs的io方法,以下为open出错的模拟代码
static int tvfsInjectFault(TestFaultInject *p){ int ret = 0; if( p->eFault ){//开启出错模拟 p->iCnt--;//出错之前调用的次数 if( p->iCnt==0 || (p->iCnt<0 && p->eFault==FAULT_INJECT_PERSISTENT ) ){ ret = 1; p->nFail++;//记录出错次数 } } return ret; } static int tvfsInjectIoerr(Testvfs *p){ return tvfsInjectFault(&p->ioerr_err);//磁盘io错误 } static int tvfsInjectFullerr(Testvfs *p){ return tvfsInjectFault(&p->full_err);//磁盘空间不足错误 } static int tvfsInjectCantopenerr(Testvfs *p){ return tvfsInjectFault(&p->cantopen_err);//打开错误 } if( (p->mask&TESTVFS_OPEN_MASK) && tvfsInjectIoerr(p) ) return SQLITE_IOERR; if( tvfsInjectCantopenerr(p) ) return SQLITE_CANTOPEN; if( tvfsInjectFullerr(p) ) return SQLITE_FULL;