MudOS阅读笔记


1      总述

主要看了网络处理相关代码,并将select模型修改为kqueue模型;阅读了定时处理相关代码,并将单层时间轮改为多层时间轮;阅读内存以及虚拟机相关代码。

2      网络及并发

Select缺陷与kqueue改进:

mudOS的网络处理是基于select模型的,该接口存在一系列不足:

1)           内核不存储事件标记,那么每次调用select时都有大量内存复制开销,将用户态描述符集复制到内核态描述符集中。

2)           其本身具有内核数据结构的限制,这点是在内核编译阶段确定的,限制了最大描述符数量。

3)           最重要的是,select的内部实现是唤醒轮询机制的,即使有很少的描述符是活跃的(如网络包可读),也需要遍历整个描述符集以返回可用描述符集合。

select的这些不足降低了整个系统性能。所以有必要选择替换接口,在freeBSD下kqueue是类似Linux下epoll的可替代接口。其内部实现是基于注册回调机制的,每当描述符事件到来,会回调注册到可用链表中,那么就避免了遍历整个描述符集的开销,在大量连接,少量活跃的情况下,相比select的性能提升是明显的。

具体修改:

在对kqueue具体应用中,为mudOS中的多个网络相关结构(external_port,interactive_t, lpc_sock)对应一个唯一的控制单元,当描述符事件到来时通过kevent结构中的udata字段指向的控制单元(注册事件时设定的)可以定位到相应的网络处理结构,进入到新用户创建,用户数据读写,或lpc中socket处理。如果用户interactive_t中还存在cmd尚未处理,则设定描述符读事件的DISABLE标记,该标记忽略读事件,同理当interactive_t中写缓冲区无数据时,写事件DISABLE。考虑到每次调用kevent前都需要考察所有网络结构的状态,来置位DISABLE等标记,我们在控制单元中加入了一层标记缓存,只有在状态改变时才执行系统调用,这才不增加额外负担的情况下减少了一部分系统开销。

***修改补充***:前面检查所有fd的网络状态是需要避免的,这样会把kqueue弱化为select了,每次数据到来直接跟上处理逻辑即可;而且也不用缓存读写状态,这样只需用udata分字段就能定位操作类型和操作偏移(操作类型<<16 | 操作偏移)。

网络与逻辑:

       整个mudOS的服务处理循环是:网络处理;命令处理;心跳和定时;单线程的好处是简单可实现,容易维护,系统复杂度可扩充,不必考虑多线程同步和死锁这些所带来的系统不稳定性。但由于完全基于单线程实现,那么每一部分的执行效率都不可避免地影响其他部分和被影响,如软定时机制往往会因为网络处理,命令处理的开销而延迟,考虑做成硬定时的话,可以减少延迟,但同步带来的复杂性又显著上升了。在并发方面,mudOS中通过循环轮的形式平衡多用户处理优先级(越近访问的优先级越低),每个用户的命令处理优先级都是均摊的,又通过特定的执行时间函数来限制开销过大的命令处理以保证对所有用户的平均响应时间。

       mudOS本身的并发需求是比较低的,只需保证命令的平均响应时间,用户对及时并发其实并不敏感,有合理的逻辑顺序即可。如在攻击中,用户不太关心是自己先敲的攻击命令还是对方早于自己先敲的攻击命令,重要的是有一个攻击开始到结束的流程响应给用户,那么整个mudOS基于这样的单线程架构对于这种低并发串行的命令式交互还是比较合适的。

3      定时

单层时间轮定时:

mudOS中的定时处理是基于每次服务循环心跳的软定时机制,用一个有限刻度的时间轮(32个slot的数组)来放置timer,假设timer的定时延迟是delay,那么(current_time + delay )% 32则是timer 对应的slot,(current_time + delay )/32 则是轮转次数。当前时间对应slot下的timer轮转次数为0时就触发定时事件。同一slot下的timer采取的是增量存储,即如果第一个timer轮转次数为2,现在新加一个timer轮转次数为3,那么新加的timer轮转次数为3-2=1,放到最后。采取这种方法,在时间轮更新的过程中,每个timer的平均更新时间都是O(1),因为每次都只需对当前slot的第一个timer进行处理,触发定时事件移除定时,要不就是定时事件轮转计数减一。

       这种时间轮机制理论上可以满足所有时间长度的定时处理,心跳时间则是最小精度。但当定时器较多时,每次插入定时器的开销会是O(N),而且在mudOS实现中,通过函数名来删除定时器的开销是O(N),需要遍历所有定时器,基于这两点原因,考虑改为多层次定时轮,使得大部分定时器(秒,分时间刻度内)的插入时间开销为O(1),而且通过双向链表来管理定时器,以达到O(1)时间删除。

多层时间轮:

       整个时间轮分为时,分,秒三层,时层采取前面的维护方法,保证支持任意长度的定时器,分和秒层则按照计时方法,不再轮转,可放置0~3599秒范围内的定时器。定时器放在可放置的最高层次,例如现在是1:1:30,一个定时器delay是1小时50秒,那么定时器在2:2:20到期,所以该定时器首先放在时层slot2位置,之后放在分层slot2位置,最后放在秒层slot20位置。

       这种多层时间轮的方法有效缩短了大量定时器的插入时间,删除和tick处理也是O(1),但多层的机制带来了定时器在层次间移动的开销,每次层次间的移动开销O(1)。增加了多层slot的内存开销,以及双向链表额外的指针存储。

其他定时机制:

       除了时间轮机制,比较常用的还有基于优先队列的定时,插入删除开销都是O(logN),每次tick处理的时间是O(1)。这种定时开销比较稳定,不会出现退化到O(N)的情况,但最好也会有O(logN)的开销。当然还有基于排序链表的定时器,插入时间为O(N),删除时间和tick时间都为O(1)。

4      内存

mudOS中的内存管理提供了malloc,smalloc,BSDmalloc三套内存分配和释放机制:

1)           Malloc是默认的系统库实现。

2)           smalloc将内存分为small blocks 和large blocks,当分配内存时判断请求大小是否大于某个阈值。大于则从largeblocks中分配,小于则从smallblocks 中根据size大小找到最合适的内存块分配。这种方法对小块内存和大块内存采取不同的管理方式,其假设小内存是分配释放频繁的,尽量满足内存分配的需求,减少内存碎片。

3)           bsdmalloc对内存块统一处理,按照内存块的大小进行分类,4,8,16.32.64,128..。从最接近的内存块列表中分配内存,在当前列表中不存在空闲块时则申请一个页大小的内存,分解为若干个空闲块。没有split和merge策略,bdsMalloc的分配释放内存较快,但又不可避免的引入了更多的内存碎片。

5      虚拟机

大概看了lpc代码编译到执行的流程,并没有对具体的词法分析,语法分析这些进行探究。init_load_object(simulate.c)负责对象文件的装载编译与初始化:

1)           Compile_file编译文件,生成lpcode。

2)           Get_empty_object分配新的对象结构,分配变量空间,这些变量分配在对象结构的末端。

3)           Hash映射对象。

4)           Init_object初始化对象状态信息,如uid的分配。

5)           Call_create调用对象create函数。

Eval_instruction(interpret.c)解释执行lpcode,维护执行期堆栈信息与状态。这部分以后有时间继续细看,并比较和lua虚拟机实现上的差异。

你可能感兴趣的:(游戏)