0%

Lua虚拟机函数原型共享的研究

针对在 Skynet 间数据的共享,有了 ShareTable,但是他有一个缺点,就是无法共享函数。因此,我们如何把相同的函数逻辑在不同的虚拟机服务间共享,需要好好的想想。关于 ShareTable 可以看这里 Skynet中服务间的数据共享方案-Sharetable

非常规方式?

在 Sharetable 服务的构造中,我看到了云风利用一个函数,来构造一个服务的方式。具体利用 string.dump 来将一个函数 dump 成二进制的格式,然后发送到其他服务去,进行执行,构造服务:请看这里的代码,本质上就是利用了 string.dump。那么,我们是不是也可以将我们需要共享的函数这样操作过去呢。在 Github 上提问了之后,作者给出了答复

sharetable 是一个 trick ,用来解决不变数据的共享时性能问题。它不应该变成同步数据的常规手段。

解决问题应该优先考虑常规手段,你这个问题用 rpc 把需要的数据传递过去同步就够了。

因为暂时放弃了这种想法。

缓存?

另外,在云风的文章中看到:

在不同的 lua vm 间共享 Proto

共享 lua vm 间的小字符串

Lua 虚拟机间函数原型共享的改进

而在 Github 上的 Wiki 中有对这个特性的介绍

skynet 修改了 Lua 的官方实现(可选),加入了一个新特性,可以让多个 Lua VM 共享相同的函数原型1。当在同一个 skynet 进程中开启了大量 lua VM 时,这个特性可以节省不少内存,且提高了 VM 启动速度。

这个特性的使用,对一般用户来说是透明的。它改写了 lua 的辅助 API luaL_loadfilex ,所有直接或间接调用这个 api 都会受其影响。比如:loadfile 、require 等。它以文件名做 key ,一旦检索到之前有加载过相同文件名的 lua 文件,则从内存中找到之前的函数原型替代。注:Lua 函数是由函数原型以及 0 或多个 upvalue 绑定而成。

loadstring 不受其影响。所以,如果你需要多次加载一份 lua 文件,可以使用 io.open 打开文件,并使用 load 加载。

代码缓存采用只增加不删除的策略,也就是说,一旦你加载过一份脚本,那么到进程结束前,它占据的内存永远不会释放(也不会被加载多次)。在大多数情况下,这不会有问题。

skynet 留出了接口清理缓存,以做一些调试工作。接口模块叫做 skynet.codecache 。

这个问题能够实现的关键在于:

我们知道,lua 里的 function 是 first-class 类型的。lua 把函数称为 closure ,它其实是函数原型 proto 和绑定在上面的 upvalue 的复合体。对于 Lua 实现的函数,即使没有绑定 upvalue ,我们在语言层面看到的 function 依然是一个 closure ,只不过其 upvalue 数量为 0 罢了。

btw, 用 C 编写的 function 不同:不绑定 upvalue 的 C function 被称为 light C function ,可视为只有原型的函数。

如果函数的实现是一致的,那么函数原型就也是一致的。无论你的进程中开启了多少个 lua 虚拟机,它们只要跑着一样的代码,那么用到的函数原型也应该是一样的。只不过用 C 编写的函数原型可以在进程的代码段只存在一份,而 Lua 编写的函数原型由于种种原因必须逐个复制到独立的虚拟机数据空间中。

这里又区别了几个内容:

因为 Lua 每加载一个文件就会返回一个函数,因此 云风在此做了点修改,用这个加载文件返回的函数原型来判断是否进行缓存(共享)。

为了更好的利用这个特性,我在 skynet 中,改写了 luaL_loadfilex 。这个 patch 版的文件加载函数是线程安全的。它为每个文件名对应的函数(lua 中加载一个源文件,就生成一个函数)创建一份独立的 lua 虚拟机,并将生成好的函数原型指针记录下来。之后同名文件的加载就不再有文件 IO ,不必再次解析文件,直接用 lua_clonefunction 复制一份出来。

我们可以来看看这段代码:

LUALIB_API int luaL_loadfilex (lua_State *L, const char *filename,
const char *mode) {
int level = cache_level(L);
// 没有开启缓存,调用Lua本身的加载逻辑
if (level == CACHE_OFF) {
return luaL_loadfilex_(L, filename, mode);
}

// 找到原型,直接将原型复制到当前虚拟机
const void * proto = load_proto(filename);
if (proto) {
lua_clonefunction(L, proto);
return LUA_OK;
}
// 没找到原型,但只缓存已经存在的文件,那就重新加载
if (level == CACHE_EXIST) {
return luaL_loadfilex_(L, filename, mode);
}

// 没找到原型,开启了代码共享
lua_State * eL = luaL_newstate();
if (eL == NULL) {
lua_pushliteral(L, "New state failed");
return LUA_ERRMEM;
}

// 在一个新的虚拟机中加载文件
int err = luaL_loadfilex_(eL, filename, mode);
if (err != LUA_OK) {
size_t sz = 0;
const char * msg = lua_tolstring(eL, -1, &sz);
lua_pushlstring(L, msg, sz);
lua_close(eL);
return err;
}
lua_sharefunction(eL, -1);
proto = lua_topointer(eL, -1);
// 将函数原型存储在 CC 这个虚拟机中,如果原型已经存在,会直接返回
const void * oldv = save_proto(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;
}

事实上,我们利用这个特性,就可以做到共享代码文件了。但是,这个仅仅是解决了代码加载的问题,所以它叫 codecahe,而不叫 codeshare。

当缓存的文件中的代码,依赖于一些虚拟机内部状态的时候,就有可能并不能达到想要的结果,因此,尽量不要依赖一些虚拟机状态的值(如全局变量)。