2-logger服务
新入门skynet系列视频b站网址 https://www.bilibili.com/video/BV19d4y1678X
首先我们总是要写日志的。谁还不喜欢写日志呢。我们经常这样使用
skynet.error("hello world")
上面的代码就是在写日志。默认是写到stdout。当然前提是 日志服务 要先创建。之后写日志主要分两步:
- 把日志转变成 skynet_message 然后push到日志服务队列
- 日志服务处理消息时把日志提取出来,写入文件或者标准输出。
logger服务创建
那么logger服务是怎么来的呢?我们这次从skynet进程的main函数来看。我们来到 skynet_main.c文件。看main函数
int
main(int argc, char *argv[]) {
struct skynet_config config;
struct lua_State *L = luaL_newstate();//生成luastate的目的主要是读取配置文件中的配置项
luaL_openlibs(L); // link lua lib
int err = luaL_loadbufferx(L, load_config, strlen(load_config), "=[skynet config]", "t");
assert(err == LUA_OK);
lua_pushstring(L, config_file);
err = lua_pcall(L, 1, 1, 0);//执行配置文件
_init_env(L);
//把配置文件里面的配置项 都读出来
config.logger = optstring("logger", NULL);//保存日志的文件名字
config.logservice = optstring("logservice", "logger");//默认日志模块是 logger 即 service_logger.c 所代表的模块
config.profile = optboolean("profile", 1);
lua_close(L);
skynet_start(&config);//next
skynet_globalexit();
return 0;
}
【skynet_start的代码】
void
skynet_start(struct skynet_config * config) {//next
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();
//这里创建日志服务
struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);//参数分别是 "logger" NULL
skynet_handle_namehandle(skynet_context_handle(ctx), "logger");//给日志服务绑定一个名字
//...
}
skynet_context_new 就是在创建一个服务。【skynet_context_new 的代码】
struct skynet_context *
skynet_context_new(const char * name, const char *param) {
struct skynet_module * mod = skynet_module_query(name);//获取模块
void *inst = skynet_module_instance_create(mod);//根据模块创建对应实例
struct skynet_context * ctx = skynet_malloc(sizeof(*ctx));//内存分配
CHECKCALLING_INIT(ctx)
ctx->mod = mod;//模块
ctx->instance = inst;//模块的实例
ATOM_INIT(&ctx->ref , 2);//初始化引用数为 2
ctx->cb = NULL;
ctx->cb_ud = NULL;
ctx->session_id = 0;
// Should set to 0 first to avoid skynet_handle_retireall get an uninitialized handle
ctx->handle = 0; //不设置为的话 是一个随机值
ctx->handle = skynet_handle_register(ctx);
struct message_queue * queue = ctx->queue = skynet_mq_create(ctx->handle);
// init function maybe use ctx->handle, so it must init at last
context_inc();
CHECKCALLING_BEGIN(ctx)
int r = skynet_module_instance_init(mod, inst, ctx, param);
CHECKCALLING_END(ctx)
if (r == 0) {
struct skynet_context * ret = skynet_context_release(ctx);//减少引用一次
if (ret) {
ctx->init = true;
}
skynet_globalmq_push(queue);//服务队列加入全局队列
return ret;
}
}
skynet_context_new函数的第一行就是通过名字获取模块.
模块实际上就是一些函数集合在一起,用来专门实现某种功能的。我们要用到一个模块,一般是这样使用的。
0 获取到这个模块 m
1 拿出这个模块的实例化函数,首先实例化一个对象。比如 inst = m.create()就是创建了一个实例
2 然后使用这个模块的函数处理这个实例化对象。比如 m.init(inst,param) m.xxx(inst,param) m.yyy(inst,param)
3 用完后,就可以释放这个实例了 比如 m.relaase(inst)
skynet里面什么是模块?
我们main函数在初始化读取配置信息时,有如下代码
config.module_path = optstring("cpath","./cservice/?.so");//默认路径下的动态链接库有gate.so harbor.so logger.so snlua.so 常用snlua.so来创建模块实例
也就是说我们在config.module_path
目录下有一些 以so为后缀的文件。xxx.so就对应一个模块。假设这个模块第一次被查询。skynet_module_query(name)
获取模块的具体步骤是:根据名字读取指定路径下的 xxx.so文件。然后把文件里面的函数提取出来作为一个模块缓存起来。 然后返回这个模块。
static int
open_sym(struct skynet_module *mod) {
mod->create = get_api(mod, "_create");//创建模块的实例
mod->init = get_api(mod, "_init");//对实例进行初始化
mod->release = get_api(mod, "_release");
mod->signal = get_api(mod, "_signal");
return mod->init == NULL;
}
struct skynet_module *
skynet_module_query(const char * name) {//next
struct skynet_module * result = _query(name);//查询
if (result)//说明不是第一次查询 因为已经缓存了 则直接返回
return result;
SPIN_LOCK(M)
result = _query(name); // double check
//如果是第一次查询
if (result == NULL && M->count < MAX_MODULE_TYPE) {//这个模块是第一次被查询
int index = M->count;//M可以认为是一个模块集合, 里面保存了所有模块 index是为新添加的模块分配一个位置
void * dl = _try_open(M,name);//打开这个模块对应的so文件
if (dl) {
M->m[index].name = name;//设置这个模块的名字
M->m[index].module = dl;//设置这个模块对应的动态库
if (open_sym(&M->m[index]) == 0) {//用动态库里面的函数填充模块
M->m[index].name = skynet_strdup(name);
M->count ++;
result = &M->m[index];//result是返回结果 即模块
}
}
}
SPIN_UNLOCK(M)
return result;
}
显然我们这里获取的是 logger.so 对应的模块。 查看skynet_context_new 代码 接下来是实例化模块。
void *
skynet_module_instance_create(struct skynet_module *m) {//next
if (m->create) {
return m->create();//next
} else {
return (void *)(intptr_t)(~0);
}
}
当前模块里面的 create函数 是从logger.so提取出来,而logger.so文件是下面这个文件编译出来的。
也就是说模块里面的函数其实都是在 service_logger.c 这里定义的。所以实例化logger模块就是调用下面 logger_create函数。最终就是内存分配了一个struct logger
struct logger {
FILE * handle;
char * filename;
uint32_t starttime;
int close;
};
struct logger *
logger_create(void) {//next
struct logger * inst = skynet_malloc(sizeof(*inst));
inst->handle = NULL;
inst->close = 0;
inst->filename = NULL;
return inst;
}
查看skynet_context_new代码 ,我们看到实例化模块后, 创建了一个 skynet_context。每一个服务都会分配一个handle,每个服务的队列也会保存这个handle。skynet_handle_register就是给去注册服务并且获取分配的handle
uint32_t//next
skynet_handle_register(struct skynet_context *ctx) {//注册一个服务 返回为这个服务分配的handle
struct handle_storage *s = H;
rwlock_wlock(&s->lock);
for (;;) {
int i;
uint32_t handle = s->handle_index;
for (i=0;i<s->slot_size;i++,handle++) {
if (handle > HANDLE_MASK) {//我们handle的高八位是留给harbor的 所以handle用来查找槽位是靠剩下的24位
// 0 is reserved
handle = 1;
}
int hash = handle & (s->slot_size-1);
if (s->slot[hash] == NULL) {
s->slot[hash] = ctx;
s->handle_index = handle + 1;//下一次分配handle就从这个值开始计算
rwlock_wunlock(&s->lock);
handle |= s->harbor;
return handle;//返回值由两部分合并而成
}
}
assert((s->slot_size*2 - 1) <= HANDLE_MASK);//下面这段代码是扩容
struct skynet_context ** new_slot = skynet_malloc(s->slot_size * 2 * sizeof(struct skynet_context *));
memset(new_slot, 0, s->slot_size * 2 * sizeof(struct skynet_context *));
for (i=0;i<s->slot_size;i++) {//把老槽位里面的服务数据转移到合适的新的槽位中
int hash = skynet_context_handle(s->slot[i]) & (s->slot_size * 2 - 1);
assert(new_slot[hash] == NULL);
new_slot[hash] = s->slot[i];
}
skynet_free(s->slot);
s->slot = new_slot;
s->slot_size *= 2;
}
}
返回的handle是32位的 ,主要是两部分组成。高8位的habor+剩余24位。剩余的24位才可以认为是ctx所在的槽位。
查看skynet_context_new代码 接下来是创建一个队列 skynet_mq_create
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 message_queue *
skynet_mq_create(uint32_t handle) {
struct message_queue *q = skynet_malloc(sizeof(*q));
q->handle = handle;
q->cap = DEFAULT_QUEUE_SIZE;//64
q->head = 0;
q->tail = 0;
SPIN_INIT(q)
// When the queue is create (always between service create and service init) ,
// set in_global flag to avoid push it to global queue .
// If the service init success, skynet_context_new will call skynet_mq_push to push it to global queue.
q->in_global = MQ_IN_GLOBAL;
q->release = 0;
q->overload = 0;//记录过载状态时的负载是多少
q->overload_threshold = MQ_OVERLOAD;//过载的警戒线
q->queue = skynet_malloc(sizeof(struct skynet_message) * q->cap);
q->next = NULL;
return q;
}
之后是对模块的实例进行初始化skynet_module_instance_init
int
skynet_module_instance_init(struct skynet_module *m, void * inst, struct skynet_context *ctx, const char * parm) {
return m->init(inst, ctx, parm);
}
这里依旧去service_logger.c去找对应的函数 logger_init
int
logger_init(struct logger * inst, struct skynet_context *ctx, const char * parm) {//next
const char * r = skynet_command(ctx, "STARTTIME", NULL);
inst->starttime = strtoul(r, NULL, 10);
if (parm) {
inst->handle = fopen(parm,"a");//这里的handle是打开的系统文件描述符 不要和skynet服务的handle搞混了
if (inst->handle == NULL) {
return 1;
}
inst->filename = skynet_malloc(strlen(parm)+1);
strcpy(inst->filename, parm);
inst->close = 1;//表示在释放的时候 需要关闭文件描述符
} else {//默认情况下是走这里
inst->handle = stdout;//没有配置日志写入哪个文件 就写入标准输出
}
if (inst->handle) {//next
skynet_callback(ctx, inst, logger_cb);//设置模块实例 和 回调函数
return 0;
}
return 1;//走到这里表示已经出现错误了
}
上面就是对模块的实例的各个成员进行填充。注意默认情况下日志服务是把日志写到标准输出的。注意skynet_callback绑定了日志服务的回调函数。也就是当日志服务队列里面有消息需要处理时,就交给这个回调函数去做。
void
skynet_callback(struct skynet_context * context, void *ud, skynet_cb cb) {
context->cb = cb;
context->cb_ud = ud;//就是logger实例
}
上面就是把服务的回调函数设置好。查看skynet_context_new代码 接下来调用 skynet_globalmq_push 把之前创建的服务队列加入到全局队列。
void
skynet_globalmq_push(struct message_queue * queue) {
struct global_queue *q= Q;
SPIN_LOCK(q)
assert(queue->next == NULL);//要求queue必须是独立的
if(q->tail) {
q->tail->next = queue;
q->tail = queue;
} else {
q->head = q->tail = queue;
}
SPIN_UNLOCK(q)
}
到这里通过skynet_context_new创建日志服务的代码总算调用完成了。查看skynet_start代码
实际上在lua层调用skynet.error(str)写日志的基本原理是:首先把要写入的字符串信息包装成一个skynet_message,然后push到日志服务的队列。之后工作线程检查到日志队列中有消息,则取出消息,并调用日志服务的回调函数处理。 通过skynet基本概况.md 里面介绍就知道,日志服务和bootstrap服务创建后,才启动了多种线程。也就是说,如果当前日志服务里面有消息,也是不会输出的,因为还没有 工作线程 来驱动日志服务干活。
push消息到日志服务队列
logger服务已经有了,那么我们可以调用 skynet.error("hello agang")
写日志了。在skynet.lua中定义如下:
local c = require "skynet.core" --这里实际上是一个c库 全局查找一下 skynet_core 即可知道这个库里面都往lua里面塞了什么函数
skynet.error = c.error
也即是说我们在lua中写下 require "skynet.core"
,可以认为就是调用了 c层函数 luaopen_skynet_core
,最后返回了一个lua表。
LUAMOD_API int
luaopen_skynet_core(lua_State *L) {
luaL_checkversion(L);
luaL_Reg l[] = {
{ "error", lerror },//这个是我们这次关注的
{ NULL, NULL },
};
// functions without skynet_context
luaL_Reg l2[] = {
{ "tostring", ltostring },
{ "pack", luaseri_pack },
{ NULL, NULL },
};
lua_createtable(L, 0, sizeof(l)/sizeof(l[0]) + sizeof(l2)/sizeof(l2[0]) -2);//给lua创建一个表 x
lua_getfield(L, LUA_REGISTRYINDEX, "skynet_context");
struct skynet_context *ctx = lua_touserdata(L,-1);//从注册表中拿到了ctx
if (ctx == NULL) {
return luaL_error(L, "Init skynet context first");
}
luaL_setfuncs(L,l,1);//把l中的函数填充到表x中 并共享上值 ctx
luaL_setfuncs(L,l2,0);//把l2中的函数填充到表x中 但没有共享的上值
return 1;
}
上面的代码主要是把一些函数塞进返回的lua表中。所以我们调用skynet.error实际上是调用c函数 lerror
static int
lerror(lua_State *L) {
struct skynet_context * context = lua_touserdata(L, lua_upvalueindex(1));//获取context
int n = lua_gettop(L);
luaL_Buffer b;
luaL_buffinit(L, &b);
int i;
for (i=1; i<=n; i++) {
luaL_tolstring(L, i, NULL);
luaL_addvalue(&b);
if (i<n) {
luaL_addchar(&b, ' ');
}
}
luaL_pushresult(&b);
skynet_error(context, "%s", lua_tostring(L, -1));//next
return 0;
}
上面最后调用了skynet_error.里面有个参数是context。这个context表示的是lua层调用skynet.error时所在的服务。
实际上服务是分为不同类型的。我们skynet主要关注的服务其实是 snlua类型。以后我们就叫lua服务。
struct skynet_message {
uint32_t source;
int session;
void * data;
size_t sz;
};
char *
skynet_strdup(const char *str) {
size_t sz = strlen(str);
char * ret = skynet_malloc(sz+1);//分配内存
memcpy(ret, str, sz+1);
return ret;
}
void //
skynet_error(struct skynet_context * context, const char *msg, ...) {//context主要用来设置 skynet_message 的source属性
static uint32_t logger = 0;
if (logger == 0) {
logger = skynet_handle_findname("logger");//找到默认的日志服务
}
if (logger == 0) {
return;
}
char tmp[LOG_MESSAGE_SIZE];
char *data = NULL;
va_list ap;
va_start(ap,msg);
int len = vsnprintf(tmp, LOG_MESSAGE_SIZE, msg, ap);
va_end(ap);
if (len >=0 && len < LOG_MESSAGE_SIZE) {
data = skynet_strdup(tmp);//分配内存
}
struct skynet_message smsg;
if (context == NULL) {
smsg.source = 0;
} else {
smsg.source = skynet_context_handle(context);//设置source
}
smsg.session = 0;
smsg.data = data;//就是"hello world"
smsg.sz = len | ((size_t)PTYPE_TEXT << MESSAGE_TYPE_SHIFT);//高八位表示skynet消息类型是 PTYPE_TEXT
skynet_context_push(logger, &smsg);//把消息push到logger服务
}
上面的代码实际主要是 把 skynet.error(str) 中的str 拷贝一份,然后包装成 skynet_message,push到 logger服务 对应的队列中。
logger服务处理消息
处理队列消息 已经说明了服务是怎么处理消息的。最后消息的处理是交给ctx的cb函数处理的。也就是说logger服务的消息最后交给 logger_cb处理
static int
logger_cb(struct skynet_context * context, void *ud, int type, int session, uint32_t source, const void * msg, size_t sz) {
struct logger * inst = ud;//logger实例
switch (type) {
case PTYPE_SYSTEM://这里表示打开一个新的日志文件
if (inst->filename) {
inst->handle = freopen(inst->filename, "a", inst->handle);
}
break;
case PTYPE_TEXT:
if (inst->filename) {//如果指定了日志文件 则加点时间信息
char tmp[SIZETIMEFMT];
int csec = timestring(ud, tmp);
fprintf(inst->handle, "%s.%02d ", tmp, csec);
}
fprintf(inst->handle, "[:%08x] ", source);
fwrite(msg, sz , 1, inst->handle);
fprintf(inst->handle, "\n");
fflush(inst->handle);
break;
}
return 0;
}
注意我们push消息到logger队列时 ,消息类型指定为 PTYPE_TEXT。这里实际上就是直接写入标准输出
tips:
我们这里讨论的日志功能的服务是默认的 日志服务。实际上,也可以配置为一个snlua服务,这样当调用skynet.error时,会发送一个 PTYPE_TEXT 消息给一个snlua服务。
我们来看看对比。默认创建日志服务代码如下
//这里创建一个服务 这个日志服务是service_logger.c代表的默认日志服务
//参数分别是 "logger" NULL
struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
实际上可以改成
//这里创建一个服务 这个日志服务是service_snlua.c代表的lua服务
//参数分别是 "snlua" xxx;xxx表示lua服务对应的脚本文件
struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
即 config->logservice
, config->logger
这两个配置项决定了默认的日志功能服务到底是什么。