作者:[email protected],转载请注明作者
skynet中的源码已经分析得差不多了,还有启动过程没有分析。skynet的配置文件是以lua格式来写的。使用过skynet的都清楚skynet的启动命令是skynet config_file_name。配置文件名是作为命令行参数传给skynet进程的。
skynet进程启动以后,会读取config文件,然后解析这个lua文件。然后把相关的配置信息设置到lua的环境变量里。
C层读取配置的话是要从lua环境变量里去取的。所以C的配置数据结构的填充,对于某些人来说,是一团迷雾,不直观,而且难懂。
先看看配置文件路径的读取,skynet是用C写的,所以它的入口是main函数,在skynet_main.c中:
int
main(int argc, char *argv[]) {
const char * config_file = NULL ; //配置文件路径
if (argc > 1) {
config_file = argv[1]; //注意写死了,它就是第一个参数
} else {
fprintf(stderr, "Need a config file. Please read skynet wiki : https://github.com/cloudwu/skynet/wiki/Config\n"
"usage: skynet configfilename\n");
return 1;
}
这个配置文件的路径保存在config_file指针上,那么它是怎么加载的呢,这个又要涉及lua c api了。首先,云风写了一段lua代码,硬编码在skynet_main.c文件里,然后调用lua c api的函数去执行这段代码。这段lua代码会加载配置文件。先看一下这段lua代码:
static const char * load_config = "\
local result = {}\n\
--函数 getenv
--这个getenv是取进程的环境变量,比如$PATH
local function getenv(name) return assert(os.getenv(name), [[os.getenv() failed: ]] .. name) end\n\
--取文件路径分隔符
local sep = package.config:sub(1,1)\n\
--当前路径,linux下就是./
local current_path = [[.]]..sep\n\
--函数 include
local function include(filename)\n\
local last_path = current_path\n\
local path, name = filename:match([[(.*]]..sep..[[)(.*)$]])\n\
if path then\n\
if path:sub(1,1) == sep then -- root\n\
current_path = path\n\
else\n\
current_path = current_path .. path\n\
end\n\
else\n\
name = filename\n\
end\n\
--加载文件
local f = assert(io.open(current_path .. name))\n\
local code = assert(f:read [[*a]])\n\
code = string.gsub(code, [[%$([%w_%d]+)]], getenv)\n\
f:close()\n\
--注意load函数 @表示代码在文件里,t表示是文本
assert(load(code,[[@]]..filename,[[t]],result))()\n\
current_path = last_path\n\
end\n\
--注意这里有元表操作
setmetatable(result, { __index = { include = include } })\n\
--三个点代表可变参数
local config_name = ...\n\
--这里调了include函数,参数是可变的
include(config_name)\n\
--这里又有一个元表操作
setmetatable(result, nil)\n\
return result\n\
";
对于package.config
775 lua_pushliteral(L, LUA_DIRSEP "\n" LUA_PATH_SEP "\n" LUA_PATH_MARK "\n"
776 LUA_EXEC_DIR "\n" LUA_IGMARK "\n");
777 lua_setfield(L, -2, "config");
元表恐怕是需要单独两三篇文章才能介绍得比较全面了,因为这篇文章是分配配置加载的,对这里的元表操作只能介绍它是干嘛的,具体原理就不讲了。
lua中每一个值都有一个元表,这个元表就是lua表,定义了一个值在某些特定操作下的行为。比如gc,取表中元素等。或者更容易被接受的,数字类型的加法/减法等操作。
setmetatable就是用来替换元表的。不能再往深处讲了,再讲下去收不住了。
还有三个函数要说明一下,先说__index,__index实际上可以理解为t[key]:
__index: The indexing access table[key]. This event happens when table is not a table or when key is not present in table. The metamethod is looked up in table.
然后是assert,assert如果成功,返回它所有的参数
assert (v [, message])
Calls [error
](http://www.lua.org/manual/5.3/manual.html#pdf-error) if the value of its argument v
is false (i.e., **nil** or **false**); otherwise, returns all its arguments. In case of error, message
is the error object; when absent, it defaults to "assertion failed!
"
然后是load函数,load函数返回一个lua函数。
不再继续介绍lua的编程知识了,还是直接说明load_config这段代码块的功能吧。它的功能实际上就是设一个Include函数,这个include在config文件中出现时,执行include函数,最终能够在config文件里加载include包含的lua文件。
struct skynet_config config;
struct lua_State *L = luaL_newstate();
luaL_openlibs(L); // link lua lib
//加载load_config指向的lua代码块
//参数2表示代码块
//参数3表示代码块长度
//参数4表示是文件中的代码块,不是文件,同时用于调试和报错
//参数5表示代码块是文本格式,不是二进制
int err = luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t");
assert(err == LUA_OK);
//参数入栈
lua_pushstring(L, config_file);
//执行代码块,参数1个,返回结果1个
err = lua_pcall(L, 1, 1, 0);
if (err) {
fprintf(stderr,"%s\n",lua_tostring(L,-1));
lua_close(L);
return 1;
}
_init_env(L);
上面这段代码先加载了load_config对应的lua代码,然后执行它,实际上就是执行了include函数。include则会加载lua代码,最终这些代码形成了一个表格。这个表格呢会被_init_env用到。
static void
_init_env(lua_State *L) {
lua_pushnil(L); /* first key */
while (lua_next(L, -2) != 0) { //遍历表格
/* uses 'key' (at index -2) and 'value' (at index -1) */
int keyt = lua_type(L, -2);
if (keyt != LUA_TSTRING) {
fprintf(stderr, "Invalid config table\n");
exit(1);
}
const char * key = lua_tostring(L,-2);
if (lua_type(L,-1) == LUA_TBOOLEAN) {
int b = lua_toboolean(L,-1);
//设置key/value到_ENV
skynet_setenv(key,b ? "true" : "false" );
} else {
const char * value = lua_tostring(L,-1);
if (value == NULL) {
fprintf(stderr, "Invalid config table key = %s\n", key);
exit(1);
}
//设置key/value到_ENV
skynet_setenv(key,value);
}
lua_pop(L,1);
}
lua_pop(L,1);
}
_init_env这个函数就是遍历load_config那段lua代码形成的表格,把里面的key和value全部取出来,然后设置到_ENV中。按前面的说法就是设置到lua的环境变量中。skynet_setenv再往下会讲到,那里面会说明最终做了什么操作。
文章的开头讲到C里面取配置是从lua环境变量里取的,它是怎么实现的呢?
C里面取配置有三个辅助函数,optint/optboolean/optstring
static int
optint(const char *key, int opt) {
const char * str = skynet_getenv(key); //这个就是从lua环境变量里取值
if (str == NULL) {
char tmp[20];
sprintf(tmp,"%d",opt);
skynet_setenv(key, tmp);
return opt;
}
return strtol(str, NULL, 10);
}
static int
optboolean(const char *key, int opt) {
const char * str = skynet_getenv(key);//这个就是从lua环境变量里取值
if (str == NULL) {
skynet_setenv(key, opt ? "true" : "false");
return opt;
}
return strcmp(str,"true")==0;
}
static const char *
optstring(const char *key,const char * opt) {
const char * str = skynet_getenv(key);//这个就是从lua环境变量里取值
if (str == NULL) {
if (opt) {
skynet_setenv(key, opt);
opt = skynet_getenv(key);
}
return opt;
}
return str;
}
optint/optboolean/optstring都是从lua环境变量里取值,取完了以后再做一次格式转换。同时这三个函数还提供一个默认值叫opt,如果环境变量里没有key对应的配置,就把这个默认值给设到环境变量里去。
下面看看从lua环境变量里取数据是怎么弄的。
const char *
skynet_getenv(const char *key) {
SPIN_LOCK(E)
lua_State *L = E->L;
lua_getglobal(L, key); //从全局表中取名字key的值,然后把它压栈
const char * result = lua_tostring(L, -1); //把这个值转换为string
lua_pop(L, 1); //把前面压栈的值弹出来,还原栈
SPIN_UNLOCK(E)
return result;
}
skynet_getenv实际上就是从_ENV里取key对应的值,用大家比较熟悉的写法就是_ENV.key。
An assignment to a global name x = val
is equivalent to the assignment _ENV.x = val
(see [§2.2](http://www.lua.org/manual/5.3/manual.html#2.2)).
而skynet_setenv稍微曲折一点,先取_ENV.key,如果为nil,就操作_ENV.key=opt。
void
skynet_setenv(const char *key, const char *value) {
SPIN_LOCK(E)
lua_State *L = E->L;
lua_getglobal(L, key); //取全局表中的key,压栈
assert(lua_isnil(L, -1)); //为空?这个key没设置过?
lua_pop(L,1); //还原栈
lua_pushstring(L,value); //入栈
lua_setglobal(L,key); //把数据出栈,把key设进全局表
SPIN_UNLOCK(E)
}
到了这里,准备知识基本上就讲完了。终于可以开始讲C里面的配置了。
_init_env(L);
//名字叫thread的配置,默认为8
config.thread = optint("thread",8);
//名字叫cpath的配置,默认为./cservice/?.so
config.module_path = optstring("cpath","./cservice/?.so");
//名字叫harbor的配置,默认为1
config.harbor = optint("harbor", 1);
//bootstrap脚本
config.bootstrap = optstring("bootstrap","snlua bootstrap");
//daemon,默认是空
config.daemon = optstring("daemon", NULL);
//logger日志文件名
config.logger = optstring("logger", NULL);
//日志服务,默认是logger
config.logservice = optstring("logservice", "logger");
//profile,优化选项,默认开启
config.profile = optboolean("profile", 1);
以上就是C层需要的配置列表,再详细介绍一下各个参数到底是什么意思。
thread,工作线程个数,这个在分析消息处理的时候介绍过了。
cpath,服务所在的路径,以;号分隔,可以是多个路径,这个在模块加载的时候介绍过了。
harbor,这个还没介绍过,是不是开启集群模式。
bootstrap,这个也没介绍过,就是一个脚本,主要做服务启动前准备工作。
daemon,是不是以后台模式运行skynet。
logger,日志通道,比如说文件,远程日志服务等方式。
logservice,日志服务,默认使用skynet自己提供的logger服务。
profile,优化开启,开启后会收集一些运行时的信息,通过日志和命令方式给码畜提供参考信息。分析cpu使用时间。