skynet源码分析(四)认识 module

  module 是 skynet 中一切 actor 的基石,每个 actor 的本质都是一种 module 的具象化。本篇会讲解跟 module 有关的内容。

概述

  skynet 自带了四个 module, 分别是 gate,harbor,logger 和 snlua。在 service-src 目录下面每个 service_xxx.c 的文件是一个对应的 module。
  在编译完成之后,module 所在的目录是 cservice, 目录下的 xxx.so 就是对应的 module 动态库。启动一个服务的操作其实就是启动一个 module 实例,比如最常见的 lua 层的服务,就全部都是 service_snlua 的实例。

管理器 modules

  每个 skynet 进程都有一个全局的 module 管理器 modules *M,它会在进程启动的时候通过函数 skynet_module_init 进行初始化。它本身是有锁的,修改的时候要加锁,不过这个锁影响不大,因为很少增加模块。最多只支持 32 个 module 的注册,再多要自己改一下,不过一般不会用到那么多,因为大部分都是 lua 服务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#define MAX_MODULE_TYPE 32

struct modules {
    int count; // 已经加载的 module 的总数量
    struct spinlock lock; // 自旋锁
    const char *path; // module 的加载目录
    struct skynet_module m[MAX_MODULE_TYPE]; // 全部的模块
};

// 全局对象管理全部的 C 模块
static struct modules *M = NULL;

skynet_module 结构

  module 有自己的名字,在查找的时候是通过名字来查找对应的 module 的。每个 module 会被单独编译,编译成一个动态库文件 *.so, 在需要的时候才会被加载。module 可以有四个指定函数,分别为:

  • xxx_create
  • xxx_init
  • xxx_release
  • xxx_signal
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
typedef void * (*skynet_dl_create)(void);
typedef int (*skynet_dl_init)(void * inst, struct skynet_context *, const char * parm);
typedef void (*skynet_dl_release)(void * inst);
typedef void (*skynet_dl_signal)(void * inst, int signal);

struct skynet_module {
    const char *name; // 模块的名字
    void *module; // 模块的地址
    skynet_dl_create create; // create 函数地址
    skynet_dl_init init; // init 函数地址
    skynet_dl_release release; // release 函数地址
    skynet_dl_signal signal; // signal 函数地址
};

  create 接口用于创建一份私有数据结构体出来,init 用来初始化这个私有数据,release 在服务退出的时候被调用,用来释放私有数据,signal 用来接收 debug 控制台的信号指令。

module 的加载

  在创建一个新服务的时候,创建函数 skynet_context_new 会调用 skynet_module_query 来查找指定名字的 module 地址。

 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
// 按名字查找是否已经创建过模块
static struct skynet_module *_query(const char *name) {
    int i;
    for (i = 0; i < M->count; i++) {
        if (strcmp(M->m[i].name, name) == 0) {
            return &M->m[i];
        }
    }
    return NULL;
}

struct skynet_module *skynet_module_query(const char *name) {
    struct skynet_module *result = _query(name);
    if (result)
        return result;

    // 上锁
    SPIN_LOCK(M)
    // 因为可能就是刚刚交出锁的那个线程添加了这个模块,所以要再检查一次
    result = _query(name); // double check

    // 判断数量
    if (result == NULL && M->count < MAX_MODULE_TYPE) {
        int index = M->count;
        void *dl = _try_open(M, name);
        if (dl) {
            // 如果打开动态库成功,临时保存名字和句柄给 open_sym 使用
            M->m[index].name = name;
            M->m[index].module = dl;

            if (open_sym(&M->m[index]) == 0) {
                // 真正保存模块名字
                M->m[index].name = skynet_strdup(name);
                // 累加模块记录
                M->count++;
                result = &M->m[index];
            }
        }
    }
    // 解锁
    SPIN_UNLOCK(M)

    return result;
}

  在 skynet_module_query 中,首先通过 _query 查询了指定名字的 module 是否已经加载过了。在 _query 中会遍历所有已经加载的 module 来查询,因为 module 总数很少,所以遍历并没有什么问题。如果找到了,则直接返回 module 的地址给查询方。
  如果没查到,则会调用 _try_open 走 module 的加载流程。

 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
// 尝试打开动态库
// 依次遍历 M 中的每个目录,查找目标名字的模块
static void *_try_open(struct modules *m, const char *name) {
    const char *l;
    const char *path = m->path;
    size_t path_size = strlen(path);
    size_t name_size = strlen(name);

    int sz = path_size + name_size;
    // search path
    void *dl = NULL;
    char tmp[sz];
    do {
        memset(tmp, 0, sz);
        while (*path == ';')
            path++;
        if (*path == '\0')
            break; // 如果 path 已经查找完了,则跳出循环
        l = strchr(path, ';'); // 查找分号位置
        if (l == NULL)
            l = path + strlen(path);
        int len = l - path;
        int i;
        for (i = 0; path[i] != '?' && i < len; i++) {
            tmp[i] = path[i];
        }
        memcpy(tmp + i, name, name_size); // 组合出本路径下的完整 module 名字
        if (path[i] == '?') {
            strncpy(tmp + i + name_size, path + i + 1, len - i - 1);
        } else {
            fprintf(stderr, "Invalid C service path\n");
            exit(1);
        }
        dl = dlopen(tmp, RTLD_NOW | RTLD_GLOBAL); // 尝试打开组合好的路径
        path = l;
    } while (dl == NULL); // 如果打开失败则继续循环,只要有一个打开成功了则认为找到了

    // 上面的循环 break 以后,如果 dl 还是 NULL 就拿错误信息
    if (dl == NULL) {
        fprintf(stderr, "try open %s failed : %s\n", name, dlerror());
    }

    return dl;
}

  加载的逻辑比较简单,因为 path 是支持多地址的,多地址用 “;” 分隔,所以加载的过程其实就是依次将每个地址与目标动态库名字拼接在一起,然后使用系统调用 dlopen 尝试加载拼接结果的动态库文件,只要有一个加载成功了,即可认为成功找到了。
  成功加载动态库以后,会使用 open_sym 读取动态库中上面提到的四个接口的地址。

 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
// 拿到模块中定义好的指定函数的地址
// 并没有在乎有没有拿到,调用的时候会再判断
static void *get_api(struct skynet_module *mod, const char *api_name) {
    size_t name_size = strlen(mod->name);
    size_t api_size = strlen(api_name);
    char tmp[name_size + api_size + 1];
    memcpy(tmp, mod->name, name_size);
    memcpy(tmp + name_size, api_name, api_size + 1); // 组合 module 名字和 api 名字
    char *ptr = strrchr(tmp, '.');
    if (ptr == NULL) {
        ptr = tmp;
    } else {
        ptr = ptr + 1;
    }
    return dlsym(mod->module, ptr);
}

// 读取动态库内的函数地址
// 返回是否读取到了 init 函数
static int open_sym(struct skynet_module *mod) {
    mod->create = get_api(mod, "_create");
    mod->init = get_api(mod, "_init");
    mod->release = get_api(mod, "_release");
    mod->signal = get_api(mod, "_signal");

    return mod->init == NULL;
}

  在 open_sym 函数中,会使用系统调用 dlsym 来分别拿到动态库中 xxx_create/xxx_init/xxx_release/xxx_signal 的地址,将函数地址保存在 module 结构体中。

module 实例的创建

  创建一个 module 的实例使用的方法是 skynet_module_instance_create,它只是对 module 的 create 接口的简单调用。

1
2
3
4
5
6
7
8
9
// 创建模块的副本
void *skynet_module_instance_create(struct skynet_module *m) {
    if (m->create) {
        // 使用之前注册好的模块的 create 方法获取模块副本
        return m->create();
    } else {
        return (void *) (intptr_t) (~0);
    }
}

  本质上就是创建一个 module 的私有数据结构体,这份数据在不同的 module 中结构不一样,比如在 snlua 中,结构体就是 struct snlua, 其中包含了 lua 虚拟机等数据。同一个 module 创建出的不同实例之间共享 module 提供的接口,但是数据是互相独立的。
  创建过程中,会首先调用 module 的 create 接口创建出一份私有数据,然后再调用 init 接口初始化这份数据。在释放的时候会调用 release 接口释放这份数据。

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