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创建一个进程(从一个页面链开来的页面,属于同一个siteinstance),但,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对进程工作集的控制算法还是比较简单的。首先,在进程启动的时候,需要指明进程工作的内存环境,是高内存,低内存,还是中等内存,默认模式下,是中等内存(我以为Chrome会动态计算的,没想到竟然是启动时指定…)。在高内存模式,不存在对工作集的调整,使劲用就完事了;在低内存的模式下,调整也很简单,一旦一个进程不再有页面面对观众了,尝试释放其所有工作集。相比来说,中等模式下,算法相对复杂一些,当一个进程从直接面对观众,沦落到切换到后台的悲惨命运,其工作集会缩减,算法为: TargetWorkingSetSize = (LastWorkingSet/2 + CurrentWorkingSet) /2;其中,TargetWorkingSetSize指的是预期降到的工作集大小,CurrentWorkingSet指的是进程当前的工作集(在Chrome中,工作集的大小,包含私有的和可共享的两部分内存,而不包含已经共享了的内存空间…),LastWorkingSet,等于上一次的CurrentWorkingSet除以DampingFactor,默认的DampingFactor为2。而反之,当一个进程从幕后走向台前,它的工作集会被放大为 LastWorkingSet * DampingFactor * 2,了解过LastWorkingSet的含义,你已经知道,这就是将工作集放大两倍的另类版写法。
Chrome的Render进程工作集调整,除了发生在tab切换(或新页面建立)的时候,还会发生在整个Chrome的idle事件触发后。Chrome有个计时器,统计Chrome空闲的时长,当时长超过30s后(此工作会反复进行…),Chrome会做一系列工作,其中就包括,调整进程的工作集。被调整的进程,不仅仅是Render进程,还包括Plugin进程和Browser进程,换句话描述,就是所有Chrome进程。
这个算法导致一个很悲凉的状况,当你去蹲了个厕所回到电脑前,切换了一个Chrome页面,你发现页面一片惨白,一阵硬盘的骚动过后,好不容易恢复了原貌。如果再切,相同的事情又会发生,孜孜不倦,直到你切过每一个进程。这个惨案发生的主要原因,就是由于所有Chrome进程的工作集都被释放了,页面的重载和Render需要不少的一坨时间,这就大大影响了用户感受,毕竟,总看到惨白的画面,容易产生不好的情绪。强烈感觉这个不算一个很出色的策略,应该有一个工作集切换的底限,或者是在Chrome从idle中被激活的时候,偷偷摸摸的统一扩大工作集,发几个事件刺激一下,把该加载的东西加载起来。
整体感觉,Chrome对进程开销的控制,并不像想象中的有非常精妙绝伦的策略在里面,通过工作集这总手段并不算华丽,而且,如果想很好的工作的话,有一个非常非常重要的前提,就是被切换的页面,很少再被继续浏览。个人觉得这个假设并不是十分可靠,这就使得在某些情况下,产生非常不好的用户体验,也许Chrome需要进一步在这个地方琢磨点方法的。