test_vfs主要是用来做测试的,用来模拟文件系统出错的,代码实现在test_vfs.c里面。对于一些诸如机械故障、文件系统空间耗尽的错误是很难实际测出来的,test_vfs在OS接口层和VFS之间插入一层测试的VFS用来模拟文件系统出错,进而验证SQLite对错误处理的稳定性。由Tcl脚本来控制模拟,在正常情况下还是调用原来的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[]
)
命令的回调函数是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 xShmLock
那么只有这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;//在出错前正常的次数
/*接下来在命令中取得相关参数,代码从略*/
……
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
}
如果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;