lua
在 Lua 中,一共只有 9 种数据类型,分别为 nil 、boolean 、lightuserdata 、number 、string 、 table 、 function 、 userdata 和 thread 。 其中,只有?string table function thread 四种在 vm 中以引用方式共享,是需要被 GC 管理回收的对象。其它类型都以值形式存在。 但在 Lua 的实现中,还有两种类型的对象需要被 GC 管理。分别是 proto (可以看作未绑定 upvalue 的函数), upvalue (多个 upvalue 会引用同一个值)。 闭包(closure)。 GCObject定义中,gch用于垃圾回收;ts表示用于字符串表的类型;u表示userdata;cl表示闭合函数;h表示表;p表示函数;uv表示upvalue;th表示线程,每一个lua_State相当于一个线程; Lua 是以 union + type 的形式保存值。
在 skynet 这种应用中,同一个系统进程里很轻易的就会创建数千个 lua 虚拟机。lua 虚拟机本身的开销很小,在不加载任何库(包括基础库)时,仅几百字节。但是,实际应用时,还需要加载各种库。 在 lua 虚拟机中加载 C 语言编写的库,同一进程中只会存在一份 C 函数原型。但 lua 编写的库则需要在每个虚拟机中创建一份拷贝。当有几千个虚拟机运行着同一份脚本时,这个浪费是巨大的。 我们知道,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 编写的函数原型由于种种原因必须逐个复制到独立的虚拟机数据空间中。 这些限制有哪些呢?
函数原型包含了三类数据:字节码、常量表、调试信息(包括字节码对应的行号、函数名、局部变量名等等)。这些数据都是只读的,理论上是可以被共享的。 但是函数原型(proto) 也是 lua 的基础类型(但没有暴露到语言层),依然是被 Lua 虚拟机管理的 gcobject ,它需要参于垃圾收集的过程。Lua 在实现时并没有考虑将多个虚拟机共享数据。 上面的定义中,除了8种基本的数据类型之外,还包括未知类型和light userdata,light userdata表示仅仅在中保存了userdata的指针,占用的内存不归管。Value代表变量的具体值,b表示整形,n表示浮点型;gc表示可以用于垃圾回收的对象的指针;当gc取gch值时,p应该是对象的指针,否则有可能只想TValue本身。其中相关的定义如下: 如果我们需要共享,第一步就是要改变 proto 类型的生命期管理。不能再由单个 lua 虚拟机的 gc 扫描流程决定是否要释放一个不再被引用的 proto 。 一个完备的方案是对 proto 做一个线程安全的引用计数,但我们也可以简单粗暴的直接在内存中保留所有的 proto 对象,无论是否有人引用它。 保留所有用过的函数在内存中这种做法是广泛存在的,如果你对比看 C 层次的函数,即使 C 函数存在于动态库中,我们也不能轻易卸载动态库,这有让其它模块保留过动态库中函数指针变得无效。另外,由于调试信息的存在,引用计数的方案会对 lua 实现做相当大的改变。 第二步,我们需要考虑常量表。对于常量字符串,往往是不可以被多个 lua 虚拟机共享的。尤其是短字符串,lua 会对短字符串做唯一化 (string interning) 处理,同样的短字符串在同一个 lua 虚拟机中只有一份。不同的 lua 虚拟机中的短字符串一定会被判定为不同的。如果对常量表中的字符串也做共享处理,那么除了需要给 lua 实现增加一种字符串类型(不被 gc 管理的字符串)外,还会降低字符串处理速度(目前 lua 在做短字符串比较时,直接比较对象指针,可以达到 O(1) 的处理速度;而如果常量字符串在不同的虚拟机中的话,比较会变成 O(n) 的复杂度)。 第三步,每个 proto 对象中带有一个 closure cache 。绑定同样 upvalue 的 proto 生成的 closure 可以被复用。但如果 proto 是跨虚拟机的,这个 cache 就很难正常工作了。 第四步,调试信息中也有大量的字符串。考察一下 Lua 实现可以发现,Lua 的 api 仅将这些字符串用内部字符串对象储存参与 gc 管理,但并不会把这些字符串对象传递到别的地方。所有 api 都是返回这些字符串对象的 C string 指针的。 针对这些问题,我们可以开始对 lua 的实现做改造了。 我们可以将 proto 数据结构拆分成可共享和不可共享两部分。不可共享的有常量表和 cache ,其它都可以共享。不可共享部分继承原有的 proto 结构,再用一个指针指向共享部分即可。我们需要在共享的数据结构中保留一个它实际存在于的 lua 虚拟机的指针。只有这个虚拟机才有权利回收它所占的内存。而其它引用它的 lua 虚拟机在 gc 时,可以检查这个指针来决定是否要标记清除它。 lua 提供了一个 api? 这里引入了一个新 api 叫? 我给 lua 5.2.3 打好了 patch 支持这个特性?。并将它合并到 skynet 的主干上了。 为了更好的利用这个特性,我在 skynet 中,改写了? 为了 skynet 服务器可以热更新 lua 脚本,还增加了 clear cache 的方法(skynet.cache.clear),可以将 cache 重置。当然,之前加载过的代码其实是没有从内存中清理掉的,这一定程度上会带来一些内存泄露。但考虑到这个 patch 可以给系统节约的内存,不是过于频繁的热更新是可以接受的。 这个 patch 可以带来的好处:
但是,这个 patch 也增加了热更新的复杂度。需要主动清理 cache ,并考虑历史上的过期版本的代码占据内存不能回收的问题。如果不想在 skynet 中使用这个 patch ,可以在 makefile 中调整 lua 库的链接,指向官版的 lua 即可。 --------------------------------------------------------------------------- lua标准库 模块--------包库为Lua提供简易的加载及创建模块的方法,由require、module方法及package表组成 module(name[,. . .]) require(modname) 表----------table table.concat(table[,sep[,i[,j]]])功能:返回用Sep连接表中的字串 table。insert(table,[]) 算术------math 基本函数----- 1,assert(v[,message]) 2,collectgarbage(opt[,arg])垃圾收集器的通用接口,用于操作垃圾收集器 3,dofile(filename)打开并执行一个lua块 4,error(message[,level])终止正在执行的函数,并返回message的内容作为错误信息 5,_G全局环境表(全局变量) 6,getfenv(f)返回函数f的当前环境表 7,getmetatable(object)返回指定对象的元素((若object的元表.__metatable项有值,则返回object的元表.__metatable的值),当object没有元表时将返回nil) 字符串处理--------(string ?manipulation) 输入输出处理--------(io,file) 操作系统处理---------(operating system facilites) 协同程序处理--------(coroutine manipulation) (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |