skynet原理解析

一、消息队列

上图摘自Actor模型解析,每个Actor都有一个专用的MailBox来接收消息,这也是Actor实现异步的基础。当一个Actor实例向另外一个Actor发消息的时候,并非直接调用Actor的方法,而是把消息传递到对应的MailBox里,就好像邮递员,并不是把邮件直接送到收信人手里,而是放进每家的邮箱,这样邮递员就可以快速的进行下一项工作。所以在Actor系统里,Actor发送一条消息是非常快的。

struct message_queue {
    struct spinlock lock;
    uint32_t handle;
    int cap;
    int head;
    int tail;
    int release;
    int in_global;
    int overload;
    int overload_threshold;
    struct skynet_message *queue;
    struct message_queue *next;
};

struct global_queue {
    struct message_queue *head;
    struct message_queue *tail;
    struct spinlock lock;
};

static struct global_queue *Q = NULL;

struct spinlock是自旋锁,用来解决并发问题的。

skynet也实现了Actor模型,每个服务都有一个专用的MailBox用来接收消息,这个队列即struct message_queue结构,skynet有两种消息队列,每个服务有一个刚刚谈到的被称为次级消息队列,skynet还有一个全局消息队列即static struct global_queue *Q = NULL;,头尾指针分别指向一个次级队列,在skynet启动时初始化全局队列。

struct message_queue结构体种有一个struct skynet_message *queue这个就是每个服务的次级消息队列,是来自其他服务需要本服务处理的消息,这是一个数组实现的队列,在往队列push消息的时候会检查队列是否已满,满了会扩容一倍,跟vector有点类似。

配置文件中有个配置是thread = 8,这个就是worker线程的个数,worker线程每次会从global_mqpop一条次级消息队列(每条次级消息对应特定服务),并根据线程权重和次级消息队列的回调函数,对次级消息队列中的消息进行消费。处理完毕后,会从global_mq中再pop一条次级消息队列供下次调用,同时将本次使用的次级消息队列pushglobal_mq的尾部。

二、模块

struct modules {
    int count;
    struct spinlock lock;
    const char * path;// 用c编写的服务编译后so库路经,不配置默认是./cservice/?.so
    struct skynet_module m[MAX_MODULE_TYPE]; //存放服务模块的数组
};

static struct modules * M = NULL;

struct skynet_module { //单个模块结构
    const char * name; //c服务名称,一般是指c服务的文件名
    void * module; //访问so库的dl句柄,通过dlopen获得该句柄
    skynet_dl_create create; //通过dlsym绑定so库中的xxx_create函数,调用create即调用xxx_create接口
    skynet_dl_init init; //绑定xxx_init接口
    skynet_dl_release release; //绑定xxx_release接口
    skynet_dl_signal signal; //绑定xxx_signal接口
};

M->path在初始化(skynet_module_init)时赋值,对应配置文件的cpath,不配置默认是./cservice/?.so。

用c编写的服务编译成so库后放在cpath目录下,当创建一个ctx时(skynet_context_new),通过名称在skynet_module里找对应的module(skynet_module_query),如果M->m存有同名称的module,返回即可。

如果第一次创建该名称的服务,先找到该名称对应的so库的路径,然后通过dlopen函数获取so库的访问句柄dl(try_open),再通过dlsym函数获取so库中xxx_create, xxx_init,xxx_release, xxx_signal4个函数地址(open_sym),将这些地址赋值给skynet_module->create, skynet_module->init, skynet_module->release, skynet_module->signal。通常一个c服务需要提供这4个接口,定义这些接口时一定要以服务名称为前缀,通过下划线和函数名称连接起来:

xxx_create:创建ctx过程中调用,通常是申请内存。返回该服务的实例inst,设置ctx->instance=inst,之后initreleasesignal都需要用到该实例

xxx_init:创建ctx期间调用,除了初始化,最主要的工作是向ctx注册callback函数(skynet_callback),之后ctx才能正确的处理收到的消息(调用callback函数)

xxx_release:释放ctx时调用(skynet_context_release)

xxx_signal:ctx收到信号时调用

最后将skynet_module保存到M->m里,之后创建同名称ctx就不用获取so库的访问句柄了。

三、服务

skynet是以服务为主体进行运作的,服务称作为skynet_context(简称ctx),是一个c结构,是skynet里最重要的结构,整个skynet的运作都是围绕ctx进行的。skynet_server提供的api主要分两大类:
1.对ctx的一系列操作,比如创建,删除ctx等
2.如何发送消息和处理自身的消息

ctx的结构如下,创建一个服务时,会构建一个skynet上下文skynet_context,然后把该ctx存放到handle_storage(skynet_handle.c)里。ctx有一个引用计数(ctx->ref)控制其生命周期,当引用计数为0,删除ctx,释放内存。

struct skynet_context { //一个skynet服务ctx的结构
        void * instance; //每个ctx自己的数据块,不同类型ctx有不同数据结构,相同类型ctx数据结构相同,但具体的数据不一样,由指定module的create函数返回
        struct skynet_module * mod; //保存module的指针,方便之后调用create,init,signal,release
        void * cb_ud; //给callback函数调用第二个参数,可以是NULL
        skynet_cb cb; //消息回调函数指针,通常在module的init里设置
        struct message_queue *queue; //ctx自己的消息队列指针
        FILE * logfile; //日志句柄
        uint64_t cpu_cost;      // in microsec
        uint64_t cpu_start;     // in microsec
        char result[32]; //保存skynet_command操作后的结果
        uint32_t handle; //标识唯一的ctx id
        int session_id; //本方发出请求会设置一个对应的session,当收到对方消息返回时,通过session匹配是哪一个请求的返回
        int ref; //引用计数,当为0,可以删除ctx
        int message_count; //累计收到的消息数量
        bool init; //标记是否完成初始化
        bool endless; //标记消息是否堵住
        bool profile; //标记是否需要开启性能监测

        CHECKCALLING_DECL // 自旋锁
};

为了统一对ctx操作的接口,采用指令的格式,定义了一系列指令(cmd_xxx),cmd_launch创建一个新服务,cmd_exit服务自身退出,cmd_kill杀掉一个服务等,上层统一调用skynet_command接口即可执行这些操作。对ctx操作,通常会先调用skynet_context_grab将引用计数+1,操作完调用skynet_context_release将引用计数-1,以保证操作ctx过程中,不会被其他线程释放掉。下面介绍几个常见的操作:

struct command_func { //skynet指令结构
        const char *name;
        const char * (*func)(struct skynet_context * context, const char * param);
};

static struct command_func cmd_funcs[] = { //skynet可接收的一系列指令
        { "TIMEOUT", cmd_timeout },
        { "REG", cmd_reg },
        { "QUERY", cmd_query },
        { "NAME", cmd_name },
        { "EXIT", cmd_exit },
        { "KILL", cmd_kill },
        { "LAUNCH", cmd_launch },
        { "GETENV", cmd_getenv },
        { "SETENV", cmd_setenv },
        { "STARTTIME", cmd_starttime },
        { "ABORT", cmd_abort },
        { "MONITOR", cmd_monitor },
        { "STAT", cmd_stat },
        { "LOGON", cmd_logon },
        { "LOGOFF", cmd_logoff },
        { "SIGNAL", cmd_signal },
        { NULL, NULL },
};

cmd_launch,启动一个新服务,最终会通过skynet_context_new创建一个ctx,初始化ctx中各个数据

struct skynet_context * 
skynet_context_new(const char * name, const char *param) { //启动一个新服务ctx
        struct skynet_module * mod = skynet_module_query(name); //从skynet_module获取对应的模板
        ...
        void *inst = skynet_module_instance_create(mod); //ctx独有的数据块,最终会调用c服务里的xxx_create
        ...
        ctx->mod = mod;
        ctx->instance = inst;
        ctx->ref = 2; //初始化完成会调用skynet_context_release将引用计数-1,ref变成1而不会被释放掉
        ...
        // Should set to 0 first to avoid skynet_handle_retireall get an uninitialized handle
        ctx->handle = 0;        
        ctx->handle = skynet_handle_register(ctx); //从skynet_handle获得唯一的标识id
        struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle); //初始化次级消息队列
        ...
        CHECKCALLING_BEGIN(ctx)
        int r = skynet_module_instance_init(mod, inst, ctx, param);//初始化ctx独有的数据块
        CHECKCALLING_END(ctx)
}

在skynet的main函数中有config.bootstrap = optstring("bootstrap","snlua bootstrap"),随后bootstrap(ctx, config->bootstrap),在这里会启动一个snlua服务,将snlua.so模块加载进来,snlua 是lua的沙盒服务,所有的 lua服务 都是一个 snlua 的实例。

snlua 实例化的过程:
这里我们来看一下 snlua 模块的实例化方法,源码在 service-src/service_snlua.c 中的 snlua_create(void) 函数:

struct snlua * snlua_create(void) {
    struct snlua * l = skynet_malloc(sizeof(*l));
    memset(l,0,sizeof(*l));
    l->mem_report = MEMORY_WARNING_REPORT;
    l->mem_limit = 0;
    //创建一个lua虚拟机(Lua State)
    l->L = lua_newstate(lalloc, l);
    return l;
}

最后返回的是一个通过 lua_newstate 创建出来的 Lua vm(lua虚拟机),也就是一个沙盒环境,这是为了达到让每个 lua服务 都运行在独立的虚拟机中。
上面的实例化步骤,只是生成了 lua服务 的运行沙盒环境,至于沙盒内运行的具体内容,是在初始化的时候才填充进来的,这里我们再来简单剖析一下初始化函数 snlua_init 的源码:

int snlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {
    int sz = strlen(args);
    //在内存中准备一个空间(动态内存分配)
    char * tmp = skynet_malloc(sz);
    //内存拷贝:将args内容拷贝到内存中的temp指针指向地址的内存空间
    memcpy(tmp, args, sz);
    //注册回调函数为launch_cb这个函数,有消息传入时会调用回调函数并处理
    skynet_callback(ctx, l , launch_cb);
    const char * self = skynet_command(ctx, "REG", NULL);
    //当前lua实例自己的句柄id(转为无符号长整型)
    uint32_t handle_id = strtoul(self+1, NULL, 16);
    // it must be first message
    // 给自己发送一条消息,内容为args字符串
    skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);
    return 0;
}

这个初始化函数主要完成了两件事:

  • 给当前服务实例注册绑定了一个回调函数 launch_cb;
  • 给本服务发送一条消息,内容就是之前传入的参数 bootstrap 。
    当此服务的消息队列被push进全局的消息队列后,本服务收到的第一条消息就是上述在初始化中给自己发送的那条消息,此时便会调用回调函数launch_cb并执行处理逻辑:
static int launch_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source , const void * msg, size_t sz) {
    assert(type == 0 && session == 0);
    struct snlua *l = ud;
    //将服务原本绑定的句柄和回调函数清空
    skynet_callback(context, NULL, NULL);
    //设置各项资源路径参数,并加载loader.lua
    int err = init_cb(l, context, msg, sz);
    if (err) {
        skynet_command(context, "EXIT", NULL);
    }

    return 0;
}

这个方法里把服务自己在C语言层面的回调函数给注销了,使它不再接收消息,目的是:在lua层重新注册它,把消息通过lua接口来接收。
紧接着执行init_cb方法:

  • 设置了一些虚拟机环境变量(主要是资源路径类的)
    同时把真正要加载的文件(此时是 bootstrap.lua)作为参数传给它,最终 bootstrap.lua 脚本会被加载并执行脚本中的逻辑, 控制权就开始转到lua层。

在bootstrap.lua执行了 skynet.start 这个接口,这也是所有 lua服务 的标准启动入口,服务启动完成后,就会调用这个接口,传入的参数就是一个function(方法),而且这个方法就是此 lua服务 的在lua层的回调接口,本服务的消息都在此回调方法中执行。

skynet.start 接口:
关于每个lua服务的启动入口 skynet.start 接口的实现代码在 service/skynet.lua 中:

function skynet.start(start_func)
    --重新注册一个callback函数,并且指定收到消息时由dispatch_message分发
    c.callback(skynet.dispatch_message)
    skynet.timeout(0, function()
        skynet.init_service(start_func)
    end)
end

具体如何实现回调方法的注册过程,需要查看c.callback这个C语言方法的底层实现,源码在 lualib-src/lua-skynet.c:

static int lcallback(lua_State *L) {
    struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));
    int forward = lua_toboolean(L, 2);
    luaL_checktype(L,1,LUA_TFUNCTION);
    lua_settop(L,1);
    lua_rawsetp(L, LUA_REGISTRYINDEX, _cb);

    lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_MAINTHREAD);
    lua_State *gL = lua_tothread(L,-1);

    if (forward) {
        skynet_callback(context, gL, forward_cb);
    } else {
        skynet_callback(context, gL, _cb);
    }

    return 0;
}

与上面snlua初始化中的一致,使用 skynet_callback 来实现回调方法的注册。

服务启动流程图:


参考文章
分布式高并发下,Actor模型如此优秀
skynet源码分析之skynet_module
skynet源码分析之skynet_server
Skynet服务器框架(二) C源码剖析启动流程
skynet start exit newservice

你可能感兴趣的:(skynet原理解析)