精讀《深入瞭解現代瀏覽器一》

黃子毅發表於2021-11-29

Inside look at modern web browser 是介紹瀏覽器實現原理的系列文章,共 4 篇,本次精讀介紹第一篇。

雖然本文寫於 2018 年,但如今依然值得學習,因為瀏覽器實現非常複雜,從細節開始學習很容易迷失方向,缺乏整體感,而這篇文章從巨集觀層面開始介紹,幾乎沒有涉及程式碼實現,全都是思路性的描述,非常適合培養對瀏覽器整體框架性思維。

原文有非常多形象的插圖與動圖,便於加深對知識的理解,所以也推薦直接閱讀原文。

概述

文章先從 CPU、GPU、作業系統開始介紹,因為這些是瀏覽器執行的基座。

CPU、GPU、作業系統、應用的關係

CPU 即中央處理器,可以處理幾乎所有計算。以前的 CPU 是單核的,現在大部分筆記電腦都是多核的,專業伺服器甚至有高達 100 多核的。CPU 計算能力很強,但只能一件件事處理,

GPU 一開始是為影像處理設計的,即主要處理畫素點,所以擁有大量並行的處理簡單事物的能力,非常適合用來做矩陣運算,而矩陣運算又是計算機圖形學的基礎,所以大量用在視覺化領域。

CPU、GPU 都是計算機硬體,這些硬體各自都提供了一些介面供組合語言呼叫;而作業系統則基於它們之上用 C 語言(如 linux)將硬體管理了起來,包括程式排程、記憶體分配、使用者核心態切換等等;執行在作業系統之上的則是應用程式了,所以應用程式不直接和硬體打交道,而是通過作業系統間接操作硬體。

為什麼應用程式不能直接操作硬體呢?這樣做有巨大的安全隱患,因為硬體是沒有任何抽象與安全措施的,這意味著理論上一個網頁可以通過 js 程式,在你開啟網頁時直接訪問你的任意記憶體地址,讀取你的聊天記錄,甚至讀取歷史輸入的銀行卡密碼進行轉賬操作。

顯然,瀏覽器作為一個應用程式,執行在作業系統之上。

程式與執行緒

為了讓程式執行的更安全,作業系統創造了程式與執行緒的概念(linux 對程式與執行緒的實現是同一套),程式可以分配獨立的記憶體空間,程式內可以建立多個執行緒進行工作,這些執行緒共享記憶體空間。

因為執行緒間共享記憶體空間,因此不需通訊就能交流,但記憶體地址相互隔離的程式間也有通訊需求,需通過 IPC(Inter Process Communication)進行通訊。

程式之間相互獨立,即一個程式掛了不會影響到其它程式,而在一個程式中可以建立一個新程式,並與之通訊,所以瀏覽器就採用了這種策略,將 UI、網路、渲染、外掛、儲存等模組程式獨立,並且任意掛掉後都可以被重新喚起。

瀏覽器架構

瀏覽器可以拆分為許多獨立的模組,比如:

  • 瀏覽器模組(Browser):負責整個瀏覽器內行為協調,呼叫各個模組。
  • 網路模組(Network):負責網路 I/O。
  • 儲存模組(Storage):負責本地 I/O。
  • 使用者介面模組(UI):負責瀏覽器提供給使用者的介面模組。
  • GPU 模組:負責繪圖。
  • 渲染模組(Renderer):負責渲染網頁。
  • 裝置模組(Device):負責與各種本地裝置互動。
  • 外掛模組(Plugin):負責處理各類瀏覽器外掛。

基於這些模組,瀏覽器有兩種可用的架構設計,一種是少程式,一種是多程式。

少程式是指將這些模組放在一個或有限的幾個程式裡,也就是每個模組一個執行緒,這樣做的好處是最大程度共享了記憶體空間,對裝置要求較低,但問題是隻要一個執行緒掛了都會導致整個瀏覽器掛掉,因此穩定性較差。

多程式是指為每個模組(儘量)開闢一個程式,模組間通過 IPC 通訊,因此任何模組掛掉都不會影響其它模組,但壞處是記憶體佔用較大,比如瀏覽器 js 解析與執行引擎 V8 就要在這套架構下拷貝多份例項執行在每個程式中。

Chrome 多程式架構的優勢

Chrome 儘量為每個 tab 單獨建立一個程式,所以我們才能在某個 tab 未響應時,從容的關閉它,而其它 tab 不會受到影響。不僅是 tab 間,一個 tab 內的 iframe 間也會建立獨立的程式,這樣做是為了保護網站的安全性。

服務化 - 單/多程式彈性架構

Chrome 並不滿足於採用一種架構,而是在不同環境下切換不同的架構。Chrome 將各功能模組化後,就可以自由決定當前將哪些模組放在一個程式中,將哪些模組啟動獨立程式,即可以在執行時決定採用哪套程式架構。

這樣做的好處是,可以在資源受限的機器上開啟單程式模式,以儘量節約記憶體開銷,實際上在手機應用上就是這麼做的;而在資源豐富、核心數量充足的機器上採用獨立程式模式,雖然消耗了更多資源,但獲得了更好的穩定性。

Iframe 獨佔程式

site-isolation 將同一個 tab 內不同 iframe 包裹在不同的程式內執行,以確保 iframe 間資源的獨佔性,以及安全性。該功能直到 2018.7 才更新,是因為背後有許多複雜的工作要處理,比如開發者工具的除錯、網頁的全域性搜尋功能,都不能因為程式的隔離而受到影響,Chrome 必須讓每個程式單獨響應這些操作,並最終聚合在一起,讓使用者感受不到程式間的阻隔。

精讀

本文從瀏覽器如何基於作業系統提供的程式、執行緒概念構建自己的應用程式開始,從硬體、作業系統、軟體的分層開始,介紹到瀏覽器是如何劃分模組的,並且分配程式或執行緒給這些模組執行,這背後的思考非常有價值。

從巨集觀角度看,要設計一個安全穩定、高效能、具有擴充性的瀏覽器,首先要把各功能模組劃分清楚,並定義好各模組的通訊關係,在各業務場景下制定一套模組協作的流程。

瀏覽器的主從架構

類似應用程式的主從模式,瀏覽器的 Browser 模組可以看作主模組,它本身用於協調其它模組的執行,並維持其它各模組的正常工作,在其它模組失去響應時等待或重新喚起,或者在模組銷燬時進行記憶體回收。

各從模組也分工明確,比如在瀏覽器敲擊 URL 地址時,會先通過 UI 模組響應使用者的輸入,並判斷輸入是否為 URL 地址,因為輸入的可能是其它非法引數,或一些查詢或設定命令。若輸入的確實是 URL 地址,則校驗通過後,會通知 Network 網路模組傳送請求,UI 模組就不再關心請求是如何處理了。Network 模組也是相對獨立的,僅處理請求的傳送與接收,如果接收到的是 HTML 網頁,則交給 Renderer 模組進行渲染。

有了這些相對獨立且分工明確的模組劃分後,將這些模組作為執行緒或程式管理就都不會影響它們的業務邏輯了,唯一影響的就是記憶體是否共享,以及某個模組 crash 後是否會影響到其它模組了,所以基於這個架構,判斷裝置型別,以採用單程式或多程式模式就變得簡單了很多,且這個程式彈性架構本身也不需要入侵各模組業務邏輯,本身就是一套獨立的機制。

瀏覽器作為非常複雜的應用程式,想要持續維護,就必須對每個功能點都進行合理的設計,讓模組間高內聚、低耦合,這樣才不至於讓任何修改牽一髮而動全身。

tab、iframe 程式隔離

微前端的沙箱隔離方案也比較火,這裡可以和瀏覽器 tab/iframe 隔離做個對比。

基於 js 執行時的沙箱方案大多都因為吐槽 iframe 慢而誕生的,一般會基於 with 改變沙箱程式碼的上下文,修改訪問的全域性物件引用,但基於 js 原型鏈特徵,為了阻斷向原型鏈追溯到主應用程式碼,一般會採用 proxywith mock 的變數進行訪問阻斷。

還有一些方案利用建立空 iframe 獲取到 document 變數傳遞給沙箱,一定程度做到了訪問隔離,且對 document 新增的監聽會隨 iframe 銷燬而銷燬,便於控制。

還有一些更加徹底的嘗試,將 js 程式碼扔到 web worker 執行,並通過 mock 模擬了 worker 執行時缺失的 dom API。

對比這些方案可以發現,只有最後 worker 的方案是最徹底的,因為瀏覽器建立的 worker 程式是完全資源隔離的,想要和瀏覽器主執行緒通訊只能利用 postMessage,雖然有一些基於 ArrayBuffer 的記憶體共享方案,但因為支援的資料型別具有針對性,也不會存在安全問題。

回到瀏覽器開發者的視角,為什麼 iframe 隔離要花費九牛二虎之力拆分多程式,最後再費很大功夫拼接回來,還原出一個相對無縫的體驗?瀏覽器廠商其實完全可以利用上面提到的 js 執行時能力,對 API 語法進行改造,建立一個邏輯上的沙盒環境。

我認為本質原因是瀏覽器要實現的沙盒必須是程式層面的,也就是對記憶體訪問許可權的絕對隔離,因為邏輯層面的隔離可能隨著各瀏覽器廠商實現差異,或 API 本身存在的邏輯漏洞而導致越權情況的出現,所以如果需要構造一個完全安全的沙盒,最好利用瀏覽器提供的 API 建立新的程式處理沙盒程式碼。

總結

本文介紹了瀏覽器是如何基於作業系統做巨集觀架構設計的,主要就說了一件事,即對程式,執行緒模型的彈性使用。同時在 tab、iframe 的設計中也要考慮到安全性要求,在必要的時候採用程式,在瀏覽器自身模組間因為沒有安全性問題,所以可對程式模型進行靈活切換。

討論地址是:精讀《深入瞭解現代瀏覽器一》· Issue #374 · dt-fe/weekly

如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。

關注 前端精讀微信公眾號

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證

相關文章