Chrome源码剖析【⼆】
原⽂地址为:
【⼆】的进程间通信
1. Chrome进程通信的基本模式
进程间通信,叫做IPC(Inter-Process Communication),在Chrome不多的⽂档中,有⼀篇就是介绍这个的,在 。Chrome最主要有
Render进程,前⾯也提过了;另外还有⼀类⼀直Browser主进程,我们⼀直尊称它⽼⼈家为⽼⼤;还有⼀类是各个 Render进程
三类进程,⼀类是 Browser主进程
Plugin进程,每⼀个插件,在Chrome中都是以进程的形式呈现,等到后⾯说插件的时候再提罢了。 Render进程和Plugin进没说过,是 Plugin进程
程都与⽼⼤保持进程间的通信,Render进程与Plugin进程之间也有彼此联系的通路,唯独是多个Render进程或多个Plugin进程直接,没有互相联系的途径,全靠⽼⼤协调。。。
进程与进程间通信,需要仰仗操作系统的特性,能玩的花着实不多,在Chrome中,⽤到的就是 有名管道(Named Pipe),只不过,它⽤⼀个IPC::Channel类,封装了具体的实现细节。Channel可以有两种⼯作模式,⼀种是Client,⼀种是Server,Server和Client分属两个进程,维系⼀个共同的管道名,Server负责创建该管道,Client会尝试连接该管道,然后双发往各⾃管道缓冲区中读写数据(在Chrome 中,⽤的是⼆进制流,异步IO...),完成通信。。。
Channel::Listener,最后⼀个是
Message::Sender,⼀个是 Channel::Listener
Channel中,有三个⽐较关键的⾓⾊,⼀个是 Message::Sender
MessageLoopForIO::Watcher。Channel本⾝派⽣⾃Sender和Watcher,⾝兼两⾓,⽽Listener是⼀个抽象类,具体由Channel的MessageLoopForIO::Watcher
使⽤者来实现。顾名思义,Sender就是发送消息的接⼝,Listener就是处理接收到消息的具体实现,但这个Watcher是啥?如果你觉得Watcher这东西看上去很眼熟的话,我会激动的热泪盈眶的,没错,在前⾯(第⼀部分第⼀⼩节...)说消息循环的时候,从那个表中可以看到,IO线程(记住,在Chrome中,IO指的是⽹络IO,*_*)的循环会处理注册了的Watcher。其实Watcher很简单,可以视为⼀个信号量和⼀个带有OnObjectSignaled⽅法对象的对,当消息循环检测到信号量开启,它就会调⽤相应的OnObjectSignaled⽅法。。。
图5 Chrome的IPC处理流程图
⼀图解千语,如上图所⽰,整个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线程来处理即可。当然,由于这定可以想到,做法很简单, 先将对Channel的操作放到Task中,将此Task放到IO线程队列⾥,让IO线程来处理即可
ChannelProxy,来帮助你完成这⼀切。。。
种事情发⽣的太频繁,每次都⼈⾁做⼀次颇为繁琐,于是有⼀个代理类,叫做 ChannelProxy
从接⼝上看,ChannelProxy的接⼝和Channel没有⼤的区别(否则就不叫Proxy了...),你可以像⽤Channel⼀样,⽤ChannelProxy来
Send你的消息,ChannelProxy会⾟勤的帮你完成剩余的封装Task等⼯作。不仅如此,ChannelProxy还青出于蓝胜于蓝,在这个层⾯上
做了更多的事情,⽐如: 发送同步消息。。。
SyncChannel。在Channel那⾥,所有的消息都是异步的(在Windows 不过能发送同步消息的类不是ChannelProxy,⽽是它的⼦类, SyncChannel
中,也叫),其本⾝也不⽀持同步逻辑。为了实现同步,SyncChannel并没有另造轮⼦,⽽只是在Channel的层⾯上加了⼀
个等待操作。 当ChannelProxy的Send操作返回后,SyncChannel会把⾃⼰阻塞在⼀组信号量上,等待回包,直到永远或超时。从外表上
看同步和异步没有什么区别,但在使⽤上还是要⼩⼼,在UI线程中使⽤同步消息,是容易被发指的。。。
3. Chrome中的IPC消息格式
说了半天,还有⼀个⼤头没有提过,那就是消息包。如果说,多线程模式下,对数据的访问开销来⾃于锁,那么在多进程模式下,⼤部分的
额外开销都来⾃于进程间的消息拆装和传递。不论怎么样的模式,只要进程不同,消息的打包,序列化,反序列化,组包,都是不可避免的
⼯作。。。
在Chrome中,IPC之间的通信消息,都是派⽣⾃ IPC::Message
IPC::Message类的。对于消息⽽⾔,序列化和反序列化是必须要⽀持的,Message的
Pickle,就是⼲这个活的。Pickle提供了⼀组的接⼝,可以接受int,char,等等各种数据的输⼊,但是在Pickle内部,所有的⼀切都基类 Pickle
⼆进制流。 这个⼆进制流是32位齐位的,⽐如你只传了⼀个bool,也是最少占32位的,同时,Pickle的流是没有区别,都转化成了⼀坨 ⼆进制流
有⾃增逻辑的(就是说它会先开⼀个Buffer,如果满了的话,会加倍这个),使其可以⽆限扩展。Pickle本⾝不维护任何⼆进制流
逻辑上的信息,这个任务交到了上级处理(后⾯会有说到...),但Pickle会为⼆进制流添加⼀个头信息,这个⾥⾯会存放流的长
度,Message在继承Pickle的时候,扩展了这个头的定义,完整的消息格式如下:
图6 Chrome的IPC消息格式
其中,黄⾊部分是包头,定长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格式不够⽤了,在将来的某⼀天,
这是⼀个类型为5的Channel的消息ID声明,由于指明了最开始的两个值,所以后续枚举的值会依次递减,如此,只要维护Channel类型的唯⼀性,就可以维护所有消息ID的唯⼀性了(当然,前提是不能超过消息上限...)。但是,定义⼀个ID还不够,你还需要定义⼀个使⽤该消息ID的Message⼦类。这个步骤不但繁琐,最重要的,是违反了DIY原则,为了添加⼀个消息,你需要在两个地⽅开⼯⼲活,是可忍孰不可
这是Chrome中,定义PluginProcess消息的宏,我挖过来放在这了,如果你想添加⼀条消息,只需要添加⼀条类似与
IPC_MESSAGE_CONTROL0东东即可,这说明它是⼀个控制消息,参数为0个。你基本上可以这样理解,IPC_BEGIN_MESSAGES就相当于完成了⼀个枚举开始的声明,然后中间的每⼀条,都会在枚举⾥⾯增加⼀个ID,并声明⼀个⼦类。这个⼀宏两吃,直逼北京烤鸭两吃的
⾼超做法,可以参看ipc_message_macros.h,或者看下⾯⼀宏两吃的⼀个举例。。。
};
#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);
std::cout << TestClass::ID << std::endl;
std::cout << t._value << std::endl;
return 0;

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。