原著:duguguiyu。
整理:July。
時(shí)間:二零一一年四月二日。
出處:http://blog.csdn.net/v_JULY_v。
說(shuō)明:此Chrome源碼剖析很大一部分編輯整理自此博客:http://flyvenus.net/。我對(duì)寫(xiě)原創(chuàng)文章的作者向來(lái)是以最大的尊重的。近期想好好研究和學(xué)習(xí)下Chrome源碼,正巧看到了此duguguiyu兄臺(tái)的源碼剖析,處于學(xué)習(xí)的目的,就不客氣的根據(jù)他的博客整理了此文。若有諸多冒犯之處,還望海涵。
--------------------------------
前言:
1、之所以整理此文,有倆個(gè)目的:一是為了供自己學(xué)習(xí)研究之用;二是為了備份,以作日后反復(fù)研究。除此之外,無(wú)它。
2、此文的形式其實(shí)是有點(diǎn)倆不像的,既不是個(gè)人首創(chuàng)即原創(chuàng),又非單純的轉(zhuǎn)載(有加工),無(wú)奈之下,權(quán)且稱作翻譯吧。有不妥之處,還望原作者,及讀者見(jiàn)諒。
文中加入了我自己的一些見(jiàn)解,請(qǐng)自行辨別。順便再說(shuō)一句,duguguiyu寫(xiě)的這個(gè)Chrome源碼剖析,真不錯(cuò),勾起了偶對(duì)源碼剖析的莫大興趣。
順便透露下:在此份Chrome源碼剖析之后,互聯(lián)網(wǎng)上即將,首次出現(xiàn)sgi stl v3.3版的源碼剖析拉。作者:本人July。是的,本人最近在研究sgi stl v3.3版的源碼,正在做源碼剖析,個(gè)人首創(chuàng),敬請(qǐng)期待。
在具體針對(duì)源碼剖析之前,再粗略回答一下網(wǎng)友可能關(guān)心的問(wèn)題:chrome速度維護(hù)如此之快?據(jù)網(wǎng)上資料顯示:有幾個(gè)主要的關(guān)鍵技術(shù):DNS預(yù)解析、 Google自主開(kāi)發(fā)的V8 Javacript引擎、DOM綁定技術(shù)以及多進(jìn)程架構(gòu)等等。但這不是本文的重點(diǎn),所以略過(guò)不談。
ok,激動(dòng)人心的Chrome源碼剖析旅程,即刻開(kāi)始。
Chrome源碼剖析【序】
此序成于08年末,Chrome剛剛推出之際。
duguguiyu:“有的人一看到Chrome用到多進(jìn)程就說(shuō)垃圾廢物肯定低能。拜托,大家都是搞技術(shù)的,你知道多進(jìn)程的缺點(diǎn),Google也知道,他 們不是政客,除了搞個(gè)噱頭扯個(gè)蛋就一無(wú)所知了,人家也是有臉有皮的,寫(xiě)一坨屎一樣的開(kāi)源代碼放出來(lái)遭世人恥笑難道會(huì)很開(kāi)心?所謂技術(shù)的優(yōu)劣,是不能一概而 論的,同樣的技術(shù)在不同場(chǎng)合不同環(huán)境不同代碼實(shí)現(xiàn)下,效果是有所不同的。....”
Chrome對(duì)我來(lái)說(shuō),有吸引力的地方在于(排名分先后…):
1、它是如何利用多進(jìn)程(其實(shí)也會(huì)有多線程一起)做并發(fā)的,又是如何解決多進(jìn)程間的一些問(wèn)題的,比如進(jìn)程間通信,進(jìn)程的開(kāi)銷;
2、做為一個(gè)后來(lái)者,它的擴(kuò)展能力如何,如何去權(quán)衡對(duì)原有插件的兼容,提供怎么樣的一個(gè)插件模型;
3、它的整體框架是怎樣,有沒(méi)有很NB的架構(gòu)思想;
4、它如何實(shí)現(xiàn)跨平臺(tái)的UI控件系統(tǒng);
5、傳說(shuō)中的V8,為啥那么快。
但Chrome是一個(gè)跨平臺(tái)的瀏覽器,其Linux和Mac版本正在開(kāi)發(fā)過(guò)程中,所以我把所有的眼光都放在了windows版本中,所有的代碼剖析都是基于windows版本的。有錯(cuò)誤請(qǐng)指正。
關(guān)于Chrome的源碼下載和環(huán)境配置,大家可自行查找資料,強(qiáng)調(diào)一點(diǎn),一定要嚴(yán)格按照說(shuō)明來(lái)配置環(huán)境,特別是vs2005的補(bǔ)丁和windows SDK的安裝,否則肯定是編譯不過(guò)的。
最后,寫(xiě)這部分唯一不是廢話的內(nèi)容,請(qǐng)記住以下這幅圖,這是Chrome最精華的一個(gè)縮影:
圖1 Chrome的線程和進(jìn)程模型
Chrome源碼剖析【一】—— 多線程模型
【一】 Chrome的多線程模型
0. Chrome的并發(fā)模型
如果你仔細(xì)看了前面的圖,對(duì)Chrome的線程和進(jìn)程框架應(yīng)該有了個(gè)基本的了解。Chrome有一個(gè)主進(jìn)程,稱為Browser進(jìn)程,它是老大,管理 Chrome大部分的日常事務(wù);其次,會(huì)有很多Renderer進(jìn)程,它們?nèi)Φ囟危鞴芾硪唤M站點(diǎn)的顯示和通信(Chrome在宣傳中一直宣稱一個(gè) tab對(duì)應(yīng)一個(gè)進(jìn)程,其實(shí)是很不確切的…),它們彼此互不搭理,只和老大說(shuō)話,由老大負(fù)責(zé)權(quán)衡各方利益。它們和老大說(shuō)話的渠道,稱做IPC(Inter- Process Communication),這是Google搭的一套進(jìn)程間通信的機(jī)制,基本的實(shí)現(xiàn)后面自會(huì)分解。
Chrome的進(jìn)程模型
Google 在宣傳的時(shí)候一直都說(shuō),Chrome是one tab one process的模式,其實(shí),這只是為了宣傳起來(lái)方便如是說(shuō)而已,基本等同廣告,實(shí)際療效,還要從代碼中來(lái)看。實(shí)際上,Chrome支持的進(jìn)程模型遠(yuǎn)比宣 傳豐富,簡(jiǎn)單的說(shuō),Chrome支持以下幾種進(jìn)程模型:
1.Process-per-site-instance:就是你打開(kāi)一個(gè)網(wǎng)站,然后從這個(gè)網(wǎng)站鏈開(kāi)的一系列網(wǎng)站都屬于一個(gè)進(jìn)程。這是Chrome的默認(rèn)模式。
2.Process-per-site:同域名范疇的網(wǎng)站放在一個(gè)進(jìn)程,比如www.google.com(由于此文形成于08年,所以無(wú)法訪問(wèn),你懂的)和www.google.com/bookmarks就屬于一個(gè)域名內(nèi)(google有自己的判定機(jī)制),不論有沒(méi)有互相打開(kāi)的關(guān)系,都算作是一個(gè)進(jìn)程中。用命令行–process-per-site開(kāi)啟。
3.Process-per-tab:這個(gè)簡(jiǎn)單,一個(gè)tab一個(gè)process,不論各個(gè)tab的站點(diǎn)有無(wú)聯(lián)系,就和宣傳的那樣。用–process-per-tab開(kāi)啟。
4.Single Process:這個(gè)很熟悉了吧,即傳統(tǒng)瀏覽器的模式:沒(méi)有多進(jìn)程只有多線程,用–single-process開(kāi)啟。
關(guān)于各種模式的優(yōu)缺點(diǎn),官方有官方的說(shuō)法,大家自己也會(huì)有自己的評(píng)述。不論如何,至少可以說(shuō)明,Google不是由于白癡而采取多進(jìn)程的策略,而是實(shí)驗(yàn)出來(lái)的效果。
大家可以用Shift+Esc觀察各模式下進(jìn)程狀況,至少我是觀察失敗了(每種都和默認(rèn)的一樣…),原因待跟蹤。
不論是Browser進(jìn)程還是Renderer進(jìn)程,都不只是光桿司令,它們都有一系列的線程為自己打理各種業(yè)務(wù)。對(duì)于Renderer進(jìn)程,它們通常有兩個(gè)線程:一個(gè)是Main thread,它負(fù)責(zé)與老大進(jìn)行聯(lián)系,有一些幕后黑手的意思;另一個(gè)是Render thread,它們負(fù)責(zé)頁(yè)面的渲染和交互,一看就知道是這個(gè)幫派的門(mén)臉級(jí)人物。
相比之下,Browser進(jìn)程既 然是老大,小弟自然要多一些,除了大腦般的Main thread,和負(fù)責(zé)與各Renderer幫派通信的IO thread,其實(shí)還包括負(fù)責(zé)管文件的file thread,負(fù)責(zé)管數(shù)據(jù)庫(kù)的db thread等等,它們各盡其責(zé),齊心協(xié)力為老大打拼。它們和各Renderer進(jìn)程的之間的關(guān)系不一樣,同一個(gè)進(jìn)程內(nèi)的線程,往往需要很多的協(xié)同工作, 這一坨線程間的并發(fā)管理,是Chrome最出彩的地方之一了。
閑話并發(fā)
單進(jìn)程單線程的編程是最愜意的事情,所看即所得,一維的思考即可。但程序員的世界總是沒(méi)有那么美好,在很多的場(chǎng)合,我們都需要有多線程、多進(jìn)程、多機(jī)器攜起手來(lái)一齊上陣共同完成某項(xiàng)任務(wù),統(tǒng)稱:并發(fā)(非官方版定義…)。在我看來(lái),需要并發(fā)的場(chǎng)合主要是要兩類:
1.為了更好的用戶體驗(yàn)。有的事情處理起來(lái)太慢,比如 數(shù)據(jù)庫(kù)讀寫(xiě)、遠(yuǎn)程通信、復(fù)雜計(jì)算等等,如果在一個(gè)線程一個(gè)進(jìn)程里面來(lái)做,往往會(huì)影響用戶感受,因此需要另開(kāi)一個(gè)線程或進(jìn)程轉(zhuǎn)到后臺(tái)進(jìn)行處理。它之所以能夠 生效,仰仗的是單CPU的分時(shí)機(jī)制,或者是多CPU協(xié)同工作。在單CPU的條件下,兩個(gè)任務(wù)分成兩撥完成的總時(shí)間,是大于兩個(gè)任務(wù)輪流完成的,但是由于彼 此交錯(cuò),給人的感覺(jué)更自然一些。
2.為了加速完成某項(xiàng)工作。大名鼎鼎的 Map/Reduce,做的就是這樣的事情,它將一個(gè)大的任務(wù),拆分成若干個(gè)小的任務(wù),分配個(gè)若干個(gè)進(jìn)程去完成,各自收工后,再匯集在一起,更快地得到最 后的結(jié)果。為了達(dá)到這個(gè)目的,只有在多CPU的情形下才有可能,在單CPU的場(chǎng)合(單機(jī)單CPU…),是無(wú)法實(shí)現(xiàn)的。
在第二種場(chǎng)合下,我們會(huì)自然而然的關(guān)注數(shù)據(jù)的分離,從而很好的利用上多CPU的能力;而在第一種場(chǎng)合,我們習(xí)慣了單CPU的模式,往往不注重?cái)?shù)據(jù)與行為的對(duì)應(yīng)關(guān)系,導(dǎo)致在多CPU的場(chǎng)景下,性能不升反降。
1. Chrome的線程模型
仔細(xì)回憶一下我們大部分時(shí)候是怎么來(lái)用線程的,在我足夠貧瘠的多線程經(jīng)歷中,往往都是這樣用的:起一個(gè)線程,傳入一個(gè)特定的入口函數(shù),看一下這個(gè)函數(shù)是否 是有副作用的(Side Effect),如果有,并且還會(huì)涉及到多線程的數(shù)據(jù)訪問(wèn),仔細(xì)排查,在可疑地點(diǎn)上鎖伺候。
Chrome的線程模型走的是另一個(gè)路子,即,極力規(guī)避鎖的存在。 換更精確的描述方式來(lái)說(shuō),Chrome的線程模型,將鎖限制了極小的范圍內(nèi)(僅僅在將Task放入消息隊(duì)列的時(shí)候才存在…),并且使得上層完全不需要關(guān)心 鎖的問(wèn)題(當(dāng)然,前提是遵循它的編程模型,將函數(shù)用Task封裝并發(fā)送到合適的線程去執(zhí)行…),大大簡(jiǎn)化了開(kāi)發(fā)的邏輯。
不過(guò),從實(shí)現(xiàn)來(lái)說(shuō),Chrome的線程模型并沒(méi)有什么神秘的地方,它用到了消息循環(huán)的手段。每一個(gè)Chrome的線程,入口函數(shù)都差不多,都是啟動(dòng)一個(gè)消息循環(huán)(參見(jiàn)MessagePump類),等待并執(zhí)行任務(wù)。
而其中,唯一的差別在于,根據(jù)線程處理事務(wù)類別的不同,所起的消息循環(huán)有所不同。比如處理進(jìn)程間通信的線程(注意,在Chrome中,這類線程都叫做IO 線程)啟用的是MessagePumpForIO類,處理UI的線程用的是MessagePumpForUI類,一般的線程用到的是 MessagePumpDefault類(只討論windows)。
不同的消息循環(huán)類,主要差異有兩個(gè),一是消息循環(huán)中需要處理什么樣的消息和任務(wù),第二個(gè)是循環(huán)流程(比如是死循環(huán)還是阻塞在某信號(hào)量上…)。下圖是一個(gè)完 整版的Chrome消息循環(huán)圖,包含處理Windows的消息,處理各種Task(Task是什么,稍后揭曉,敬請(qǐng)期待),處理各個(gè)信號(hào)量觀察者 (Watcher),然后阻塞在某個(gè)信號(hào)量上等待喚醒。
圖2 Chrome的消息循環(huán)
當(dāng)然,不是每一個(gè)消息循環(huán)類都需要跑那么一大圈的,有些線程,它不會(huì)涉及到那么多的事情和邏輯,白白浪費(fèi)體力和時(shí)間,實(shí)在是不可饒恕的。因此,在實(shí)際中,不同的MessagePump類,實(shí)現(xiàn)是有所不同的,詳見(jiàn)下表:
2. Chrome中的Task
從上面的表不難看出,不論是哪一種消息循環(huán),必須處理的,就是Task(暫且遺忘掉系統(tǒng)消息的處理和Watcher,以后,我們會(huì)緬懷它們的…)。刨去其 它東西的干擾,只留下Task的話,我們可以這樣認(rèn)為:Chrome中的線程從實(shí)現(xiàn)層面來(lái)看沒(méi)有任何區(qū)別,它的區(qū)別只存在于職責(zé)層面,不同職責(zé)的線程,會(huì) 處理不同的Task。最后,在鋪天蓋地西紅柿來(lái)臨之前,我說(shuō)一下啥是Task。
簡(jiǎn)單的看,Task就是一個(gè)類,一個(gè)包含了void Run()抽象方法的類(參見(jiàn)Task類…)。一個(gè)真實(shí)的任務(wù),可以派生Task類,并實(shí)現(xiàn)其Run方法。每個(gè)MessagePump類中,會(huì)有一個(gè) MessagePump::Delegate的類的對(duì)象(MessagePump::Delegate的一個(gè)實(shí)現(xiàn),請(qǐng)參見(jiàn)MessageLoop類…), 在這個(gè)對(duì)象中,會(huì)維護(hù)若干個(gè)Task的隊(duì)列。當(dāng)你期望,你的一個(gè)邏輯在某個(gè)線程內(nèi)執(zhí)行的時(shí)候,你可以派生一個(gè)Task,把你的邏輯封裝在Run方法中,然 后實(shí)例一個(gè)對(duì)象,調(diào)用期望線程中的PostTask方法,將該Task對(duì)象放入到其Task隊(duì)列中去,等待執(zhí)行。我知道很多人已經(jīng)抄起了板磚,因?yàn)檫@種手 法實(shí)在是太常見(jiàn)了,就不是一個(gè)簡(jiǎn)單的依賴倒置,在線程池,Undo\Redo等模塊的實(shí)現(xiàn)中,用的太多了。
但,我想說(shuō)的是,雖說(shuō)誰(shuí)家過(guò)年都是吃頓餃子,這餃子好不好吃還是得看手藝,不能一概而論。在Chrome中,線程模型是統(tǒng)一且唯一的,這就相當(dāng)于有了一套 標(biāo)準(zhǔn),它需要滿足在各個(gè)線程上執(zhí)行的幾十上百種任務(wù)的需求,因此,必須在靈活行和易用性上有良好的表現(xiàn),這就是設(shè)計(jì)標(biāo)準(zhǔn)的難度。為了滿足這些需 求,Chrome在底層庫(kù)上做了足夠的功夫:
1.它提供了一大套的模板封裝(參見(jiàn)task.h),可以將Task擺脫繼承結(jié)構(gòu)、函數(shù)名、函數(shù)參數(shù)等限制(就是基于模板的偽function實(shí)現(xiàn),想要更深入了解,建議直接看鼻祖《Modern C++》和它的Loki庫(kù)…);
2.同時(shí)派生出CancelableTask、ReleaseTask、DeleteTask等子類,提供更為良好的默認(rèn)實(shí)現(xiàn);
3.在消息循環(huán)中,按邏輯的不同,將Task又分成即時(shí)處理的Task、延時(shí)處理的Task、Idle時(shí)處理的Task,滿足不同場(chǎng)景的需求;
4.Task派生自tracked_objects::Tracked,Tracked是為了實(shí)現(xiàn)多線程環(huán)境下的日志記錄、統(tǒng)計(jì)等功能,使得Task天生就有良好的可調(diào)試性和可統(tǒng)計(jì)性;
這一套七葷八素的都搭建完,這才算是一個(gè)完整的Task模型,由此可知,這餃子,做的還是很費(fèi)功夫的。
3. Chrome的多線程模型
工欲善其事,必先利其器。Chrome之所以費(fèi)了老鼻子勁去磨底層框架這把刀,就是為了面對(duì)多線程這坨怪獸的時(shí)候殺的更順暢一些。在Chrome的多線程 模型下,加鎖這個(gè)事情只發(fā)生在將Task放入某線程的任務(wù)隊(duì)列中,其他對(duì)任何數(shù)據(jù)的操作都不需要加鎖。當(dāng)然,天下沒(méi)有免費(fèi)的午餐,為了合理傳遞Task, 你需要了解每一個(gè)數(shù)據(jù)對(duì)象所管轄的線程,不過(guò)這個(gè)事情,與紛繁的加鎖相比,真是小兒科了不知道多少倍。
圖3 Task的執(zhí)行模型
如果你熟悉設(shè)計(jì)模式,你會(huì)發(fā)現(xiàn)這是一個(gè)Command模式,將創(chuàng)建于執(zhí)行的環(huán)境相分離,在一個(gè)線程中創(chuàng)建行為,在另一個(gè)線程中執(zhí)行行為。Command模 式的優(yōu)點(diǎn)在于,將實(shí)現(xiàn)操作與構(gòu)造操作解耦,這就避免了鎖的問(wèn)題,使得多線程與單線程編程模型統(tǒng)一起來(lái),其次,Command還有一個(gè)優(yōu)點(diǎn),就是有利于命令 的組合和擴(kuò)展,在Chrome中,它有效統(tǒng)一了同步和異步處理的邏輯。
Command模式
Command 模式,是一種看上去很酷的模式,傳統(tǒng)的面向?qū)ο缶幊蹋覀兎庋b的往往都是數(shù)據(jù),在Command模式下,我們希望封裝的是行為。這件事在函數(shù)式編程中很正 常,封裝一個(gè)函數(shù)作為參數(shù),傳來(lái)傳去,稀疏平常的事兒;但在面向?qū)ο蟮木幊讨校覀冃枰ㄟ^(guò)繼承、模板、函數(shù)指針等手法,才能將其實(shí)現(xiàn)。
應(yīng)用Command模式,我們是期望這個(gè)行為能到一個(gè)不同于它出生的環(huán)境中去執(zhí)行,簡(jiǎn)而言 之,這是一種想生不想養(yǎng)的行為。我們做Undo/Redo的時(shí)候,會(huì)把在任一一個(gè)環(huán)境中創(chuàng)建的Command,放到一個(gè)隊(duì)列環(huán)境中去,供統(tǒng)一的調(diào)度;在 Chrome中,也是如此,我們?cè)谝粋€(gè)線程環(huán)境中創(chuàng)建了Task,卻把它放到別的線程中去執(zhí)行,這種寄居蟹似的生活方式,在很多場(chǎng)合都是有用武之地的。
在一般的多線程模型中,我們需要分清楚啥是同步啥是異步,在同步模式下,一切看上去和單線程沒(méi)啥區(qū)別,但同時(shí)也喪失了多線程的優(yōu)勢(shì)(淪落成為多線程串 行…)。而如果采用異步的模式,那寫(xiě)起來(lái)就麻煩多了,你需要注冊(cè)回調(diào),小心管理對(duì)象的生命周期,程序?qū)懗鰜?lái)是嗷嗷惡心。在Chrome的多線程模型下,同 步和異步的編程模型區(qū)別就不復(fù)存在了,如果是這樣一個(gè)場(chǎng)景:A線程需要B線程做一些事情,然后回到A線程繼續(xù)做一些事情;在Chrome下你可以這樣來(lái) 做:生成一個(gè)Task,放到B線程的隊(duì)列中,在該Task的Run方法最后,會(huì)生成另一個(gè)Task,這個(gè)Task會(huì)放回到A的線程隊(duì)列,由A來(lái)執(zhí)行。如此 一來(lái),同步異步,天下一統(tǒng),都是Task傳來(lái)傳去,想不會(huì),都難了。
圖4 Chrome的一種異步執(zhí)行的解決方案
4. Chrome多線程模型的優(yōu)缺點(diǎn)
一直在說(shuō)Chrome在規(guī)避鎖的問(wèn)題,那到底鎖是哪里不好,犯了何等滔天罪責(zé),落得如此人見(jiàn)人嫌恨不得先殺而后快的境地。《代碼之美》的第二十四章“美麗 的并發(fā)”中,Haskell設(shè)計(jì)人之一的Simon Peyton Jones總結(jié)了一下用鎖的困難之處,如下:
1.鎖少加了,導(dǎo)致兩個(gè)線程同時(shí)修改一個(gè)變量;
2.鎖多加了,輕則妨礙并發(fā),重則導(dǎo)致死鎖;
3.鎖加錯(cuò)了,由于鎖和需要鎖的數(shù)據(jù)之間的聯(lián)系,只存在于程序員的大腦中,這種事情太容易發(fā)生了;
4.加鎖的順序錯(cuò)了,維護(hù)鎖的順序是一件困難而又容易出錯(cuò)的問(wèn)題;
5.錯(cuò)誤恢復(fù);
6.忘記喚醒和錯(cuò)誤的重試;
7. 而最根本的缺陷,是鎖和條件變量不支持模塊化的編程。比如一個(gè)轉(zhuǎn)賬業(yè)務(wù)中,A賬戶扣了100元錢(qián),B賬戶增加了100元,即使這兩個(gè)動(dòng)作單獨(dú)用鎖保護(hù)維持 其正確性,你也不能將兩個(gè)操作簡(jiǎn)單的串在一起完成一個(gè)轉(zhuǎn)賬操作,你必須讓它們的鎖都暴露出來(lái),重新設(shè)計(jì)一番。好好的兩個(gè)函數(shù),愣是不能組在一起用,這就是 鎖的最大悲哀;
通過(guò)這些缺點(diǎn)的描述,也就可以明白Chrome多線程模型的優(yōu)點(diǎn)。它解決了鎖的最根本缺陷,即,支持模塊化的編程,你只需要維護(hù)對(duì)象和線程之間的職能關(guān)系即可,這個(gè)攤子,比之鎖的那個(gè)爛攤子,要簡(jiǎn)化了太多。對(duì)于程序員來(lái)說(shuō),負(fù)擔(dān)一瞬間從泰山降成了鴻毛。
而Chrome多線程模型的一個(gè)主要難點(diǎn),在于線程與數(shù)據(jù)關(guān)系的設(shè)計(jì)上,你需要良好的劃分各個(gè)線程的職責(zé),如果有一個(gè)線程所管轄的數(shù)據(jù),幾乎占據(jù)了大半部分的Task,那么它就會(huì)從多線程淪為單線程,Task隊(duì)列的鎖也將成為一個(gè)大大的瓶頸。
設(shè)計(jì)者的職責(zé)
一 個(gè)底層結(jié)構(gòu)設(shè)計(jì)是否成功,這個(gè)設(shè)計(jì)者是否稱職,我一直覺(jué)得是有一個(gè)很簡(jiǎn)單的衡量標(biāo)準(zhǔn)的。你不需要看這個(gè)設(shè)計(jì)人用了多少NB的技術(shù),你只需要關(guān)心,他的設(shè) 計(jì),是否給其他開(kāi)發(fā)人員帶來(lái)了困難。一個(gè)NB的設(shè)計(jì),是將所有困難都集中在底層搞定,把其他開(kāi)發(fā)人員換成白癡都可以工作的那種;一個(gè)SB的設(shè)計(jì),是自己弄 了半天,只是為了給其他開(kāi)發(fā)人員一個(gè)長(zhǎng)達(dá)250條的注意事項(xiàng),然后很NB的說(shuō),你們按照這個(gè)手冊(cè)去開(kāi)發(fā),就不會(huì)有問(wèn)題了。
從根本上來(lái)說(shuō),Chrome的線程模型解決的是并發(fā)中的用戶體驗(yàn)問(wèn)題而不是聯(lián)合工作的問(wèn)題(參見(jiàn)我前面噴的“閑話并發(fā)”),它不是和Map/Reduce 那樣將關(guān)注點(diǎn)放在數(shù)據(jù)和執(zhí)行步驟的拆分上,而是放在線程和數(shù)據(jù)的對(duì)應(yīng)關(guān)系上,這是和瀏覽器的工作環(huán)境相匹配的。設(shè)計(jì)總是和所處的環(huán)境相互依賴的,畢竟,在 客戶端,不會(huì)和服務(wù)器一樣,存在超規(guī)模的并發(fā)處理任務(wù),而只是需要盡可能的改善用戶體驗(yàn),從這個(gè)角度來(lái)說(shuō),Chrome的多線程模型,至少看上去很美。
Chrome源碼剖析【二】—— 進(jìn)程通信
【二】Chrome的進(jìn)程間通信
1. Chrome進(jìn)程通信的基本模式
進(jìn)程間通信,叫做IPC(Inter-Process Communication)。Chrome最主要有三類進(jìn)程,一類是Browser主進(jìn)程,我們一直尊稱它老人家為老大;還有一類是各個(gè)Render進(jìn) 程,前面也提過(guò)了;另外還有一類一直沒(méi)說(shuō)過(guò),是Plugin進(jìn)程,每一個(gè)插件,在Chrome中都是以進(jìn)程的形式呈現(xiàn),等到后面說(shuō)插件的時(shí)候再提罷了。 Render進(jìn)程和Plugin進(jìn)程都與老大保持進(jìn)程間的通信,Render進(jìn)程與Plugin進(jìn)程之間也有彼此聯(lián)系的通路,唯獨(dú)是多個(gè)Render進(jìn)程 或多個(gè)Plugin進(jìn)程直接,沒(méi)有互相聯(lián)系的途徑,全靠老大協(xié)調(diào)。
進(jìn)程與進(jìn)程間通信,需要仰仗操作系統(tǒng)的特性,能玩的花著實(shí)不多,在Chrome中,用到的就是有名的管道(Named Pipe),只不過(guò),它用一個(gè)IPC::Channel類,封裝了具體的實(shí)現(xiàn)細(xì)節(jié)。Channel可以有兩種工作模式,一種是Client,一種是 Server,Server和Client分屬兩個(gè)進(jìn)程,維系一個(gè)共同的管道名,Server負(fù)責(zé)創(chuàng)建該管道,Client會(huì)嘗試連接該管道,然后雙發(fā)往 各自管道緩沖區(qū)中讀寫(xiě)數(shù)據(jù)(在Chrome中,用的是二進(jìn)制流,異步IO…),完成通信。
管道名字的協(xié)商
在 Socket中,我們會(huì)事先約定好通信的端口,如果不按照這個(gè)端口進(jìn)行訪問(wèn),走錯(cuò)了門(mén),會(huì)被直接亂棍打出門(mén)去的。與之類似,有名管道期望在兩個(gè)進(jìn)程間游 走,就需要拿一個(gè)兩個(gè)進(jìn)程都能接受的進(jìn)門(mén)暗號(hào),這個(gè)就是有名管道的名字。在Chrome中(windows下…),有名管道的名字格式都是:\\.\pipe\chrome.ID。其中的ID,自然是要求獨(dú)一無(wú)二,比如:進(jìn)程ID.實(shí)例地址.隨機(jī)數(shù)。通常,這個(gè)ID是由一個(gè)Process生成(往往是Browser Process),然后在創(chuàng)建另一個(gè)進(jìn)程的時(shí)候,作為命令行參數(shù)傳進(jìn)去,從而完成名字的協(xié)商。
如果不了解并期待了解有關(guān)Windows下有名管道和信號(hào)量的知識(shí),建議去看一些專業(yè)的書(shū) 籍,比如圣經(jīng)級(jí)別的《Windows核心編程》和《深入解析Windows操作系統(tǒng)》,當(dāng)然也可以去查看SDK,你需要了解的API可能包 括:CreateNamedPipe, CreateFile, ConnectNamedPipe, WaitForMultipleObjects, WaitForSingleObject, SetEvent, 等等。
Channel中,有三個(gè)比較關(guān)鍵的角色,一個(gè)是Message::Sender,一個(gè)是Channel::Listener,最后一個(gè)是 MessageLoopForIO::Watcher。Channel本身派生自Sender和Watcher,身兼兩角,而Listener是一個(gè)抽象 類,具體由Channel的使用者來(lái)實(shí)現(xiàn)。顧名思義,Sender就是發(fā)送消息的接口,Listener就是處理接收到消息的具體實(shí)現(xiàn),但這個(gè) Watcher是啥?如果你覺(jué)得Watcher這東西看上去很眼熟的話,我會(huì)激動(dòng)的熱淚盈眶的,沒(méi)錯(cuò),在前面(第一部分第一小節(jié)…)說(shuō)消息循環(huán)的時(shí)候,從 那個(gè)表中可以看到,IO線程(記住,在Chrome中,IO指的是網(wǎng)絡(luò)IO,*_*)的循環(huán)會(huì)處理注冊(cè)了的Watcher。其實(shí)Watcher很簡(jiǎn)單,可 以視為一個(gè)信號(hào)量和一個(gè)帶有OnObjectSignaled方法對(duì)象的對(duì),當(dāng)消息循環(huán)檢測(cè)到信號(hào)量開(kāi)啟,它就會(huì)調(diào)用相應(yīng)的 OnObjectSignaled方法。
圖5 Chrome的IPC處理流程圖
一圖解千語(yǔ),如上圖所示,整個(gè)Chrome最核心的IPC流程都在圖上了,期間,刨去了一些錯(cuò)誤處理等邏輯,如果想看原汁原味的,可以自查Channel 類的實(shí)現(xiàn)。當(dāng)有消息被Send到一個(gè)發(fā)送進(jìn)程的Channel的時(shí)候,Channel會(huì)把它放在發(fā)送消息隊(duì)列中,如果此時(shí)還正在發(fā)送以前的消息(發(fā)送端被 阻塞…),則看一下阻塞是否解除(用一個(gè)等待0秒的信號(hào)量等待函數(shù)…),然后將消息隊(duì)列中的內(nèi)容序列化并寫(xiě)道管道中去。操作系統(tǒng)會(huì)維護(hù)異步模式下管道的這 一組信號(hào)量,當(dāng)消息從發(fā)送進(jìn)程緩沖區(qū)寫(xiě)到接收進(jìn)程的緩沖區(qū)后,會(huì)激活接收端的信號(hào)量。當(dāng)接收進(jìn)程的消息循環(huán),循到了檢查Watcher這一步,并發(fā)現(xiàn)有信 號(hào)量激活了,就會(huì)調(diào)用該Watcher相應(yīng)的OnObjectSignaled方法,通知接受進(jìn)程的Channel,有消息來(lái)了!Channel會(huì)嘗試從 管道中收字節(jié),組消息,并調(diào)用Listener來(lái)解析該消息。
從上面的描述不難看出,Chrome的進(jìn)程通信,最核心的特點(diǎn),就是利用消息循環(huán)來(lái)檢查信號(hào)量,而不是直接讓管道阻塞在某信號(hào)量上。這樣就與其多線程模型 緊密聯(lián)系在了一起,用一種統(tǒng)一的模式來(lái)解決問(wèn)題。并且,由于是消息循環(huán)統(tǒng)一檢查,線程不會(huì)隨便就被阻塞了,可以更好的處理各種其他工作,從理論上講,這是 通過(guò)增加CPU工作時(shí)間,來(lái)?yè)Q取更好的體驗(yàn),頗有資本家的派頭。
溫柔的消息循環(huán)
其實(shí),Chrome的很多消息循環(huán),也不是都那么霸道,也是會(huì)被阻塞在某些信號(hào)量或者某種場(chǎng)景上的,畢竟客戶端不是它家的服務(wù)器,CPU不能被全部歸在它家名下。
比如IO線程,當(dāng)沒(méi)有消息來(lái)到,又沒(méi)有信號(hào)量被激活的時(shí)候,就會(huì)被阻塞,具體實(shí)現(xiàn)可以去看MessagePumpForIO的WaitForWork方法。
不過(guò)這種阻塞是集中式的,可隨時(shí)修改策略的,比起Channel直接阻塞在信號(hào)量上,停工的時(shí)間更短。
2. 進(jìn)程間的跨線程通信和同步通信
在Chrome中,任何底層的數(shù)據(jù)都是線程非安全的,Channel不是太上老君(抑或中國(guó)足球?…),它也沒(méi)有例外。在每一個(gè)進(jìn)程中,只能有一個(gè)線程來(lái) 負(fù)責(zé)操作Channel,這個(gè)線程叫做IO線程(名不符實(shí)真是一件悲涼的事情…)。其它線程要是企圖越俎代庖,是會(huì)出大亂子的。
但是有時(shí)候(其實(shí)是大部分時(shí)候…),我們需要從非IO線程與別的進(jìn)程相通信,這該如何是好?如果,你有看過(guò)我前面寫(xiě)的線程模型,你一定可以想到,做法很簡(jiǎn) 單,先將對(duì)Channel的操作放到Task中,將此Task放到IO線程隊(duì)列里,讓IO線程來(lái)處理即可。當(dāng)然,由于這種事情發(fā)生的太頻繁,每次都人肉做 一次頗為繁瑣,于是有一個(gè)代理類,叫做ChannelProxy,來(lái)幫助你完成這一切。
從接口上看,ChannelProxy的接口和Channel沒(méi)有大的區(qū)別(否則就不叫Proxy了…),你可以像用Channel一樣,用 ChannelProxy來(lái)Send你的消息,ChannelProxy會(huì)辛勤的幫你完成剩余的封裝Task等工作。不僅如此,ChannelProxy 還青出于藍(lán)勝于藍(lán),在這個(gè)層面上做了更多的事情,比如:發(fā)送同步消息。
不過(guò)能發(fā)送同步消息的類不是ChannelProxy,而是它的子類,SyncChannel。在Channel那里,所有的消息都是異步的(在 Windows中,也叫Overlapped…),其本身也不支持同步邏輯。為了實(shí)現(xiàn)同步,SyncChannel并沒(méi)有另造輪子,而只是在 Channel的層面上加了一個(gè)等待操作。當(dāng)ChannelProxy的Send操作返回后,SyncChannel會(huì)把自己阻塞在一組信號(hào)量上,等待回 包,直到永遠(yuǎn)或超時(shí)。從外表上看同步和異步?jīng)]有什么區(qū)別,但在使用上還是要小心,在UI線程中使用同步消息,是容易被發(fā)指的。
3. Chrome中的IPC消息格式
說(shuō)了半天,還有一個(gè)大頭沒(méi)有提過(guò),那就是消息包。如果說(shuō),多線程模式下,對(duì)數(shù)據(jù)的訪問(wèn)開(kāi)銷來(lái)自于鎖,那么在多進(jìn)程模式下,大部分的額外開(kāi)銷都來(lái)自于進(jìn)程間 的消息拆裝和傳遞。不論怎么樣的模式,只要進(jìn)程不同,消息的打包,序列化,反序列化,組包,都是不可避免的工作。
在Chrome中,IPC之間的通信消息,都是派生自IPC::Message類的。對(duì)于消息而言,序列化和反序列化是必須要支持的,Message的基 類Pickle,就是干這個(gè)活的。Pickle提供了一組的接口,可以接受int,char,等等各種數(shù)據(jù)的輸入,但是在Pickle內(nèi)部,所有的一切都 沒(méi)有區(qū)別,都轉(zhuǎn)化成了一坨二進(jìn)制流。這個(gè)二進(jìn)制流是32位齊位的,比如你只傳了一個(gè)bool,也是最少占32位的,同時(shí),Pickle的流是有自增邏輯的 (就是說(shuō)它會(huì)先開(kāi)一個(gè)Buffer,如果滿了的話,會(huì)加倍這個(gè)Buffer…),使其可以無(wú)限擴(kuò)展。Pickle本身不維護(hù)任何二進(jìn)制流邏輯上的信息,這 個(gè)任務(wù)交到了上級(jí)處理(后面會(huì)有說(shuō)到…),但Pickle會(huì)為二進(jìn)制流添加一個(gè)頭信息,這個(gè)里面會(huì)存放流的長(zhǎng)度,Message在繼承Pickle的時(shí) 候,擴(kuò)展了這個(gè)頭的定義,完整的消息格式如下:
圖6 Chrome的IPC消息格式
其中,黃色部分是包頭,定長(zhǎng)96個(gè)bit,綠色部分是包體,二進(jìn)制流,由payload_size指明長(zhǎng)度。從大小上看這個(gè)包是很精簡(jiǎn)的了,除了 routing位在消息不為路由消息的時(shí)候會(huì)有所浪費(fèi)。消息本身在有名管道中是按照二進(jìn)制流進(jìn)行傳輸?shù)模ㄓ忻艿揽梢詡鬏攦煞N類型的字符流,分別是二進(jìn)制 流和消息流…),因此由payload_size + 96bits,就可以確定是否收了一個(gè)完整的包。
從邏輯上來(lái)看,IPC消息分成兩類,一類是路由消息(routed message),還有一類是控制消息(control message)。路由消息是私密的有目的地的,系統(tǒng)會(huì)依照路由信息將消息安全的傳遞到目的地,不容它人窺視;控制消息就是一個(gè)廣播消息,誰(shuí)想聽(tīng)等能夠聽(tīng) 得到。
消息的序列化
前不久讀了Google Protocol Buffers的源碼,是用在服務(wù)器端,用做內(nèi)部機(jī)器通信協(xié)議的標(biāo)準(zhǔn)、代碼生成工具和框架。它主要的思想是揉合了key/value的內(nèi)容到二進(jìn)制中,幫助生成更為靈活可靠的二進(jìn)制協(xié)議。
在Chrome中,沒(méi)有使用這套東西,而是用到了純二進(jìn)制流作為消息序列化的方式。我想這 是由于應(yīng)用場(chǎng)景不同使然。在服務(wù)端,我們更關(guān)心協(xié)議的穩(wěn)定性,可擴(kuò)展性,并且,涉及到的協(xié)議種類很多。但在一個(gè)Chrome中,消息的格式很統(tǒng)一,這方面 沒(méi)有擴(kuò)展性和靈活性的需求,而在序列化上,雖然key/value的方式很好很強(qiáng)大,但是在Chrome中需要的不是靈活性而是精簡(jiǎn)性,因此寧可不用 Protocol Buffers造好的輪子,而是另立爐灶,花了好一把力氣提供了一套純二進(jìn)制的消息機(jī)制。
4. 定義IPC消息
如果你寫(xiě)過(guò)MFC程序,對(duì)MFC那里面一大堆宏有所忌憚的話,那么很不幸,在Chrome中的IPC消息定義中,你需要再吃一點(diǎn)苦頭了,甚至,更苦大仇深 一些;如果你曾經(jīng)領(lǐng)教過(guò)用模板的特化偏特化做Traits、用模板做函數(shù)重載、用編譯期的Tuple做變參數(shù)支持,之類機(jī)制的種種麻煩的話,那么,同樣很 遺憾,在Chrome中,你需要再感受一次。。。
不過(guò),先讓我們忘記宏和模板,看人肉一個(gè)消息,到底需要哪些操作。一個(gè)標(biāo)準(zhǔn)的IPC消息定義應(yīng)該是類似于這樣的:
class SomeMessage: public IPC::Message
{
public:
enum { ID = …; }
SomeMessage(SomeType & data)
: IPC::Message(MSG_ROUTING_CONTROL, ID, ToString(data))
{…}
…
};
大概意思是這樣的,你需要從Message(或者其他子類)派生出一個(gè)子類,該子類有一個(gè)獨(dú)一無(wú)二的ID值,該子類接受一個(gè)參數(shù),你需要對(duì)這個(gè)參數(shù)進(jìn)行序列化。兩個(gè)麻煩的地方看的很清楚,如果生成獨(dú)一無(wú)二的ID值?如何更方便的對(duì)任何參數(shù)可以自動(dòng)的序列化?。
在Chrome中,解決這兩個(gè)問(wèn)題的答案,就是宏 + 模板。Chrome為每個(gè)消息安排了一種ID規(guī)格,用一個(gè)16bits的值來(lái)表示,高4位標(biāo)識(shí)一個(gè)Channel,低12位標(biāo)識(shí)一個(gè)消息的子id,也就是 說(shuō),最多可以有16種Channel存在不同的進(jìn)程之間,每一種Channel上可以定義4k的消息。目前,Chrome已經(jīng)用掉了8種 Channel(如果A、B進(jìn)程需要雙向通信,在Chrome中,這是兩種不同的Channel,需要定義不同的消息,也就是說(shuō),一種雙向的進(jìn)程通信關(guān) 系,需要耗費(fèi)兩個(gè)Channel種類…),他們已經(jīng)覺(jué)得,16bits的ID格式不夠用了,在將來(lái)的某一天,估計(jì)就被擴(kuò)展成了32bits的。書(shū)歸正 傳,Chrome是這么來(lái)定義消息ID的,用一個(gè)枚舉類,讓它從高到低往下走,就像這樣:
enum SomeChannel_MsgType
{
SomeChannelStart = 5 << 12,
SomeChannelPreStart = (5 << 12) – 1,
Msg1,
Msg2,
Msg3,
…
MsgN,
SomeChannelEnd
};
這是一個(gè)類型為5的Channel的消息ID聲明,由于指明了最開(kāi)始的兩個(gè)值,所以后續(xù)枚舉的值會(huì)依次遞減,如此,只要維護(hù)Channel類型的唯一性, 就可以維護(hù)所有消息ID的唯一性了(當(dāng)然,前提是不能超過(guò)消息上限…)。但是,定義一個(gè)ID還不夠,你還需要定義一個(gè)使用該消息ID的Message子 類。這個(gè)步驟不但繁瑣,最重要的,是違反了DIY原則,為了添加一個(gè)消息,你需要在兩個(gè)地方開(kāi)工干活,是可忍孰不可忍,于是Google祭出了宏這顆原子 彈,需要定義消息,格式如下:
IPC_BEGIN_MESSAGES(PluginProcess, 3)
IPC_MESSAGE_CONTROL2(PluginProcessMsg_CreateChannel,
int /* process_id */,
HANDLE /* renderer handle */)
IPC_MESSAGE_CONTROL1(PluginProcessMsg_ShutdownResponse,
bool /* ok to shutdown */)
IPC_MESSAGE_CONTROL1(PluginProcessMsg_PluginMessage,
std::vector<uint8> /* opaque data */)
IPC_MESSAGE_CONTROL0(PluginProcessMsg_BrowserShutdown)
IPC_END_MESSAGES(PluginProcess)
這是Chrome中,定義PluginProcess消息的宏,我挖過(guò)來(lái)放在這了,如果你想添加一條消息,只需要添加一條類似與 IPC_MESSAGE_CONTROL0東東即可,這說(shuō)明它是一個(gè)控制消息,參數(shù)為0個(gè)。你基本上可以這樣理解,IPC_BEGIN_MESSAGES 就相當(dāng)于完成了一個(gè)枚舉開(kāi)始的聲明,然后中間的每一條,都會(huì)在枚舉里面增加一個(gè)ID,并聲明一個(gè)子類。這個(gè)一宏兩吃,直逼北京烤鴨兩吃的高超做法,可以參 看ipc_message_macros.h,或者看下面一宏兩吃的一個(gè)舉例。
多次展開(kāi)宏的技巧
這是Chrome中用到的一個(gè)技巧,定義一次宏,展開(kāi)多段代碼,我孤陋寡聞,第一次見(jiàn),一個(gè)類似的例子,如下:
首先,定義一個(gè)macro.h,里面放置宏的定義:
#undef SUPER_MACRO
#if defined(FIRST_TIME)
#undef FIRST_TIME
#define SUPER_MACRO(label, type) \
enum IDs { \
label##__ID = 10 \
};
#elif defined(SECOND_TIME)
#undef SECOND_TIME
#define SUPER_MACRO(label, type) \
class TestClass \
{ \
public: \
enum {ID = label##__ID}; \
TestClass(type value) : _value(value) {} \
type _value; \
};
#endif
可以看到,這個(gè)頭文件是可重入的,每一次先undef掉之前的定義,然后判斷進(jìn)行新的定義。然后,你可以創(chuàng)建一個(gè)use_macro.h文件,利用這個(gè)宏,定義具體內(nèi)容:
#include “macros.h”
SUPER_MACRO(Test, int)
這個(gè)頭文件在利用宏的部分不需要放到ifundef…define…這樣的頭文件保護(hù)中,目的就是為了可重入。在主函數(shù)中,你可以多次define + include,實(shí)現(xiàn)多次展開(kāi)的目的:
#define FIRST_TIME
#include “use_macro.h”
#define SECOND_TIME
#include “use_macro.h”
#include <iostream>
int _tmain(int argc, _TCHAR* argv[])
{
TestClass t(5);
std::cout << TestClass::ID << std::endl;
std::cout << t._value << std::endl;
return 0;
}
這樣,你就成功的實(shí)現(xiàn),一次定義,生成多段代碼了。
此外,當(dāng)接收到消息后,你還需要處理消息。接收消息的函數(shù),是 IPC::Channel::Listener子類的OnMessageReceived函數(shù)。在這個(gè)函數(shù)中,會(huì)放置一坨的宏,這一套宏,一定能讓你想起 MFC的Message Map機(jī)制(關(guān)于此消息機(jī)制原理更具體的介紹,可參考侯捷的深入淺出MFC一書(shū)。):
IPC_BEGIN_MESSAGE_MAP_EX(RenderProcessHost, msg, msg_is_ok)
IPC_MESSAGE_HANDLER(ViewHostMsg_PageContents, OnPageContents)
IPC_MESSAGE_HANDLER(ViewHostMsg_UpdatedCacheStats,
OnUpdatedCacheStats)
IPC_MESSAGE_UNHANDLED_ERROR()
IPC_END_MESSAGE_MAP_EX()
這個(gè)東西很簡(jiǎn)單,展開(kāi)后基本可以視為一個(gè)Switch循環(huán),判斷消息ID,然后將消息,傳遞給對(duì)應(yīng)的函數(shù)。與MFC的Message Map比起來(lái),做的事情少多了。
通過(guò)宏的手段,可以解決消息類聲明和消息的分發(fā)問(wèn)題,但是自動(dòng)的序列化還不能支持(所謂自動(dòng)的序列化,就是不論你是什么類型的參數(shù),幾個(gè)參數(shù),都可以直接 序列化,不需要另寫(xiě)代碼…)。在C++這種語(yǔ)言中,所謂自動(dòng)的序列化,自動(dòng)的類型識(shí)別,自動(dòng)的XXX,往往都是通過(guò)模板來(lái)實(shí)現(xiàn)的。這些所謂的自動(dòng)化,其實(shí) 就是通過(guò)事前的大量人肉勞作,和模板自動(dòng)遞推來(lái)實(shí)現(xiàn)的,如果說(shuō).Net或Java中的自動(dòng)序列化是過(guò)山軌道,這就是那挑夫的驕子,雖然最后都是兩腿不動(dòng)到 了山頂,這底下費(fèi)得力氣真是天壤之別啊。具體實(shí)現(xiàn)技巧,有興趣的看看侯捷的《STL源碼剖析》,或者是《C++新思維》,或者Chrome中的 ipc_message_utils.h,這要說(shuō)清楚實(shí)在不是一兩句的事情。
總之通過(guò)宏和模板,你可以很簡(jiǎn)單的聲明一個(gè)消息,這個(gè)消息可以傳入各式各樣的參數(shù)(這里用到了夸張的修辭手法,其實(shí),只要是模板實(shí)現(xiàn)的自動(dòng)化,永遠(yuǎn)都是有 限制的,在Chrome的模板實(shí)現(xiàn)中,參數(shù)數(shù)量不要超過(guò)5個(gè),類型需要是基本類型、STL容器等,在不BT的場(chǎng)合,應(yīng)該夠用了…),你可以調(diào)用 Channel、ChannelProxy、SyncChannel之類的Send方法,將消息發(fā)送給其他進(jìn)程,并且,實(shí)現(xiàn)一個(gè)Listener類,用 Message Map來(lái)分發(fā)消息給對(duì)應(yīng)的處理函數(shù)。如此,整個(gè)IPC體系搭建完成。
苦力的宏和模板
不論是宏還是模板,為了實(shí)現(xiàn)這套機(jī)制,都需要寫(xiě)大量的類似代碼,比如為了支持0~N個(gè)參數(shù)的Control消息,你就需要寫(xiě)N+1個(gè)類似的宏;為了支持各種基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)的序列化,你就需要寫(xiě)上十來(lái)個(gè)類似的Write函數(shù)和Traits。
之所以做如此苦力的活,都是為了用這些東西的人能夠盡可能的簡(jiǎn)單方便,符合DIY原則。規(guī) 約到之前說(shuō)的設(shè)計(jì)者的職責(zé)上來(lái),這是一個(gè)典型的苦了我一個(gè)幸福千萬(wàn)人的負(fù)責(zé)任的行為。在Chrome中,如此的代碼隨處可見(jiàn),光Tuple那一套拳法,我 現(xiàn)在就看到了使了不下三次(我曾經(jīng)做過(guò)一套,直接吐血…),如此兢兢業(yè)業(yè),真是可歌可泣啊。
【三】 Chrome的進(jìn)程模型
1. 基本的進(jìn)程結(jié)構(gòu)
Chrome是一個(gè)多進(jìn)程的架構(gòu),不過(guò)所有的進(jìn)程都會(huì)由老大,Browser進(jìn)程來(lái)管理,走的是集中化管理的路子。在Browser進(jìn)程中,有 xxxProcessHost,每一個(gè)host,都對(duì)應(yīng)著一個(gè)Process,比如RenderProcessHost對(duì)應(yīng)著 RenderProcess,PluginProcessHost對(duì)應(yīng)著PluginProcess,有多少個(gè)host的實(shí)例,就有多少個(gè)進(jìn)程在運(yùn)行。
這是一個(gè)比較典型的代理模式,Browser對(duì)Host的操作,都會(huì)被Host封裝成IPC消息,傳遞給對(duì)應(yīng)的Process來(lái)處理,對(duì)于大部分上層的類,也就隔離了多進(jìn)程細(xì)節(jié)。
2. Render進(jìn)程
先不扯Plugin的進(jìn)程,只考慮Render進(jìn)程。前面說(shuō)了,一個(gè)Process一個(gè)tab,只是廣告用語(yǔ),實(shí)際上,每一個(gè)web頁(yè)面內(nèi)容(包括在 tab中的和在彈出窗口中的…),在Chrome中,用RenderView表示一個(gè)web頁(yè)面,每一個(gè)RenderView可以寄宿在任一一個(gè) RenderProcess中,它只是依托RenderProcess幫助它進(jìn)行通信。每一個(gè)RenderProcess進(jìn)程都可以有1到N個(gè) RenderView實(shí)例。
Chrome支持不同的進(jìn)程模型,可以一個(gè)tab一個(gè)進(jìn)程,一個(gè)site instance一個(gè)進(jìn)程等等。但基本模式都是一致的,當(dāng)需要?jiǎng)?chuàng)建一個(gè)新的RenderView的時(shí)候,Chrome會(huì)嘗試進(jìn)行選擇或者是創(chuàng)建進(jìn)程。比 如,在one site one process的模式下,如果存在此site,就會(huì)選擇一個(gè)已有的RenderProcessHost,讓它管理這個(gè)新的RenderView,否則,會(huì) 創(chuàng)建一個(gè)RenderProcessHost(同時(shí)也就創(chuàng)建了一個(gè)Process),把RenderView交給它。
在默認(rèn)的one site instance one process的模式中,Chrome會(huì)為每個(gè)新的site instance創(chuàng)建一個(gè)進(jìn)程(從一個(gè)頁(yè)面鏈開(kāi)來(lái)的頁(yè)面,屬于同一個(gè)site instance),但,Render進(jìn)程總數(shù)是有個(gè)上限的。這個(gè)上限,根據(jù)內(nèi)存大小的不同而異,比如,在我的機(jī)器上(2G內(nèi)存),最多可以容納20個(gè) Render進(jìn)程,當(dāng)達(dá)到這個(gè)上限后,你再開(kāi)新的網(wǎng)站,Chrome會(huì)隨機(jī)為你選擇一個(gè)已有的進(jìn)程,把這個(gè)網(wǎng)站對(duì)應(yīng)的RenderView給扔進(jìn)去。。。
每一次你新輸入一個(gè)站點(diǎn)信息,在默認(rèn)模式下,都必然導(dǎo)致一個(gè)進(jìn)程的誕生,很可能,伴隨著另一個(gè)進(jìn)程的死亡(如果這個(gè)進(jìn)程沒(méi)有其他承載的 RenderView的話,他就自然死亡了,RenderView的個(gè)數(shù),就相當(dāng)于這個(gè)進(jìn)程的引用計(jì)數(shù)…)。比如,你打開(kāi)一個(gè)新標(biāo)簽頁(yè)的時(shí)候,系統(tǒng)為你創(chuàng) 造了一個(gè)進(jìn)程來(lái)承載這個(gè)新標(biāo)簽頁(yè),你輸入http://www.baidu.com/,于是新標(biāo)簽頁(yè)進(jìn)程死亡,承載http://www.baidu.com/的進(jìn)程誕生。你用baidu搜索了一下,毫無(wú)疑問(wèn),你基本對(duì)它的搜索結(jié)果很失望,于是你重新輸入http://www.google.com.hk/, 老的承載baidu的進(jìn)程死亡,承載google的進(jìn)程被構(gòu)建出來(lái)。這時(shí)候你想回退到之前baidu的搜索結(jié)果,樂(lè)呵樂(lè)呵的話,一個(gè)新的承載baidu的 進(jìn)程被創(chuàng)造,之前Google的進(jìn)程死亡。同樣,你再次點(diǎn)擊前進(jìn),又來(lái)到Google搜索結(jié)果的時(shí)候,一個(gè)新的進(jìn)程有取代老的進(jìn)程出現(xiàn)了。
以上現(xiàn)象,你都可以自己來(lái)檢驗(yàn),通過(guò)觀察about:memory頁(yè)面的信息,你可以了解整個(gè)過(guò)程(記得每做一步,需要刷新一下about:memory 頁(yè)面)。我唧唧歪歪說(shuō)了半天,其實(shí)想表達(dá)的是,Chrome并沒(méi)有像我YY的一樣做啥進(jìn)程池之類的特殊機(jī)制,而是簡(jiǎn)單的履行有就創(chuàng)建、沒(méi)有就銷毀的策略。 我并不知道有沒(méi)有啥很有效的多進(jìn)程模型,這方面一點(diǎn)都沒(méi)玩過(guò),猜測(cè)Chrome之所以采取這樣的策略,是經(jīng)過(guò)琢磨的,覺(jué)得進(jìn)程生死的代價(jià)可以承受,比較可 行。
3. 進(jìn)程開(kāi)銷控制算法
說(shuō)開(kāi)銷無(wú)外乎兩方面的內(nèi)容,一為時(shí)間,二則空間。Chrome沒(méi)有在進(jìn)程創(chuàng)建和銷毀上做功夫,但是當(dāng)進(jìn)程運(yùn)行起來(lái)后,還是做了一些工作的。
節(jié)約工作首先從CPU耗時(shí)上做起,優(yōu)先級(jí)越高的進(jìn)程中的線程,越容易被調(diào)度,從而耗費(fèi)CPU時(shí)間,于是,當(dāng)一個(gè)頁(yè)面不再直接面對(duì)用戶的時(shí)候,Chrome 會(huì)將它的進(jìn)程優(yōu)先級(jí)切到Below Normal的級(jí)別,反之,則切回Normal級(jí)別。通過(guò)這個(gè)步驟,小節(jié)約了一把時(shí)間。
進(jìn)程的優(yōu)先級(jí)
在 windows中,進(jìn)程是有優(yōu)先級(jí)的,當(dāng)然,這個(gè)優(yōu)先級(jí)不是真實(shí)的調(diào)度優(yōu)先級(jí),而是該進(jìn)程中,線程優(yōu)先級(jí)計(jì)算的基準(zhǔn)。在《Windows via C/C++》(也就是《windows核心編程》的第五版)中,有一張?jiān)敿?xì)的表,表述了線程優(yōu)先級(jí)和進(jìn)程優(yōu)先級(jí)的具體對(duì)應(yīng)關(guān)系,感覺(jué)設(shè)計(jì)的很不錯(cuò),在此就 不再贅述了,有興趣的自行動(dòng)手翻書(shū)。
當(dāng)然這只是一道開(kāi)胃小菜,滿漢全席是控制進(jìn)程的工作集大小,以達(dá)到降低進(jìn)程實(shí)際內(nèi)存消耗的目的(Chrome為了體現(xiàn)它對(duì)內(nèi)存的節(jié)約,用了“更為精確”的 內(nèi)存消耗計(jì)算方法…)。提到這一點(diǎn),Chrome頗為自豪,在文檔中,順著道把單進(jìn)程的模式鄙視了一下,基本意思是:在多進(jìn)程的模式下,各個(gè)頁(yè)面實(shí)際占用 的內(nèi)存數(shù)量,更容易被控制,而在單進(jìn)程的模式下,幾乎是不能作出控制的,所以,很多時(shí)候,多進(jìn)程模式耗費(fèi)的內(nèi)存,是會(huì)小于多線程模式的。這個(gè)說(shuō)法靠不靠 譜,大家心里都有譜,就不多說(shuō)了。
具體說(shuō)來(lái),Chrome對(duì)進(jìn)程工作集的控制算法還是比較簡(jiǎn)單的。首先,在進(jìn)程啟動(dòng)的時(shí)候,需要指明進(jìn)程工作的內(nèi)存環(huán)境,是高內(nèi)存,低內(nèi)存,還是中等內(nèi)存, 默認(rèn)模式下,是中等內(nèi)存(我以為Chrome會(huì)動(dòng)態(tài)計(jì)算的,沒(méi)想到竟然是啟動(dòng)時(shí)指定…)。在高內(nèi)存模式,不存在對(duì)工作集的調(diào)整,使勁用就完事了;在低內(nèi)存 的模式下,調(diào)整也很簡(jiǎn)單,一旦一個(gè)進(jìn)程不再有頁(yè)面面對(duì)觀眾了,嘗試釋放其所有工作集。相比來(lái)說(shuō),中等模式下,算法相對(duì)復(fù)雜一些,當(dāng)一個(gè)進(jìn)程從直接面對(duì)觀 眾,淪落到切換到后臺(tái)的悲慘命運(yùn),其工作集會(huì)縮減,算法為: TargetWorkingSetSize = (LastWorkingSet/2 + CurrentWorkingSet) /2;其中,TargetWorkingSetSize指的是預(yù)期降到的工作集大小,CurrentWorkingSet指的是進(jìn)程當(dāng)前的工作集(在 Chrome中,工作集的大小,包含私有的和可共享的兩部分內(nèi)存,而不包含已經(jīng)共享了的內(nèi)存空間…),LastWorkingSet,等于上一次的 CurrentWorkingSet除以DampingFactor,默認(rèn)的DampingFactor為2。而反之,當(dāng)一個(gè)進(jìn)程從幕后走向臺(tái)前,它的工 作集會(huì)被放大為 LastWorkingSet * DampingFactor * 2,了解過(guò)LastWorkingSet的含義,你已經(jīng)知道,這就是將工作集放大兩倍的另類版寫(xiě)法。
Chrome的Render進(jìn)程工作集調(diào)整,除了發(fā)生在tab切換(或新頁(yè)面建立)的時(shí)候,還會(huì)發(fā)生在整個(gè)Chrome的idle事件觸發(fā)后。 Chrome有個(gè)計(jì)時(shí)器,統(tǒng)計(jì)Chrome空閑的時(shí)長(zhǎng),當(dāng)時(shí)長(zhǎng)超過(guò)30s后(此工作會(huì)反復(fù)進(jìn)行…),Chrome會(huì)做一系列工作,其中就包括,調(diào)整進(jìn)程的 工作集。被調(diào)整的進(jìn)程,不僅僅是Render進(jìn)程,還包括Plugin進(jìn)程和Browser進(jìn)程,換句話描述,就是所有Chrome進(jìn)程。
這個(gè)算法導(dǎo)致一個(gè)很悲涼的狀況,當(dāng)你去蹲了個(gè)廁所回到電腦前,切換了一個(gè)Chrome頁(yè)面,你發(fā)現(xiàn)頁(yè)面一片慘白,一陣硬盤(pán)的騷動(dòng)過(guò)后,好不容易恢復(fù)了原 貌。如果再切,相同的事情又會(huì)發(fā)生,孜孜不倦,直到你切過(guò)每一個(gè)進(jìn)程。這個(gè)慘案發(fā)生的主要原因,就是由于所有Chrome進(jìn)程的工作集都被釋放了,頁(yè)面的 重載和Render需要不少的一坨時(shí)間,這就大大影響了用戶感受,畢竟,總看到慘白的畫(huà)面,容易產(chǎn)生不好的情緒。強(qiáng)烈感覺(jué)這個(gè)不算一個(gè)很出色的策略,應(yīng)該 有一個(gè)工作集切換的底限,或者是在Chrome從idle中被激活的時(shí)候,偷偷摸摸的統(tǒng)一擴(kuò)大工作集,發(fā)幾個(gè)事件刺激一下,把該加載的東西加載起來(lái)。
整體感覺(jué),Chrome對(duì)進(jìn)程開(kāi)銷的控制,并不像想象中的有非常精妙絕倫的策略在里面,通過(guò)工作集這總手段并不算華麗,而且,如果想很好的工作的話,有一 個(gè)非常非常重要的前提,就是被切換的頁(yè)面,很少再被繼續(xù)瀏覽。個(gè)人覺(jué)得這個(gè)假設(shè)并不是十分可靠,這就使得在某些情況下,產(chǎn)生非常不好的用戶體驗(yàn),也許 Chrome需要進(jìn)一步在這個(gè)地方琢磨點(diǎn)方法的。
本文Chrome源碼剖析、上,完。