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文件是下面这个文件编译出来的。

image-20220529160014999

也就是说模块里面的函数其实都是在 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

image-20220528153848687

也即是说我们在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服务。

image-20220829153713851

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 这两个配置项决定了默认的日志功能服务到底是什么。

b站对应视频 https://www.bilibili.com/video/BV19d4y1678X

posted @ 2022-12-08 14:38  程序员阿钢  阅读(975)  评论(1编辑  收藏  举报