美文网首页
skynet源码分析(11)--skynet的配置加载

skynet源码分析(11)--skynet的配置加载

作者: 天一阁图书管理员 | 来源:发表于2017-08-22 21:05 被阅读279次

    作者:shihuaping0918@163.com,转载请注明作者

    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使用时间。

    相关文章

      网友评论

          本文标题:skynet源码分析(11)--skynet的配置加载

          本文链接:https://www.haomeiwen.com/subject/etsfdxtx.html