摘 要: 就目前廣泛使用的輕量級數(shù)據(jù)庫SQLite的構(gòu)架進行分析,特別是對其中的虛擬數(shù)據(jù)庫引擎(VDBE)做了原理性的剖析,并結(jié)合實例,展示了SQLite的應用及SQLite內(nèi)部VDBE指令程序的運行方式。
關(guān)鍵詞: SQLite;構(gòu)架;VDBE;虛擬機
SQLite是遵守ACID的輕量級關(guān)系型數(shù)據(jù)庫管理系統(tǒng),完全免費、開源,無需任何配置也無需任何安裝程序[1]。它廣泛應用在各種嵌入式系統(tǒng)中,在iOS和Android等系統(tǒng)中都是集成在各自的庫中。
虛擬機是當前比較流行的一種軟件構(gòu)架,特別是在解釋性編程語言領(lǐng)域。在安全領(lǐng)域,虛擬機也被用于實現(xiàn)軟件的加密,是公認的一種非常高效且實用的技術(shù)手段。SQLite用較小規(guī)模的代碼用C語言實現(xiàn)了一個程序虛擬機,提高了代碼的獨立性,降低了耦合性,同時保持了很高的效率。
1 SQLite數(shù)據(jù)庫構(gòu)架
圖1所示為SQLite系統(tǒng)的總體構(gòu)架圖[2]。整體上SQLite可以分為前端和后端:前端負責從用戶數(shù)據(jù)到平臺不相關(guān)的指令的轉(zhuǎn)換;后端處理數(shù)據(jù)流,深入到具體數(shù)據(jù)庫數(shù)據(jù)在磁盤上的操作,這些數(shù)據(jù)是和平臺相關(guān)的。SQLite的平臺無關(guān)性通過其內(nèi)部實現(xiàn)的虛擬數(shù)據(jù)庫引擎VDME(Virtual Database Engine)來完成,總地來說,就是將SQL語句先翻譯成一種專門設(shè)計的語言,然后下層再調(diào)用平臺相關(guān)的系統(tǒng)API接口,完成相應的功能。
SQLite的源代碼由96個C語言文件(.c和.h)組成,在編譯之前會由Makefile生成一個完整的文件,即為可以在官方網(wǎng)站上下載的sqlite3.c和sqlite3.h等文件,然后編譯形成所需要的庫或者可執(zhí)行文件。
圖1給出了SQLite的主要模塊及相互之間的關(guān)系,以下將分別介紹各個部分的功能。
(1)接口(Interface)
SQLite庫提供的對外不調(diào)用的接口大多數(shù)都在main.c、legacy.c和vdbeapi.c中,其他一些散布在源代碼的不同部分。對接口的查詢可以在文檔中找到詳細的介紹。為了避免命名上的沖突,所有外部可以調(diào)用的接口都以sqlite3_開頭[3]。
(2)SQL編譯器(SQL Compiler)
這是一個比較完整的編譯器構(gòu)架,分別完成詞法分析、語法分析和中間代碼生成。詞法分析器(Tokenizer)由C語言實現(xiàn),包含在tokenize.c中;語法分析器(Parser)由Lmon LALR(1)生成,和YACC/BISON類似,不兼容,但是生成的代碼是可重入且線程安全的,代碼包含在parse.c中;代碼生成器(Code Generator)生成虛擬機執(zhí)行的中間代碼,包含的文件相對較多,例如select.c、update.c等,大多和SQL命令同名對應。
(3)虛擬機VM(Virtual Machine)
代碼生成器生成的中間代碼會通過VM執(zhí)行。這部分后面會有更詳細的分解。
(4)B-Tree(B-樹)
數(shù)據(jù)庫在磁盤上的操作都是通過B-樹的,對應于數(shù)據(jù)庫中的每一個表或者索引都會有相應的B-樹。實現(xiàn)和接口分別在btree.c和btree.h中[4]。
(5)頁緩存(Page Cache)
數(shù)據(jù)的讀寫都以Chunk為單位進行,這樣可以提高效率。頁緩存負責這部分工作,同時提供了回滾(rollback)等功能,并對數(shù)據(jù)庫文件進行管理。實現(xiàn)和接口分別在pager.c和pager.h中。
(6)系統(tǒng)接口(OS Interface)
SQLite提供了一個系統(tǒng)抽象層,定義在os.h中。每個支持的平臺有自己對應的實現(xiàn)文件,例如os_uinx.c和os_win.c(及相應的頭文件os_unix.h和os_win.h)。
(7)功能和測試(Utility和Test Code)
2 VDBE框架及關(guān)鍵源碼分析
虛擬數(shù)據(jù)庫引擎VDBE(Virtual Database Engine)居于SQLite數(shù)據(jù)庫的核心部分。從整個SQLite的構(gòu)架可以看出,它處在整個系統(tǒng)的中間部分:前端代碼完成對SQL語言的編譯,相當于簡化版本的一個編譯器;后端完成物理上的操作,即利用B-Tree和Pager對物理硬盤上的數(shù)據(jù)進行實際的操作。VDBE完成了這個層次上的抽象鏈接。
整個虛擬數(shù)據(jù)庫引擎(VDBE)由若干個C語言文件組成,主題實現(xiàn)都包含在了vdbe.c(vdbe.h)中。vdbeInt.h定義了VDBE內(nèi)部使用的各種結(jié)構(gòu)和函數(shù)原型。vdbeaux.c實現(xiàn)了VDBE內(nèi)部和整個SQLite構(gòu)建VDBE程序需要的其他功能性函數(shù)代碼。vebeaip.c包含了供外部接口函數(shù)(SQLite庫外的應用程序,如sqlite3_bind系列函數(shù))使用的一些結(jié)構(gòu)。vdbemen.c 實現(xiàn)了在vdbe的存儲管理。
對于用戶的SQL語句,編譯器會生成一個虛擬機實例。虛擬機實例在內(nèi)部和外部是不同的。對內(nèi)看到的是一個vdbe結(jié)構(gòu)的實例,這個結(jié)構(gòu)定義在vdbeInt.h中,代碼如下:
struct Vdbe {
sqlite3 *db; /* 數(shù)據(jù)庫連接 */
Op *aOp; /* 保存虛擬機的空間 */
… /* 其他指令 */
int nOp; /* 生成的指令的條數(shù) */
char *zSql; /* SQL語句 */
… /* 其他指令 */
SubProgram *pProgram; /* 虛擬機使用的其他子程序,
鏈表 */
};
一個虛擬機實例可以有多個子程序,每個子程序可以由多條指令組成。下面是子程序的結(jié)構(gòu):
struct SubProgram {
VdbeOp *aOp; /* 指令 */
int nOp; /* 指令條數(shù) */
int nMem; /* 需要的內(nèi)部空間 */
int nCsr; /* 需要的游標 */
void *token; /* 循環(huán)觸發(fā)時需要的id */
SubProgram *pNext; /* 鏈表的下一個 */
};
現(xiàn)在的SQLite有142條操作指令,都定義在opcodes.h中,在vdbe.c中有相應的源代碼,將解析一些指令作為代表,詳細的技術(shù)文檔可以查看官方文檔。所有的指令大概可以分為3類:
(1)數(shù)據(jù)操作:包含算術(shù)、邏輯運算、字符串操作等;
(2)數(shù)據(jù)管理:主要關(guān)于內(nèi)存和磁盤的操作。內(nèi)存上如棧(stack)操作、數(shù)據(jù)的傳送等,磁盤操作主要是B-Tree和Pager模塊,包括打開及操作游標、事務(wù)的開始與結(jié)束等;
(3)控制流:指令的跳轉(zhuǎn)。
SQL語句在生成VDBE程序后,每條指令包含了一個操作碼(opcode)和至多5個操作數(shù)(operands:P1、P2、P3、P4和P5)。其中:
(1)P1、P2、P3都是32 bit的帶符號整數(shù),它們通常引用的是寄存器。
(2)P2在所有的有跳轉(zhuǎn)功能的指令中表示目的地址。例如上面的第2條指令將會跳轉(zhuǎn)到第10條指令,然后順序執(zhí)行。
(3)P4可以是32 bit或者64 bit的帶符號整型數(shù)據(jù)、字符串、BLOB數(shù)據(jù)(二進制大對象)、函數(shù)指針等其他多樣的對象。
(4)P5通常是無符號的字符,充當?shù)氖菢俗R位。
在SQLite的VDBE內(nèi)部,所有的指令都是VdbeOp結(jié)構(gòu)的一個實例(定義在vdbe.h中),結(jié)構(gòu)的定義也主要是這5個操作數(shù)。
struct VdbeOp {
u8 opcode; /* 操作碼類型 */
… /* 其他數(shù)據(jù)接口 */
signed char p4type; /* p4 的類型 */
u8 p5; /* p5是無符號字符型 */
int p1; /* 操作數(shù)1 */
int p2; /* 操作數(shù)2,通常是跳轉(zhuǎn)指令的目的 */
int p3; /* 操作數(shù)3 */
union { /* ... */ } p4; /* p4 是一個聯(lián)合,
可以有不同的類型 */
… /* 其他數(shù)據(jù)接口 */
};
由代碼生成器生成的程序交由VM執(zhí)行。sqlite3_step()會觸發(fā)內(nèi)部vdbe解釋生成的vdbe指令。指令的執(zhí)行在如下的函數(shù)中進行(SQLITE_PRIVATE 即為static關(guān)鍵字),此處去掉了煩瑣的細節(jié),只展示其中的關(guān)鍵結(jié)構(gòu)和一個指令的執(zhí)行。
SQLITE_PRIVATE int sqlite3VdbeExec(
Vdbe *p /* VDBE 實例 */
) {
int pc; /* 程序計數(shù)器 */
Op *aOp = p->aOp; /* 得到所有的指令 */
Op *pOp; /* 當前指令 */
int rc= SQLITE_OK; /* 返回值 */
sqlite3* db = p->db; /* 數(shù)據(jù)庫連接實例 */
u8 encoding = ENC(db);/* UTF-8編碼 */
… /* 其他初始化代碼 */
switch ( pOp->opcode ) { /* 在此之后就是一個
非常大的case代碼
case OP_Goto: {
CHECK_FOR_INTERRUPT;
pc=pOp->p2-1;/* 調(diào)整程序計數(shù)器 */
break;
}
… /* 其他的case指令 */
}
… /* 其他指令 */
}
這個函數(shù)是整個VDBE的核心執(zhí)行函數(shù),雖然重要,但是代碼的原理非常簡單,就是一系列的switch-case語句。在相應的case情況下,會執(zhí)行相應的底層代碼,進行數(shù)據(jù)庫的磁盤操作。
3 實驗
3.1 數(shù)據(jù)庫編程接口
SQLite的編程模型比較簡單,下面的例子給出了一個基本的框架。
#include "sqlite3.h"
#include <stdlib.h>
int main(int argc, char **argv)
{
char *file = "./test.db";/* 數(shù)據(jù)庫文件 */
sqlite3 *db = NULL; /* 數(shù)據(jù)庫連接實例 */
int rc = 0; /* 返回值 */
sqlite3_initialize(); /* 初始庫 */
rc= sqlite3_open_v2(file, &db,
SQLITE_OPEN_READWRITE, NULL);
/* 準備SQL語句,生成VDBE程序 */
sqlite3_stmt *stmt = NULL:
rc=sqlite3_prepare_v2(db, "SELECT * FROM FILM",
-1, &stmt, NULL);
if (rc != SQLITE_OK) exit(-1);
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *data = (const char*)
sqlite3_column_text(stmt, 0);
printf("%s\n", data?data:"[NULL]");
}
sqlite3_finalize(stmt);
sqlite3_close(db); /* 關(guān)閉 */
sqlite3_shutdown(); /* 釋放資源 */
}
在上面的例子中,使用了sqlite3_prepare_v2()和sqlite3_
step()函數(shù),這是和內(nèi)部的虛擬機聯(lián)系非常緊密的兩個函數(shù),也是了解SQLite虛擬機的兩個點。sqlite3_prepare_v2()完成的是將SQL語句提交給SQL編譯器,編譯成VDBE指令程序,sqlite3_step()將驅(qū)動VDBE執(zhí)行指令程序。
從應用上來說,這僅僅是最簡單的數(shù)據(jù)庫應用框架,更多的接口信息可以查看官方的文檔。
3.2 VDBE程序分析
在官方提供的下載中,有編譯好的命令行可執(zhí)行程序,可以作為完全的SQLite數(shù)據(jù)庫管理工具。同時,它也考慮了一些Debug和Test功能,可以利用它們深入了解SQLite的內(nèi)部機制??梢岳肧QLite命令行程序中的explain命令查看由代碼生成器生成的中間代碼的形式,這只需要在相應的SQL代碼前面加上explain就可以了。如以搜索的命令行顯示(如圖2所示,箭頭表示實際執(zhí)行順序):
圖2中,“addr”列是虛擬機的地址編號,并不是指令執(zhí)行的順序,由于跳轉(zhuǎn)指令的存在,用箭頭標示出了指令運行的實際順序,也可以在SQLite編譯時指定相應的選項,然后利用指令“pragma vdbe_trace=on;”詳細地看到指令的運行過程和堆棧的變化情況。
指令0~指令12都是對SQLite數(shù)據(jù)庫內(nèi)部的準備:由指令1跳轉(zhuǎn)到指令10,指令10(Transaction)開始一個事務(wù),指令11(VerifyCookie)在執(zhí)行一個指令前檢查數(shù)據(jù)庫模式是否發(fā)生了變化,當發(fā)生了變化時要重置,指令12(TableLock)將要讀的數(shù)據(jù)庫表鎖起來,指令13(Goto)跳轉(zhuǎn)到指令2。
從指令2開始是實際的對數(shù)據(jù)庫的操作了。指令2(OpenRead)會打開一個數(shù)據(jù)庫表的只讀游標,P1作為這個游標的標志,P2是打開的數(shù)據(jù)庫表的根頁(root page),P3==0表明是主數(shù)據(jù)庫,P4表明數(shù)據(jù)庫有兩列,P5說明是以P2的值作為根頁。(OpenRead指令的各個操作數(shù)還可以有其他含義,這里只是針對這條SQL語句的解釋,請查看技術(shù)文檔。)指令3(Rewind)~指令7(Next)完成了對所有查詢數(shù)據(jù)的遍歷。指令8(Close)關(guān)閉游標,指令9(Halt)結(jié)束這個VDBE程序。
VDBE對上層提供的就是這樣的接口,而對下層將是調(diào)用相應的接口實現(xiàn)相應的功能,并由此完成模塊上的解耦合。
由VDBE的定義、代碼分析及以上的實驗,可以總結(jié)出SQLite的整體構(gòu)架:
外部調(diào)用SQLite接口函數(shù)sqlite3_prepare(), SQL語句通過SQL編譯器生成對應的VDBE指令程序;
內(nèi)部調(diào)用sqlite3_step()驅(qū)動,內(nèi)部執(zhí)行sqlite3VdbeEx-
ec(),switch-case語句執(zhí)行相應指令。底層通過B-Tree和Pager實現(xiàn)對磁盤數(shù)據(jù)庫文件的管理,如圖3所示。
在實際應用中,可以設(shè)計一個面向應用的指令集,利用程序虛擬機設(shè)計中間抽象層,提高平臺通用性。同時程序虛擬機也為語言虛擬機、系統(tǒng)虛擬機及安全沙盒等技術(shù)提供了技術(shù)基礎(chǔ)。
參考文獻
[1] OWENS M.The definitive guide to SQLite[M].Apress,2006.
[2] KREIBICH J A.Using SQLite[M].O'Reilly Media,2010.
[3] 李蔚,陳亞峰.嵌入式數(shù)據(jù)庫SQLite及其應用研究[J].沿海企業(yè)與科技,2010(10):45-47.
[4] 杜國祥,石俊杰.SQLite嵌入式數(shù)據(jù)庫的應用[J].電腦編程技巧與維護,2010(14):43-46.