夫的骄子,虽然最后都是两腿不动到了山顶,这底下费得力气真是天壤之别啊。具体实现技巧,有兴趣的看看侯捷的《STL源码剖析》,或者是《C++新思维》,或者Chrome中的ipc_message_utils.h,这要说清楚实在不是一两句的事情。
总之通过宏和模板,你可以很简单的声明一个消息,这个消息可以传入各式各样的参数(这里用到了夸张的修辞手法,其实,只要是模板实现的自动化,永远都是有限制的,在Chrome的模板实现中,参数数量不要超过5个,类型需要是基本类型、STL容器等,在不BT的场合,应该够用了…),你可以调用Channel、ChannelProxy、SyncChannel之类的Send方法,将消息发送给其他进程,并且,实现一个Listener类,用Message Map来分发消息给对应的处理函数。如此,整个IPC体系搭建完成。
苦力的宏和模板
不论是宏还是模板,为了实现这套机制,都需要写大量的类似代码,比如为了支持0~N个参数的Control消息,你就需要写N+1个类似的宏;为了支持各种基础数据结构的序列化,你就需要写上十来个类似的Write函数和Traits。
之所以做如此苦力的活,都是为了用这些东西的人能够尽可能的简单方便,符合DIY原则。规约到之前说的设计者的职责上来,这是一个典型的苦了我一个幸福千万人的负责任的行为。在Chrome中,如此的代码随处可见,光Tuple那一套拳法,我现在就看到了使了不下三次(我曾经做过一套,直接吐血…),如此兢兢业业,真是可歌可泣啊。
【三】 Chrome的进程模型
1. 基本的进程结构
Chrome是一个多进程的架构,不过所有的进程都会由老大,Browser进程来管理,走的是集中化管理的路子。在Browser进程中,有xxxProcessHost,每一个host,都对应着一个Process,比如RenderProcessHost对应着RenderProcess,PluginProcessHost对应着PluginProcess,有多少个host的实例,就有多少个进程在运行。
这是一个比较典型的代理模式,Browser对Host的操作,都会被Host封装成IPC消息,传递给对应的Process来处理,对于大部分上层的类,也就隔离了多进程细节。
2. Render进程
先不扯Plugin的进程,只考虑Render进程。前面说了,一个Process一个tab,只是广告用语,实际上,每一个web页面内容(包括在tab中的和在弹出窗口中的…),在Chrome中,用RenderView表示一个web页面,每一个RenderView可以寄宿在任一一个RenderProcess中,它只是依托RenderProcess帮助它进行通信。每一个RenderProcess进程都可以有1到N个RenderView实例。
Chrome支持不同的进程模型,可以一个tab一个进程,一个site instance一个进程等等。但基本模式都是一致的,当需要创建一个新的RenderView的时候,Chrome会尝试进行选择或者是创建进程。比如,在one site one process的模式下,如果存在此site,就会选择一个已有的RenderProcessHost,让它管理这个新的RenderView,否则,会创建一个RenderProcessHost(同时也就创建了一个Process),把RenderView交给它。
在默认的one site instance one process的模式中,Chrome会为每个新的site instance创建一个进程(从一个页面链开来的页面,属于同一个site instance),但,Render进程总数是有个上限的。这个上限,根据内存大小的不同而异,比如,在我的机器上(2G内存),最多可以容纳20个Render进程,当达到这个上限后,你再开新的网站,Chrome会随机为你选择一个已有的进程,把这个网站对应的RenderView给扔进去。。。
每一次你新输入一个站点信息,在默认模式下,都必然导致一个进程的诞生,很可能,伴随着另一个进程的死亡(如果这个进程没有其他承载的RenderView的话,他就自然死亡了,RenderView的个数,就相当于这个进程的引用计数…)。比如,你打开一个新标签页的时候,系统为你创造了一个进程来承载这个新标签页,你输入http://www.baidu.com/,于是新标签页进程死亡,承载http://www.baidu.com/的进程诞生。你用baidu搜索了一下,毫无疑问,你基本对它的搜索结果很失望,于是你重新输入http://www.google.com.hk/,老的承载baidu的进程死亡,承载google的进程被构建出来。这时候你想回退到之前baidu的搜索结果,乐呵乐呵的话,一个新的承载baidu的进程被创造,之前Google的进程死亡。同样,你再次点击前进,又来到Google搜索结果的时候,一个新的进程有取代老的进程出现了。
以上现象,你都可以自己来检验,通过观察about:memory页面的信息,你可以了解整个过程(记得每做一步,需要刷新一下about:memory页面)。我唧唧歪歪说了半天,其实想表达的是,Chrome并没有像我YY的一样做啥进程池之类的特殊机制,而是简单的履行有就创建、没有就销毁的策略。我并不知道有没有啥很有效的多进程模型,这方面一点都没玩过,猜测Chrome之所以采取这样的策略,是经过琢磨的,觉得进程生死的代价可以承受,比较可行。
3. 进程开销控制算法
说开销无外乎两方面的内容,一为时间,二则空间。Chrome没有在进程创建和销毁上做功夫,但是当进程运行起来后,还是做了一些工作的。
节约工作首先从CPU耗时上做起,优先级越高的进程中的线程,越容易被调度,从而耗费CPU时间,于是,当一个页面不再直接面对用户的时候,Chrome会将它的进程优先级切到Below Normal的级别,反之,则切回Normal级别。通过这个步骤,小节约了一把时间。
进程的优先级
在windows中,进程是有优先级的,当然,这个优先级不是真实的调度优先级,而是该进程中,线程优先级计算的基准。在《Windows via C/C++》(也就是《windows核心编程》的第五版)中,有一张详细的表,表述了线程优先级和进程优先级的具体对应关系,感觉设计的很不错,在此就不再赘述了,有兴趣的自行动手翻书。
当然这只是一道开胃小菜,满汉全席是控制进程的工作集大小,以达到降低进程实际内存消耗的目的(Chrome为了体现它对内存的节约,用了“更为精确”的内存消耗计算方法…)。提到这一点,Chrome颇为自豪,在文档中,顺着道把单进程的模式鄙视了一下,基本意思是:在多进程的模式下,各个页面实际占用的内存数量,更容易被控制,而在单进程的模式下,几乎是不能作出控制的,所以,很多时候,多进程模式耗费的内存,是会小于多线程模式的。这个说法靠不靠谱,大家心里都有谱,就不多说了。
具体说来,Chrome对进程工作集的控制算法还是比较简单的。首先,在进程启动的时候,需要指明进程工作的内存环境,是高内存,低内存,还是中等内存,默认模式下,是中