您當前的位置:首頁 > 遊戲

skynet原始碼分析之熱更新 lua程式碼,兩個關鍵字搞定

作者:由 linux分享猿 發表于 遊戲時間:2020-12-31

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命令就可以進入控制檯,如圖。

skynet原始碼分析之熱更新 lua程式碼,兩個關鍵字搞定

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訓練營的錄播以及這期的預習資料哦!

標簽: lua  skynet  local  CC  print