多進程架構
有github賬號的話,不妨隨手star一個 https://github.com/ahangchen/Chromium_doc_zh
這個文檔描述了Chromium的高層架構
問題
構建一個從不會掛起或崩潰的渲染引擎幾乎是不可能的。構建一個完全安全的渲染引擎也是幾乎不可能的。
在某種程度上,web瀏覽器當前狀態就像一個與過去的多任務操作系統合作的單獨的用戶。正如在一個這樣的操作系統中的錯誤程序會讓整個系統掛掉,所以一個錯誤的web頁面也可以讓一個現代瀏覽器掛掉。僅僅需要一個瀏覽器或插件的bug,就餓能讓整個瀏覽器和所有正在運行的標簽頁停止運行。
現代操作系統更加魯棒,因為他們把應用程序分成了彼此隔離的獨立線程。一個程序中的crash通常不會影響其他程序或整個操作系統,每個用戶對用戶數據的訪問也是有限制的。
架構概覽
我們為瀏覽器的標簽頁使用獨立的進程,以此保護整個應用程序免受渲染引擎中的bug和故障的傷害。我們也會限制每個渲染引擎進程的相互訪問,以及他們與系統其他部分的訪問。某些程度上,這為web瀏覽提供了內存保護,為操作系統提供了訪問控制。
我們把運行UI的進程叫做主進程(main),把插件進程稱為“瀏覽器進程”或“瀏覽器(Browser)”。相似的,標簽頁相關的進程被稱作“渲染線程”或“渲染器(renderer)”。渲染器使用WebKit開源引擎來實現中斷與html的布局。
管理渲染進程
每個渲染進程有一個全局的RenderProcess對象,管理它與父瀏覽器進程之間的通信,維護全局的狀態。瀏覽器為每個渲染進程維護一個對應的RenderViewHost,用來管理瀏覽器狀態,並與渲染器交流。瀏覽器與渲染器使用Chromium’s IPC system進行交流。
管理view
每個渲染進程有一個以上的RenderView對象,由RenderProcess管理(它與標簽頁的內容相關)。對應的RenderProcessHost維護一個與渲染器中每個view相關的RenderViewHost。每個view被賦予一個view ID,以區分同一個渲染器中的不同view。這些ID在每個渲染器內是唯一的,但在瀏覽器中不是,所以區分一個view需要一個RenderProcessHost和一個view ID。
瀏覽器與一個包含內容的特定標簽頁之間的交流是通過這些RenderViewHost對象來完成的,它們知道如何通過他們的RenderProcessHost向RenderProcess和RenderView送消息。
組件與接口
在渲染進程中:
-
RenderProcess處理與瀏覽器中對應的RenderProcessHost的通信。每個渲染進程就有唯一的一個RenderProcess對象。這就是所有瀏覽器-渲染器之間的交互發生的方式。
-
RenderView對象與它在瀏覽器進程中對應的RenderViewHost和我們的webkit嵌入層通信(通過RenderProcess)。這個對象代表了一個網頁在標簽頁或一個彈出窗口的內容。
在瀏覽器進程中:
- Browser對象代表了頂級瀏覽器窗口
- RenderProcessHost對象代表了瀏覽器端瀏覽器的與渲染器的IPC連接。在瀏覽器進程里,每個渲染進程有一個RenderProcessHost對象。
- RenderViewHost對象封裝了與遠端瀏覽器的交流,RenderWidgetHost處理輸入並在瀏覽器中為RenderWidget進行繪制。
想要得到更多關於這種嵌入是如何工作的詳細信息,可以查看How Chromium displays web pages design document。
共享繪制器進程
通常,每個新的window或標簽頁是在一個新進程里打開的。瀏覽器會生成一個新的進程,然后指導它去創建一個RenderView。
有時候,有這樣一種必要或欲望在標簽頁或窗口間共享渲染進程。一個web應用程序會在期望同步交流時,打開一個新的窗口,比如,在javascript里使用window.open。這種情況下,當我們創建一個新的window或標簽頁時,我們需要重用打開這個window的進程。我們也有一些策略來把新的標簽頁分配的已有的進程(如果總的進程數太大的話,或者如果用戶已經為這個域名打開了一個進程)。這些策略在Process Models里也有闡述。
檢測crash或者失誤的渲染
每個到瀏覽器進程的IPC連接會觀察進程句柄。如果這些句柄是signaled(有信號的),那么渲染進程已經掛了,標簽頁會得到一個通知。從這時開始,我們會展示一個“sad tab”畫面來通知用戶渲染器已經掛掉了。這個頁面可以按刷新按鈕或者通過打開一個新的導航來重新加載。這時,我們會注意到沒有對應的進程,然后創建一個新的。
渲染器中的沙箱
給定的WebKit是運行在獨立的進程中的,所以我們有機會限制它對系統資源的訪問。例如,我們可以確保渲染器唯一的網絡權限是通過它的父瀏覽器進程實現。相似的,我們可以限制它對文件系統的訪問權限來使用host操作系統內置的權限。
除了限制渲染器對文件系統和網絡的訪問權限,我們也可以限制它對用戶的顯示器以及相關的東西的一些權限。我們在獨立的windows桌面(對用戶不可見)中運行每個進程。這避免了讓渲染器在新的標簽頁或捕捉按鍵之間妥協。
歸還內存
讓渲染器運行在獨立的進程中,賦予隱藏的標簽頁更低的優先級會更加直接。通常,Windows平台上的最小化的進程會把它們的內存自動房東一個“可用內存”池里。在低內存的情況下,Windows會在交換這部分內存到更高優先級內存前,把它們交換到磁盤,以保證用戶可見的程序更易響應。我們可以對隱藏的標簽頁使用相同的策略。當渲染器進程沒有頂層標簽頁時,我們可以釋放進程的“工作集”空間,作為一個給系統的信號,讓它如果必要的話,優先把這些內存交換到磁盤。因為我們發現,當用戶在兩個標簽頁間切換時,減少工作集大小也會減少標簽頁切換性能,所以我們是逐漸釋放這部分內存的。這意味着如果用戶切換回最近使用的標簽頁,這個標簽頁的內存比最近較少訪問的標簽頁更可能被換入。有着足夠內存的用戶運行他們所有的程序時根本不會注意到這個進程:事實上Windows只會在需要的時候重新聲明這塊數據,所以在有充分內存時,不會有性能瓶頸。
這能幫助我們在低內存的情況下得到最佳的內存軌跡。幾乎不被使用的后台標簽頁相關的內存可以被完全交換掉,前台標簽頁的數據可以被完全加載進內存。相反的,一個單進程瀏覽器會在它的內存里隨機分配所有標簽頁的數據,並且不可能如此清晰地隔離已使用的和未使用的數據,導致了內存和性能上的浪費。
插件
Firefox風格的NPAPI插件運行在他們自己的進程里,與渲染器隔離。這會在Plugin Architecture中描述。
如何添加新特性(不用擴充RenderView/RenderViewHost/WebContents)
問題
過去,新的特性(比如,自動填充選取樣例)可以通過把新特性的代碼導入到RenderView類(在渲染器進程里)和RenderViewHost類(在瀏覽器進程里)。如果一個新的特性是在瀏覽器進程的IO線程里處理的,那么它的IPC信息由BrowserMessageFilter調度。RenderViewHost會只為了調用WebContent對象進程調用IPC信息,這會調用另一塊代碼。所有的瀏覽器與渲染器之間的IPC信息會被聲明在一個巨大的render_messages_internal.h里,為每個新特性修改所有的這些文件意味着這些類會變得臃腫。
解決方案
我們增加了helper類和對上面的每個線程IPC信息的過濾的機制。這使得編寫自洽的特性更加容易。
渲染器端
如果你想要過濾和發送IPC信息,實現RenderViewObserver接口(content/renderer/render_view_observer.h)。RenderViewObserver基類持有一個RenderView類,管理對象的生命周期,使其綁定到RenderView(它是可重寫的)。這個類就可以過濾和發送IPC消息,此外還可以獲得許多特性需要的關於頁面加載與關閉的通知。作為一個例子,可以查看ChromeExtensionHelper (chrome/renderer/extensions/chrome_extension_helper.h)。
如果你的特性有一部分代碼是在WebKit內的,避免通過WebViewClient接口回調,這樣我們就不會使得WebViewClient變得龐大。考慮創建新的WebKit接口給WebKit代碼調用,讓渲染器端的類去實現它。作為一個例子,查看WebAutoFillClient (WebKit/chromium/public/WebAutoFillClient.h).
瀏覽器UI線程
WebContentsObserver (content/public/browser/web_contents_observer.h)接口允許UI線程的對象過濾IPC信息,以及給出關於頁面導航的通知。作為一個例子:查看TabHelper (chrome/browser/extensions/tab_helper.h)。
瀏覽器其他線程
為了過濾和發送IPC信息給其他的瀏覽器線程,比如IO/FILE/WEBKIT等等,實現BrowserMessageFilter接口(content/browser/browser_message_filter.h)。BrowserRenderProcessHost對象在它的CreateMessageFilters函數里創造和增加過濾器。
通常,如果一個特性有許多IPC消息,這些消息應該移動到一個獨立的文件(例如,不要加到render_messages_internal.h里)。這對過濾線程(除了IO線程)也有幫助。作為一個例子,查看content/common/pepper_file_messages.h。這允許他們的過濾器PepperFileMessageFilter方便的發送文件到file線程,而不用在很多位置指定它們的ID。
void PepperFileMessageFilter::OverrideThreadForMessage( const IPC::Message& message, BrowserThread::ID* thread) { if (IPC_MESSAGE_CLASS(message) == PepperFileMsgStart) *thread = BrowserThread::FILE; }