« 胡思乱想 | 返回首页 | 个人主页发布十周年纪念 »

胡思乱想续

接着昨天的写。

昨天谈到了对象生命期管理的问题。我们来看操作系统是怎么管理资源的。

对于资源的集合体,操作系统抽象出进程的概念。每个任务可以向系统索取资源,操作系统放在进程的集合内。进程在,资源在;进程死,资源收回。从操作系统看出去,一个个对象都是独立的,不用理会相互的依赖关系,有的只有对象 handle 。收回这些对象的次序是无所谓的,跟发放他们的次序无关。

这里比较重要且特殊的是内存资源,操作系统其实不直接发放物理内存给用户,用户看到的只有虚拟地址空间。真正分配出去的是地址空间。而且空间是按页分配的,到了用户那里,再由用户自行切割使用。

这么看,内存管理的确是最复杂的部分。因为用户通常不能像文件 handle 那样,拿来是什么还回去还是什么。一个简单的引用记数就可以管理的很好。内存资源必须做多层次的管理。或许未来 64 位系统普及后,这个问题会简单很多,但谁叫我们主流应用还是跑在 32 位平台上呢?而且 64 位系统未必不会出现新的问题。我们现在看 64 位系统,估计跟当年在 dos 实模式下写程序时曾经幻想以后随随便便就有 4G 内存用的感觉一样。

除去资源管理,操作系统通常都会抽象出线程这个代码执行流程,加以统一管理。线程本身会作为一种资源放在进程的管理集合中。但是操作系统又需要对所有线程的集合做统一的调度。从这个角度看,仅仅分层归组管理是不够的。

其实不仅是线程,像 socket 这样的资源同样不能简单置于进程的层次之下。一个 tcp 连接是不能简单的在进程结束后直接干脆的抹掉。另外负责网络通讯的核心模块也需要有轮询系统中所有 socket 的能力。

综上看来,对象的生命期管理在同一层次上似乎应该有交叉的两条线。一条是拥有共同的生命期的集合;另一条是同类对象的集合。


先不忙下结论,再谈谈我们现在自己设计的引擎用到的一些管理策略和最近发现的一些不足吧。

我认为,在游戏客户端程序中(或许也可以拓展到许多别的软件),程序工作时内存里的数据分两类:持久资源和上下文相关的临时数据。

持久资源指开发期生成的地图、模型、贴图、动画骨骼等等在运行期只读的数据;以及一些可以在运行期通过运算或网络加载固定下来的数据,例如连接游戏服务器后传过来的各项游戏世界的资料;另有一些不太变化的临时数据,例如通过预运算合成的带阴影的场景贴图等等。

这些资源对象相互之间有较为复杂的依赖关系,并不总能用树结构描述他们的从属关系。但是他们有一个共同特点,即是生成数据的流程相对简单(大多数通过文件加载),并且总的数量有限。后一点非常重要,我们总能预期在极限情况下,进程中会有多少个资源对象。

最简单的处理方式是,在程序初始化时把所有的资源加载到内存不再删除,对于生命期不会短于代码执行序的生命期的数据,连引用计数都是多余的。大多数其他软件都是这样干的,如果你担心启动软件的时间过长,完全可以在需要的时候再加载。

可惜现代 PC 游戏做不到这一点了。如果你的硬盘上安装有著名网络游戏《魔兽世界》的客户端,看看安装文件夹的大小就可以明白我的意思,它足有 7 个 G 以上。

固然采取动态管理的方式是个不错的方案。但换个思路或许更好。我们不能把所有数据都加载到内存,却能为所有资源对象都在内存中保留一个固定的 handle 以及少量的数据。

也就是说,当你加载一张贴图时,引擎为其分配一个 handle ,无论这张贴图用过之后是否还有人继续引用,这个 handle 就永久的分配给了它。当内存不够时,引擎可以把贴图数据清理出内存,一旦再次有人使用,重新从磁盘加载回来即可。我们只需要保留重建方法既重建需要的数据(例如文件名)即可。

这样,资源对象数据部分的生命期管理就从引擎的上层中剥离了出来。我们不再关心数据被谁引用,实际上它只和一个惟一的 handle 绑定。而 handle (通常就是一个内存指针)和重建它的方法只占用少量的内存,且总量有一个上限,完全可以全部放在单个进程的地址空间内。

ps. 把资源对象全部独立出来还有一个好处就是可以在数据头中描述相互的依赖关系,方便多线程预加载,提高用户运行时的流畅性。这部分的设计可以参考我以前写的一篇:资源的内存管理及多线程预读


资源以外的数据其实并不多。我们依旧可以把数据继续仔细分类。

例如大多数字符串都是可以被提取出来统一管理的。程序中用到的许多字符串都固定不变,这些字符串多用于数据和数据、模块和模块间的松耦合。跟大多数动态语言一样,我赞成建立一个字符串池,把有限的不变字符串放进去,并且通过 hash 表查询。在程序员可以预知它的代码不会无限次的生成不同的字符串时,我们根本不用考虑字符串的生命期问题。这样还带来一个额外好处:字符串比较只需要比较一下指针即可。

除去一些较难处理的对象后,剩下来的数据量就不大了。我很反对在数据结构描述时滥用指针,更勿谈智能指针。在数据量较小时,值拷贝可以做的足够快,且能降低复杂度,甚至节约不少内存。如果有一天要考虑并发,或许还有更大的好处。

让对象的数据结构都是 POD 的吧,每个对象都占据一片连续的内存,而不是里面若干个指针指来指去。这样你甚至可以获得许多调试的便利。指针多应用于管理器的容器中,而非对象数据之间。

在管理器加上一些简单的标记扫描策略,我们可以实现一个简单的垃圾收集设施,方便的清除死亡对象。


以上,是我们已经实现的东西。而新的发现来至于昨天提到的那个有关 timer 的 bug 。

让我们谈谈 closure 。

C/C++ 的 function 都不是 first-class ,所以并不直接提供语言层面的 closure 支持 。但我们无法回避这个概念。C 里面通常用一个函数指针加一个 void * 来模拟 closure ,而 C++ 则用一个纯虚类的对象指针。无所谓形式了,况且今天我也不想局限在特定语言里。

timer 就是一个必须依赖 closure 的设施。我们需要把一个执行序和相关对象打包,放在未来的某个时间运行。执行序总是跟要处理的对象有关。那么这个未来的时间如果超过对象的生命期该怎么办?这不是简单的用 gc 能解决的问题。

来看 timer 的问题:timer 的管理器对每个 closure 有引用,closure 对其操作的对象有引用。所以 closure 没有被 timer 管理器执行并删除前,对象就至少有一个活引用了。

或者我们应该反过来,让对象对 closure 做引用。借用 lua 中弱引用的概念,让 timer 管理器只对 closure 有一个弱引用,似乎可以解决这个生命期问题。对象本身除被 timer 中的 closure 使用到以外没有活引用时,通过收集过程,管理器可以自动取消没有执行的 closure 。

不过这样做会有新的问题,一个执行序并不能简单的归属给一个指定对象。或许使用异常机制会让问题简单一点。当访问已经死亡的对象时从另一个出口退出。不过这样又增加了新的复杂度。

看起来,我们把引用计数和垃圾收集结合起来用也不错。

我们以生命期为标准,给对象分类(就像操作系统以进程为单位分组一样),一组对象都有一个共同的最长生存期(这又类似 Apache 的内存管理机制,允许用户把一堆的内存资源绑定在一个连接上,和连接共存亡)。这组对象之间可以用垃圾收集的方式提前释放一些资源。又可以保证上层建筑最终能够在死亡时间到来时干净的一锅端。

对于其中一些需要提取出来的相互有所作用的同类对象,附加一个引用计数。用来做一些善后工作。


以上,不是我的结论,如标题所言,胡思乱想而已。

以下,轻松一点,谈谈最近玩的几个游戏。

NDS 上的《动物之森》不错,我玩的 1479 英文版的。只有玩这类游戏,才觉得游戏可以做的很活泼,而不像大多数网络游戏那么赤裸裸的功利和单调。

周末还打了两天 PSP 上的《最终幻想 VII 核心危机(Crisis Core)》。也是个发行了很久的游戏了。不要说我老,是太忙了。作为最终幻想的粉丝,我强烈推荐没玩过的朋友试试,难度很低,玩起来相当轻松。虽然表面看起来是即时战斗的模式,但本质上还是用的 ATB 系统。而操作上甚至比菜单操作的 ATB 更容易上手。对比而言,我觉得这个版本的战斗系统会比最终幻想 XII 更容易让玩家接受。

作为 RPG 最重要的故事性,打着最终幻想的招牌当然没的说了。此外,人物造型相当漂亮,CG 也颇有水准。怎么看都是个值得收藏的大作。


喔,最后报告点正经事。这两周继续在读《秦制研究》,等有机会再写写吧。

Comments

定期都会来云风大哥这看看,学一些新思想,很有收获。
云风,你在杭州?
TCP 连接主动断开后,socket 进入 time_wait 状态,需要维持 2MSL 时间才能释放。这段时间即使创建这个 socket 的进程退出也是要保证的。 所以这就要求了 socket 的生命期可以长过进程的生命期一点点 :)
对OS有些不同的观点, 进程不是OS抽象出来的概念,而且作为一组资源的管理集合, 对于用户来讲就是个虚拟机器。所有的资源都是基于ID的, 都是一些数值,内存也是用ID号分配处理。在32位上 进程就可以拥有4G。 用户层线程的调度基本上是由应用层库自身调度,进程才是 OS的资源调度单元。Windows上是不是线程是OS的调度对象? 上面谈到一个进程就是一个虚拟的机器,如果这个虚拟机器 没有了,tcp与谁通讯?socket只是内核提供给用户层的接口 无非就是一堆的数据结构,tcp到了网络上,和udp之类的没啥 区别。tcp在异地网的意义大。到了本地,局域网上,UDP的传输 之类是可以保证的。
我在云老大的博客上用关键字UML进行搜索,没有找到相关文章。不知老大在建模的时候用不用这种东西?
在云风大哥的blog里翻来看去 增加了不少兴趣 为了能进入云风大哥那种状态 努力了
太赞了。。 有一个问题想请教一下老大。管理容器应该怎么定义呢? 比如一个网格的element。它算是容器么?是用值还是指针(或者类似的Index)来做? 谢谢:)
新人关注
终于看完了,字体好小,看着好辛苦
思想甚好,云风兄可否考虑将这套设计哲学抽象出一种语言,将这些管理机制直接集成到语言里,觉得应该很有前景,造福人群啊^-^
看来不抢楼都不行了。别的不说,云风兄够辛苦的,周末还要码这么多字。

Post a comment

非这个主题相关的留言请到:留言本