Skynet环境准备的内容主要分为以下几方面
1,全局初始化函数:skynet_globalinit(),用于记录服务的总数量、初始化线程并设置信息
2,环境初始化函数:skynet_env_init(),建立env Lua虚拟机,用于存储配置信息
3,使用temp Lua虚拟机对配置文件进行解析、读取
4,对Skynet进程进行信号设置
5,luaL_initcodecache()对服务间Lua代码共享做一些设置
6,把temp Lua虚拟机中的配置,传递给 env Lua虚拟机 和 skynet_config结构体中
第一,全局初始化做了什么
skynet_globalinit() 函数在 skynet_main.c 文件中,第128行开始调用
static struct skynet_node G_NODE; // 全局单例
struct skynet_node {
// 声明一个原子字段,用于记录服务的总数量
// 每当一个服务创建、退出都会改变这个字段
// ATOM_INT是Skynet封装的一个原子操作的对象,详细请看《Skynet专题之:原子操作》
ATOM_INT total;
int init; // 标记是否完成初始化
uint32_t monitor_exit; // 标记监视器线程是否退出
pthread_key_t handle_key; // 声明线程私有数据,详细请看《Skynet专题之:线程》
bool profile; // default is on
};
void
skynet_globalinit(void) {
// 初始化原子操作
// ATOM_INIT()是Skynet封装的一个原子操作的函数,详细请看《Skynet专题之:原子操作》
ATOM_INIT(&G_NODE.total, 0);
G_NODE.monitor_exit = 0;
G_NODE.init = 1;
// 创建线程私有数据
// pthread_key_create() 用于创建一个多线程私有数据,详细请看《Skynet专题之:线程》
if (pthread_key_create(&G_NODE.handle_key, NULL)) {
fprintf(stderr, "pthread_key_create failed");
exit(1);
}
// set mainthread's key
// 头文件skynet_imp.h
//
// #define THREAD_WORKER 0
// #define THREAD_MAIN 1
// #define THREAD_SOCKET 2
// #define THREAD_TIMER 3
// #define THREAD_MONITOR 4
skynet_initthread(THREAD_MAIN);
}
// 初始化线程
void
skynet_initthread(int m) {
uintptr_t v = (uint32_t)(-m);
// 设置线程私有数据
// pthread_setspecific() 用于设置一个多线程私有数据,详细请看《Skynet专题之:线程》
// 为什么要设置线程私有数据?
// 详情请看《Skynet源码之:进程启动》中的 dispatch_message() 函数分析
pthread_setspecific(G_NODE.handle_key, (void *)v);
}
// 读取数据
int
skynet_context_total() {
return ATOM_LOAD(&G_NODE.total);
}
// 把数值+1
// 每当有服务启动时,就会把total+1
static void
context_inc() {
ATOM_FINC(&G_NODE.total);
}
// 把数值-1
// 每当有服务退出时,就会把total-1
static void
context_dec() {
ATOM_FDEC(&G_NODE.total);
}
// ATOM_LOAD,ATOM_FINC,ATOM_FDEC的知识,详细请看《Skynet专题之:原子操作》
第二,环境初始化做了什么
skynet_env_init() 函数在 skynet_main.c 文件中,第129行开始调用
static struct skynet_env *E = NULL; // 全局单例
struct skynet_env {
struct spinlock lock; // 自旋锁
lua_State *L; // 新建了一个env虚拟机,用来存储key-value的配置
};
void
skynet_env_init() {
E = skynet_malloc(sizeof(*E));
SPIN_INIT(E)
E->L = luaL_newstate();
}
// 读取环境配置
// 根据key键,获取value
const char *
skynet_getenv(const char *key) {
SPIN_LOCK(E)
lua_State *L = E->L;
lua_getglobal(L, key);
const char * result = lua_tostring(L, -1);
lua_pop(L, 1);
SPIN_UNLOCK(E)
return result;
}
// 设置环境配置
// 根据key键,设置value
void
skynet_setenv(const char *key, const char *value) {
SPIN_LOCK(E)
lua_State *L = E->L;
lua_getglobal(L, key);
assert(lua_isnil(L, -1));
lua_pop(L, 1);
lua_pushstring(L, value);
lua_setglobal(L, key);
SPIN_UNLOCK(E)
}
第三,配置读取是如何实现的
int
main(int argc, char *argv[]) {
// 读取用户传入的启动参数,例如:./file_peth/config
// 传入的参数 config_file = argv[1] 保存的就是用户传入的 [配置文件路径]
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;
}
skynet_globalinit(); // 属于第1部分,全局初始化
skynet_env_init(); // 属于第2部分,环境初始化
sigign(); // 属于第4部分,进程的信号设置
// *********************************第3部分,用于 配置读取 的代码*********************************
struct skynet_config config; // 在skynet/skynet-src/skynet_imp.h 文件中定义
// 初始化Lua的代码缓存功能
// 看第5部分
#ifdef LUA_CACHELIB
luaL_initcodecache();
#endif
struct lua_State *L = luaL_newstate(); // 新建一个temp虚拟机
luaL_openlibs(L); // 连接Lua库
// 参看 代码段1 文件,本质就是一个lua文件,转为字符串存储在文件中, 可以查看load_config.lua
// luaL_loadbufferx() 函数是Lua C API中的一个函数,用于将Lua代码加载到Lua虚拟机中并编译为可执行的函数
//
// 函数原型
// int luaL_loadbufferx(lua_State *L, const char *buff, size_t size, const char *name, const char *mode);
// L:Lua虚拟机的状态对象指针
// buff:包含Lua代码的缓冲区
// size:缓冲区的大小(字节数)
// name:代码块的名称,用于错误信息和调试目的
// mode:代码块的模式,可以是字符串 "t"(文本模式)或 "b"(二进制模式)
int err = luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t");
assert(err == LUA_OK);
// lua_pushstring() 是Lua C API中的一个函数,用于将一个C字符串(以零结尾的字符数组)压入Lua栈中作为一个Lua字符串对象
//
// 函数原型:
// void lua_pushstring(lua_State *L, const char *s)
// L:Lua虚拟机的状态对象指针
// s:要压入栈中的C字符串
lua_pushstring(L, config_file);
// lua_pcall() 是Lua C API中的一个函数,用于在保护模式下调用Lua函数
//
// 函数原型:
// int lua_pcall(lua_State *L, int nargs, int nresults, int errfunc)
// L:Lua虚拟机的状态对象指针
// nargs:传递给被调用函数的参数数量
// nresults:期望从被调用函数中返回的结果数量
// errfunc:错误处理函数在栈中的索引位置。如果为0,则使用默认的错误处理函数
// 总结:
// luaL_loadbufferx() 函数将 load_config 字符串中的 Lua 代码加载到 Lua 虚拟机中,并将其编译为 Lua 函数
// 然后,使用 lua_pushstring() 函数将 config_file 字符串压入栈中作为参数传递给 Lua 函数
// 也就是说等于执行命令:lua load_config.lua config_file
//
// 验证:请看代码段1 和 验证结果
err = lua_pcall(L, 1, 1, 0);
if (err) {
fprintf(stderr,"%s\n",lua_tostring(L,-1));
lua_close(L);
return 1;
}
// 根据代码段1 和 验证结果得知
// 上面的代码就是把读取配置文件中的数据,把数据存储为Lua中table的key-value格式
// 上面通过Lua语言,把配置文件的内容解析,读取出来了
// 现在配置数据还是放在temp虚拟机中的
// 我们接着看 代码段2 _init_env的实现
// 其实就是把各项key-value配置,存储到前面初始化好的,专门用来保存配置的env虚拟机中
_init_env(L);
// 这里特别注意:
// _init_env()是把配置中各项key-value配置都读取到env虚拟机中了
// 也就是说,无论你设置什么key-value,都会被读取并存储,例如 china = "中国",也会被存储
// 而下面的 config 操作,只是确保配置中,一定会有对应的配置
// 即下面的 config,不代表全部的配置,而仅仅是必须,不可缺少的配置而已
// 并且这个 config 是个C结构体
// 代码段4,看看optstring(),optint(),optboolean()的实现
// struct skynet_config {
// int thread;
// int harbor;
// int profile;
// const char * daemon;
// const char * module_path;
// const char * bootstrap;
// const char * logger;
// const char * logservice;
// };
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_close(L);
// *********************************第1部分,用于 配置读取 的代码*********************************
skynet_start(&config); // skynet正式启动
skynet_globalexit();
return 0;
}
代码段1
local result = {}
-- 获取相应的环境变量
local function getenv(name)
return assert(os.getenv(name), [[os.getenv() failed: ]] .. name)
end
-- 获取路径的分隔符,在 linux 下 sep = /
local sep = package.config:sub(1,1)
-- 将 . 和 / 合并得到了当前目录的相对路径 ./
local current_path = [[.]] .. sep
local function include(filename)
local last_path = current_path
-- 以最后一个/为分界将 filename 分割为路径 path 和文件名 name
local path, name = filename:match([[(.*]] .. sep .. [[)(.*)$]])
if path then
-- 若 path 为绝对路径,则起始字符为 /
if path:sub(1,1) == sep then -- root
current_path = path
else
-- path 为相对路径的情况
current_path = current_path .. path
end
else
-- 若 path 为 nil,则说明 filename 不包含路径
name = filename
end
-- 打开配置文件
local f = assert(io.open(current_path .. name))
-- 读取配置文件中的所有内容
local code = assert(f:read [[*a]])
-- 如果配置文件中存在形如 $(环境变量) 的字符串,则调用 getenv 将其替换成环境变量的值
code = string.gsub(code, [[%$([%w_%d]+)]], getenv)
-- 关闭配置文件
f:close()
-- 将 code 中的内容以文本的形式加载到 result,其中 t 代表文本模式
assert(load(code,[[@]]..filename,[[t]],result))()
current_path = last_path
end
-- 设置 result 的元表,这样在调用 include 的过程中,如果 result 中没有对应的键则会自动调用 include 函数
setmetatable(result, { __index = { include = include } })
-- config_name 是可变长参数
local config_name = ...
-- 使用 include 调用 config_name 脚本
include(config_name)
-- 调用完 include 后将 result 的元表清除,避免遍历 result 时受到元表的影响
setmetatable(result, nil)
-- 打印验收一下内容
for k,v in pairs(result) do
print("k==v",k,v)
end
return result
验证结果
[root@localhost examples]# pwd /root/skynet/examples [这个是目录] [root@localhost examples]# [root@localhost examples]# [root@localhost examples]# lua load_config.lua ./config [这个是命令] k==v snax ./examples/?.lua;./test/?.lua k==v lualoader ./lualib/loader.lua k==v lua_path ./lualib/?.lua;./lualib/?/init.lua k==v master 127.0.0.1:2013 k==v thread 8 k==v lua_cpath ./luaclib/?.so k==v cpath ./cservice/?.so k==v standalone 0.0.0.0:2013 k==v bootstrap snlua bootstrap k==v start main k==v address 127.0.0.1:2526 k==v luaservice ./service/?.lua;./test/?.lua;./examples/?.lua;./test/?/init.lua k==v root ./ k==v logpath . k==v harbor 1
[以上是结果]
代码段2
static void
_init_env(lua_State *L) {
// 在每次调用 lua_next 之前,需要将一个键压入栈中作为下一个键值对的起始点
lua_pushnil(L);
// 关于 lua_next() 函数的使用,详细请看《Skynet涉及的Lua语言知识》
// 使用 lua_next(L, -2) 函数遍历表中的键值对
// 该函数会将栈顶的键值对弹出,并将下一个键值对压入栈中
// 如果遍历结束,则返回 0
while (lua_next(L, -2) != 0) {
int keyt = lua_type(L, -2);
if (keyt != LUA_TSTRING) {
fprintf(stderr, "Invalid config table\n");
exit(1);
}
const char * key = lua_tostring(L,-2); // 读取key
if (lua_type(L,-1) == LUA_TBOOLEAN) {
int b = lua_toboolean(L,-1);
// 重点代码,看代码段3
skynet_setenv(key, b ? "true" : "false" ); // 把读取到的配置,设置到前面创建的env虚拟机
} else {
const char * value = lua_tostring(L,-1); // 读取value
if (value == NULL) {
fprintf(stderr, "Invalid config table key = %s\n", key);
exit(1);
}
// 重点代码,看代码段3
skynet_setenv(key,value); // 把读取到的配置,设置到前面创建的env虚拟机
}
lua_pop(L,1);
}
lua_pop(L,1);
}
// 用于从 Lua 表中读取配置信息,并将其设置为环境变量
// 函数的作用是遍历 Lua 表(在栈顶),读取表中的键值对,并根据键值的类型设置相应的环境变量
代码段3
void
skynet_setenv(const char *key, const char *value) {
SPIN_LOCK(E)
lua_State *L = E->L;
lua_getglobal(L, key);
assert(lua_isnil(L, -1));
lua_pop(L, 1);
lua_pushstring(L, value);
lua_setglobal(L, key);
SPIN_UNLOCK(E)
}
代码段4
// 处理整型的值
static int
optint(const char *key, int opt) {
const char * str = skynet_getenv(key); // 从env虚拟机中读取配置
if (str == NULL) { //若是没有,表示用户的config文件没有配置
char tmp[20];
sprintf(tmp, "%d", opt);
skynet_setenv(key, tmp); // 设置skynet默认的配置
return opt; // 直接返回的是c代码中的默认配置
}
return strtol(str, NULL, 10);
}
// 处理布尔型的值
static int
optboolean(const char *key, int opt) {
const char * str = skynet_getenv(key);
if (str == NULL) {
skynet_setenv(key, opt ? "true" : "false");
return opt;
}
return strcmp(str, "true")==0;
}
// 处理字符串的值
static const char *
optstring(const char *key,const char * opt) {
const char * str = skynet_getenv(key);
if (str == NULL) {
if (opt) {
skynet_setenv(key, opt);
opt = skynet_getenv(key);
}
return opt;
}
return str;
}
总结一下
首先开启了一个env虚拟机
接着开启了一个temp虚拟机,利用temp虚拟机来解析config配置文件,并把配置数据存在env虚拟机中
(使用temp虚拟机的作用是利用Lua来解析配置文件,比C语言解析配置文件比较方便)
最后又逐一从env虚拟机中读取配置,并把这份配置赋值到C语言的skynet_config结构体中
最后temp虚拟机销毁,env虚拟机保存配置,C语言的结构体传递给程序继续使用
第四,信号设置做了什么
sigign()函数在 skynet_main.c 文件中,第131行开始调用
// 这部分的知识都是信号,详细见《Skynet专题之:信号》
int sigign() {
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sa.sa_flags = 0;
sigemptyset(&sa.sa_mask);
sigaction(SIGPIPE, &sa, 0);
return 0;
}
第五,luaL_initcodecache()
luaL_initcodecache() 首次被调用出现在 skynet_main.c 文件中的第137行
// main.c文件的include包含
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
// 其他代码省略
#ifdef LUA_CACHELIB
// init the lock of code cache
luaL_initcodecache();
#endif
LUA_CACHELIB的作用
背景
我们根据资料《Skynet框架分解1》知道:Skynet不同的业务,通过不同的服务来实现,而每个服务都运行在独立的Lua虚拟机
这会造成一个问题:同样的一份代码,每个Lua虚拟机都需要独立加载一份到内存,对于成千上万的服务来说,这样会造成极大的内存浪费
因此,云风的优化是:更改Lua虚拟机的代码,可以让每个虚拟机共用一份代码,这样便可以极大地减少内存的占用
但因为修改了Lua虚拟机实现的部分代码,因此在使用上会有一些差别:
它改写了Lua的辅助API : luaL_loadfilex,所有直接或间接调用这个API都会受其影响,比如:loadfile 、require等
它以文件名做key ,一旦检索到之前有加载过相同文件名的lua文件,则从内存中找到之前的函数原型替代
参考:CodeCache · cloudwu/skynet Wiki · GitHub
总的来说,如果你要使用Skynet,一般都不会去修改这部分的代码,因为这已经是Skynet设计的基础之一
如果贸然去修改,那肯定是会影响内存占用情况或者引起其他的未知的原因
但云风依然给你保留了选择,选择官方的Lua的虚拟机:
请查看 Build · cloudwu/skynet Wiki · GitHub 介绍页面中的 About Lua 部
在skynet/3rd/lua/文件夹下
// 头文件
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
// c文件
lauxlib.c
在 lualib.h 头文件中,定义了 LUA_CACHELIB
#define LUA_CACHELIB
LUAMOD_API int (luaopen_cache) (lua_State *L);
LUALIB_API void (luaL_initcodecache) (void);
/* open all previous libraries */
LUALIB_API void (luaL_openlibs) (lua_State *L);
因此我们的函数 luaL_initcodecache() 是会执行的
在 lauxlib.c 文件中,实现了 luaL_initcodecache() 代码
static struct codecache CC; // 声明一个全局单例
// 声明函数
void luaL_initcodecache(void);
// 定义函数
LUALIB_API void
luaL_initcodecache(void) {
SPIN_INIT(&CC); // 初始化自旋锁
}
// 代码缓存,实质就是一个 自旋锁 + Lua虚拟机
struct codecache {
struct spinlock lock;
lua_State *L;
};
现在我们知道,luaL_initcodecache() 函数就是把缓存代码的Lua虚拟机加上一个自旋锁
缓存代码的Lua虚拟机,等待后面的流程会使用