Skynet的日志输出是由service_src/service_logger.c
文件实现的,用于运行Lua脚本的snlua
服务等,编译成.so
库供Skynet框架使用。
日志输出服务工作原理
当Skynet启动的时会根据配置文件指定的日志文件创建一个logger context
日志上下文,具体过程:先找到logger.so
动态链接库文件,而后调用其logger_create
函数构建日志服务对应的context
上下文。重要的是,在构建context
上下文对象时,会注册该服务的回调函_logger()
和消息队列,最后执行logger_init
函数将logger
的消息队列放入global queue
全局队列中去。
日志输出服务工作方式
在Skynet启动时会启动一个logger
服务,默认是logger
类型服务,也可以配置为snlua
类型。
// skynet/skynet-src/skynet_start.c
struct skynet_context *ctx = skynet_context_new(config->logservice, config->logger);
可以配置logger
服务类型,默认为logger
。
//skynet/example/config
daemon = "./skynet.pid" // 以后台模式启动Skynet
logger= "./skynet.log" //配置后台模式下日志输出的目录文件
当在后台模式下启动时,日志便输出到当前目录下的skynet.log
文件中,这种做法的缺点是当日志文件过大时Skynet不会自动删除,因此对系统影响很大。现在需要按天数保存日志,使超过1周的日志自动删除掉。另外一个问题是,如果将skynet.log
文件cp
复制之后删除在重建的话,日志是不会打印在这个文件中的,需要重启Skynet服务器才会生效。
//skynet/skynet-src/skynet_main.c
config.logger = optstring("logger", NULL);
config.logservice = optstring("logservice", "logger");
启动一个logger
日志输出服务时会调用到logger_create
这个API,用于申请struct logger
结构,最后赋值给ctx->instance
。
//skynet/service-src/service_logger.c
struct logger
{
FILE * handle;//文件句柄
char * filename;//文件名称
int close; //0表示文件句柄是标准输出,1表示输出到文件
}
struct logger *
logger_create(void)
{
struct logger * inst = skynet_malloc(sizeof(*inst));
inst->handle = NULL;
inst->close = 0;
inst->filename = NULL;
return inst;
}
当logger
服务退出时,会调用logger_release
函数这个API进行清理回收。
//skynet/service-src/logger_service.c
void
logger_release(struct logger * inst)
{
if(inst->close)
{
fclose(inst->handle);
}
skynet_free(inst->filename);
skynet_free(inst);
}
之后会调用logger_init
函数API,参数param
是配置中的config->logger
。若指定文件路径则打开该文件inst->handle = fopen(param, "w")
。否则,设置文件句柄为标准输出inst->handle = stdout
。最后设置ctx
上下文的消息回调函数为logger_cb(skynet_callback)
,并给ctx
注册一个名称为skynet_command(ctx, "REG", "logger")
。
//skynet/servivce-src/service_logger.c
int logger_init(struct logger * inst, struct skynet_context *ctx, const char * param)
{
if(param)
{
inst->handle = fopen(param, "w");
if(inst->handle == NULL)
{
return 1;//打开失败
}
inst->filename = skynet_malloc(strlen(param) + 1);
strcpy(inst->filename, param);
}
else
{
// 使用标准输出
inst->handle = stdout;
}
if(inst->handle)
{
//注册回调函数,用来处理消息的
skynet_callback(ctx, inst, logger_cb);
//注册命令的局部名字
skynet_command(ctx, "REG", ".logger");
return 0;
}
return 1;
}
Skynet输出日志通常是调用skynet_error
函数API,Lua层调用skynet.error
最后也是调用skynet_error
函数。查找名为logger
对应的ctx
的handle
,然后向该id
发送消息包skynet_context_push
,消息包的类型为PTYPE_TEXT
,若没有设置PTYPE_ALLOCSESSION
标记则表示不需要接收方返回。
//skynet/skynet-src/skynet_error.c
void skynet_error(struct skynet_context * context, const char msg, ... )
{
static uint32_t logger = 0;
if(logger == 0)
{
logger = skynet_handle_findname("logger");
}
...
//将错误消息字符串构建为skynet_message并放入logger消息队列
struct skynet_message smsg;
if(context == NULL)
{
smsg.source = 0;
}
else
{
//source是ctx的hangle编号
smsg.source = skynet_context_handle(context);
}
smsg.session = 0;
smsg.data = data;
//sz 表示低24位保存的是数据大小
smsg.sz = len | ((size_t)PTYPE_TEXT << MESSAGE_TYPE_SHIFT);
// 将消息放到logger的队列中
skynet_context_push(logger, &smsg);
}
代码中错误处理的skynet_error()
函数就是日志输出函数,它会将错误字符串构建为一个skynet_message
,并放入到logger
的消息队列中,msg
中会保存消息来来源handle
。
那么消息何时输出呢?Skynet启动时会创建 worker
工作线程,它的工作就是消费这些消息并执行对应的回调函数,对于logger
而言,相当于把消息输出到对应的文件句柄中。
static void
_dispatch_message(struct skynet_context *ctx, struct skynet_message *msg)
当工作线程分发这条消息包时,最终会调用logger
服务的消息回调函数logger_cb
,不同服务类型的消息回调函数接口参数是一样的。
//skynet/service-src/logger_service.c
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;
switch(type)
{
case PTYPE_SYSTEM:
if(inst->filename)
{
inst->handle = freopen(inst->filename, "a", inst->handle);
}
break;
case PTYPE_TEXT:
fprintf(inst->handle, "[:%08x]", source);
fwrite(msg, sz, 1, inst->handle);
fprintf(inst->handle, "\n");
fflush(inst->handle);
break;
}
return 0;
}
由于skynet_error
发送的消息包的type
是PTYPE_TEXT
,它会将小细胞源地址以及消息包数据一起写到文件句柄中。在skynet_context_new
成功启动一个服务后,会调用skynet_error(ctx, "LAUNCH %s %s", name, param ? param:"")
,于是就有了经典的Skynet启动画面。
网友评论