skynet底层源码阅读(8)-启动流程

来源:互联网 发布:诺基亚软件怎么下载 编辑:程序博客网 时间:2024/05/20 06:05

之前大体上复习了skynet的几个模块,现在可以分析skynet的启动流程了。skynet中编译生成了一个可执行程序,启动的时候./skynet config,其中,config是配置文件。在Skynet_main.c中,main函数如下:

intmain(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;}//初始化lua环境luaS_initshr();skynet_globalinit();skynet_env_init();//忽略SIGPIPEsigign();//包含配置项的结构体struct skynet_config config;//与lua相关的初始化 //新建lua状态机struct lua_State *L = luaL_newstate();//打开lua标准库luaL_openlibs(L);// link lua lib//luaL_loadstring函数,加载load_config代码int err = luaL_loadstring(L, load_config);assert(err == LUA_OK);//config_file  配置文件名,将其压入到栈中lua_pushstring(L, config_file);//lua_pcall(lua_State *L, int nargs, int nresults, int errfunc)//执行加载的lua脚本字符串,这将会加载config_file定义的lua脚本用于Skynet配置//调用了load_config,参数是  config_fileerr = lua_pcall(L, 1, 1, 0);if (err) {fprintf(stderr,"%s\n",lua_tostring(L,-1));lua_close(L);return 1;}//初始化 lua 环境_init_env(L);//加载配置项,从全局表中加载,如果不存在,会将变量存入的skynet_env_init();的lua全局表中 ,,初始化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);//会将logservrece:logger存入到环境中//返回的是   "logger"config.logservice = optstring("logservice", "logger");config.profile = optboolean("profile", 1);//关闭创建的Lua状态机lua_close(L);//传入配置参数并启动Skynet的各个组件和线程//这个函数定义在skynet_start.c文件中skynet_start(&config);//销毁线程全局变量//对应上面的skynet_globalinit(),用于删除线程存储keyskynet_globalexit();luaS_exitshr();return 0;

这部分代码涉及到lua和c代码的交互。

在Skynet_env.c中,定义了结构体:

struct skynet_env {//使用原子操作实现的锁,或者是pthread_mutex_t,struct spinlock lock;lua_State *L;};
用于保存lua环境。
//skynet 环境 配置,主要是获取lua的环境变量static struct skynet_env *E = NULL;
可以设置或者获取:

//获取的是 lua 的全局变量 key 值const char * skynet_getenv(const char *key) {//#define SPIN_LOCK(q) spinlock_lock(&(q)->lock);//加锁SPIN_LOCK(E)lua_State *L = E->L;//获取lua全局变量key的值,并压入lua栈, 在栈顶lua_getglobal(L, key);//从lua栈中弹出该变量值,并赋值给resultconst char * result = lua_tostring(L, -1);//弹出该变量值lua_pop(L, 1);//解锁SPIN_UNLOCK(E)return result;}//设置lua全局变量void skynet_setenv(const char *key, const char *value) {SPIN_LOCK(E)lua_State *L = E->L;//获取lua全局变量key的值,并压入lua栈lua_getglobal(L, key);//断言该变量值一定是空的assert(lua_isnil(L, -1));//弹出该变量值lua_pop(L,1);//将vaue压入lua栈lua_pushstring(L,value);//从lua栈中弹出value,将lua变量值设为value//key存放在lua全局表中lua_setglobal(L,key);SPIN_UNLOCK(E)}//初始化环境,创建 struct skynet_env  luaL_newstatevoidskynet_env_init() {E = skynet_malloc(sizeof(*E));//初始化锁SPIN_INIT(E)E->L = luaL_newstate();}
在Skynet_imp.h中,定义了skynet_config结构体,用于保存从配置文件读到的配置信息:

//skynet 的配置 信息struct skynet_config {int thread;//线程数int harbor;//harborint profile;const char * daemon;const char * module_path;  // 模块 即服务的路径 .so文件路径const char * bootstrap;const char * logger;//日志服务const char * logservice;};

在main函数中,启动了一个lua虚拟机,之后调用luaL_loadstring,使用lua编译了字符串:

static const char * load_config = "\local config_name = ...\local f = assert(io.open(config_name))\local code = assert(f:read \'*a\')\local function getenv(name) return assert(os.getenv(name), \'os.getenv() failed: \' .. name) end\code = string.gsub(code, \'%$([%w_%d]+)\', getenv)\f:close()\local result = {}\assert(load(code,\'=(load)\',\'t\',result))()\return result\";

之后调用lua_pushstring执行这段lua代码。这段lua代码宏 local config_name=...中的...就是开始时传入的配置文件路径,该代码读取配置文件。之后将配置项保存在skynet_evn和config中。之后调用skynet_start(&config);在Skynet_start.c中:

// skynet 启动的时候 初始化//在skynet_main.c的mian函数中调用void skynet_start(struct skynet_config * config) {// register SIGHUP for log file reopenstruct sigaction sa;//信号的回调函数sa.sa_handler = &handle_hup;sa.sa_flags = SA_RESTART;sigfillset(&sa.sa_mask);sigaction(SIGHUP, &sa, NULL);//config为保存配置参数变量的结构体if (config->daemon) {//初始化守护线进程,由配置文件确定是否启用。该函数在skynet_damenon.c中if (daemon_init(config->daemon)) {exit(1);}}//初始化节点模块,用于集群,转发远程节点的消息,该函数定义在skynet_horbor.c中skynet_harbor_init(config->harbor);//初始化句柄模块,用于给每个Skynet//初始化handler_storage,一个存储skynet_context指针的数组skynet_handle_init(config->harbor);//初始化全局的消息队列模块,这是Skynet的主要数据结构。这个函数定义在skynet_mq.c中skynet_mq_init();//初始化服务动态库加载模块,主要用于加载符合Skynet服务模块接口的动态链接库。//这个函数定义在skynet_module.c中//初始化全局的modules,实质就是有一个指针数组,缓存skynet_mudulesskynet_module_init(config->module_path);//初始化定时器模块,该函数定义在skynet_socket.c中//初始化 static struct timer * TI skynet_timer_init();//初始化网络模块。这个函数定义在skynet_socket.c 中//底层初始化了一个 socket_server结构体, 调用的epoll_create()函数skynet_socket_init();skynet_profile_enable(config->profile);//创建 skynet_context对象 ,加载日志模块            ("logger",NULL)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);}//加载引导模块。主要在Skynet配置文件中定义,默认为boostrap="snlua boostrap",表示引导//程序加载snlua.so模块,并有snlua服务启动boostrap.lua脚本。如果不使用snlua也可以直接启动//其他服务的动态库bootstrap(ctx, config->bootstrap);//创建monitor()监视线程,用create_thread()创建,create_thread()封装了系统函数pthread_create()//创建socket网络线程,//创建timer()定时器线程//创建worker()工作线程,工作线程的数量有Skynet配置文件中的thread=8定义。//config中保存了从配置文件读取的work线程数start(config->thread);// harbor_exit may call socket send, so it should exit before socket_freeskynet_harbor_exit();//释放网络模块skynet_socket_free();if (config->daemon) {//退出守护进程。daemon_exit(config->daemon);}}
在start()中,创建了工作线程,网络线程,时间线程

//创建线程static voidstart(int thread) {// 线程数+3 3个线程分别用于 _monitor _timer  _socket 监控 定时器 socket IOpthread_t pid[thread+3]; //创建监控线程的结构体struct monitor *m = skynet_malloc(sizeof(*m));memset(m, 0, sizeof(*m));m->count = thread;m->sleep = 0;//为 struct skynet_monitor *指针数组分配内存空间m->m = skynet_malloc(thread * sizeof(struct skynet_monitor *));int i;for (i=0;i<thread;i++) {//创建struct skynet_monitor,返回指针,数据都初始化为 0m->m[i] = skynet_monitor_new();}//初始化m的锁if (pthread_mutex_init(&m->mutex, NULL)) {fprintf(stderr, "Init mutex error");exit(1);}//初始化m的条件变量if (pthread_cond_init(&m->cond, NULL)) {fprintf(stderr, "Init cond error");exit(1);}//创建监控线程,底层调用的是pthread_create(),thread_monitor是回调函数,m是回调函数的参数create_thread(&pid[0], thread_monitor, m);//创建timer()定时器线程create_thread(&pid[1], thread_timer, m);//创建socket网络线程,create_thread(&pid[2], thread_socket, m);//大概就是,把工作线程分组,前四组每组8个,超过的归入第五组。?//A,E组每次调度处理一条消息,B组每次处理n/2条,C组每次处理n/4条,//D组每次处理n/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;//如果 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); }//销毁struct monitorfree_monitor(m);}
在skynet_start中,首先加载了日志服务,之后调用bootstrap,是比较关键的加载引导模块的代码,bootstrap(ctx, config->bootstrap);config->bootstrap默认是snlua boostrap
,

static voidbootstrap(struct skynet_context * logger, const char * cmdline) {//cmdline 为 snlua boostrapint sz = strlen(cmdline);char name[sz+1];// snluachar args[sz+1];// boostrapsscanf(cmdline, "%s %s", name, args);//加载模块 snlua  boostrapstruct skynet_context *ctx = skynet_context_new(name, args);if (ctx == NULL) {skynet_error(NULL, "Bootstrap error : %s\n", cmdline);//处理ctx的循环消息队列中的所有消息,调用 回调函数skynet_context_dispatchall(logger);exit(1);}}
创建服务
skynet_context_new(snlua,boostrap)
程序加载snlua.so模块,源代码在Service-src/Service_snlua.c中,创建函数如需下:

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;//创建虚拟机vm     lalloc是内存分配函数  l是每次调用lalloc是传入的一个参数l->L = lua_newstate(lalloc, l);return l;}
初始化函数如下:

//在skyney_context_new()中被调用int//boostrapsnlua_init(struct snlua *l, struct skynet_context *ctx, const char * args) {int sz = strlen(args);char * tmp = skynet_malloc(sz);memcpy(tmp, args, sz);//初始化struct skynet_context的回调函数指针,回调函数参数//即注册回调,l为参数 skynet_callback(ctx, l , launch_cb);//根据"REG"执行函数,为ctx命名,如果为null,并不是命名,而是获取handle的字符串形式 //self是对于的skynet_context中的handleconst char * self = skynet_command(ctx, "REG", NULL);//获得ctx对应的 handleuint32_t handle_id = strtoul(self+1, NULL, 16);//skynet_send(struct skynet_context * context, //                 uint32_t source, uint32_t destination , int type, int session, void * data, size_t sz)// it must be first message//发送消息,到自己的模块对应的skynet_context的消息队列中,处理消息时,会调用回调函数 ,消息就是"boostrap"skynet_send(ctx, 0, handle_id, PTYPE_TAG_DONTCOPY,0, tmp, sz);return 0;}
消息处理函数如launch_cb.并且向自己发送消息,之后便会调用launch_cb,
static intlaunch_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;//将回调函数指针设置为NULL,把自己的回调函数给注销了,使他不在接受消息,为的是在lua层重新注册他//吧消息通过lua接口来接受skynet_callback(context, NULL, NULL);//调用初始化函数init_cb()  设置各项资源路径参数,并加载loader.lua  ,msg是"boostrap"int err = init_cb(l, context, msg, sz);if (err) {skynet_command(context, "EXIT", NULL);}return 0;}
调用init_cb函数:

//初始化函数//设置一些虚拟机环境变量后,就加载执行了loader.lua文件,同时把真正要加载的文件//这个时候是bootsrap作为参数传递给他,控制权就开始转到lua层//loader.lua是用来加载lua文件的,在loader.lua中会判断是否需要preload,最终会加载执行bootsrap.lua文件//skynet_context的回调函数调用了这个函数//设置各项资源路径参数,并加载loader.luastatic int                                                       // "boostrap"init_cb(struct snlua *l, struct skynet_context *ctx, const char * args, size_t sz) {lua_State *L = l->L;l->ctx = ctx;//该函数控制垃圾收集器,LUA_GCSTOP表示 停止垃圾收集器lua_gc(L, LUA_GCSTOP, 0);lua_pushboolean(L, 1);  /* signal for libraries to ignore env. vars. *///设置注册表中  相当于 LUA_NOENV=1  LUA_REGISTRYINDEX注册表位置,是元表//lua_setfield是用来设置元表 lua_setfield(L, LUA_REGISTRYINDEX, "LUA_NOENV");luaL_openlibs(L);//轻量级usrdata,将指针入栈lua_pushlightuserdata(L, ctx);//设置注册表  {skynet_context=ctx}lua_setfield(L, LUA_REGISTRYINDEX, "skynet_context");luaL_requiref(L, "skynet.codecache", codecache , 0);//从栈中弹出一个元素lua_pop(L,1);//设置lua_pathconst char *path = optstring(ctx, "lua_path","./lualib/?.lua;./lualib/?/init.lua");lua_pushstring(L, path);//设置各项lua全局变量  LUA_PATH LUA_CPATH LUA_SERVICE LUA_PRELOAD // #define lua_setglobal(L,s)  lua_setfield(L, LUA_GLOBALSINDEX, s)  //从栈中弹出 path,设置全局变量 LUA_PATH=./lualib/?.lua;./lualib/?/init.lualua_setglobal(L, "LUA_PATH");//设置lua_cpathconst char *cpath = optstring(ctx, "lua_cpath","./luaclib/?.so");lua_pushstring(L, cpath); //从栈中弹出 cpath,设置全局变量 LUA_CPATH=./luaclib/?.solua_setglobal(L, "LUA_CPATH");  //设置 luaserviceconst char *service = optstring(ctx, "luaservice", "./service/?.lua");lua_pushstring(L, service); //从栈中弹出 service,设置全局变量 LUA_SERVICE=./service/?.lualua_setglobal(L, "LUA_SERVICE");// skynet_command 根据命令名称查找对应的回调函数,并且调用//为获取preloadconst char *preload = skynet_command(ctx, "GETENV", "preload");lua_pushstring(L, preload); //从栈中弹出 preload,设置全局变量 LUA_PRELOAD=lua_setglobal(L, "LUA_PRELOAD");//在这之前,保存sc(即skynet_context),加载配置项(config里的lua_path,lua_cpath,lua_server等)//将c函数入栈lua_pushcfunction(L, traceback);//返回栈定元素的索引,该索引值就是站中元素的个数,断言栈中只有一个元素assert(lua_gettop(L) == 1);const char * loader = optstring(ctx, "lualoader", "./lualib/loader.lua");//以bootsrap为参数,执行lualib/loader.lua:  loader的做用是以 cmd参数为名去各项代码目录查找lua文件//找到后loadfile并执行(等效于dofile)  //只编译,不运行  dofile运行代码//会将编译后代码作为一个类似于匿名函数的,压入栈顶//loader就是 ./lualib/loader.lua,该函数编译该lua文件//把一个编译好的代码块作为一个 Lua 函数压到栈顶int r = luaL_loadfile(L,loader);if (r != LUA_OK) {skynet_error(ctx, "Can't load %s : %s", loader, lua_tostring(L, -1));report_launcher_error(ctx);return 1;}//压入参数 //boostraplua_pushlstring(L, args, sz);//以bootsrap为参数,执行lualib/loader.luar = lua_pcall(L,1,0,1);if (r != LUA_OK) {skynet_error(ctx, "lua loader error : %s", lua_tostring(L, -1));report_launcher_error(ctx);return 1;}lua_settop(L,0);//在这之前,加载了一个lua的加载器脚本,用它来设置这些配置项,并运行入口脚本,各个配置项含义如下/*lua_service:lua服务(actor)所在的路径,与lua_path的语义一样,不占用lua本身的lua_path,但有一个小行为,当服务名不是一个文件名,而是一个目录名时,会把目录加入lua_path,也就是搜索路径为:/?/main.lua,服务名为foo,会把/foo/?.lua加入lua_path。这样为了不同服务之间的脚步加载发方便。//服务名来自init_cb的args,args[0]为服务名,后续为入口脚本的参数*//*lua_path,lua_cpath与lua本身的语义一致,加载器仅仅将他们赋值给package.path,package.cpathlua_preload:如果指定,则在运行入口脚本前先运行他之所以要用一个lua加载器来间接运行入口脚本,主要是为了实现起来方便,加载器最后还会写入两个全局变量:SERVICE_NAME(服务名),SERVICE_PATH(服务目录)*///第三步:看看脚本有没有设置memlimit(vm内存上限),如果有,就设置到snlua_ud,这样vm在分配内存时就可以//做判断。这个参数也只可能在这个时机里设置,因为后面snlua就打酱油了,再也回不到它的领空了if (lua_getfield(L, LUA_REGISTRYINDEX, "memlimit") == LUA_TNUMBER) {size_t limit = lua_tointeger(L, -1);l->mem_limit = limit;skynet_error(ctx, "Set memory limit to %.2f M", (float)limit / (1024 * 1024));lua_pushnil(L);lua_setfield(L, LUA_REGISTRYINDEX, "memlimit");}lua_pop(L, 1);lua_gc(L, LUA_GCRESTART, 0);//至此,一个lua服务就得以运行,入口脚本只需利用skynet的lua api注册一次回调函数,//那么就可以接管消息处理了return 0;}
该函数较为复杂,设计到lua与 c的交互。设置了lua虚拟机的环境后,加载执行lualib/load.lua,最终执行了Bootstrap.lua.  Service/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)--会根据standalone配置项判断你启动的是一个master节点还是slave节点local standalone = skynet.getenv "standalone"--断言已经启动的launcher服务local launcher = assert(skynet.launch("snlua","launcher"))--给launcher命名为.launcherskynet.name(".launcher", launcher)--通过harbor是否配置0来判断你是否启动的是一个单节点skynet网络--单节点模式下,是不需要通过内置的harbor机制做节点通信的,但为了兼容(因为你还是有可能--注册全局名字),需要启动一个叫做cdmmy的服务,他负责拦截对外广播的全局名字变更local harbor_id = tonumber(skynet.getenv "harbor" or 0)--如果是单节点if harbor_id == 0 thenassert(standalone ==  nil)standalone = true--设置为环境skynet.setenv("standalone", "true")--调用skynet.newservice("cdummy"),即启动服务local ok, slave = pcall(skynet.newservice, "cdummy")if not ok thenskynet.abort()endskynet.name(".cslave", slave)else--如果是多节点模式,对应master节点,需要启动cmaster服务做节点调度用,此外,每个节点(--包括master节点自己)都需要启动cslave服务,用于节点间的消息转发,以及头部全局名字if standalone then--启动cmaster服务if not pcall(skynet.newservice,"cmaster") thenskynet.abort()endendlocal ok, slave = pcall(skynet.newservice, "cslave")if not ok thenskynet.abort()endskynet.name(".cslave", slave)end--如果是单节点,启动服务if standalone thenlocal datacenter = skynet.newservice "datacenterd"skynet.name("DATACENTER", datacenter)end--然后启动用于UniqueService管理的service_mgrskynet.newservice "service_mgr"--最后,从config中读取start配置项,作为用户定义的服务启动入口脚本。成功后,把自己退出--默认为启动main--pcall 执行函数 pcall(函数,可变参数)--默认为启动mainpcall(skynet.newservice,skynet.getenv "start" or "main")skynet.exit()end)
最终执行了skynet.start。





原创粉丝点击