Skynet源码赏析三 | 服务启动
skynet启动一个C服务
查看怎么启动C服务的最好的办法是打断点,看源码。
启动logger日志服务
我拿日志服务来举例子,怎么启动日志服务的呢?
- 传参数
name = "logger", param = null,struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);; - 从modules服务模块中取出名字为 
logger的服务,struct skynet_module * result = _query(name);; - 如果找不到则从cpath目录中找对应的.so文件,打开服务,并且存储到modules模块中。使用的函数是
static void * _try_open(struct modules *m, const char * name); - 创建服务实例
m->create(),放到上下文skynet_context的inst中; - 把
skynet_context放到skynet_context list中,skynet_handle_register(ctx);; - 创建logger服务对应的次级消息队列
struct message_queue *queue,并且把消息队列push到全局消息队列中skynet_mq_create(ctx->handle);,skynet_globalmq_push(queue);; - 服务实例初始化
m->init(),int r = skynet_module_instance_init(mod, inst, ctx, param); 
总结下来,启动一个服务的过程就是,1.获取服务;2.创建服务实例;3.创建服务对应的上下文,并且把上下文放到skynet_context list管理模块中;4.绑定次级消息队列与对应的服务;5.服务初始化; 这五步过程。
snlua服务
boostrap服务是通过snlua服务启动的,所以在启动boostrap服务的时候先把snlua服务启动了,并放在了modules模块中,snlua服务的主要作用是启动lua服务。
- 创建
snlua服务,先调用snlua_create, 
struct snlua * snlua_create(void) {
    //初始化snlua结构
	struct snlua * l = skynet_malloc(sizeof(*l));
	memset(l,0,sizeof(*l));
	l->mem_report = MEMORY_WARNING_REPORT;
    ...
    //初始化一个lua虚拟机
	l->L = lua_newstate(lalloc, l);
    ...
	return l;
}
- 创建一个snlua结构,创建一个Lua虚拟机,内存分配指定的是lalloc,目的是为了监控Lua分配的内存。
MEMORY_WARNING_REPORT为Lua服务的内存阀值,超过该值,会报警 
snlua服务被创建后,初始化,调用的snlua_init
int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
    ...
    //指定回掉函数
	skynet_callback(ctx, l , launch_cb);
    //取本地服务的句柄
	const char * self = skynet_command(ctx, "REG", NULL);
   	uint32_t handle_id = strtoul(self+1, NULL, 16);
	// it must be first message
    //发送第一条消息,在launc_cb可见
	skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
	return 0;
}
- 指定初始化的回调函数,launch_cb
 - 取得本地服务句柄
 - 向服务发送第一条消息, 打上
PTYPE_TAG_DONTCOPY的tag,这表示skynet内部不会重新分配内存拷贝tmp。 
- launch_cb函数处理
snlua服务收到自己给自己发送的第一条消息,删除回调函数,并且调用init_cb函数,前面删除了回调,后面必定会把回调函数给加上(下文启动lua服务的时候会在lua脚本里设置回调),直接看init_cb函数的逻辑。 代码太长,我就只贴关键的代码: 
static int init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {
	lua_State *L = l->L;
	l->ctx = ctx;
	lua_gc(L, LUA_GCSTOP, 0);
    ...
    //LUA_PATH:Lua搜索路径,在config.lua_path指定。
	const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");
	lua_pushstring(L, path);
	lua_setglobal(L, "LUA_PATH");
    //LUA_CPATH:C模块的搜索路径,在config.lua_cpath指定。
	const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");
	lua_pushstring(L, cpath);
	lua_setglobal(L, "LUA_CPATH");
    //LUA_SERVICE:Lua服务的搜索路径,在config.luaservice指定。
	const char *service = optstring(ctx, "luaservice", "./service/?.lua");
	lua_pushstring(L, service);
	lua_setglobal(L, "LUA_SERVICE");
    //LUA_PRELOAD:预加载脚本,这些脚本会在所有服务开始之前执行,可以用它来初始化一些全局的设置。执行loader.lua,把要执行的脚本传进去,由loader去加载执行,skynet初始执行bootstrap.lua。
    const char *preload = skynet_command(ctx, "GETENV", "preload");
	lua_pushstring(L, preload);
	lua_setglobal(L, "LUA_PRELOAD");
	const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");
	int r = luaL_loadfile(L,loader);
    ...
	lua_pushlstring(L, args, sz);
    //执行lua脚本,调用脚本里 skynet.start的地方
	r = lua_pcall(L,1,0,1);
	if (r != LUA_OK) {
		skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1));
		report_launcher_error(ctx);
		return 1;
	}
    ...
	lua_gc(L, LUA_GCRESTART, 0);
	return 0;
}
总结一下init_cb做的事情,设置lua全局变量:
- LUA_PATH:Lua搜索路径,在config.lua_path指定。
 - LUA_CPATH:C模块的搜索路径,在config.lua_cpath指定。
 - LUA_SERVICE:Lua服务的搜索路径,在config.luaservice指定。
 - LUA_PRELOAD:预加载脚本,这些脚本会在所有服务开始之前执行,可以用它来初始化一些全局的设置。
 - 执行loader.lua,把要执行的脚本传进去,由loader去加载执行,skynet初始执行
bootstrap.lua. 
最后一步执行loader.lua,这一步很重要, r = lua_pcall(L,1,0,1);, 初始执行boostrap.lua脚本文件中,skynet.start()的地方。其他lua服务的skynet.start()也是在这里调用的。
boostrap服务
boostrap是引导服务,他是通过snlua服务启动的,属于lua服务。我们可以从boostrap.lua脚本看到该服务做的事情如下:
- 启动
launcher服务,这个launcher服务为服务启动器 - 如果指定harborid,则说明这是一个主从分布式的skynet结构,不过skynet已经不推荐使用这种构架,略过。
 - 调用
skynet.newservice启动datacenterd服务 - 调用
skynet.newservice启动service_mgr服务 - 调用
skynet.newservice启动startormain服务 ,这是我们逻辑的入口 
skynet启动一个lua服务
我们知道了snlua服务和boostrap服务的启动, skynet.launch是创建服务的通用版本,要在Lua创建某个C写的服务,可以使用它。但如果要创建一个Lua服务,则应该使用skynet.newservice。
如果我有在A服务创建B服务,则有:
- 调用
skynet.newservice(B,...)这个函数使A阻塞; - B服务创建成功,B.lua这个脚本被执行,
skynet.start(function()...end)这个函数被调用,表示服务B启动,可以接收消息,此详细过程可以看snlua服务启动的最后一步执行loader.lua; - 当
skynet.start(function() ... end)这个函数执行完了之后A的skynet.newservice(B,...)才返回,并且A得到了B的句柄。 
内部怎么实现的呢?
skynet.newservice()函数实现
function skynet.newservice(name, ...)
	return skynet.call(".launcher", "lua" , "LAUNCH", "snlua", name, ...)
end
调用的是.launcher服务的LAUNCH函数,参数是snlua name, .launcher服务收到了消息后的处理:
--launcher.lua
local function launch_service(service, ...)
	local param = table.concat({...}, " ")
    --创建一个服务,并返回服务句柄
	local inst = skynet.launch(service, param)
	local session = skynet.context()
    --取一个resonse闭包,先存起来,等skynet.start返回再调用。
	local response = skynet.response()
	if inst then
		services[inst] = service .. " " .. param
		instance[inst] = response
		launch_session[inst] = session
	else
		response(false)
		return
	end
	return inst
end
function command.LAUNCH(_, service, ...)
	launch_service(service, ...)
	return NORET
end
- 实际上只用看
launch_service函数即可,service=snluaparam=服务名B - 通过
snlua创建lua服务B,并返回B的句柄 - 取一个resonse闭包,先存起来,等
skynet.start返回再调用。 
B服务创建后,调用B.lua脚本,调用脚本里的skynet.start函数
function skynet.start(start_func)
	c.callback(skynet.dispatch_message)
	init_thread = skynet.timeout(0, function()
		skynet.init_service(start_func)
		init_thread = nil
	end)
end
- 前文snlua启动的时候,在函数
launch_cb中,删除了回调函数,这里c.callback,重新设置了收到消息后的回调。 c.callback,代码在lua-skynet.c中,,以后向这个服务发消息,skynet.dispatch_message就会被调用。
调用skynet.init_service(start_func):
function skynet.init_service(start)
	local function main()
		skynet_require.init_all()
		start()
	end
	local ok, err = xpcall(main, traceback)
	if not ok then
		skynet.error("init service failed: " .. tostring(err))
		skynet.send(".launcher","lua", "ERROR")
		skynet.exit()
	else
		skynet.send(".launcher","lua", "LAUNCHOK")
	end
end
B服务在skynet.start调用完毕之后,会发送 LAUNCHOK消息:
function command.LAUNCHOK(address)
	-- init notice
	local response = instance[address]
	if response then
		response(true, address)
		instance[address] = nil
		launch_session[address] = nil
	end
	return NORET
end
- 从
instance取出response函数,调用它,传入true表示成功,后面跟的address就是skynet.call的返回值,这样A服务终于从skynet.newservice返回,并得到了B的地址(句柄)。 - 所有经过skynet.newsevice创建的服务,都会记录在launcher服务中,launcher提供了些函数用于查询服务的状态。
 
为什么skynet.newservice阻塞
为什么A创建B服务的时候skynet.newservice会阻塞,并等到到B服务创建成功并返回句柄,才继续往下走,请看 skynet.call的代码
function skynet.call(addr, typename, ...)
	local tag = session_coroutine_tracetag[running_thread]
	if tag then
		c.trace(tag, "call", 2)
		c.send(addr, skynet.PTYPE_TRACE, 0, tag)
	end
	local p = proto[typename]
	local session = c.send(addr, p.id , nil , p.pack(...))
	if session == nil then
		error("call to invalid address " .. skynet.address(addr))
	end
	return p.unpack(yield_call(addr, session))
end
c.send直接返回,阻塞在return p.unpack(yield_call(addr, session))- yield_call是挂起了当前协程
 
在B服务创建成功,并且调用skynet.repsonse()之后,往A服务发送了一个协议skynet.PTYPE_RESPONSE
ret = c.send(co_address, skynet.PTYPE_RESPONSE, co_session, pack(...))
A服务收到skynet.PTYPE_RESPONSE协议后,会回到yield_call挂起协程的地方
local function raw_dispatch_message(prototype, msg, sz, session, source)
	-- skynet.PTYPE_RESPONSE = 1, read skynet.h
	if prototype == 1 then
		local co = session_id_coroutine[session]
		if co == "BREAK" then
			session_id_coroutine[session] = nil
		elseif co == nil then
			unknown_response(session, source, msg, sz)
		else
			local tag = session_coroutine_tracetag[co]
			if tag then c.trace(tag, "resume") end
			session_id_coroutine[session] = nil
            -- 在这一步重新调用协程函数
			suspend(co, coroutine_resume(co, true, msg, sz))
		end
        ...
end
- 这样就能解释为为什么skynet.newservice是阻塞的了。
 - 同理skynet.call是阻塞的.
 
--完--
- 原文作者: 留白
 - 原文链接: https://zfunnily.github.io/2021/10/skynetthree/
 - 更新时间:2024-04-16 01:01:05
 - 本文声明:转载请标记原文作者及链接