« March 2014 | Main | May 2014 »

April 30, 2014

skynet 的新组播方案

最近在做 skynet 的 0.2 版。主要增加的新特性是重新设计的组播模块。

组播模块在 skynet 的开发过程中,以不同形式存在过。最终在 0.1 版发布前删除了。原因是我不希望把这个模块放在核心层中。

随着 skynet 的基础设施逐步完善,在上层提供一个组播方案变得容易的多。所以我计划在 0.2 版中重新提供这个模块。注:在 github 的仓库中,0.2 版的开发在 dev 分支中,只到 0.2 版发布才会合并到 master 分支。这部分开发中的特性的实现和 api 随时都可能改变。

目前,我打算提供 publish/subscribe 风格的 API 。组播消息通过 publish 接口发布出去,所有调用过 subscribe 接口的服务都可以收到消息。

在设计上,每个 skynet 节点都存在一个用于组播的专门服务。组播并非核心层模块,组播消息不是直接发布出去的,而需要通过组播服务进行。

在设计组播模块的结构时,我们需要分两种情况处理。对于在节点内部传播的消息,由于在同一进程内,所以会共享一块内存,用引用计数管理声明期。对于在网络中传播的消息,跨节点时需要复制一份消息内容,用于网络投递。

先来看较简单的内部消息的传播过程:

单个 skynet 节点中有一个唯一的 multicastd 的服务,在首次请求组播服务时会启动。

首先,必须有人请求创建一个 channel 。这个 channel 是一个 32bit 的数字,循环后可复用。低 8bit 必须是节点号,这样可以保证不同节点上创建出来的 channel 号是互不相同的。

multicastd 是用 lua 编写的,用了一张表记录 channel 号对应的订阅者。这里暂不考虑跨节点订阅的情况,这张表里全部记录的是本地服务地址。

multicast 模块的 api 全部在 lualib/multicast.lua 中以库形式提供。一共就是 new, delete, publish, subscribe, unsubscribe 几个。这些 api 最终都是把请求转给 multicastd 去处理。也就是说,发布组播消息,其实比普通的点对点消息传播路径要长。

所以值得注意的是:即使在同一服务中,先发送的组播消息未必比后发出的点对点消息要先抵达。但是,对 multicastd 发出 publish 请求,是一个有回应的 rpc call 。所以同一个 coroutine 中按次序做 publish 和 send 操作还是能保证时序的。这也是为什么 publish 方法要设计成会阻塞住当前 coroutine 运行的原因。

组播消息在提交到 multicastd 之前,在发起方就已经被打包成一个 C 结构指针。

struct mc_package {
    int reference;
    uint32_t size;
    void *data;
};

注意被打包的是指针而不是结构。这样才能做引用计数。消息内容也是用另一个指针间接引用的,这样方便消息打包。

由于在服务间传递的是一个指针,所以这条消息是禁止传播到进程之外的。这点由 multicast 库保证(用户不得直接向 multicastd 服务发送任何请求)。

multicastd 收到 publish 请求后,会统计本地订阅者的数量,给数据包加上准确的引用次数值,并将消息转发给所有订阅者。因为仅仅是转发一个指针,比转发消息体要廉价的多(这也是组播服务的存在意义)。

订阅和退订 channel 也是通过 multicastd 进行的。由于时序问题的存在,所以订阅和退订都被实现的有一定的容错行。重复订阅和重复退订(以及删除 channel 和退订的时序)都会被忽略。这里退订被实现为非阻塞的(不会打断发起退订方的 coroutine ,不必等待确认),是因为它需要在 gc 的流程中进行,而 gc 的执行上下文是不可控的。


如果组播只存在于进程内,那么以上都很容易实现。不可忽略的复杂性在于跨进程的多节点组播。

为了可以让多个节点间的组播可行。我为 skynet 增加了一个叫做 datacenter 的基础组件(当然这个组件在以后别的设施中也将用到)。datacenter 在 master 节点上启动了一个 lua 实现的树结构内存数据库。它对整个 skynet 网络都是可见的。它就像一个全局注册表一样,任何接入 skynet 网络的节点都可以读写它。

每个节点的 multicastd 启动后,都会把自己的地址注册到 datacenter 中,这样别的节点的 multicastd 都可以查询到兄弟的地址。

如果 multicastd 收到订阅请求后,它会先检查 channel 是不是在本地创建的。如果不是,除了要维护本地订阅这个 channel 的订阅者名单外,在本地第一次订阅这个 channel 的同时,要通知 channel 的所有者本节点要订阅这个 channel 。每个 channel 的管理者地址都可以在 datacenter 内查到。

收到远程订阅请求后,本地管理器仅记录这个 channel 被哪些节点订阅而不记录记录在每个远程节点上有具体哪些订阅者。当消息被组播时,对于有远程订阅者的 channel ,需要把 struct mc_package 的数据内容提取出来打包传输。这里的一个实现上的优化是,直接把消息走终端客户的组播通道。因为 multicastd 本身不会按常规用户那样订阅消息,所以数据格式可以不同(常规客户订阅的消息收到的是带引用计数的结构指针,而 multicastd 收到的就是消息本身)。

对于发布一个远程组播包(发布者和 channel 的创建位置不在同一个节点内),直接把包投递到 channel 所有地,看成是从那里发起的(但发送源地址不变)。


对于订阅者,它收到的组播消息是从专门的协议通道(PTYPE 为 2 )获取的。为了使用灵活,并没有规定协议的具体编码形式。需要订阅者自己注册 pack 和 unpack 以及 dispatch 函数。默认使用 lua 编码协议,但可以改写。

因为 channel 是一个 32bit 整数,而组播消息是不需要应答的,所以可以复用消息的 session 字段,这也算是一个小优化。


multicast 目前仅提供 lua 层面的 API 。虽然理论上是可以通过 C 层直接收发包,但意义不大。API 以对象形式提供,每个 channel 都是一个 lua 对象。如果创建对象时没有填具体的 channel 编号,就会调用本地的 multicastd 创建出一个新的 channel 。对象在 gc 时不会销毁 channel (因为这个 channel 号有可能被传递到别的服务中继续使用),需要显式的调用 delete 方法销毁。但 channel gc 的时候,如果曾有订阅,会自动退订。

已知的设计缺陷:

由于 multicast 不在核心层实现,所以当一条组播消息被推送到目标消息队列中,在处理消息之前,服务退出。是没有任何渠道去减消息的引用。这在某些边界情况下会导致一定的内存泄露。

如果要解决这个泄露问题,必须在发送消息时记录下消息发给了谁(因为消息订阅者可能发生变化)。然后再想其它途径去释放它。做到这一点,除了结构上增加复杂度的成本外,运行成本的增加可能也会抵消掉组播的好处(减少数据复制带来的成本)。

所以,暂时不考虑完全解决这个问题。

April 23, 2014

简悦 QC 招聘

由于我们公司 简悦 在移动平台游戏业务的发展需要,现在想招聘 QC 一名。

工作职责:

负责网络游戏的测试,包括:

  • 协助策划保证游戏设定的合理性;
  • 制定测试计划和方案,完成测试用例;
  • 执行测试过程,并跟进缺陷;
  • 推进开发和测试流程的的持续改进;
  • 配合项目组解决工作中遇到的问题,并能对项目组提出合理化建议。

工作要求:

  1. 理工科专业,本科以上学历(计算机相关专业尤佳);
  2. 2年以上游戏测试经验(具备手游测试经验者优先);
  3. 熟悉项目开发流程、测试流程,熟练掌握各种测试技巧,熟悉测试用例及测试文档,测试报告的编写;
  4. 具备一定的编程能力,有一定的自动测试工具的研发或二次开发经验;
  5. 具备较强的逻辑分析及学习能力,有良好的团队合作意识,有强烈的责任心和积极主动的工作态度,较强的沟通能力和表达能力;
  6. 热爱网游,具备丰富的游戏经验,对游戏有自己的思考和见解

如果具备以下经验更佳:

  1. 熟悉iPhone或Android手机平台的测试工作,对移动终端游戏玩法或测试方法有一定经验
  2. 对手机客户端的功能,性能,中断等测试内容有一定经验
  3. 有一定的自动化测试脚本编写经验, 熟悉服务器端性能测试方法

工作地点在广州

待遇方面可以通过 hr@ejoy.com 详谈。

Skynet 发布第一个正式版

距离 skynet 开源项目的公布 已经有 20 月+ 了,如果从闭源阶段算起,已经超过了 30 个月。在我们公司内部有五个项目使用 skynet 开发,据有限的了解,在我们公司之外,至少有两个正式项目使用了相当长的时间。是时候发布一个正式版了。

今天 skynet 的第一个正式版本 v0.1.0 发布了。

在发布之前,我花了几天时间帮助公司内部的项目合并代码。最后全部统一使用这个版本。而在此之前,每个项目都是由一个负责人 fork 出一份,根据项目需要自己修改。merge 工作总是做的痛苦不堪。

通过这次发布,希望未来可以统一维护基础框架部分。

我推荐的项目组织形式是,把 skynet 作为一个 submodule 引用,不要自行修改任何其中的代码。在项目目录中编写自己的 Makefile 调用 skynet 的 Makefile 编译出 skynet 的二进制文件。如果需要修改编译目标地址,可以在 make 调用时传入。skynet 的编译目标地址都是用宏指定的,覆盖比较容易。

如果要修改 skynet 自带的模块,可以在自己的项目中编写同名的模块,通过项目的 config 文件指定模块搜索次序来优先加载。或者复制一份换一个名字。目前 skynet 自带的模块极少,不太会对项目具体需求造成影响。


为了这次 release ,我删除了之前在 skynet 中遗留的很多目前我们项目已经不再使用(或不会对现有项目造成影响)的特性。比如组播模块、性能分析模块等。这样可以使 skynet 核心更加精简。

增加了一组新的 api snax 方便快速开发。

性能分析模块重新独立实现了,这次以一个 lua module (profile) 的形式注入 lua 原生的 coroutine 库。在 snax 里做了演示。

接下来,skynet github 的 master 分支在 v0.2 版发布之前,不再增加新特性。只会定期合并 bugfix ,这些小版本将被命名为 v0.1.x 。新的版本将在 dev 分支上进行。

暂时的开发计划是重新以上层模块的形式重新支持广播和组播特性。


关于文档、例子和 demo

由于精力有限,虽然一直有这个计划,但是未能实施。目前只能看我的 blog 来理清 skynet 的脉络了。我也会逐步把一些文档性质的 blog 整理到 github 的 wiki 上。

同时、使用 skynet 的人越来越多,我相信社区的力量会越来越大。

如果有问题,可以在 github 上提 Issue ,更欢迎提交 Pull-request 。skynet 项目发展到现在,已经合并过来至于 14 个不同同学的 PR 了,相信以后会更多。

最后,来个有中国特色的:

有热心的同学为 skynet 建立了 qq 交流群:340504014 。群内已经有许多有 skynet 使用经验的同学热心解答问题。当然,我也为此重新安装了 qq 。

April 17, 2014

skynet 的 snax 框架及热更新方案

skynet 目前的 api 提供的偏底层,由于一些历史原因,某些 api 的设计也比较奇怪。(比如 skynet.ret 是不对返回数据打包的)

我想针对一些最常见的应用环境重新给出一套更简单的 api ,如果按固定模式来编写 skynet 的内部服务会简单的多。

这就是这两天实现的 snax 模块。今天我已经将其提交到 github 的 snax 分支上,如果没有明显的问题,将合并入主干。

snax 仅解决一个简单的需求:编写一个 skynet 内部服务,处理发送给它的消息。snax 并不会取代 skynet 原有的 api ,只是方便实现这类简单需求而已。

通常,我们需要编写一个 skynet 服务,它可以响应消息。用 skynet api 实现通常是这样的:

skynet.start(function()
  skynet.dispatch("lua", function(session, address, ...)
    dispatch(...)
  end)

我们需要先用 skynet.start 注册一个 skynet 服务的启动函数。然后确定这个服务用什么协议来解析消息。一般会选择 lua 协议,因为这种协议为 lua 设计,可以最高效的把 lua 原生数据序列化。

然后,我们会规定消息的第一个数据是字符串,表示消息类型。然后我们可以利用这个类型做不同的响应。

local command = {}

function command.foobar(...)
end

local function dispatch(cmd, ...)
  command[cmd](...)
end

skynet 内部消息可以是单向推送的,也可以是发起一个请求,然后等待回应。发送消息的人必须知晓这一点,分别用 skynet.send 或 skynet.call 来发送消息。

在服务实现的代码中,我们使用 skynet.ret 来回应请求。由于某些历史原因,这个 api 并不负责数据打包的工作。如果采用 lua 协议, 通常要写成 skynet.ret(skynet.pack(...)) 的形式。

snax 框架对这套流程做了一些简化, 把通用的代码放在了一个简单的框架里。编写一个服务就变成了这样:

-- foobar service
local i = 0
local hello = "hello"

function response.echo(data)
    return data, i
end

function subscribe.touch()
    i = i + 1
end

function init( ... )
    print ("service start:", ...)
end

function exit(...)
    print ("service exit:", ...)
end

这是一个简单的封装,只是把上面提到的 skynet.start skynet.dispatch 等调用藏起来了。init() 是启动会执行的代码,exit() 是退出要执行的代码。

response. 开头的函数表示返回值会调用 skynet.ret 返回,而 subscribe 函数则会扔掉返回值。

由于这段代码必须工作在 snax 框架下,所以,启动服务以及调用这个服务得使用 snax 的 api 。

  local p = snax.newservice ("foobar", "hello world")
  print(p.req.echo("foobar"))
  print(p.pub.touch())
  snax.exit(p)

snax.newservice 会在 config 文件中配置的 snax 路径上找到 "foobar" 模块并加载。由于 skynet 的核心是 C 编写的,所以服务的启动参数只能是一个字符串。为了更加灵活,snax 在实现时规定了启动服务的消息,newserice 的启动参数是用 lua 协议打包,通过启动消息传递的,也就不受限制了。

newservice 会产生一个对象方便使用。如果你从别的渠道拿到一个服务地址,那么可以用 snax.bind(handle, type) 生成同样的对象。这里的 type 是必须的,它用来分析协议组。

实际上 snax 的发送方和接收方都可以通过服务实现的源文件分析出这个服务支持哪些命令。这样可以正确感知发送的一条消息是否会收到回应。同时,也可以把命令类型从字符串转换为一个内部数字编号(减少数据传输量,以及提高编码和解码的性能)。

这里保留了 req 和 pub 分别对应 response 和 subscribe 。还是需要让调用者心里明确自己在做什么。因为在 skynet 中,发送一条消息是不会使 coroutine 挂起的,而等待回应则有可能中间插入其它 coroutine 的运行而导致状态改变。


热更新

早些年我们的项目是这样做热更新的:把修改过的 lua 文件重新加载一次,把消息处理函数换掉。但这种方法并非完备的。如果你理解 lua 如何工作,就能看出里面的坑来。

当服务本身有状态时,新旧版本的代码如何交接数据就是个难题。几乎无法给出一个使用者完全不用关心的方案。

snax 在给出热更新方法时,遵循了简单原则。我们把热更新作为一个产品上线后,紧急修复 bug 的非常手段。而并不打算用它做服务器的不停机升级。准确说,这是一个热修复接口。

基于这个定位,我并不打算让使用者更新他的源文件,命令 snax 重新加载它们。而是给了一个提交 patch 的接口,当发现有 bug 需要修复时,仅仅提交需要修改的那几个函数,并提供一个方法可以读写线上运行中服务的内部数据(通常是一些 local 变量)。

得益于 lua 5.2 提供的 upvalueid 和 upvaluejoin 一对新 api ,这些操作很容易实现。我们只需要把 patch 代码以字符串的形式传递进去。snax 独立编译这段 patch ,然后从正在运行中的环境里提取出 patch 可能涉及的 local 变量,用 upvaluejoin 绑定到 patch 中,最后将 patch 里提供的新函数替换掉前一个版本即可。

比如想换掉 subscribe.touch 的实现,只用

snax.hotfix(p, [[
local i   -- 必须有这个声明,用于标示引用的局部变量。

function subscribe.touch()
    i = i + 2
end
]])

这样就可以了。

如果在 patch 中实现一个hotfix() 函数,还可以在热更新的时候做一些额外的事情,或读取返回服务的内部状态。例如:

snax.hotfix(p, [[
local i

function hotfix(t)
  local tmp = i
  i = t
  return tmp
end
]],100)

这个 patch 可以把 i 设置为新的值,并返回之前的数值。


另外,这次 snax 集成了之前 mqueue 的特性。如果你需要让一个服务按次序处理消息(一个消息由于 io 操作挂起,也不会处理新到的请求),那么让 init 函数返回 "queue" 就可以开启这个模式。

April 15, 2014

对 skynet 的 gate 服务的重构

由于历史原因,skynet 中的 gate 服务最早是用 C 写的独立服务。后来 skynet 将 socket 的管理模块加入核心后又经历过一次重构,用后来增加的 socket api 重新编写了一遍。

目前,skynet 的各个基础设施逐步完善,并确定了以 lua 开发为主的基调,所以是时候用 lua 重写这个服务了。

如果是少量的连接且不关心性能的话,直接用 skynet 的 lua socket 库即可。这里有一个例子

gate 定位于高效管理大量的外部 tcp 长连接。它不是 skynet 的核心组件,但对于网络游戏业务,必不可少。

skynet 的内核已经集成了 epoll/kqueue 可以高效处理 socket 事件。但是离处理长连接还差一步,那就是对数据流的分包。

skynet 目前的 socket api ,采用回调的方式接收 socket 数据流。在一条 tcp 连接上,无论每次收到多少字节,都会使用 PTYPE_SOCKET 通道转发给绑定这条 tcp 连接的 skynet 服务。它是不关心数据流是如何组织的。

通常,我们会用 长度+数据内容 的形式对 TCP 数据流进行切分。这是在网络游戏中最常见的协议设计方案。

当然,也可以按 http 或其它流行网络协议(pop3 imap 等)那样,以回车换行符以及文本数字的方式来分包,但除了增加切包算法的复杂度外,没有太多好处。

目前 skynet 的 gate 服务约定的协议是,2 字节( 大头编码)表示一个 64K 字节内的数据包,然后接下来就是这个长度的字节数。我曾经考虑过使用 4 字节或 google proto buffer 用的 varint ,但最后都放弃了。

考虑到实现的便捷,通常收到长度后,会在内存考虑指定长度的 buffer 等待后续的数据输入。这样,如果有大量攻击者发送超长包头,就会让服务器内存瞬间消进。所以,这种协议只要实现的不小心,很容易变成攻击弱点。

注:skynet 最早期的 gate 实现反而没有这个问题。因为它使用了单一的 ringbuffer ,只发送包头却不发送数据的连接会在 ringbuffer 回绕的时候被踢掉。

游戏服务器如果只使用一条 TCP 长连接的情况下,单个数据包过大(> 64K),也是不合适的。大包会阻塞应用逻辑(收取和发送它们都需要很长的时间),如果在应用层有心跳控制的话,也很容易造成心跳超时。所以一般在应用层对大数据包再做上层协议的切割处理。在本文的最后,会对此做一些讨论。


gate 的职责应该是保持大量 TCP 长连接,按协议切分。对于不完整的数据包,按连接分别置入独立的缓冲区中。对于每个完整的包,转发给需要的服务。

这里的工作分两部分,分包和转发。

分包以及对不完整的包做缓存是个细活,交个 C 代码去处理比较合适。但转发控制这部分业务比较复杂,lua 做更好。这就是这次重构的指导思想。

这次我把分包的工作放在了一个叫做 netpack 的 lua 扩展库里。参考:lua-netpack.c

然后 gate 的调度逻辑放在了 lua 版的 gate.lua 中。相较于上一版完全用 C 实现,会损失一点性能,但扩展性和可维护性都能提高很多。


最初在设计 gate 的时候,希望可以把多个连接上的数据转发给一个服务处理。比如你想用一个认证服务处理所有连接的最初登陆流程。又不想对网络数据包再打包(加上连接号),因为这样会造成额外的开销。

为了让接受数据的服务区分不同的数据源,我制作了一个代理服务 service_client 用来发送数据。并且在转发的时候,伪造这个代理服务作为数据源(真实的数据来源于 TCP 连接)。这样,处理数据的服务只需要按来源回应数据包就可以让网络数据包正确的返回。

而且,这种做法可以将 TCP 连接上的数据包通过 skynet.filter 包装成 skynet 内部消息格式。即,收到网络数据包 (PTYPE_CLIENT) 时,数据包内其实包含有必要的 session 号,先把 session 和其他数据分离,通过 skynet.filter 分别传给下游。这样就把网络连接从业务层中屏蔽掉了。

这也是为什么 gate 的转发协议需要提供两个服务地址的原因。

这次新写的 gate 继承了这个用法。但过往的实践中,这个用法略显复杂。如果业务简单,其实用不着实现这么多配套服务。直接把 socket fd 交给业务处理的服务,它直接向 socket 发包即可。

重新写的 examples 里的 agent 就按这个思路实现。

examples 展示了简单的客户端服务器通讯协议的封装方法:消息主体使用 json 。在主体前面加上文本的 数字 session 加一个符号 + 或 - 。+ 表示这是一条请求消息,- 表示这条消息是对前面对应 session 号的消息的回应。

由于直接调用 socket api 发送数据包,所以 agent 不再需要和 service_client 配套使用。

原来 C 编写的 client demo 已经删除,换成了 lua 版的 client demo 。网络层使用了一个简单的 socket 库。如果用于实际项目,还需要完善 socket 库(客户端也不一定用 lua 实现)。


这次 examples 只是简单的重新理了一下代码。它还远远不是一个有复杂业务的 demo 。我一直在考虑到底提供一个怎样的 demo 可以完整的展示 skynet 的特点。目前还没有想好。

对于我们已经上线和即将上线的项目,结构比这个 example 要复杂的多。

首先我们使用的是 google proto buffer 协议,作为消息主体。但外围做了一些封装。消息由消息类型、session 消息主体构成。消息类型用自定义的小语言描述,用于消息分发。session 对应 skynet 内部的 session 号。根据消息类型,可以知道消息主体如何编码。

其次,每个连接建立后,不会立刻创建 agent 和它对接,因为这可能使登陆流程(尤其是未完成的登陆流程)给系统造成过大的负载。登陆过程的交互统一转给认证服务处理。认证服务是无状态的,所以可以在系统内启动多份以提高处理效率。认证结束后,才真正创建 agent 和连接对接。

创建 agent 的过程可能比较慢 ,所以我们会在服务启动的时候预先创建好数千个 agent 待用。有一个 agent pool 服务管理这些备用 agent 。一旦系统有空闲,就会不断补充备用。

我们在 TCP 连接做了进一步的加密处理,目前是对 gate 做了一些改造完成的。由于 gate 并非 skynet 核心模块,所以可以复制一份出来定制需要的功能。将来还希望加上断线重连的特性。


下一步,我希望给 skynet 的 socket 层加上低优先级数据包的队列。就是说,从现在的单一队列改成两个。如果你需要启用第二队列,那么这将是一个低优先级的发送队列。socket 发送规则如下:

  1. 如果 socket 可写,且两个队列都为空,立即发送。

  2. 如果上一次有一个数据包没有发送完,无论它在哪个队列里,都确保先将其发送完。

  3. 如果高优先级队列有数据包,先保证发送高优先级队列上的数据。

  4. 如果高优先级队列为空,且低优先级队列不为空,发送一个低优先级队列上的数据包。

这可以用来解决前面提到的大数据包的问题。

在应用层,我们可以把大数据包分割成不大于 4K 的小数据包。给大数据包添加一个唯一的编号。这些分割后的小数据包进入低优先级队列,那么它们就会被切碎传输给对端。一个大数据包可能被切成数百份,其它的数据会穿插再其间。尤其不会影响心跳控制包的传输。

例如,客户端向服务器请求拍卖行目录,或是全球排行榜这种数据量很大的数据时,走这个大数据包通道,就不会影响正常的交互流程了。

April 02, 2014

lua-conf 让配置信息在不同的 lua 虚拟机间共享

有时候我们的项目需要大量的配置表(尤其是网络游戏) 。因为主要用 lua 做开发,我们倾向于直接用 lua table 保存这些配置常量。

海量的数据有两个问题:

这些配置数据在运行期是不变的,但树型结构复杂,放在 lua 虚拟机内会生成大量的 gc object ,拖慢 lua 的垃圾收集器。因为每次扫描都需要把所有配置数据都标记一遍。

在服务器端,我们使用 skynet 框架,会启动数千个 lua 虚拟机。如果每个虚拟机都加载一份配置信息,会带来大量的内存浪费。

基于这两点,我实现了 lua-conf 这个模块。它可以把一个 lua 表转换成一个 C 对象。在 lua 中得到的是一个lightuserdata (不是 userdata ,也不能自动回收,这是因为我希望它可以被多个 lua state 共享)。

经过简单的包装,可以在语法上模拟 lua table 来访问它。weaktable 用来 cache 常用的配置数据项。

这个 C 对象的访问是线程安全的,所以你可以放心的在多线程的多个 lua state 中共享访问它。


注:lua-conf 支持的数据类型是有限的。它必须是一个无环的树型结构。key 必须是整数(除了正整数,也支持 0 或负数)或字符串。而 value 必须是 boolean string number 或 table 。

April 01, 2014

内存安全的 Lua api 调用

Lua 的 API 设计的非常精良,整个 lua 核心库把内存管理都托管给了 lua_Alloc 这个用户注入的函数。任何时候在发生内存不足,lua 的 api 都可以正确处理异常。

考虑一下 lua_newtable 或是 lua_pushlstring 这些 api ,它们都需要创建新的 gcobject ,这些时候如果发生 lua_Alloc 分配不出内存怎么办?这些 api 可都是无返回值的。

lua 的行为是:抛出一个内存错误,如果外界没能捕获这个错误,则触发 panic 函数。

在编写将 lua 嵌入到宿主程序中的一个常见的错误是:

先用 lua_newstate 创建出一个 lua 虚拟机,然后直接调用 luaL_openlibs 等函数初始化它。如果你希望你的代码足够严谨,就必须了解,初始化的过程是有可能遇到内存申请不到的情况的。

正确的做法在 lua 自带的解释器实现中有一个很好的范例:你可以写一个 lua c function ,在里面做后续对 lua_State 的操作。在 lua_newstate 后,立刻 lua_pcall 这个 C 函数,而不是直接调用它。这样,所有的内存异常都会被这次 pcall 捕获住。

btw, 早期版本的 lua 有一个 lua_cpcall 函数,自从 lua 支持 light c function 后就去掉了这个 api 。

整个系统中都把内存不足的问题考虑清楚是件需要特别小心的事情。只要有一个角落没写对就前功尽弃。代码也会复杂很多。所以我自己的大部分代码都放弃了处理这个问题,包括如非必要,不检查 malloc 的返回值等。因为我了解我的程序的应用环境,一旦发生内存不足,系统很难继续工作下去。

与其把代码搞的过于复杂,还不一定面面俱到,不如直接放弃解决这个问题。交给操作系统去做就好,如果进程因为内存不足而崩溃也只好认了吧。