欢迎光临
我们一直在努力

Chrome源码剖析【二】

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

进程与进程间通信,需要仰仗操作系统的特性,能玩的花着实不多,在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方法。。。

一图解千语,如上图所示,整个Chrome最核心的IPC流程都在图上了,期间,刨去了一些错误处理等逻辑,如果想看原汁原味的,可以自查Channel类的实现。当有消息被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中,也叫Overlapped...),其本身也不支持同步逻辑。为了实现同步,SyncChannel并没有另造轮子,而只是在Channel的层面上加了一个等待操作。当ChannelProxy的Send操作返回后,SyncChannel会把自己阻塞在一组信号量上,等待回包,直到永远或超时。从外表上看同步和异步没有什么区别,但在使用上还是要小心,在UI线程中使用同步消息,是容易被发指的。。。

3. Chrome中的IPC消息格式

说了半天,还有一个大头没有提过,那就是消息包。如果说,多线程模式下,对数据的访问开销来自于锁,那么在多进程模式下,大部分的额外开销都来自于进程间的消息拆装和传递。不论怎么样的模式,只要进程不同,消息的打包,序列化,反序列化,组包,都是不可避免的工作。。。

在Chrome中,IPC之间的通信消息,都是派生自IPC::Message类的。对于消息而言,序列化和反序列化是必须要支持的,Message的基类Pickle,就是干这个活的。Pickle提供了一组的接口,可以接受int,char,等等各种数据的输入,但是在Pickle内部,所有的一切都没有区别,都转化成了一坨二进制流这个二进制流是32位齐位的,比如你只传了一个bool,也是最少占32位的,同时,Pickle的流是有自增逻辑的(就是说它会先开一个Buffer,如果满了的话,会加倍这个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消息定义应该是类似于这样的:

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规格,用一个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声明,由于指明了最开始的两个值,所以后续枚举的值会依次递减,如此,只要维护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<uint8> /* 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,或者看下面一宏两吃的一个举例。。。

此外,当接收到消息后,你还需要处理消息。接收消息的函数,是IPC::Channel::Listener子类的OnMessageReceived函数。在这个函数中,会放置一坨的宏,这一套宏,一定能让你想起MFC的Message Map机制:

通过宏的手段,可以解决消息类声明和消息的分发问题,但是自动的序列化还不能支持(所谓自动的序列化,就是不论你是什么类型的参数,几个参数,都可以直接序列化,不需要另写代码...)。在C++这种语言中,所谓自动的序列化,自动的类型识别,自动的XXX,往往都是通过模板来实现的。这些所谓的自动化,其实就是通过事前的大量人肉劳作,和模板自动递推来实现的,如果说.Net或Java中的自动序列化是过山轨道,这就是那挑夫的骄子,虽然最后都是两腿不动到了山顶,这底下费得力气真是天壤之别啊。具体实现技巧,有兴趣的看看《STL源码剖析》,或者是《C++新思维》,或者Chrome中的ipc_message_utils.h,这要说清楚实在不是一两句的事情。。。

  • 海报
海报图正在生成中...
赞(0) 打赏
声明:
1、本博客不从事任何主机及服务器租赁业务,不参与任何交易,也绝非中介。博客内容仅记录博主个人感兴趣的服务器测评结果及一些服务器相关的优惠活动,信息均摘自网络或来自服务商主动提供;所以对本博客提及的内容不作直接、间接、法定、约定的保证,博客内容也不具备任何参考价值及引导作用,访问者需自行甄别。
2、访问本博客请务必遵守有关互联网的相关法律、规定与规则;不能利用本博客所提及的内容从事任何违法、违规操作;否则造成的一切后果由访问者自行承担。
3、未成年人及不能独立承担法律责任的个人及群体请勿访问本博客。
4、一旦您访问本博客,即表示您已经知晓并接受了以上声明通告。
文章名称:《Chrome源码剖析【二】》
文章链接:https://www.456zj.com/20508.html
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址