雲風的 BLOG: 虛擬檔案系統的自舉
首先我不想做的太複雜。我們不需要特別彈性的不同檔案系統模組掛接到虛擬檔案系統的不同目錄上的功能。所以我寫死了一個叫 .firmware 的目錄,專門存放用來自舉所需的基礎程式碼(的備用版本)。這塊程式碼在啟動後可以在網路模組載入完畢後,用新的版本覆蓋。
其次,除了非常必要的 C 程式碼(例如呼叫 os 的檔案訪問 api)外,我希望全部用 lua 實現。
但是,所有 lua 程式碼,包括檔案系統的實現都是放在檔案系統內的。我們需要初始化 lua 虛擬機器,載入必要的程式碼,然後才能建立起最低可執行的環境。也就是說,建立這個環境時,lua 虛擬機器還並不存在。這樣就需要解決先有雞還是先有蛋的問題。
好在我們先封裝了 lua 虛擬機器模組,簡化了使用。我基於它,創建出一個最小可用的虛擬機器環境,只用來讀取虛擬檔案系統支援模組(先不載入網路模組),然後封裝成 C API ,藏起 lua vm 這個細節,專供自舉階段使用。當自舉完成,就可以銷燬這個虛擬機器,在正式的環境重新載入相關 Lua 模組了。
大概是這樣的:
struct vfs * vfs_init(const char *firmware, const char *repo); const char * vfs_load(struct vfs *V, const char *path); void vfs_exit(struct vfs *V);
初始化的時候,傳入一個 firmware 的路徑,把自舉所需的最小 lua 程式碼放進去。再傳入 repo 倉庫路徑,供 vfs lua 程式碼可以工作後,作為新版本替代。
也就是說,我先把 bootstrap 的 lua 程式碼以原生檔案的形式,放在 firmware 裡,(針對 ios 版,就是打包在 app 中)一開始加載出來使用。用這塊程式碼創建出可以訪問 repo 的最小環境(但不包括網路功能)。repo 是我們的資源倉庫,裡面有上次從網路上同步過來的最新程式碼。然後,我們從 repo 中再次載入新版本的 vfs 支援程式碼,之後用最新版本的 vfs 支援程式碼去訪問 repo 倉庫中的其它部分。
一旦新版本出了問題,也可以直接把 repo 刪乾淨,回退到最老的 firmware 版本上。
vfs 的 C 實現儘量少嵌入寫死的 lua 程式碼,大約是這樣的:
struct vfs { struct luavm *L; int handle; }; static int linitvfs(lua_State *L) { luaL_checktype(L,1, LUA_TLIGHTUSERDATA); struct vfs ** V = (struct vfs **)lua_touserdata(L, 1); *V = lua_newuserdata(L, sizeof(struct vfs)); return 1; } extern int luaopen_winfile(lua_State *L); static int lfs(lua_State *L) { // todo: use lfs return luaopen_winfile(L); } static int cfuncs(lua_State *L) { luaL_checkversion(L); luaL_Reg l[] = { { "initvfs", linitvfs }, { "lfs", lfs }, { NULL, NULL }, }; luaL_newlib(L, l); return 1; } static const char * init_source = "local _, firmware = ... ; loadfile(firmware .. '/bootstrap.lua')(...)"; struct vfs * vfs_init(const char *firmware, const char *dir) { struct luavm *L = luavm_new(); if (L == NULL) return NULL; struct vfs *V = NULL; const char * err = luavm_init(L, init_source, "ssfp", firmware, dir, cfuncs, &V); if (err) { fprintf(stderr, "Init error: %s\n", err); luavm_close(L); return NULL; } if (V == NULL) { luavm_close(L); return NULL; } V->L = L; err = luavm_register(L, "return _LOAD", "=vfs.load", &V->handle); if (err) { // register failed fprintf(stderr, "Register error: %s\n", err); luavm_close(L); return NULL; } return V; } void vfs_exit(struct vfs *V) { if (V) { luavm_close(V->L); } } const char * vfs_load(struct vfs *V, const char *path) { const char * ret = NULL; const char * err = luavm_call(V->L, V->handle, "sS", path, &ret); if (err) { fprintf(stderr, "Load error: %s\n", err); return NULL; } return ret; }
這裡在初始化的時候僅僅是用原生的 loadfile 讀入了 bootstrap.lua 這個檔案而已。所以可以在不動任何 C 程式碼的基礎上做到業務邏輯的更新。
最後來看看 bootstrap.lua 的實現:
local errlog, firmware, dir, cfuncs, V = ... cfuncs = cfuncs() package.preload.lfs = cfuncs.lfs -- init lfs local vfs = assert(loadfile(firmware .. "/vfs.lua"))() local repo = vfs.new(firmware, dir) local f = repo:open(".firmware/vfs.lua") -- try load vfs.lua in vfs if f then local vfs_source = f:read "a" f:close() vfs = assert(load(vfs_source, "@.firmware/vfs.lua"))() repo = vfs.new(firmware, dir) end local function readfile(f) if f then local content = f:read "a" f:close() return content end end local bootstrap = readfile(repo:open(".firmware/bootstrap.lua")) if bootstrap then local newboot = load(bootstrap, "@.firmware/bootstrap.lua") local selfchunk = string.dump(debug.getinfo(1, "f").func, true) if string.dump(newboot, true) ~= selfchunk then -- reload bootstrap newboot(...) return end end function _LOAD(path) local f = repo:open(path) if f then local content = f:read "a" f:close() return content end end _VFS = cfuncs.initvfs(V) -- init V , store in _G
它會去載入 vfs.lua 建立一個最小環境,然後用新加載出來的 vfs 模組,重載入 bootstrap.lua 自身,看是否有更新,最終保證採用的是倉庫中最新的程式碼來載入檔案。