[skynet 源码阅读系列] 01_从 main 函数开始

2021-05-12 20:34:05 +08:00
 yiouejv

skynet 是 C 语言写的框架,我们采用学习过程中最基本的方式去阅读 skynet,从 C 语言的 main 函数开始。

首先我们找到框架的入口main函数,在 skynet/skynet-src/skynet_main.c 文件内。

main 函数的代码如下:

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;
    }

    skynet_globalinit();
    skynet_env_init();

    sigign();

    struct skynet_config config;

#ifdef LUA_CACHELIB
    // init the lock of code cache
    luaL_initcodecache();
#endif

    struct lua_State *L = luaL_newstate();
    luaL_openlibs(L);   // link lua lib

    int err =  luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t");
    assert(err == LUA_OK);
    lua_pushstring(L, config_file);

    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);

    config.thread =  optint("thread",8);
    config.module_path = optstring("cpath","./cservice/?.so");
    config.harbor = optint("harbor", 1);
    config.bootstrap = optstring("bootstrap","snlua bootstrap");
    config.daemon = optstring("daemon", NULL);
    config.logger = optstring("logger", NULL);
    config.logservice = optstring("logservice", "logger");
    config.profile = optboolean("profile", 1);

    lua_close(L);

    skynet_start(&config);
    skynet_globalexit();

    return 0;
}

我们一段一段查看


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;
    }

    //...
}

定义了一个指针, 指针指向常量, const char* config_file, config_file 赋值为启动时的第二个参数,也就是配置文件的路径。


skynet_globalinit();

// skynet/skynet-src/skynet_server.c
struct skynet_node {
    ATOM_INT total;
    int init;
    uint32_t monitor_exit;
    pthread_key_t handle_key;
    bool profile;   // default is off
};
static struct skynet_node G_NODE;

void 
skynet_globalinit(void) {
    ATOM_INIT(&G_NODE.total , 0);
    G_NODE.monitor_exit = 0;
    G_NODE.init = 1;
    if (pthread_key_create(&G_NODE.handle_key, NULL)) {
        fprintf(stderr, "pthread_key_create failed");
        exit(1);
    }
    // skynet/skynet-src/skynet_imp.h
    /*
        #define THREAD_WORKER 0
        #define THREAD_MAIN 1
        #define THREAD_SOCKET 2
        #define THREAD_TIMER 3
        #define THREAD_MONITOR 4
    */
    skynet_initthread(THREAD_MAIN);
}

skynet_initthread(int m) {
    // skynet/skynet-src/atomic.h
    // #define ATOM_POINTER volatile uintptr_t
    uintptr_t v = (uint32_t)(-m);
    pthread_setspecific(G_NODE.handle_key, (void *)v);
}

初始化全局节点信息,total 为 0,monitor_exit 为 0,init 1,

pthread_key_create(&G_NODE.handle_key, NULL) 创建了一个多线程私有数据 handle_key,可参考文章: https://www.jianshu.com/p/d78d93d46fc2

skynet_initthread(THREAD_MAIN); 将当前线程状态由 THREAD_MAIN 切换为 THREAD_WORKER 状态并记录在 handle_key 。


skynet_env_init();

// skynet/skynet-src/skynet_env.c
struct skynet_env {
    struct spinlock lock;
    lua_State *L;
};

static struct skynet_env *E = NULL;

void
skynet_env_init() {
    E = skynet_malloc(sizeof(*E));
    SPIN_INIT(E)
    E->L = luaL_newstate();
}

E 一个skynet_env结构体,结构体内包含一个 spinlock 自旋锁,一个 lua 虚拟机指针。

skynet_malloc 为结构体 E 分配内存,skynet_malloc内部暂时不细究。

SPIN_INIT(E)

通过查找代码得知, 这是在 skynet/skynet-src/spinlick.h 中定义的一个宏。
#define SPIN_INIT(q) spinlock_init(&(q)->lock);
对 E 中的 lock 进行初始化。

E->L = luaL_newstate(); L 绑定了一个 lua 虚拟机。


sigign();

#include <signal.h>

int sigign() {
    struct sigaction sa;
    sa.sa_handler = SIG_IGN;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);
    sigaction(SIGPIPE, &sa, 0);
    return 0;
}

main 函数同文件下的 sigign() 函数。

定义了一个 sigaction 结构体,将 sa_handler 设置为 SIG_IGN,表示要忽略信号的产生的动作。

sigaction(SIGPIPE, &sa, 0); 将 SIGPIPE 的行为替换为 sa 结构体定义的形式,表示当前进程忽略 SIGPIPE 信号。

这里简单记录了一下 sigaction 的资料。 01ext_sigaction


struct skynet_config config;

定义了结构体 config

struct skynet_config {
    int thread;
    int harbor;
    int profile;
    const char * daemon;
    const char * module_path;
    const char * bootstrap;
    const char * logger;
    const char * logservice;
};

luaL_initcodecache();

// skynet/skynet-src/skynet_main.c
#ifdef LUA_CACHELIB
    luaL_initcodecache();
#endif

// skynet/3rd/lauxlib.c
static struct codecache CC;
struct codecache {
    struct spinlock lock;
    lua_State *L;
};
LUALIB_API void
luaL_initcodecache(void) {
    SPIN_INIT(&CC);
}

static const char * load_config = "\
    local result = {}\n\
    local function getenv(name) return assert(os.getenv(name), [[os.getenv() failed: ]] .. name) end\n\
    local sep = package.config:sub(1,1)\n\
    local current_path = [[.]]..sep\n\
    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\
        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(config_name)\n\
    setmetatable(result, nil)\n\
    return result\n\
";

struct lua_State *L = luaL_newstate();
luaL_openlibs(L);   // link lua lib

int err =  luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t");
assert(err == LUA_OK);
lua_pushstring(L, config_file);

err = lua_pcall(L, 1, 1, 0);
if (err) {
    fprintf(stderr,"%s\n",lua_tostring(L,-1));
    lua_close(L);
    return 1;
}

luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t");

加载了一段 lua 代码到内存里,并压入 lua 栈内。

load_config 这段代码实现的功能: 将配置文件内的 $var 替换成了环境变量的内容, 并返回了一个 result 表。

lua_pcall(L, 1, 1, 0);

执行压入的 load_config 代码块,第二个参数 1 表示压入的栈的个数为 1,lua_pushstring(L, config_file); 被压栈的配置文件名。 执行完函数之后,函数和参数自动出栈,此时栈为空。 函数的返回值被压栈,此时栈内只有一个表 result, result 内包含了配置在 config_file 内的键值对。


_init_env(L);

static void
_init_env(lua_State *L) {
    lua_pushnil(L);  /* first key */
    while (lua_next(L, -2) != 0) {
        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);
            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);
            }
            skynet_setenv(key,value);
        }
        lua_pop(L,1);
    }
    lua_pop(L,1);
}

// skynet/skynet-src/skynet_env.c
void 
skynet_setenv(const char *key, const char *value) {
    SPIN_LOCK(E)
    
    lua_State *L = E->L;
    lua_getglobal(L, key);
    assert(lua_isnil(L, -1));
    lua_pop(L,1);
    lua_pushstring(L,value);
    lua_setglobal(L,key);

    SPIN_UNLOCK(E)
}

// 从堆栈上弹出一个值,并将其设为全局变量 name 的新值。
void lua_setglobal (lua_State *L, const char *name);

// 把全局变量 name 里的值压栈,返回该值的类型。
int lua_getglobal (lua_State *L, const char *name);

将 lua 栈表内的键值对设置到 &E->L 的全局环境中。


config.thread =  optint("thread",8);
config.module_path = optstring("cpath","./cservice/?.so");
config.harbor = optint("harbor", 1);
config.bootstrap = optstring("bootstrap","snlua bootstrap");
config.daemon = optstring("daemon", NULL);
config.logger = optstring("logger", NULL);
config.logservice = optstring("logservice", "logger");
config.profile = optboolean("profile", 1);

static int
optint(const char *key, int opt) {
    const char * str = skynet_getenv(key);
    if (str == NULL) {
        char tmp[20];
        sprintf(tmp,"%d",opt);
        skynet_setenv(key, tmp);
        return opt;
    }
    return strtol(str, NULL, 10);
}

// skynet/skynet-src/skynet_env.c
const char * 
skynet_getenv(const char *key) {
    SPIN_LOCK(E)

    lua_State *L = E->L;
    
    lua_getglobal(L, key);
    const char * result = lua_tostring(L, -1);
    lua_pop(L, 1);

    SPIN_UNLOCK(E)

    return result;
}

optint, optstring, optboolean 从 &E->L 的全局环境中取得对应键的值,如果全局环境内未定义,则第二个参数 opt 设为 key 的默认值。


lua_close(L);

关闭 main 函数内创建的 lua 虚拟机。


skynet_start(&config);

下一节的内容。


skynet_globalexit();

void 
skynet_globalexit(void) {
    pthread_key_delete(G_NODE.handle_key);
}

删除在 skynet_initthread 中定义的特殊的线程数据。

1753 次点击
所在节点    C
13 条回复
orange
2021-05-12 21:08:49 +08:00
支持一下
yiouejv
2021-05-12 22:11:16 +08:00
@orange 谢谢
join
2021-05-12 23:15:53 +08:00
挺优美的,我也喜欢读。不搞游戏从来没用过。
zhengxiaowai
2021-05-12 23:44:30 +08:00
源码阅读文章不是那么写的,大段的贴代码,没啥意义

应该抽取核心流程,删除防御检查类代码,5-10 行最好,配置详细的文字说明,实在简单就略过
yiouejv
2021-05-12 23:47:45 +08:00
@zhengxiaowai 我就是想要一句句详细解读啊
zhengxiaowai
2021-05-12 23:52:25 +08:00
@yiouejv 诉我直言,那你直接在 code 上注释就好了。

这样文章四不像,无法阅读
yiouejv
2021-05-12 23:56:19 +08:00
@zhengxiaowai 我倒是觉得很清晰哦,阅读源码没有具体的写法公式吧
orange
2021-05-13 01:40:33 +08:00
建议画一些图,对整体有更清晰的描述
yiouejv
2021-05-13 09:37:32 +08:00
@orange 看这个确实需要自己细细的究细节点,下次注意画图
b00tyhunt3r
2021-05-13 09:41:37 +08:00
感谢楼主!同时赞同画图可以加深理解 我看源码肯定要备一个笔记本的

其实蛮好奇云风为什么选择 c 语言写这个架构
paoqi2048
2021-05-13 10:07:06 +08:00
@b00tyhunt3r 他是 C 厨
helllllloworld
2021-05-13 13:23:23 +08:00
@b00tyhunt3r 可以看看云风早些年的 blog 就明白了
luoqeng
2021-05-13 20:59:17 +08:00
skynet 这种单机服务不怎么值得研究,现在的发展方向 微服务 service-mesh 之类的技术

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/776568

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX