CodeCache 应该算是 skynet 的一个特色功能了,是用来优化不同 LuaState 加载同一份代码的速度。因为 skynet 大量启动了 LuaState 来作为 Acotr 使用,所以才在重复加载代码上需要这样的优化。本篇会尝试分析 CodeCache 的实现。
作用
首先要了解 CodeCache 解决的是什么问题。简单来说,是在文件第一次加载的时候,将其从磁盘中读取出来,解析完毕以后,把函数原型缓存在一个独立的 LuaState 中。之后其它 LuaState 有加载需求时首先会尝试从缓存中加载,并且函数原型 Proto 可以直接共享缓存中的数据。
缓存的加入可以达到减少文件 IO,减少代码解析步骤,减少函数原型的内存占用的目的。云风大佬有一篇博客很详细的写了构思过程和要达到的目的。
实现
结构
用来从磁盘加载代码并且缓存加载结果的结构是 codecache,它的结构很简单,有一个 lua_State 用来加载和保存结果,还有一个自旋锁,因为加载很可能是并行的,所以修改的时候需要加锁。
|
|
保存
Lua 加载文件的最终入口是 luaL_loadfilex,skynet 特供版的 Lua 就是通过修改了这个函数来实现的 CodeCache 功能。
为了让加载的过程实现线程安全,每次需要加载的时候,skynet 都会创建一个新的 LuaState 进行文件加载。加载完毕以后的 Lua 函数原型 Proto 通过 lua_topointer 拿到地址,并且通过函数 save 把它的指针保存在 CC.L 里面。这个为了加载文件创建出的临时 LuaState 并不会被关闭,在整个进程的生命周期中都存在。
save 会首先检查 CC 有没有被初始化,如果没有的话会调用 init 来初始化 CC 中的 L 变量。保存的时候会用文件名作为 key 来保存对应的 proto 变量的指针,存在了 CC.L 的 LUA_REGISTRYINDEX 表中。
加载
修改以后的 luaL_loadfilex 会在一开始的时候首先执行 load 尝试从 CC.L 中加载出之前保存过的文件名对应的 proto 变量。
load 的执行过程比较简单,先锁住 CC 的锁 lock,然后在 LUA_REGISTRYINDEX 表中按文件名尝试读取 proto 结构体。
如果读取到了,则可以通过 lua_clonefunction 在源 LuaState 中根据已有的 proto 创建一个 closure 出来。在新创建的 LClosure 结构体变量中,共享的部分就是结构体中 proto 的部分,这部分会直接指向在缓存 LuaState 中读取到的指针,从而可以节约掉这部分数据本来需要占用的内存。
共享模式
CodeCache 一共有三种共享模式,分别是 OFF/EXIST/ON 模式。共享模式是每个 LuaState 独立的,可以根据本服务的特性来设置。
OFF 模式会关闭共享,不管什么情况,都自己加载自己使用,既不复用之前的缓存,也不新增缓存。EXIST 模式只会加载已经存在的缓存,如果不存在,则自己加载自己使用,不会新增缓存。ON 模式是默认模式,会先尝试加载之前的缓存,如果不存在缓存的,会从文件中读取,并且新增缓存。
可以通过 Lua 层的接口 codecache.mode 设置当前 LuaState 的共享模式。C 层接口cache_level 可以用来获取 LuaState 的共享模式。
清空缓存
CodeCache 加载过的缓存会在本进程的生命时间里一直存在。不过 skynet 提供了一个可以清空所有缓存的接口,一般用在特殊场景的热更新操作中。
Lua 层的接口 codecache.clear 可以清空当前的所有缓存,同时在 debug 后台里也有一个命令 clearcache 支持调用 codecache.clear 清空缓存。
清空缓存的实现很简单,因为缓存都在 CC.L 里面放着,所以锁上 CC 的锁以后,直接调用 lua_close 关闭掉 CC.L,之后再给 CC.L 创建一个新的 LuaState 即可完成清空。
可以看到清空缓存其实清掉的只是函数原型缓存的指针而已,并没有释放掉原来的缓存,只是当下一次加载一个文件的时候,会重新生成缓存而已,之前的服务还是引用的旧的函数原型,既不会更新也不会失效。