skynet源码分析(二)启动流程

  本篇大概讲一下 skynet 进程的启动流程,完全了解启动流程基本上也就明白了 skynet 的原理了,本篇会大概提到全部的启动流程。

main

  所有 C 程序的可执行入口都是 main 函数,skynet 的 main 函数在 skynet_main.c 中实现,来看一下它执行了哪些操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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;
    }

    // 初始化 GNODE 的数据
    skynet_globalinit();

    // 初始化环境变量
    skynet_env_init();

    // 忽略信号 SIGPIPE, 防止写数据时被意外终止进程
    sigign();

    struct skynet_config config;

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

    // 开一个 lua 虚拟机来加载配置用
    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);

    // 使用上面的 lua 代码解析 config 文件
    err = lua_pcall(L, 1, 1, 0);
    if (err) {
        fprintf(stderr, "%s\n", lua_tostring(L, -1));
        lua_close(L);
        return 1;
    }
    // 从 config 文件中读内容,写入到环境变量中
    _init_env(L);

    // 从环境变量中拿这些值放到 config 结构中
    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 虚拟机
    lua_close(L);

    // 通过 config 结构中的内容,开始正式启动
    skynet_start(&config);

    // 退出进程
    skynet_globalexit();

    return 0;
}

  虽然有一点长,但是在 main 函数中做的工作并不是非常多,其它更多关于启动服务要进行的初始化工作会交给最后调用的 skynet_start 函数来执行。在 main 函数中最主要做的是三件事。

  1. 解析配置文件
  2. 将解析的结果保存在环境变量里
  3. 使用解析的结果调用 skynet_start 继续进行后续的初始化

skynet_start

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
void skynet_start(struct skynet_config *config) {
    // register SIGHUP for log file reopen
    struct sigaction sa;
    sa.sa_handler = &handle_hup;
    sa.sa_flags = SA_RESTART;
    sigfillset(&sa.sa_mask);
    sigaction(SIGHUP, &sa, NULL);

    if (config->daemon) {
        if (daemon_init(config->daemon)) {
            exit(1);
        }
    }
    // 初始化节点数量
    skynet_harbor_init(config->harbor);
    // 初始化 handle 存储器
    skynet_handle_init(config->harbor);
    // 初始化全局消息队列
    skynet_mq_init();
    // 初始化 C 模块管理器,设置查找路径
    skynet_module_init(config->module_path);
    // 初始化全局时间
    skynet_timer_init();
    // 初始化 socket 管理器
    skynet_socket_init();
    // 标记是否开了性能测试
    skynet_profile_enable(config->profile);

    // 创建 logger C服务
    struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
    if (ctx == NULL) {
        fprintf(stderr, "Can't launch %s service\n", config->logservice);
        exit(1);
    }
    // 注册 logger 服务的名字
    skynet_handle_namehandle(skynet_context_handle(ctx), "logger");

    // 启动,执行 config 文件中的 bootstrap 命令
    bootstrap(ctx, config->bootstrap);

    // 启动全部线程
    start(config->thread);

    // harbor_exit may call socket send, so it should exit before socket_free
    skynet_harbor_exit();
    skynet_socket_free();
    if (config->daemon) {
        daemon_exit(config->daemon);
    }
}

  在 skynet_start 中,主要的操作是将剩余的全局变量都进行了初始化。然后调用了 bootstrapstart 来继续处理后续的工作。

bootstrap

  在看源码之前,先来关注一下 bootstrap 函数的调用方法。

1
bootstrap(ctx, config->bootstrap);

  参数 ctx 是刚刚创建的 logger 服务,参数 config->bootstrap 的内容是配置文件里指定的启动命令,在 skynet 给出的默认配置文件中,这个命令是 snlua bootstrap
  了解清楚了参数的内容,再来看一下 bootstrap 的源码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static void bootstrap(struct skynet_context *logger, const char *cmdline) {
    int sz = strlen(cmdline);
    char name[sz + 1];
    char args[sz + 1];
    sscanf(cmdline, "%s %s", name, args); // 从 cmdline 中读取服务名字和它的参数
    // 创建初始服务
    struct skynet_context *ctx = skynet_context_new(name, args);
    if (ctx == NULL) {
        skynet_error(NULL, "Bootstrap error : %s\n", cmdline);
        skynet_context_dispatchall(logger);
        exit(1);
    }
}

  源码非常简单,主要做了两件事情,首先从参数中获取要启动的服务和要传给服务的启动参数,然后创建了这个服务。在默认配置下就是创建了一个 snlua 服务,参数为 bootstrap
  关于在这个名为 bootstrap 的 lua 服务中会进行的操作,后续的文章中会专门讲解。

start

  start 函数接收的参数是配置文件中指明的 worker 线程的数量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
static void start(int thread) {
    // 保存所有线程的 ID
    pthread_t pid[thread + 3];

    struct monitor *m = skynet_malloc(sizeof(*m));
    memset(m, 0, sizeof(*m));

    // 初始化全局 monitor 的线程总数和睡眠线程数变量
    m->count = thread;
    m->sleep = 0;

    // 每条 worker 线程分配一个 skynet_monitor 监控
    m->m = skynet_malloc(thread * sizeof(struct skynet_monitor *));
    int i;
    for (i = 0; i < thread; i++) {
        // 初始化线程的 monitor
        m->m[i] = skynet_monitor_new();
    }
    // 初始化锁
    if (pthread_mutex_init(&m->mutex, NULL)) {
        fprintf(stderr, "Init mutex error");
        exit(1);
    }
    // 初始化条件
    if (pthread_cond_init(&m->cond, NULL)) {
        fprintf(stderr, "Init cond error");
        exit(1);
    }

    // 创建 monitor 线程
    create_thread(&pid[0], thread_monitor, m);
    // 创建 timer 线程
    create_thread(&pid[1], thread_timer, m);
    // 创建 socket 线程
    create_thread(&pid[2], thread_socket, m);

    // 创建 worker 线程
    // 每次处理的工作量权重,是服务队列中消息总数右移的位数,小于 0 的每次只读一条
    // 前四个线程每次只处理一条消息
    // 后面的四个每次处理队列中的全部消息
    // 再后面分别是每次 1/2,1/4,1/8
    static int weight[] = {
        -1, -1, -1, -1, 0, 0, 0, 0,
        1, 1, 1, 1, 1, 1, 1, 1,
        2, 2, 2, 2, 2, 2, 2, 2,
        3, 3, 3, 3, 3, 3, 3, 3,
    };
    struct worker_parm wp[thread];
    for (i = 0; i < thread; i++) {
        wp[i].m = m;
        wp[i].id = i;
        if (i < sizeof(weight) / sizeof(weight[0])) {
            wp[i].weight = weight[i];
        } else {
            wp[i].weight = 0;
        }
        create_thread(&pid[i + 3], thread_worker, &wp[i]);
    }

    for (i = 0; i < thread + 3; i++) {
        pthread_join(pid[i], NULL);
    }

    free_monitor(m);
}

  这个函数虽然比较长,但是进行的操作并不复杂,它启动了所有类型的线程,一共是 3 + N 条,然后使用了 pthread_join 等待所有线程结束,主线程后续会一直阻塞在这里,不会进行其它操作。其中使用的 create_thread 只是对 pthread_create 的简单封装,就不展开说了。
  还有一点需要注意的是 weight 数组,它定义了 worker 线程的工作权重。在 worker 线程创建的时候,数组里对应的权重被传给了线程的启动函数。关于这个权重的用途,后续讲 worker 线程作用的文章中会提到。

Licensed under CC BY-NC-SA 4.0
Built with Hugo
主题 StackJimmy 设计