Skynet源码之:环境准备(3)

JavenLaw

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虚拟机,等待后面的流程会使用