skynet源码分析之热更新(转)
skynet有两种方法支持热更新lua代码:clearcache和inject,在介绍skynet热更新机制之前,先介绍skynet控制台,参考官方wiki https://github.com/cloudwu/skynet/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发送给控制台
1 -- service/debug_console.lua
2 skynet.start(function()
3 local listen_socket = socket.listen (ip, port)
4 skynet.error("Start debug console at " .. ip .. ":" .. port)
5 socket.start(listen_socket , function(id, addr)
6 local function print(...)
7 local t = { ... }
8 for k,v in ipairs(t) do
9 t[k] = tostring(v)
10 end
11 socket.write(id, table.concat(t,"\t"))
12 socket.write(id, "\n")
13 end
14 socket.start(id)
15 skynet.fork(console_main_loop, id , print)
16 end)
17 end)
18
19 local function console_main_loop(stdin, print)
20 print("Welcome to skynet console")
21 skynet.error(stdin, "connected")
22 local ok, err = pcall(function()
23 while true do
24 local cmdline = socket.readline(stdin, "\n")
25 ...
26 if cmdline ~= "" then
27 docmd(cmdline, print, stdin)
28 end
29 end
30 end)
31 ...
32 end
33
34 local function docmd(cmdline, print, fd)
35 local split = split_cmdline(cmdline)
36 local command = split[1]
37 local cmd = COMMAND[command]
38 local ok, list
39 if cmd then
40 ok, list = pcall(cmd, table.unpack(split,2))
41 else
42 ...
43 end
44
45 if ok then
46 ...
47 print(list)
48 print("<CMD OK>")
49 else
50 print(list)
51 print("<CMD Error>")
52 end
53 end
比如,在控制台输入"list",最终会调用到COMMAND.list(),获取当前服务信息,然后返回给控制台。于是就有了上面截图的信息。
1 -- service/debug_console.lua
2 function COMMAND.list()
3 return skynet.call(".launcher", "lua", "LIST")
4 end
2. clearcache更新方法
clearcache用于新建服务的热更新,比如agent,对已有的服务不能热更新。使用方法很简单:在控制台输入"clearcache"即可,下面分析其原理:
每个snlua服务会启动一个单独的lua VM,对于同一份Lua文件,N个服务就要加载N次到内存。skynet对此做了优化,每个Lua文件只加载一次到内存,保存Lua文件-内存映射表,下一个服务加载的时候copy一份内存即可,提高了VM的启动速度(省掉读取Lua文件和解析Lua语法的过程)。参考官方wiki https://github.com/cloudwu/skynet/wiki/CodeCache
第2-6行,全局的Lua状态机,以Lua文件名为key,内存指针为value,保存在状态机的注册表里,位于栈上有效伪索引LUA_REGISTERYINDEX处。
第8行,修改了官方的luaL_loadfilex接口:
第11-15行,调用load从全局状态机的注册表里获取文件名对应的内存块,调用lua_clonefunction拷贝一份后即可返回
第16-18行,第一次加载文件到内存里
第19-26行,调用save保存文件名-内存块的映射,如果有旧的内存块,返回旧的,否则返回刚加载的内存块
1 // 3rd/lua/lauxlib.c
2 struct codecache {
3 struct spinlock lock;
4 lua_State *L;
5 };
6 static struct codecache CC;
7
8 LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename,
9 const char *mode) {
10 ...
11 const void * proto = load(filename);
12 if (proto) {
13 lua_clonefunction(L, proto);
14 return LUA_OK;
15 }
16 lua_State * eL = luaL_newstate();
17 int err = luaL_loadfilex_(eL, filename, mode);
18 proto = lua_topointer(eL, -1);
19 const void * oldv = save(filename, proto);
20 if (oldv) {
21 lua_close(eL);
22 lua_clonefunction(L, oldv);
23 } else {
24 lua_clonefunction(L, proto);
25 /* Never close it. notice: memory leak */
26 }
27
28 return LUA_OK;
29 }
load接口,从全局状态机CC的注册表里获取指定文件对应的内存块(可能不存在)
1 // 3rd/lua/lauxlib.c
2 static const void *
3 load(const char *key) {
4 if (CC.L == NULL)
5 return NULL;
6 SPIN_LOCK(&CC)
7 lua_State *L = CC.L;
8 lua_pushstring(L, key);
9 lua_rawget(L, LUA_REGISTRYINDEX);
10 const void * result = lua_touserdata(L, -1);
11 lua_pop(L, 1);
12 SPIN_UNLOCK(&CC)
13
14 return result;
15 }
save接口,先获取旧的内存块(12-15行),如果有则直接返回,否则把新内存块加载到注册表中(17-19行)
1 static const void *
2 save(const char *key, const void * proto) {
3 lua_State *L;
4 const void * result = NULL;
5
6 SPIN_LOCK(&CC)
7 if (CC.L == NULL) {
8 init();
9 L = CC.L;
10 } else {
11 L = CC.L;
12 lua_pushstring(L, key);
13 lua_pushvalue(L, -1);
14 lua_rawget(L, LUA_REGISTRYINDEX);
15 result = lua_touserdata(L, -1); /* stack: key oldvalue */
16 if (result == NULL) {
17 lua_pop(L,1);
18 lua_pushlightuserdata(L, (void *)proto);
19 lua_rawset(L, LUA_REGISTRYINDEX);
20 } else {
21 lua_pop(L,2);
22 }
23 }
24 SPIN_UNLOCK(&CC)
25 return result;
26 }
clearcache的原理就是删除这个全局的状态机,这样新服务就可以用最新的Lua文件(load接口返回NULL),且不影响已有服务的运行。此时,新服务运行新的代码,旧服务运行旧的代码。
在控制台输入"clearcache"后,最终调用到c中的clearcache,删除旧的全局VM,然后新建一个(19-20行)。
1 -- service/debug_console.lua
2 function COMMAND.clearcache()
3 codecache.clear()
4 end
5
6 // 3rd/lua/lauxlib.c
7 static int
8 cache_clear(lua_State *L) {
9 (void)(L);
10 clearcache();
11 return 0;
12 }
13
14 static void
15 clearcache() {
16 if (CC.L == NULL)
17 return;
18 SPIN_LOCK(&CC)
19 lua_close(CC.L);
20 CC.L = luaL_newstate();
21 SPIN_UNLOCK(&CC)
22 }
3. inject更新方法
inject译为“注入”,即将新代码注入到已有的服务里,让服务执行新的代码,可以热更已开启的服务,使用方法简单,在控制台输入"inject address xxx.lua"即可,难点在于lua代码的编写,建议只做一些简单的热更。其实现原理是:给服务发送消息,让其执行新代码,新代码修改已有的函数原型(包括upvalues),完成对函数的更新。
第10行,给指定服务发送"DEBUG"类型消息
第20行,最终调用inject接口注入代码修改函数原型(包括闭包)。注:只需修改服务的register_protocol接口以及消息分发接口
1 -- service/debug.lua 2 function COMMAND.inject(address, filename) 3 address = adjust_address(address) 4 local f = io.open(filename, "rb") 5 if not f then 6 return "Can't open " .. filename 7 end 8 local source = f:read "*a" 9 f:close() 10 local ok, output = skynet.call(address, "debug", "RUN", source, filename) 11 if ok == false then 12 error(output) 13 end 14 return output 15 end 16 17 -- lualib/skynet/debug.lua 18 function dbgcmd.RUN(source, filename) 19 local inject = require "skynet.inject" 20 local ok, output = inject(skynet, source, filename , export.dispatch, skynet.register_protocol) 21 collectgarbage "collect" 22 skynet.ret(skynet.pack(ok, table.concat(output, "\n"))) 23 end
inject的处理过程:
第7-9行,获取接口的函数原型(包括闭包),保存在u里
第11-21行,遍历所有的消息分发函数(每种消息类型对应一个函数),通过getupvaluetable接口保存函数原型(包括闭包)
第22-23行,执行新的Lua代码,通过env里的_U,_P获取原有的函数原型
1 -- lualib/skynet/inject.lua
2 return function(skynet, source, filename , ...)
3 local output = {}
4 local u = {}
5 local unique = {}
6 local funcs = { ... }
7 for k, func in ipairs(funcs) do
8 getupvaluetable(u, func, unique)
9 end
10 local p = {}
11 local proto = u.proto
12 if proto then
13 for k,v in pairs(proto) do
14 local name, dispatch = v.name, v.dispatch
15 if name and dispatch and not p[name] then
16 local pp = {}
17 p[name] = pp
18 getupvaluetable(pp, dispatch, unique)
19 end
20 end
21 end
22 local env = setmetatable( { print = print , _U = u, _P = p}, { __index = _ENV })
23 local func, err = load(source, filename, "bt", env)
24 ...
25
26 return true, output
27 end
示例:比如启动了一个test服务
-- test.lua
1 local skynet = require "skynet" 2 3 local CMD = {} 4 5 local function test(...) 6 print(...) 7 skynet.ret(skynet.pack("OK")) 8 end 9 10 function CMD.ping(msg) 11 test(msg) 12 end 13 14 skynet.dispatch("lua", function(session, source, cmd, ...) 15 local f = CMD[cmd] 16 if f then 17 f(...) 18 end 19 end) 20 21 skynet.start(function() 22 end)
在控制台输入"inject address inject_test.lua"热更test服务,
第23行,通过全局环境变量_P获取lua类型消息分发函数里的接口CMD
第24行,获取CMD.ping接口的所有闭包
第25行,得到test的函数原型
第27-30行,更新接口,完成热更。
1 -- inject_test.lua
2 print("hotfix begin")
3
4 if not _P then
5 print("hotfix faild, _P not define")
6 return
7 end
8
9 local function get_upvalues(f)
10 local u = {}
11 if not f then return u end
12 local i = 1
13 while true do
14 local name, value = debug.getupvalue(f, i)
15 if name == nil then
16 return u
17 end
18 u[name] = value
19 i = i + 1
20 end
21 end
22
23 local CMD = _P.lua.CMD
24 local upvalues = get_upvalues(CMD.ping)
25 local test = upvalues.test
26
27 CMD.ping = function(msg)
28 local postfix = "aaa"
29 test(msg .. postfix)
30 end
31
32 print("hotfix end")

浙公网安备 33010602011771号