當前位置:首頁 > 芯聞號 > 充電吧
[導讀]1. Chrome進程通信的基本模式 進程間通信,叫做IPC(Inter-Process Communication),在Chrome不多的文檔中,有一篇就是介紹這個的,在這里。Chrome最主要有

1. Chrome進程通信的基本模式

進程間通信,叫做IPC(Inter-Process Communication),在Chrome不多的文檔中,有一篇就是介紹這個的,在這里。Chrome最主要有三類進程,一類是Browser主進程,我們一直尊稱它老人家為老大;還有一類是各個Render進程,前面也提過了;另外還有一類一直沒說過,是Plugin進程,每一個插件,在Chrome中都是以進程的形式呈現,等到后面說插件的時候再提罷了。Render進程和Plugin進程都與老大保持進程間的通信,Render進程與Plugin進程之間也有彼此聯系的通路,唯獨是多個Render進程或多個Plugin進程直接,沒有互相聯系的途徑,全靠老大協(xié)調。。。

進程與進程間通信,需要仰仗操作系統(tǒng)的特性,能玩的花著實不多,在Chrome中,用到的就是有名管道(Named Pipe), 只不過,它用一個IPC::Channel類,封裝了具體的實現細節(jié)。Channel可以有兩種工作模式,一種是Client,一種是?Server,Server和Client分屬兩個進程,維系一個共同的管道名,Server負責創(chuàng)建該管道,Client會嘗試連接該管道,然后雙發(fā)往 各自管道緩沖區(qū)中讀寫數據(在Chrome中,用的是二進制流,異步IO…),完成通信。。。

管道名字的協(xié)商

在Socket中,我們會事先約定好通信的 端口,如果不按照這個端口進行訪問,走錯了門,會被直接亂棍打出門去的。與之類似,有名管道期望在兩個進程間游走,就需要拿一個兩個進程都能接受的進門暗 號,這個就是有名管道的名字。在Chrome中(windows下…),有名管道的名字格式都是:.pipechrome.ID。其中的 ID,自然是要求獨一無二,比如:進程ID.實例地址.隨機數。通常,這個ID是由一個Process生成(往往是Browser Process),然后在創(chuàng)建另一個進程的時候,作為命令行參數傳進去,從而完成名字的協(xié)商。。。
如果不了解并期待了解有關Windows下有名管道和信號量的知識,建議去看一些專業(yè)的書籍,比如圣經級別的《Windows核心編程》和《深入解析Windows操作系統(tǒng)》,當然也可以去查看SDK,你需要了解的API可能包括:CreateNamedPipe, CreateFile, ConnectNamedPipe, WaitForMultipleObjects,?WaitForSingleObject,?SetEvent, 等等。。。

Channel中,有三個比較關鍵的角色,一個是Message::Sender,一個是Channel::Listener,最后一個是MessageLoopForIO::Watcher。 Channel本身派生自Sender和Watcher,身兼兩角,而Listener是一個抽象類,具體由Channel的使用者來實現。顧名思 義,Sender就是發(fā)送消息的接口,Listener就是處理接收到消息的具體實現,但這個Watcher是啥?如果你覺得Watcher這東西看上去 很眼熟的話,我會激動的熱淚盈眶的,沒錯,在前面(第一部分第一小節(jié)…)說消息循環(huán)的時候,從那個表中可以看到,IO線程(記住,在Chrome 中,IO指的是網絡IO,*_*)的循環(huán)會處理注冊了的Watcher。其實Watcher很簡單,可以視為一個信號量和一個帶有 OnObjectSignaled方法對象的對,當消息循環(huán)檢測到信號量開啟,它就會調用相應的OnObjectSignaled方法。。。

圖5 Chrome的IPC處理流程圖

一圖解千語,如上圖所示,整個Chrome最核 心的IPC流程都在圖上了,期間,刨去了一些錯誤處理等邏輯,如果想看原汁原味的,可以自查Channel類的實現。當有消息被Send到一個發(fā)送進程的 Channel的時候,Channel會把它放在發(fā)送消息隊列中,如果此時還正在發(fā)送以前的消息(發(fā)送端被阻塞…),則看一下阻塞是否解除(用一個等 待0秒的信號量等待函數…),然后將消息隊列中的內容序列化并寫道管道中去。操作系統(tǒng)會維護異步模式下管道的這一組信號量,當消息從發(fā)送進程緩沖區(qū)寫 到接收進程的緩沖區(qū)后,會激活接收端的信號量。當接收進程的消息循環(huán),循到了檢查Watcher這一步,并發(fā)現有信號量激活了,就會調用該Watcher 相應的OnObjectSignaled方法,通知接受進程的Channel,有消息來了!Channel會嘗試從管道中收字節(jié),組消息,并調用 Listener來解析該消息。。。

從上面的描述不難看出,Chrome的進程通信,最核心的特點,就是利用消息循環(huán)來檢查信號量,而不是直接讓管道阻塞在某信號量上。這樣就與其多線程模型緊密聯系在了一起,用一種統(tǒng)一的模式來解決問題。并且,由于是消息循環(huán)統(tǒng)一檢查,線程不會隨便就被阻塞了,可以更好的處理各種其他工作,從理論上講,這是通過增加CPU工作時間,來換取更好的體驗,頗有資本家的派頭。。。

溫柔的消息循環(huán)

其實,Chrome的很多消息循環(huán),也不是都那么霸道,也是會被阻塞在某些信號量或者某種場景上的,畢竟客戶端不是它家的服務器,CPU不能被全部歸在它家名下。。。
比如IO線程,當沒有消息來到,又沒有信號量被激活的時候,就會被阻塞,具體實現可以去看MessagePumpForIO的WaitForWork方法。。。
不過這種阻塞是集中式的,可隨時修改策略的,比起Channel直接阻塞在信號量上,停工的時間更短。。。

2. 進程間的跨線程通信和同步通信

在Chrome中,任何底層的數據都是線程非安 全的,Channel不是太上老君(抑或中國足球?…),它也沒有例外。在每一個進程中,只能有一個線程來負責操作Channel,這個線程叫做IO 線程(名不符實真是一件悲涼的事情…)。其它線程要是企圖越俎代庖,是會出大亂子的。。。

但是有時候(其實是大部分時候…),我們需要從非IO線程與別的進程相通信,這該如何是好?如果,你有看過我前面寫的線程模型,你一定可以想到,做法很簡單,先將對Channel的操作放到Task中,將此Task放到IO線程隊列里,讓IO線程來處理即可。當然,由于這種事情發(fā)生的太頻繁,每次都人肉做一次頗為繁瑣,于是有一個代理類,叫做ChannelProxy,來幫助你完成這一切。。。

從接口上看,ChannelProxy的接口和 Channel沒有大的區(qū)別(否則就不叫Proxy了…),你可以像用Channel一樣,用ChannelProxy來Send你的消 息,ChannelProxy會辛勤的幫你完成剩余的封裝Task等工作。不僅如此,ChannelProxy還青出于藍勝于藍,在這個層面上做了更多的 事情,比如:發(fā)送同步消息。。。

不過能發(fā)送同步消息的類不是ChannelProxy,而是它的子類,SyncChannel。在Channel那里,所有的消息都是異步的(在Windows中,也叫Overlapped…),其本身也不支持同步邏輯。為了實現同步,SyncChannel并沒有另造輪子,而只是在Channel的層面上加了一個等待操作。當ChannelProxy的Send操作返回后,SyncChannel會把自己阻塞在一組信號量上,等待回包,直到永遠或超時。從外表上看同步和異步沒有什么區(qū)別,但在使用上還是要小心,在UI線程中使用同步消息,是容易被發(fā)指的。。。

3. Chrome中的IPC消息格式

說了半天,還有一個大頭沒有提過,那就是消息包。如果說,多線程模式下,對數據的訪問開銷來自于鎖,那么在多進程模式下,大部分的額外開銷都來自于進程間的消息拆裝和傳遞。不論怎么樣的模式,只要進程不同,消息的打包,序列化,反序列化,組包,都是不可避免的工作。。。

在Chrome中,IPC之間的通信消息,都是派生自IPC::Message類的。對于消息而言,序列化和反序列化是必須要支持的,Message的基類Pickle,就是干這個活的。Pickle提供了一組的接口,可以接受int,char,等等各種數據的輸入,但是在Pickle內部,所有的一切都沒有區(qū)別,都轉化成了一坨二進制流。這個二進制流是32位齊位的, 比如你只傳了一個bool,也是最少占32位的,同時,Pickle的流是有自增邏輯的(就是說它會先開一個Buffer,如果滿了的話,會加倍這個 Buffer…),使其可以無限擴展。Pickle本身不維護任何二進制流邏輯上的信息,這個任務交到了上級處理(后面會有說到…),但 Pickle會為二進制流添加一個頭信息,這個里面會存放流的長度,Message在繼承Pickle的時候,擴展了這個頭的定義,完整的消息格式如下:

圖6 Chrome的IPC消息格式

其中,黃色部分是包頭,定長96個bit,綠色 部分是包體,二進制流,由payload_size指明長度。從大小上看這個包是很精簡的了,除了routing位在消息不為路由消息的時候會有所浪費。 消息本身在有名管道中是按照二進制流進行傳輸的(有名管道可以傳輸兩種類型的字符流,分別是二進制流和消息流…),因此由payload_size + 96bits,就可以確定是否收了一個完整的包。。。

從邏輯上來看,IPC消息分成兩類,一類是路由 消息(routed message),還有一類是控制消息(control message)。路由消息是私密的有目的地的,系統(tǒng)會依照路由信息將消息安全的傳遞到目的地,不容它人窺視;控制消息就是一個廣播消息,誰想聽等能夠聽 得到。。。

消息的序列化

前不久讀了Google Protocol Buffers的源碼,是用在服務器端,用做內部機器通信協(xié)議的標準、代碼生成工具和框架。它主要的思想是揉合了key/value的內容到二進制中,幫助生成更為靈活可靠的二進制協(xié)議。。。

在Chrome中,沒有使用這套東西,而是用到了純二進制流作為消息序列化的方式。我想這是由于應用場景不同使然。在服務端,我們更關心協(xié)議的穩(wěn)定性,可 擴展性,并且,涉及到的協(xié)議種類很多。但在一個Chrome中,消息的格式很統(tǒng)一,這方面沒有擴展性和靈活性的需求,而在序列化上,雖然 key/value的方式很好很強大,但是在Chrome中需要的不是靈活性而是精簡性,因此寧可不用Protocol Buffers造好的輪子,而是另立爐灶,花了好一把力氣提供了一套純二進制的消息機制。。。

4. 定義IPC消息

如果你寫過MFC程序,對MFC那里面一大堆宏 有所忌憚的話,那么很不幸,在Chrome中的IPC消息定義中,你需要再吃一點苦頭了,甚至,更苦大仇深一些;如果你曾經領教過用模板的特化偏特化做 Traits、用模板做函數重載、用編譯期的Tuple做變參數支持,之類機制的種種麻煩的話,那么,同樣很遺憾,在Chrome中,你需要再感受一 次。。。

不過,先讓我們忘記宏和模板,看人肉一個消息,到底需要哪些操作。一個標準的IPC消息定義應該是類似于這樣的:

class SomeMessage
: public IPC::Message
{
public:
enum { ID = …; }
SomeMessage(SomeType & data)
: IPC::Message(MSG_ROUTING_CONTROL, ID, ToString(data))
{…}

};

大概意思是這樣的,你需要從Message(或者其他子類)派生出一個子類,該子類有一個獨一無二的ID值,該子類接受一個參數,你需要對這個參數進行序列化。兩個麻煩的地方看的很清楚,如果生成獨一無二的ID值?如何更方便的對任何參數可以自動的序列化?。。。

在Chrome中,解決這兩個問題的答案,就是 宏 + 模板。Chrome為每個消息安排了一種ID規(guī)格,用一個16bits的值來表示,高4位標識一個Channel,低12位標識一個消息的子id,也就是 說,最多可以有16種Channel存在不同的進程之間,每一種Channel上可以定義4k的消息。目前,Chrome已經用掉了8種 Channel(如果A、B進程需要雙向通信,在Chrome中,這是兩種不同的Channel,需要定義不同的消息,也就是說,一種雙向的進程通信關 系,需要耗費兩個Channel種類…),他們已經覺得,16bits的ID格式不夠用了,在將來的某一天,估計就被擴展成了32bits的。書歸正 傳,Chrome是這么來定義消息ID的,用一個枚舉類,讓它從高到低往下走,就像這樣:

enum SomeChannel_MsgType
{
SomeChannelStart = 5 << 12,
SomeChannelPreStart = (5 << 12) – 1,
Msg1,
Msg2,
Msg3,

MsgN,
SomeChannelEnd
};

這是一個類型為5的Channel的消息ID聲 明,由于指明了最開始的兩個值,所以后續(xù)枚舉的值會依次遞減,如此,只要維護Channel類型的唯一性,就可以維護所有消息ID的唯一性了(當然,前提 是不能超過消息上限…)。但是,定義一個ID還不夠,你還需要定義一個使用該消息ID的Message子類。這個步驟不但繁瑣,最重要的,是違反了 DIY原則,為了添加一個消息,你需要在兩個地方開工干活,是可忍孰不可忍,于是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 /* opaque data */)
IPC_MESSAGE_CONTROL0(PluginProcessMsg_BrowserShutdown)
IPC_END_MESSAGES(PluginProcess)

這是Chrome中,定義 PluginProcess消息的宏,我挖過來放在這了,如果你想添加一條消息,只需要添加一條類似與IPC_MESSAGE_CONTROL0東東即 可,這說明它是一個控制消息,參數為0個。你基本上可以這樣理解,IPC_BEGIN_MESSAGES就相當于完成了一個枚舉開始的聲明,然后中間的每 一條,都會在枚舉里面增加一個ID,并聲明一個子類。這個一宏兩吃,直逼北京烤鴨兩吃的高超做法,可以參看ipc_message_macros.h,或 者看下面一宏兩吃的一個舉例。。。

多次展開宏的技巧
這是Chrome中用到的一個技巧,定義一次宏,展開多段代碼,我孤陋寡聞,第一次見,一個類似的例子,如下:
首先,定義一個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

可以看到,這個頭文件是可重入的,每一次先undef掉之前的定義,然后判斷進行新的定義。然后,你可以創(chuàng)建一個use_macro.h文件,利用這個宏,定義具體內容:

#include “macros.h”
SUPER_MACRO(Test, int)

這個頭文件在利用宏的部分不需要放到ifundef…define…這樣的頭文件保護中,目的就是為了可重入。在主函數中,你可以多次define + include,實現多次展開的目的:

#define FIRST_TIME
#include “use_macro.h”
#define SECOND_TIME
#include “use_macro.h”
#include
int _tmain(int argc, _TCHAR* argv[])
{
TestClass t(5);
std::cout << TestClass::ID << std::endl;
std::cout << t._value << std::endl;
return 0;
}

這樣,你就成功的實現,一次定義,生成多段代碼了。。。

此外,當接收到消息后,你還需要處理消息。接收消息的函數,是IPC::Channel::Listener子類的OnMessageReceived函數。在這個函數中,會放置一坨的宏,這一套宏,一定能讓你想起MFC的Message Map機制:

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()

這個東西很簡單,展開后基本可以視為一個Switch循環(huán),判斷消息ID,然后將消息,傳遞給對應的函數。與MFC的Message Map比起來,做的事情少多了。。。

通過宏的手段,可以解決消息類聲明和消息的分發(fā) 問題,但是自動的序列化還不能支持(所謂自動的序列化,就是不論你是什么類型的參數,幾個參數,都可以直接序列化,不需要另寫代碼…)。在C++這種 語言中,所謂自動的序列化,自動的類型識別,自動的XXX,往往都是通過模板來實現的。這些所謂的自動化,其實就是通過事前的大量人肉勞作,和模板自動遞 推來實現的,如果說.Net或Java中的自動序列化是過山軌道,這就是那挑夫的驕子,雖然最后都是兩腿不動到了山頂,這底下費得力氣真是天壤之別啊。具 體實現技巧,有興趣的看看《STL源碼剖析》,或者是《C++新思維》,或者Chrome中的ipc_message_utils.h,這要說清楚實在不 是一兩句的事情。。。

總之通過宏和模板,你可以很簡單的聲明一個消 息,這個消息可以傳入各式各樣的參數(這里用到了夸張的修辭手法,其實,只要是模板實現的自動化,永遠都是有限制的,在Chrome的模板實現中,參數數 量不要超過5個,類型需要是基本類型、STL容器等,在不BT的場合,應該夠用了…),你可以調用Channel、ChannelProxy、 SyncChannel之類的Send方法,將消息發(fā)送給其他進程,并且,實現一個Listener類,用Message Map來分發(fā)消息給對應的處理函數。如此,整個IPC體系搭建完成。。。

苦力的宏和模板

不論是宏還是模板,為了實現這套機制,都需要寫大量的類似代碼,比如為了支持0~N個參數的Control消息,你就需要寫N+1個類似的宏;為了支持各種基礎數據結構的序列化,你就需要寫上十來個類似的Write函數和Traits。。。

之所以做如此苦力的活,都是為了用這些東西的人能夠盡可能的簡單方便,符合DIY原則。規(guī)約到之前說的設計者的職責上來,這是一個典型的苦了我一個幸福千 萬人的負責任的行為。在Chrome中,如此的代碼隨處可見,光Tuple那一套拳法,我現在就看到了使了不下三次(我曾經做過一套,直接吐血…), 如此兢兢業(yè)業(yè),真是可歌可泣啊。。。


本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯系該專欄作者,如若文章內容侵犯您的權益,請及時聯系本站刪除。
關閉
關閉