【专题4】搞明白skynet的C语言到lua环境建立之一(lua被加载过程)

skynet

参考文档

背景

skynet一个关键的优势是使用lua语言撰写脚本,而使用脚本语言写逻辑的一个大好处就是可以使用顺序逻辑描述业务。表面的平整之下实际是C语言对lua虚拟机的调度器在起作用。

阻塞API从lua中yield回C代码中,之后有了事件再次resume,看起来实现很简单,但是更加复杂的是错误的处理,API调用不知道会经历多少艰辛,出错、超时如何处理?这个是关键所在。

本文尝试从这个角度分析代码。

看代码

main()中:config.bootstrap = optstring("bootstrap","snlua bootstrap");

之后在skynet_start.c中,skynet_start()函数中:bootstrap(ctx, config->bootstrap);

其中实现大致如下:static void

bootstrap(struct skynet_context * logger, const char * cmdline) {

//这里 cmdline 是 “snlua bootstrap”

...

sscanf(cmdline, "%s %s", name, args);

//这里args是 “bootstrap”

struct skynet_context *ctx = skynet_context_new(name, args);//加载c模块

...

}

记住这里“snlua bootstrap” 的 name 是 snlua,而参数 args是 bootstrap。

下面加载snlua模块。snlua是一个service,以skynet模块的方式被加载。

service_snlua.c 定义了snlua模块,老办法,一个主要数据结构,create、release、init和最重要的callback。

service_snlua.c中,主结构体定义很简单,如下:struct snlua {

lua_State * L;

struct skynet_context * ctx;

};

create创建了一个 lua_newstate,

init的代码也不多:snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {

//这里 args 是 “bootstrap”

...

char * tmp = skynet_malloc(sz);

memcpy(tmp, args, sz);

//关键部位,安装callback

skynet_callback(ctx, l , _launch);

//下面仅仅从skynet_ctx中获取handle值

const char * self = skynet_command(ctx, "REG", NULL);

uint32_t handle_id = strtoul(self+1, NULL, 16);

// 第一条msg将“bootstrap”发给“自己”

skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);

return 0;

}

callback函数出场:typedef int (*skynet_cb)(

struct skynet_context * context, //skynet ctx

void *ud, //callback ctx

int type,

int session,

uint32_t source , //from

const void * msg, //raw buf

size_t sz //size

);

static int

_launch(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz) {

...

struct snlua *l = ud;

skynet_callback(context, NULL, NULL); //先关掉自己callback能力,主要是为了在lua中安装cb

int err = _init(l, context, msg, sz); //上面将参数“bootstrap”放在msg中传过来,这里就开始运行了。

if (err) {

skynet_command(context, "EXIT", NULL);

}

return 0;

}

可见 _launch(),其实只被运行了一遍,来为lua_state做初始化,之后便自我取缔。实际上,这个callback会在lua的环境里被更新。

关键位置了,看一下lua被引导的细节:static int

_init(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {

lua_State *L = l->L;

l->ctx = ctx;

...

lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");

luaL_openlibs(L);

lua_pushlightuserdata(L, ctx);

-- 这里挺重要的 一开始将把skynet_context 上下文挂到注册表了

lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");

...

//前面都是各种初始化,设置路径什么的

...

lua_pushcfunction(L, traceback); //这里设置出错处理的函数

assert(lua_gettop(L) == 1);

const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");

int r = luaL_loadfile(L,loader);

...

//这里push参数“bootstrap”

lua_pushlstring(L, args, sz);

r = lua_pcall(L,1,0,1); //引导loader.lua文件。

...

lua_settop(L,0);

lua_gc(L, LUA_GCRESTART, 0);

return 0;

}

lua_pcall()这里的细节:int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);

如果 msgh 是 0 , 返回在栈顶的错误消息就和原始错误消息完全一致。 否则, msgh 就被当成是 错误处理函数 在栈上的索引位置。 (在当前的实现里,这个索引不能是伪索引。) 在发生运行时错误时, 这个函数会被调用而参数就是错误消息。 错误处理函数的返回值将被 lua_pcall 作为错误消息返回在堆栈上。

后面,会进入 lualib/loader.lua...

local filename = string.gsub(pat, "?", SERVICE_NAME)

local f, msg = loadfile(filename)

if not f then

table.insert(err, msg)

else

pattern = pat

main = f

break

end

end

...

main(select(2, table.unpack(args)))

最后,main实际就是 loadfile("bootstrap") —— 别忘了上面 加载snlua时的参数。

关于 select函数select (index, ···)

如果 index 是个数字, 那么返回参数中第 index 个之后的部分; 负的数字会从后向前索引(-1 指最后一个参数)。 否则,index 必须是字符串 "#", 此时 select 返回参数的个数。

后面,会进入 service/bootstrap.lualocal skynet = require "skynet"

...

require "skynet.manager" -- import skynet.launch, ...

local memory = require "memory"

skynet.start(function()

...

local launcher = assert(skynet.launch("snlua","launcher")) --

skynet.name(".launcher", launcher)

...

skynet.newservice "service_mgr"

pcall(skynet.newservice,skynet.getenv "start" or "main")

skynet.exit()

end)

bootstrap 基本上各种初始化,细节看完整代码,其中感兴趣的是 launch 了 launcher

skynet.launch 见 “lualib/skynet/manager.lua”,function skynet.launch(...)

local addr = c.command("LAUNCH", table.concat({...}," ")) --参数是:"snlua launcher"

if addr then

return tonumber("0x" .. string.sub(addr , 2))

end

end

去找 c.command,实际是 lualib-src/lua-skynet.c 中使用 luaopen_skynet_core(...)支援的一个lib。其中“command”命令定义在 skynet_server.c中,而“LAUNCH”指令定义如:static const char *

cmd_launch(struct skynet_context * context, const char * param) {

...

struct skynet_context * inst = skynet_context_new(mod,args);

...

}

可见lua层面使用 skynet.launch 可以直接在C 层面调用了加载模块。而上面参数是:"snlua launcher",那就是先加载snlua service,而后启动 launcher了,和上面 “snlua bootstrap” 一样嘛。

后面可以想象,snlua已经加载过了,所以还是老一套,C层面snlua -> loader.lua -> launcher.lua,更加细节内容可以看一下 “【专题5】skynet的luaAPI实现专项分析”文章的“API研究 skynet.newservice()” 章节。

至此,lua被引导之前的逻辑分析完毕。