skynet原始碼分析之熱更新 lua程式碼,兩個關鍵字搞定
skynet有兩種方法支援熱更新lua程式碼:clearcache和inject,在介紹skynet熱更新機制之前,先介紹skynet控制檯,參考官方wiki
https://
github。com/cloudwu/skyn
et/wiki/DebugConsole
1。 skynet控制檯
想要使用skynet控制檯,需啟動debug_console服務skynet。newservice(“debug_console”, ip, port),指定一個地址。skynet啟動後,用nc命令就可以進入控制檯,如圖。
debug_console服務啟動後,監聽外部連線(第3行)。
第15行,當開啟控制檯連線建立後,fork一個協程在console_main_loop裡處理這個tcp連線的通訊互動
第6-13行,使用特定的print,資料不是輸出到螢幕上,而是透過socket。write傳送給控制檯
第24-28行,獲取控制檯發來的資料,然後呼叫docmd
第35-52行,解析出相應指令,執行完後,透過print傳送給控制檯
—— service/debug_console。lua
skynet。start(function()
local listen_socket = socket。listen (ip, port)
skynet。error(“Start debug console at ” 。。 ip 。。 “:” 。。 port)
socket。start(listen_socket , function(id, addr)
local function print(。。。)
local t = { 。。。 }
for k,v in ipairs(t) do
t[k] = tostring(v)
end
socket。write(id, table。concat(t,“\t”))
socket。write(id, “\n”)
end
socket。start(id)
skynet。fork(console_main_loop, id , print)
end)
end)
local function console_main_loop(stdin, print)
print(“Welcome to skynet console”)
skynet。error(stdin, “connected”)
local ok, err = pcall(function()
while true do
local cmdline = socket。readline(stdin, “\n”)
。。。
if cmdline ~= “” then
docmd(cmdline, print, stdin)
end
end
end)
。。。
end
local function docmd(cmdline, print, fd)
local split = split_cmdline(cmdline)
local command = split[1]
local cmd = COMMAND[command]
local ok, list
if cmd then
ok, list = pcall(cmd, table。unpack(split,2))
else
。。。
end
if ok then
。。。
print(list)
print(“
else
print(list)
print(“
end
end
比如,在控制檯輸入“list”,最終會呼叫到COMMAND。list(),獲取當前服務資訊,然後返回給控制檯。於是就有了上面截圖的資訊。
—— service/debug_console。lua
function COMMAND。list()
return skynet。call(“。launcher”, “lua”, “LIST”)
end
2。 clearcache更新方法
clearcache用於新建服務的熱更新,比如agent,對已有的服務不能熱更新。使用方法很簡單:在控制檯輸入“clearcache”即可,下面分析其原理:
每個snlua服務會啟動一個單獨的lua VM,對於同一份Lua檔案,N個服務就要載入N次到記憶體。skynet對此做了最佳化,每個Lua檔案只加載一次到記憶體,儲存Lua檔案-記憶體對映表,下一個服務載入的時候copy一份記憶體即可,提高了VM的啟動速度(省掉讀取Lua檔案和解析Lua語法的過程)。參考官方wiki
https://
github。com/cloudwu/skyn
et/wiki/CodeCache
第2-6行,全域性的Lua狀態機,以Lua檔名為key,記憶體指標為value,儲存在狀態機的登錄檔裡,位於棧上有效偽索引LUA_REGISTERYINDEX處。
第8行,修改了官方的luaL_loadfilex介面:
第11-15行,呼叫load從全域性狀態機的登錄檔裡獲取檔名對應的記憶體塊,呼叫lua_clonefunction複製一份後即可返回
第16-18行,第一次載入檔案到記憶體裡
第19-26行,呼叫save儲存檔名-記憶體塊的對映,如果有舊的記憶體塊,返回舊的,否則返回剛載入的記憶體塊
// 3rd/lua/lauxlib。c
struct codecache {
struct spinlock lock;
lua_State *L;
};
static struct codecache CC;
LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename,
const char *mode) {
。。。
const void * proto = load(filename);
if (proto) {
lua_clonefunction(L, proto);
return LUA_OK;
}
lua_State * eL = luaL_newstate();
int err = luaL_loadfilex_(eL, filename, mode);
proto = lua_topointer(eL, -1);
const void * oldv = save(filename, proto);
if (oldv) {
lua_close(eL);
lua_clonefunction(L, oldv);
} else {
lua_clonefunction(L, proto);
/* Never close it。 notice: memory leak */
}
return LUA_OK;
}
load介面,從全域性狀態機CC的登錄檔裡獲取指定檔案對應的記憶體塊(可能不存在)
// 3rd/lua/lauxlib。c
static const void *
load(const char *key) {
if (CC。L == NULL)
return NULL;
SPIN_LOCK(&CC)
lua_State *L = CC。L;
lua_pushstring(L, key);
lua_rawget(L, LUA_REGISTRYINDEX);
const void * result = lua_touserdata(L, -1);
lua_pop(L, 1);
SPIN_UNLOCK(&CC)
return result;
}
save介面,先獲取舊的記憶體塊(12-15行),如果有則直接返回,否則把新記憶體塊載入到登錄檔中(17-19行)
static const void *
save(const char *key, const void * proto) {
lua_State *L;
const void * result = NULL;
SPIN_LOCK(&CC)
if (CC。L == NULL) {
init();
L = CC。L;
} else {
L = CC。L;
lua_pushstring(L, key);
lua_pushvalue(L, -1);
lua_rawget(L, LUA_REGISTRYINDEX);
result = lua_touserdata(L, -1); /* stack: key oldvalue */
if (result == NULL) {
lua_pop(L,1);
lua_pushlightuserdata(L, (void *)proto);
lua_rawset(L, LUA_REGISTRYINDEX);
} else {
lua_pop(L,2);
}
}
SPIN_UNLOCK(&CC)
return result;
}
clearcache的原理就是刪除這個全域性的狀態機,這樣新服務就可以用最新的Lua檔案(load介面返回NULL),且不影響已有服務的執行。此時,新服務執行新的程式碼,舊服務執行舊的程式碼。
在控制檯輸入“clearcache”後,最終呼叫到c中的clearcache,刪除舊的全域性VM,然後新建一個(19-20行)。
—— service/debug_console。lua
function COMMAND。clearcache()
codecache。clear()
end
// 3rd/lua/lauxlib。c
static int
cache_clear(lua_State *L) {
(void)(L);
clearcache();
return 0;
}
static void
clearcache() {
if (CC。L == NULL)
return;
SPIN_LOCK(&CC)
lua_close(CC。L);
CC。L = luaL_newstate();
SPIN_UNLOCK(&CC)
}
3。 inject更新方法
inject譯為“注入”,即將新程式碼注入到已有的服務裡,讓服務執行新的程式碼,可以熱更已開啟的服務,使用方法簡單,在控制檯輸入“inject address xxx。lua”即可,難點在於lua程式碼的編寫,建議只做一些簡單的熱更。其實現原理是:給服務傳送訊息,讓其執行新程式碼,新程式碼修改已有的函式原型(包括upvalues),完成對函式的更新。
第10行,給指定服務傳送“DEBUG”型別訊息
第20行,最終呼叫inject介面注入程式碼修改函式原型(包括閉包)。注:只需修改服務的register_protocol介面以及訊息分發介面
—— service/debug。lua
function COMMAND。inject(address, filename)
address = adjust_address(address)
local f = io。open(filename, “rb”)
if not f then
return “Can‘t open ” 。。 filename
end
local source = f:read “*a”
f:close()
local ok, output = skynet。call(address, “debug”, “RUN”, source, filename)
if ok == false then
error(output)
end
return output
end
—— lualib/skynet/debug。lua
function dbgcmd。RUN(source, filename)
local inject = require “skynet。inject”
local ok, output = inject(skynet, source, filename , export。dispatch, skynet。register_protocol)
collectgarbage “collect”
skynet。ret(skynet。pack(ok, table。concat(output, “\n”)))
end
inject的處理過程:
第7-9行,獲取介面的函式原型(包括閉包),儲存在u裡
第11-21行,遍歷所有的訊息分發函式(每種訊息型別對應一個函式),透過getupvaluetable介面儲存函式原型(包括閉包)
第22-23行,執行新的Lua程式碼,透過env裡的_U,_P獲取原有的函式原型
—— lualib/skynet/inject。lua
return function(skynet, source, filename , 。。。)
local output = {}
local u = {}
local unique = {}
local funcs = { 。。。 }
for k, func in ipairs(funcs) do
getupvaluetable(u, func, unique)
end
local p = {}
local proto = u。proto
if proto then
for k,v in pairs(proto) do
local name, dispatch = v。name, v。dispatch
if name and dispatch and not p[name] then
local pp = {}
p[name] = pp
getupvaluetable(pp, dispatch, unique)
end
end
end
local env = setmetatable( { print = print , _U = u, _P = p}, { __index = _ENV })
local func, err = load(source, filename, “bt”, env)
。。。
return true, output
end
示例:比如啟動了一個test服務
—— test。lua 1 local skynet = require “skynet”
local CMD = {}
local function test(。。。)
print(。。。)
skynet。ret(skynet。pack(“OK”))
end
function CMD。ping(msg)
test(msg)
end
skynet。dispatch(“lua”, function(session, source, cmd, 。。。)
local f = CMD[cmd]
if f then
f(。。。)
end
end)
skynet。start(function()
end)
在控制檯輸入“inject address inject_test。lua”熱更test服務,
第23行,透過全域性環境變數_P獲取lua型別訊息分發函數里的介面CMD
第24行,獲取CMD。ping介面的所有閉包
第25行,得到test的函式原型
第27-30行,更新介面,完成熱更。
—— inject_test。lua
print(“hotfix begin”)
if not _P then
print(“hotfix faild, _P not define”)
return
end
local function get_upvalues(f)
local u = {}
if not f then return u end
local i = 1
while true do
local name, value = debug。getupvalue(f, i)
if name == nil then
return u
end
u[name] = value
i = i + 1
end
end
local CMD = _P。lua。CMD
local upvalues = get_upvalues(CMD。ping)
local test = upvalues。test
CMD。ping = function(msg)
local postfix = “aaa”
test(msg 。。 postfix)
end
print(“hotfix end”)
本篇文章就寫到這,在2021年1月13/14號我會開一個
遊戲開發核心技術點 skynet訓練營
,也就是兩個禮拜之後,現在已經開放報名,對遊戲開發感興趣的諸位同好可以訂閱一下,
訓練營內容大概如下:
1。 多核併發程式設計
2。 訊息佇列,執行緒池
3。 actor訊息排程
4。 網路模組實現
5。 時間輪定時器實現
6。 lua/c介面程式設計
7。 skynet程式設計精要
8。 demo演示actor程式設計思維
期待與諸位同好共襄技術盛舉
憑藉報名截圖可以進群973961276領取上一期skynet訓練營的錄播以及這期的預習資料哦!