如何在 skynet 死循环中输出堆栈

skynet版本:v1.4.0 (2020-11-16)

  最近在做武侠mmo的游戏。因为武侠战场有一个tick在计算状态,然而在tick里的逻辑出现了问题,具体原因是一个怪物的ai返回错误码值有问题,而外层tick又没有捕捉到这个错误码,所以出现了死循环,导致一直在执行ai,创建了一堆table,最后导致内存爆了。

  这里不讨论逻辑问题,主要讲述当时是如何定位到bug代码的,然后可以如何改进。

  当时得亏还在内测阶段出现问题,而且这个是可以重现的,具体表现是在固定的副本里的固定的怪物释放了固定的技能导致,当然这是找到的结果。

  在出现问题的时候,因为是比较难触发到,所以也无法立刻判断出是技能的bug,在重现过程中,依赖于skynet本身的debug_conole 的 signal 1命令来输出当前的堆栈,最终也能定位到问题。

  解决完bug后,后面想如何能在出现问题的时候尽可能得收集到当前bug的相关信息呢?回想到查找bug的过程,主要还是要打印当前的堆栈,所以想是否能在 monitor 线程监控的时候发现死循环时打印出lua当前的堆栈呢?觉得是可行的,因为signal 1 的信号实际也是调用了c层面的方法而已。

  很容易的,能够想到在 skynet_monitor.c 里加入如下代码,直接调用command接口:

复制代码
 1 void 
 2 skynet_monitor_check(struct skynet_monitor *sm) {
 3     if (sm->version == sm->check_version) {
 4         if (sm->destination) {
 5             skynet_context_endless(sm->destination);
 6             skynet_error(NULL, "A message from [ :%08x ] to [ :%08x ] maybe in an endless loop (version = %d)", sm->source , sm->destination, sm->version);
 7 
 8             char tmp[20];                    // 加入的代码,调用具体服务的command接口。
 9             sprintf(tmp, ":%08x 9", sm->source);
10             skynet_command(NULL, "SIGNAL", tmp);
11 
12             sprintf(tmp, ":%08x 9", sm->destination);
13             skynet_command(NULL, "SIGNAL", tmp);
14         }
15     } else {
16         sm->check_version = sm->version;
17     }
18 }
复制代码

  如上述代码的第8到13行,是分别向两个服务的 serivce_snlua 发出 signal 9 调用具体的回调函数。

  那么接下来是对 service_snlua.c 的代码进行修改,这里一开始写了一版在多线程环境下有bug的代码,如下:

 

复制代码
 1 void
 2 snlua_signal(struct snlua *l, int signal) {
 3     skynet_error(l->ctx, "recv a signal %d", signal);
 4     if (signal == 0) {
 5         if (ATOM_LOAD(&l->trap) == 0) {
 6             // only one thread can set trap ( l->trap 0->1 )
 7             if (!ATOM_CAS(&l->trap, 0, 1))
 8                 return;
 9             lua_sethook (l->activeL, signal_hook, LUA_MASKCOUNT, 1);
10             // finish set ( l->trap 1 -> -1 )
11             ATOM_CAS(&l->trap, 1, -1);
12         }
13     } else if (signal == 1) {
14         skynet_error(l->ctx, "Current Memory %.3fK", (float)l->mem / 1024);
15     } else if (signal == 9){
16         lua_State* L = l->activeL;
17         CallInfo* ci = L->ci;
18         for ( ci = L->ci; ci != NULL && ci != &L->base_ci; ci = ci->previous) {
19             if ((ci->func.p->tbclist.tt_ & 0x3F) == 6) {
20                 Proto *sp = ((union GCUnion *)(ci->func.p->tbclist.value_.gc))->cl.l.p;
21                 char * filename = sp->source ? (char *) ((char *)(sp->source) + 
22                     sizeof(TString)) : "unknown";
23                 skynet_error(l->ctx, "<TRACE %s %d %d>" , filename, sp->linedefined, 
24                     sp->lastlinedefined);
25             }
26         }
27     }
28 }
复制代码

  上面的第15行开始就是所加代码,当时没考虑在monitor的线程中直接对lua虚拟机进行操作的同时,在worker线程调用中可能会修改到lua虚拟机的一些状态,另外也是直接对callinfo的链表进行了输出,可能格式也有一些难看。

  后面参考了siganl 0 的操作,然后就有了最终版本:

  

复制代码
 1 static void
 2 signal_9_hook(lua_State *L, lua_Debug *ar) {
 3     void *ud = NULL;
 4     lua_getallocf(L, &ud);
 5     struct snlua *l = (struct snlua *)ud;
 6 
 7     lua_sethook (L, NULL, 0, 0);
 8     if (ATOM_LOAD(&l->trap)) {
 9         ATOM_STORE(&l->trap , 0);
10         luaL_where(L, 1);
11         const char *msg = lua_tostring(L, -1);
12         luaL_traceback(L, L, msg, 1);
13         skynet_error(l->ctx, lua_tostring(L, -1));
14     }
15 }
16 
17 void
18 snlua_signal(struct snlua *l, int signal) {
19     skynet_error(l->ctx, "recv a signal %d", signal);
20     if (signal == 0) {
21         if (ATOM_LOAD(&l->trap) == 0) {
22             // only one thread can set trap ( l->trap 0->1 )
23             if (!ATOM_CAS(&l->trap, 0, 1))
24                 return;
25             lua_sethook (l->activeL, signal_hook, LUA_MASKCOUNT, 1);
26             // finish set ( l->trap 1 -> -1 )
27             ATOM_CAS(&l->trap, 1, -1);
28         }
29     } else if (signal == 1) {
30         skynet_error(l->ctx, "Current Memory %.3fK", (float)l->mem / 1024);
31     } else if (signal == 9){
32         // lua_State* L = l->activeL;
33         // CallInfo* ci = L->ci;
34         // for ( ci = L->ci; ci != NULL && ci != &L->base_ci; ci = ci->previous) {
35         //     if ((ci->func.p->tbclist.tt_ & 0x3F) == 6) {
36         //         Proto *sp = ((union GCUnion *)(ci->func.p->tbclist.value_.gc))->cl.l.p;
37         //         char * filename = sp->source ? (char *) ((char *)(sp->source) + 
38         //             sizeof(TString)) : "unknown";
39         //         skynet_error(l->ctx, "<TRACE %s %d %d>" , filename, sp->linedefined, 
40         //             sp->lastlinedefined);
41         //     }
42         // }
43         
44         if (ATOM_LOAD(&l->trap) == 0) {
45             // only one thread can set trap ( l->trap 0->1 )
46             if (!ATOM_CAS(&l->trap, 0, 1))
47                 return;
48         
49             lua_sethook (l->activeL, signal_9_hook, LUA_MASKCOUNT, 1);
50             ATOM_CAS(&l->trap, 1, -1);
51         }
52     }
53 }
复制代码

  最终版本的操作,主要是做了重入的判断,以及对lua虚拟机直接进行了sethook操作,在worker线程中执行了LUA_MASKCOUNT 条指令后,调用一次 signal_9_hook函数,这样也可以保证同时只有一个线程对lua环境进行操作,另外也直接调用了

luaL_traceback 函数生成了堆栈的信息,也比较好看一些。

  下面是使用修改后的skynet的一些实际效果图:

 

posted @   小乐虎  阅读(173)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示