共享 lua state 中的数据

今天和倩女幽魂的同事讨论一个问题:他们的游戏 client 中,有大量策划填写的表格直接导入 lua state 中的大量数据。大约有 100M 以上。这样,如果玩家在一台机器上启动多个 client ,就会占用大量的内存。

而这些数据,一旦加载进 lua ,就不会再修改,且每个 client 中数据都是一致的,这是一种浪费。

问题是:如何利用进程间的数据共享,在多开 client 时节省这些空间。(同时也可以加快开第二个 client 的启动速度)

如果数据不存在于 lua state 中,而直接用 C 访问。可以用简单的共享内存的方式解决。关于 Windows 下共享内存的方法,我曾经写过一篇 blog 介绍

如果是非 windows 系统,这个问题解决起来也很容易。

在 lua 加载完所有数据后,做一次 fork ,让所有多开的 client 都是一个子进程即可。


一开始,我们讨论了设置多个 lua state 的方案。让策划的数据表放在一个独立的 state 里。访问这些数据用跨 lua state 的方式进行。

不过这个方案实现起来比较麻烦,而且性能很低。如果想透明的访问数据 state ,需要做大量的 metatable 。甚至并不能节省内存空间。

最终可行的方案是这样的。

使用一个自定义的内存管理器。第一份 client 启动后,

  1. 初始化 lua state ,不初始化任何用到的库。
  2. 通知内存管理器,切换一个 heap ,并加载所有策划表格。以及用到的 C 库。(因为 client 相同,这些库中函数指针地址也相同)
  3. 做一次完整的 gc 。再次通知自定义内存管理器切换一个 heap 。
  4. 此时应该有 3 个 heap 。一个保存了 lua state 的结构,一个保存了策划表的数据,一个是空的,用来存放以后 lua state 中的所有数据。把第一个 heap 复制一份共享,(并提供原始的地址信息)。第二个 heap 直接共享,如有可能,把这个 heap 的页设置成只读。
  5. 以后的内存管理全部在新的第三个 heap 中进行。并在 free 操作中对企图在老 heap 上的操作做 assert 。

第二份以后的 client 启动时,如果发现有共享的 heap ,把第一份 heap 取到,并复制到指定的地址空间。以只读方式映射第二个 heap 。并在指定位置创建第三个 heap (和主 client 的三个 heap 的地址保持一致)

btw. 如果实现上允许,可以让内存管理器讲第一和第三个 heap 合并成一个。中间只是一个切换的过程。那么,总共只需要两个 heap 即可。

在 client 进程退出时,应当按如下次序:

  1. 取消内存管理器中的 assert
  2. 所有针对第二份 heap (存放策划表格的那个)上数据的 free 操作全部忽略掉。
  3. 关闭 lua state

或者,直接选择不关闭 lua state


这样做之所以可行,基于以下几点:

  1. 我们可以利用自己的内存管理器,让每个 client 在初始化后,内存布局完全相同。所有的策划表格存放在一致的内存地址上。(相同地址的 heap page ),lua state 的指针也完全相同,结构体内的指针也正确的指向相同的位置(在独立的 heap page 中)。
  2. lua 的 C 库中函数指针在所有 client 中的地址一定相同,所以是一致的,可以共享。
  3. lua state 的结构在初始化完毕后是一致的,虽然以后会被修改,但我们是以复制的方式存在于每个 client ,所以相互不会受影响。