chromium源码剖析(⼆)
1. Chrome进程通信的基本模式
主进程,进程间通信,叫做IPC(Inter-Process Communication),在Chrome不多的⽂档中,有⼀篇就是介绍这个的,在。Chrome最主要有三类进程,⼀类是Browser主进程
进程,每⼀个插件,在Chrome中都是以进程的形式呈
进程,前⾯也提过了;另外还有⼀类⼀直没说过,是Plugin进程
我们⼀直尊称它⽼⼈家为⽼⼤;还有⼀类是各个Render进程
现,等到后⾯说插件的时候再提罢了。 Render进程和Plugin进程都与⽼⼤保持进程间的通信,Render进程与Plugin进程之间也有彼此联系的通路,唯独是多个Render进程或多个Plugin进程直接,没有互相联系的途径,全靠⽼⼤协调。。。
进程与进程间通信,需要仰仗操作系统的特性,能玩的花着实不多,在Chrome中,⽤到的就是有名管道
(Named Pipe),只不过,它⽤⼀个IPC::Channel类,封装了具体的实现细节。Channel可以有两种⼯作模式,⼀种是Client,⼀种是Server,Server和Client分属两个进程,维系⼀个共同的管道名,Server负责创建该管道,Client会尝试连接该管道,然后双发往各⾃管道缓冲区中读写数据(在Chrome中,⽤的是⼆进制流,异步IO...),完成通信。。。
Channel中,有三个⽐较关键的⾓⾊,⼀个是Message::Sender,⼀个是Channel::Listener,最后⼀个是MessageLoopForIO::Watcher。Channel本⾝派⽣⾃Sender和Watcher,⾝兼两⾓,⽽Listener是⼀个抽象类,具体由Channel的使⽤者来实现。顾名思义,Sender就是发送消息的接⼝,Listener就是处理接收到消息的具体实现,但这个Watcher是啥?如果你觉得Watcher这东西看上去很眼熟的话,我会激动的热泪盈眶的,没错,在前⾯(第⼀部分第⼀⼩节...)说消息循环的时候,从那个表中可以看到,IO线程(记住,在Chrome中,IO指的是⽹络IO,*_*)的循环会处理注册了的Watcher。其实Watcher很简单,可以视为⼀个信号量和⼀个带有OnObjectSignaled⽅法对象的对,当消息循环检测到信号量开启,它就
会调⽤相应的OnObjectSignaled⽅法。。。
Send到⼀个发送进程的Channel的时候,Channel会把它放在发送消息队列中,如果此时还正在发送以前的消息(发送端被阻塞...),则看⼀下阻塞是否解除(⽤⼀个等待0秒的信号量等待函数...),然后将消息队列中的内容序列化并写道管道中去。操作系统会维护异步模式下管道的这⼀组信号量,当消息从发送进程缓冲区写到接收进程的缓冲区后,会激活接收端的信号量。当接收进程的消息循环,循到了检查Watcher这⼀步,并发现有信号量激活了,就会调⽤该Watcher相应的OnObjectSignaled⽅法,通知接受进程的Channel,有消息来了!Channel会尝试从管道中收字节,组消息,并调⽤Listener来解析该消息。。。
从上⾯的描述不难看出,Chrome的进程通信,最核⼼的特点,就是利⽤消息循环来检查信号量,⽽不是直接让管道阻塞在某信号量上。这样就与其多线程模型紧密联系在了⼀起,⽤⼀种统⼀的模式来解决问题。并且,由于是消息循环统⼀检查,线程不会随便就被阻塞了,可以更好的处理各种其他⼯作,从理论上讲,这是通过增加CPU⼯作时间,来换取更好的体验,颇有资本家的派头。。。
2. 进程间的跨线程通信和同步通信
在Chrome中,任何底层的数据都是线程⾮安全的,Channel不是太上⽼君(抑或中国⾜球?...),它也没有例外。在每⼀个进程中,只能有⼀个线程来负责操作Channel,这个线程叫做IO线程(名不符实真是⼀件悲凉的事情...)。其它线程要是企图越俎代庖,是会出⼤乱⼦的。。。
但是有时候(其实是⼤部分时候...),我们需要从⾮IO线程与别的进程相通信,这该如何是好?如果,你有看过我前⾯写的线程模型,你⼀定可以想到,做法很简单,先
线程来处理即可。当然,由于这种事情发⽣的太频繁,每次都⼈⾁做⼀次颇为繁琐,于是有⼀个将对Channel的操作放到Task中,将此Task放到IO线程队列⾥,让IO线程来处理即可
代理类,叫做ChannelProxy,来帮助你完成这⼀切。。。
从接⼝上看,ChannelProxy的接⼝和Channel没有⼤的区别(否则就不叫Proxy了...),你可以像⽤Channel⼀样,⽤ChannelProxy来Send你的消息,ChannelProxy会⾟勤的帮你完成剩余的封装Task等⼯作。不仅如此,ChannelProxy还青出于蓝胜于蓝,在这个层⾯上做了更多的事情,⽐如:发送同步消息。。。
不过能发送同步消息的类不是ChannelProxy,⽽是它的⼦类,SyncChannel。在Channel那⾥,所有的消息都是异步的(在Windows中,也叫),其本⾝也不⽀持同步逻辑。为了实现同步,Sy
ncChannel并没有另造轮⼦,⽽只是在Channel的层⾯上加了⼀个等待操作。当ChannelProxy的Send操作返回后,SyncChannel会把⾃⼰阻塞在⼀组信号量上,等待回包,直到永远或超时。从外表上看同步和异步没有什么区别,但在使⽤上还是要⼩⼼,在UI线程中使⽤同步消息,是容易被发指的。。。3. Chrome中的IPC消息格式
说了半天,还有⼀个⼤头没有提过,那就是消息包。如果说,多线程模式下,对数据的访问开销来⾃于锁,那么在多进程模式下,⼤部分的额外开销都来⾃于进程间的消息拆装和传递。不论怎么样的模式,只要进程不同,消息的打包,序列化,反序列化,组包,都是不可避免的⼯作。。。
在Chrome中,IPC之间的通信消息,都是派⽣⾃IPC::Message类的。对于消息⽽⾔,序列化和反序列化是必须要⽀持的,Message的基类Pickle,就是⼲这个活的。
⼆进制流。这个⼆进制流是32位齐位Pickle提供了⼀组的接⼝,可以接受int,char,等等各种数据的输⼊,但是在Pickle内部,所有的⼀切都没有区别,都转化成了⼀坨⼆进制流
的,⽐如你只传了⼀个bool,也是最少占32位的,同时,Pickle的流是有⾃增逻辑的(就是说它会先开⼀个Buffer,如果满了的话,会加倍这个),使其可以⽆限扩展。Pickle本⾝不维护任何⼆进制流逻辑上的信息,这个任务交到了上级处理(后⾯会有说到...),但Pickle会为⼆进制流添加⼀个头信息,这个⾥⾯会存放流的长
度,Message在继承Pickle的时候,扩展了这个头的定义,完整的消息格式如下:
其中,黄⾊部分是包头,定长96个bit,绿⾊部分是包体,⼆进制流,由payload_size指明长度。从⼤⼩上看这个包是很精简的了,除了routing位在消息不为路由消息的时候会有所浪费。消息本⾝在有名管道中是按照⼆进制流进⾏传输的(有名管道可以传输两种类型的字符流,分别是⼆进制流和消息流...),因此由payload_size + 96bits,就可以确定是否收了⼀个完整的包。。。
从逻辑上来看,IPC消息分成两类,⼀类是路由消息(routed message),还有⼀类是控制消息(control message)。路由消息是私密的有⽬的地的,系统会依照路由信息将消息安全的传递到⽬的地,不容它⼈窥视;控制消息就是⼀个⼴播消息,谁想听等能够听得到。。。
4. 定义IPC消息
如果你写过MFC程序,对MFC那⾥⾯⼀⼤堆宏有所忌惮的话,那么很不幸,在Chrome中的IPC消息定义中,你需要再吃⼀点苦头了,甚⾄,更苦⼤仇深⼀些;如果你曾经领教过⽤模板的特化偏特化做Traits、⽤模板做函数重载、⽤编译期的Tuple做变参数⽀持,之类机制的种种⿇烦的话,那么,同样很遗憾,在Chrome中,你需要再感受⼀次。。。
不过,先让我们忘记宏和模板,看⼈⾁⼀个消息,到底需要哪些操作。⼀个标准的IPC消息定义应该是类似于这样的:
⼤概意思是这样的,你需要从Message(或者其他⼦类)派⽣出⼀个⼦类,该⼦类有⼀个独⼀⽆⼆的ID值,该⼦类接受⼀个参数,你需要对这个参数进⾏序列化。两个⿇烦的地⽅看的很清楚,如果⽣成独⼀
⽆⼆的ID值?如何更⽅便的对任何参数可以⾃动的序列化?。。。
在Chrome中,解决这两个问题的答案,就是宏 + 模板。Chrome为每个消息安排了⼀种ID规格,⽤⼀个16bits的值来表⽰,⾼4位标识⼀个Channel,低12位标识⼀个消息的⼦id,也就是说,最多可以有16种Channel存在不同的进程之间,每⼀种Channel上可以定义4k的消息。⽬前,Chrome已经⽤掉了8种Channel(如果A、B进程需要双向通信,在Chrome中,这是两种不同的Channel,需要定义不同的消息,也就是说,⼀种双向的进程通信关系,需要耗费两个Channel种类...),他们已经觉得,16bits的ID格式不够⽤了,在将来的某⼀天,估计就被扩展成了32bits的。书归正传,Chrome是这么来定义消息ID的,⽤⼀个枚举类,让它从⾼到低往下⾛,就像这样:
进程通信方式这是⼀个类型为5的Channel的消息ID声明,由于指明了最开始的两个值,所以后续枚举的值会依次递减,如此,只要维护Channel类型的唯⼀性,就可以维护所有消息ID的唯⼀性了(当然,前提是不能超
过消息上限...)。但是,定义⼀个ID还不够,你还需要定义⼀个使⽤该消息ID的Message⼦类。这个步骤不但繁琐,最重要的,是违反了DIY原则,为了添加⼀个消息,你需要在两个地⽅开⼯⼲活,是可忍孰不可忍,于是Google祭出了宏这颗原⼦弹,需要定义消息,格式如下:
这是Chrome中,定义PluginProcess消息的宏,我挖过来放在这了,如果你想添加⼀条消息,只需要添加⼀条类似与IPC_MESSAGE_CONTROL0东东即可,这说明它是⼀个控制消息,参数为0个。你基本上可以这样理解,IPC_BEGIN_MESSAGES就相当于完成了⼀个枚举开始的声明,然后中间的每⼀条,都会在枚举⾥⾯增加⼀个ID,并声明⼀个⼦类。这个⼀宏两吃,直逼北京烤鸭两吃的⾼超做法,可以参看ipc_message_macros.h,或者看下⾯⼀宏两吃的⼀个举例。。。
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掉之前的定义,然后判断进⾏新的定义。然后,你可以创建⼀个use_macro.h⽂件,利⽤这个宏,定义具体内容:
#include "macros.h"
SUPER_MACRO(Test, int)
这个头⽂件在利⽤宏的部分不需要放到这样的头⽂件保护中,⽬的就是为了可重⼊。在主函数中,你可以多次define + include,实现多次展开的⽬的:
#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);
此外,当接收到消息后,你还需要处理消息。接收消息的函数,是IPC::Channel::Listener⼦类的OnMessageReceived函数。在这个函数中,会放置⼀坨的宏,这⼀套宏,⼀定能让你想起MFC的Message Map机制:
这个东西很简单,展开后基本可以视为⼀个Switch循环,判断消息ID,然后将消息,传递给对应的函数。与MFC的Message Map⽐起来,做的事情少多了。。。
通过宏的⼿段,可以解决消息类声明和消息的分发问题,但是⾃动的序列化还不能⽀持(所谓⾃动的序列化,就是不论你是什么类型的参数,⼏个参数,都可以直接序列化,不需要另写代码...)。在C++这种语⾔中,所谓⾃动的序列化,⾃动的类型识别,⾃动的XXX,往往都是通过模板来实现的。这些所谓的⾃动化,其实就是通过事前的⼤量⼈⾁劳作,和模板⾃动递推来实现的,如果说.Net或Java中的⾃动序列化是过⼭轨道,这就是那挑夫的骄⼦,虽然最后都是两腿不动到了⼭顶,这底下费得⼒⽓真是天壤之别啊。具体实现技巧,有兴趣的看看《STL源码剖析》,或者是《C++新思维》,或者Chrome中的ipc_message_utils.h,这要说清楚实在不是⼀两句的事情。。。
总之通过宏和模板,你可以很简单的声明⼀个消息,这个消息可以传⼊各式各样的参数(这⾥⽤到了夸张的修辞⼿法,其实,只要是模板实现的⾃动化,永远都是有限制的,在Chrome的模板实现中,参数数量不要超过5个,类型需要是基本类型、STL容器等,在不BT的场合,应该够⽤了...),你可以调⽤Channel、ChannelProxy、SyncChannel之类的Send⽅法,将消息发送给其他进程,并且,实现⼀个Listener类,⽤Message Map来分发消息给对应的处理函数。如此,整个IPC体系搭建完成。。。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论