分享web开发知识

注册/登录|最近发布|今日推荐

主页 IT知识网页技术软件开发前端开发代码编程运营维护技术分享教程案例
当前位置:首页 > 技术分享

Skynet服务器框架(二) C源码剖析启动流程

发布时间:2023-09-06 01:27责任编辑:赖小花关键词:暂无标签

引言:

之前我们已经完成了在Linux下配置安装skynet的环境,并成功启动了skynet服务框架,为了从底层更好地理解整个框架的实现过程,我们有必要剖析一下源码,由于底层的源码都是用C语言写的,lua脚本基本是用来进行业务层开发,所以我们从C源码开始解读框架。打开下载包的skynet-src目录,这里是skynet框架的核心C源码,接下来我们就要来解读skynet_main.cskynet_start.c这两个与skynet启动相关的C源码。

1.入口函数和初始化:

我们启动skynet使用的指令./skynet example/config实际上就是调用skynet-src/skynet_main.c脚本的入口main函数,调用时将config配置文件地址传入到函数中,并在此函数中完成:设置环境加载配置文件

//skynet_main.cint main(int argc, char *argv[]) { ???//保存config文件地址的变量 ???const char * config_file = NULL ; ???if (argc > 1) { ???????//读取配置文件config的地址,保存在config_file变量中 ???????config_file = argv[1]; ???} else { ???????//不传入config文件地址会提示错误并结束程序 ???????fprintf(stderr, "Need a config file. Please read skynet wiki : https://github.com/cloudwu/skynet/wiki/Config\n" ???????????"usage: skynet configfilename\n"); ???????return 1; ???} ???//初始化操作 ???luaS_initshr(); ???//全局初始化,为线程特有数据使用pthread_key_create()函数创建一个key,然后使用pthread_setspecific()函数为这个key设置value值 ???skynet_globalinit(); ???//初始化lua环境,创建一个全局数据结构struct skynet_env *E,并初始化结构的值 ???skynet_env_init(); ???//设置信号处理函数,用于忽略SIGPIPE信号的处理 ???sigign(); ???//创建启动skynet所需的必要配置信息结构数据 ???struct skynet_config config; ???//申请一个lua虚拟机 ???struct lua_State *L = luaL_newstate(); ???//链接一些必要的lua库到刚刚申请的lua虚拟机中 ???luaL_openlibs(L); ??// link lua lib ???//执行config配置文件在lua中的读取 ???int err = ?luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t"); ???assert(err == LUA_OK); ???//把C读取的config配置文件内容串压入栈顶 ???lua_pushstring(L, config_file); ???//执行栈顶的chunk,实际上就是加载config这个lua脚本字符串的内容 ???err = lua_pcall(L, 1, 1, 0); ???if (err) { ???????fprintf(stderr,"%s\n",lua_tostring(L,-1)); ???????lua_close(L); ???????return 1; ???} ???//初始化保存config信息的环境env ???_init_env(L); ???//通过skynet_getenv()接口从env中获取配置文件的信息(其实内部机制是通过lua_setglobal把之前压入栈顶的config_file转成lua中作为全局变量) ???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); ???//关闭上面创建的L(lua虚拟机) ???lua_close(L); ???//开始执行skynet的真是启动skynet服务程序的操作 ???skynet_start(&config); ???//对应上面的skynet_globalinit(),用于删除 线程存储的Key。 ???skynet_globalexit(); ???//对应上面的luaS_initshr() ???luaS_exitshr(); ???return 0;}
  • 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

2.配置信息结构体:

必要的数据被定义在一个skynet-src/skynet_imp.h中的skynet_config结构体内:

//skynet_imp.hstruct skynet_config { ???int thread; ????????????????//启动工作线程数量,不要配置超过实际拥有的CPU核心数 ???int harbor; ????????????????//skynet网络节点的唯一编号,可以是 1-255 间的任意整数。一个 skynet 网络最多支持 255 个节点。每个节点有必须有一个唯一的编号。如果 harbor 为 0 ,skynet 工作在单节点模式下。此时 master 和 address 以及 standalone 都不必设置。 ???int profile; ???????????????//是否开启统计功能,统计每个服务使用了多少cpu时间,默认开启 ???const char * daemon; ???????//后台模式:daemon = "./skynet.pid"可以以后台模式启动skynet(注意,同时请配置logger 项输出log) ???const char * module_path; ??//用 C 编写的服务模块的位置,通常指 cservice 下那些 .so 文件 ???const char * bootstrap; ????//skynet 启动的第一个服务以及其启动参数。默认配置为 snlua bootstrap ,即启动一个名为 bootstrap 的 lua 服务。通常指的是 service/bootstrap.lua 这段代码。 ???const char * logger; ???????//它决定了 skynet 内建的 skynet_error 这个 C API 将信息输出到什么文件中。如果 logger 配置为 nil ,将输出到标准输出。你可以配置一个文件名来将信息记录在特定文件中。 ???const char * logservice; ???//默认为 "logger" ,你可以配置为你定制的 log 服务(比如加上时间戳等更多信息)。可以参考 service_logger.c 来实现它。注:如果你希望用 lua 来编写这个服务,可以在这里填写 snlua ,然后在 logger 配置具体的 lua 服务的名字。在 examples 目录下,有 config.userlog 这个范例可供参考。};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

启动skynet服务程序:

skynet-src/skynet_main.cmain函数末尾,完成环境设置和配置信息加载之后,调用了skynet_start(&config);函数,这是在skynet-src/skynet_start.c中定义的,接下来我们来看一下实现的源码:

//skynet_start.cvoid 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); ???skynet_handle_init(config->harbor); ???skynet_mq_init(); ???skynet_module_init(config->module_path); ???skynet_timer_init(); ???skynet_socket_init(); ???skynet_profile_enable(config->profile); ???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); ???} ???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); ???}}
  • 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

代码解析:

  • 根据配置信息进行各个服务的初始化:
    使用->间接引用运算符,config是指向skynet_config结构体的指针,config->是引用结构体成员变量:

    //根据配置信息进行一系列初始化if (config->daemon) { ???//初始化守护进程 ???if (daemon_init(config->daemon)) { ???????exit(1); ???}}//初始化节点模块,用于集群,转发远程节点的消息skynet_harbor_init(config->harbor);//初始化句柄模块,用于给每个Skynet服务创建一个全局唯一的句柄值skynet_handle_init(config->harbor);//初始化消息队列模块,这是Skynet的主要数据结构skynet_mq_init();//初始化服务动态库加载模块,主要用于加载符合Skynet服务模块接口的动态链接库(.so)skynet_module_init(config->module_path);//初始化定时器模块skynet_timer_init();//初始化网络模块skynet_socket_init();//加载日志模块skynet_profile_enable(config->profile);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
  • 创建第一个模块logger服务的实例,并启动这个服务:
    使用skynet_context_new(...)函数实例化一个服务:

    struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
    • 1

    这里传入两个参数:参数一是加载模块的名称,参数二是初始化由模块生成的实例时所需的传入设置参数,下面是创建一个服务的具体流程:

    • 会从logger.so中把模块加载出来:

      struct skynet_module * mod = skynet_module_query(name);
      • 1
    • 让加载出来的模块自动生成一个新的实例:

      void *inst = skynet_module_instance_create(mod);
      • 1
    • 给新实例注册一个事件处理的handle

      ctx->handle = skynet_handle_register(ctx);
      • 1
    • 创建这个实例的消息队列:

      struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);
      • 1
    • 调用模块的初始化方法

      int r = skynet_module_instance_init(mod, inst, ctx, param);
      • 1
    • 将实例的消息队列加到全局的消息队列中,这样才能收到消息回调

      skynet_globalmq_push(queue);
      • 1
  • 加载bootstrap引导模块:

    bootstrap(ctx, config->bootstrap);
    • 1

    安装默认config的配置内容,config->bootstrap的内容就是一串字符串bootstrap = "snlua bootstrap",下面来看一下bootstrap函数的具体实现过程:

    static void ?bootstrap(struct skynet_context * logger, const char * cmdline) { ???//获取字符串长度 ???int sz = strlen(cmdline); ???char name[sz+1]; ???char args[sz+1]; ???//将传入的cmdline字符串按照格式分割成两部分,前部分模块名,后部分为模块初始化参数 ???sscanf(cmdline, "%s %s", name, args); ???//创建并启动指定模块的一个服务 ???struct skynet_context *ctx = skynet_context_new(name, args); ???//假如创建失败 ???if (ctx == NULL) { ???????//通过传入的logger服务接口构建错误信息假如logger消息队列 ???????skynet_error(NULL, "Bootstrap error : %s\n", cmdline); ???????//输出消息队列中的错误信息 ???????skynet_context_dispatchall(logger); ???????//结束程序 ???????exit(1); ???}}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    同样使用skynet_context_new()与上面启动logger服务一样,先把snlua.so模块加载进来,然后调用此模块自身的实例化方法,去实例化一个snlua服务,并传入要实例化的lua服务的脚本名称bootstarp,bootstrap会根据config中luaservice配置的目录去获取指定名称的lua脚本,按照默认目录最后会匹配到service/bootstrap.luasnlua是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;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    最后返回的是一个通过lua_newstate创建出来的Lua vm(lua虚拟机),也就是一个沙盒环境,这是为了达到让每个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;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

    • 给当前服务实例注册绑定了一个回调函数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;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    • 设置了一些虚拟机环境变量(紫瑶是资源路径类的):

      const char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");lua_pushstring(L, path);lua_setglobal(L, "LUA_PATH");const char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");lua_pushstring(L, cpath);lua_setglobal(L, "LUA_CPATH");const char *service = optstring(ctx, "luaservice", "./service/?.lua");lua_pushstring(L, service);lua_setglobal(L, "LUA_SERVICE");const char *preload = skynet_command(ctx, "GETENV", "preload");lua_pushstring(L, preload);lua_setglobal(L, "LUA_PRELOAD");
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
    • 加载执行了lualib\loader.lua文件:

      const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");
      • 1

      loader的作用是以cml参数为名去各项代码目录查找lua文件,找到后loadfile并执行(等效于dofile)。

    • 同时把真正要加载的文件(此时是bootstrap.lua)作为参数传给它,最终bootstrap.lua脚本会被加载并执行脚本中的逻辑,控制权就开始转到lua层。


Lua脚本逻辑起点:

完成上述的所有底层C语言逻辑之后,我们开始执行lua层的业务逻辑,起点就是上述最后加载和执行的bootstrap.lua,打开脚本,脚本内容如下:

local skynet = require "skynet"local harbor = require "skynet.harbor"require "skynet.manager" ???-- import skynet.launch, ...local memory = require "memory"skynet.start(function() ???local sharestring = tonumber(skynet.getenv "sharestring" or 4096) ???memory.ssexpand(sharestring) ???local standalone = skynet.getenv "standalone" ???local launcher = assert(skynet.launch("snlua","launcher")) ???skynet.name(".launcher", launcher) ???local harbor_id = tonumber(skynet.getenv "harbor" or 0) ???if harbor_id == 0 then ???????assert(standalone == ?nil) ???????standalone = true ???????skynet.setenv("standalone", "true") ???????local ok, slave = pcall(skynet.newservice, "cdummy") ???????if not ok then ???????????skynet.abort() ???????end ???????skynet.name(".cslave", slave) ???else ???????if standalone then ???????????if not pcall(skynet.newservice,"cmaster") then ???????????????skynet.abort() ???????????end ???????end ???????local ok, slave = pcall(skynet.newservice, "cslave") ???????if not ok then ???????????skynet.abort() ???????end ???????skynet.name(".cslave", slave) ???end ???if standalone then ???????local datacenter = skynet.newservice "datacenterd" ???????skynet.name("DATACENTER", datacenter) ???end ???skynet.newservice "service_mgr" ???pcall(skynet.newservice,skynet.getenv "start" or "main") ???skynet.exit()end)
  • 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

源码剖析:

这里执行了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
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    具体如何实现回调方法的注册过程,需要查看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;}
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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


总结:

跟随逻辑去查看源码,大致了解到skynet服务框架的启动实现流程大致为:

  • 加载配置文件 -> 配置文件存入lua的全局变量evn-> 创建和启动C服务logger-> 启动引导模块并启动第一个lua服务(例如:bootstrap)。

第一个启动的lua服务其实都会由config配置文件中的bootstrap配置项所决定的,可以根据项目实际情况进行修改,当然也可以保持默认设置,保持使用bootstrap作为第一个lua服务,直接或间接地去启动其他的lua服务。

Skynet服务器框架(二) C源码剖析启动流程

原文地址:http://www.cnblogs.com/decode1234/p/7905582.html

知识推荐

我的编程学习网——分享web前端后端开发技术知识。 垃圾信息处理邮箱 tousu563@163.com 网站地图
icp备案号 闽ICP备2023006418号-8 不良信息举报平台 互联网安全管理备案 Copyright 2023 www.wodecom.cn All Rights Reserved