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
启动start
ormain
服务 ,这是我们逻辑的入口
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=snlua
param=服务名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
- 本文声明:转载请标记原文作者及链接