赞
踩
转自CSDN
前言:
1、之所以整理此文,有俩个目的:一是为了供自己学习研究之用;二是为了备份,以作日后反复研究。除此之外,无它。
2、此文的形式其实是有点俩不像的,既不是个人首创即原创,又非单纯的转载(有加工),无奈之下,权且称作翻译吧。有不妥之处,还望原作者,及读者见谅。
Chrome源码剖析【序】
此序成于08年末,Chrome刚刚推出之际。
Chrome对我来说,有吸引力的地方在于(排名分先后…):
图1 Chrome的线程和进程模型
Chrome源码剖析【一】——多线程模型
【一】Chrome的多线程模型
0.Chrome的并发模型
Chrome的进程模型
1.Process-per-site-instance:就是你打开一个网站,然后从这个网站链开的一系列网站都属于一个进程。这是Chrome的默认模式。
2.Process-per-site:同域名范畴的网站放在一个进程,比如www.google.com(由于此文形成于08年,所以无法访问,你懂的)和www.google.com/bookmarks就属于一个域名内(google有自己的判定机制),不论有没有互相打开的关系,都算作是一个进程中。用命令行–process-per-site开启。
3.Process-per-tab:这个简单,一个tab一个process,不论各个tab的站点有无联系,就和宣传的那样。用–process-per-tab开启。
4.SingleProcess:这个很熟悉了吧,即传统浏览器的模式:没有多进程只有多线程,用–single-process开启。
关于各种模式的优缺点,官方有官方的说法,大家自己也会有自己的评述。不论如何,至少可以说明,Google不是由于白痴而采取多进程的策略,而是实验出来的效果。
大家可以用Shift+Esc观察各模式下进程状况,至少我是观察失败了(每种都和默认的一样…),原因待跟踪。
闲话并发
单进程单线程的编程是最惬意的事情,所看即所得,一维的思考即可。但程序员的世界总是没有那么美好,在很多的场合,我们都需要有多线程、多进程、多机器携起手来一齐上阵共同完成某项任务,统称:并发(非官方版定义…)。在我看来,需要并发的场合主要是要两类:
1.为了更好的用户体验。有的事情处理起来太慢,比如数据库读写、远程通信、复杂计算等等,如果在一个线程一个进程里面来做,往往会影响用户感受,因此需要另开一个线程或进程转到后台进行处理。它之所以能够生效,仰仗的是单CPU的分时机制,或者是多CPU协同工作。在单CPU的条件下,两个任务分成两拨完成的总时间,是大于两个任务轮流完成的,但是由于彼此交错,给人的感觉更自然一些。
2.为了加速完成某项工作。大名鼎鼎的Map/Reduce,做的就是这样的事情,它将一个大的任务,拆分成若干个小的任务,分配个若干个进程去完成,各自收工后,再汇集在一起,更快地得到最后的结果。为了达到这个目的,只有在多CPU的情形下才有可能,在单CPU的场合(单机单CPU…),是无法实现的。
在第二种场合下,我们会自然而然的关注数据的分离,从而很好的利用上多CPU的能力;而在第一种场合,我们习惯了单CPU的模式,往往不注重数据与行为的对应关系,导致在多CPU的场景下,性能不升反降。
1.Chrome的线程模型
图2 Chrome的消息循环
2.Chrome中的Task
这一套七荤八素的都搭建完,这才算是一个完整的Task模型,由此可知,这饺子,做的还是很费功夫的。
3.Chrome的多线程模型
图3 Task的执行模型
Command模式
Command模式,是一种看上去很酷的模式,传统的面向对象编程,我们封装的往往都是数据,在Command模式下,我们希望封装的是行为。这件事在函数式编程中很正常,封装一个函数作为参数,传来传去,稀疏平常的事儿;但在面向对象的编程中,我们需要通过继承、模板、函数指针等手法,才能将其实现。
应用Command模式,我们是期望这个行为能到一个不同于它出生的环境中去执行,简而言之,这是一种想生不想养的行为。我们做Undo/Redo的时候,会把在任一一个环境中创建的Command,放到一个队列环境中去,供统一的调度;在Chrome中,也是如此,我们在一个线程环境中创建了Task,却把它放到别的线程中去执行,这种寄居蟹似的生活方式,在很多场合都是有用武之地的。
图4 Chrome的一种异步执行的解决方案
4.Chrome多线程模型的优缺点
1.锁少加了,导致两个线程同时修改一个变量;
2.锁多加了,轻则妨碍并发,重则导致死锁;
3.锁加错了,由于锁和需要锁的数据之间的联系,只存在于程序员的大脑中,这种事情太容易发生了;
4.加锁的顺序错了,维护锁的顺序是一件困难而又容易出错的问题;
5.错误恢复;
6.忘记唤醒和错误的重试;
7.而最根本的缺陷,是锁和条件变量不支持模块化的编程。比如一个转账业务中,A账户扣了100元钱,B账户增加了100元,即使这两个动作单独用锁保护维持其正确性,你也不能将两个操作简单的串在一起完成一个转账操作,你必须让它们的锁都暴露出来,重新设计一番。好好的两个函数,愣是不能组在一起用,这就是锁的最大悲哀;
设计者的职责
一个底层结构设计是否成功,这个设计者是否称职,我一直觉得是有一个很简单的衡量标准的。你不需要看这个设计人用了多少NB的技术,你只需要关心,他的设计,是否给其他开发人员带来了困难。一个NB的设计,是将所有困难都集中在底层搞定,把其他开发人员换成白痴都可以工作的那种;一个SB的设计,是自己弄了半天,只是为了给其他开发人员一个长达250条的注意事项,然后很NB的说,你们按照这个手册去开发,就不会有问题了。
Chrome源码剖析【二】——进程通信
【二】Chrome的进程间通信
1.Chrome进程通信的基本模式
管道名字的协商
在Socket中,我们会事先约定好通信的端口,如果不按照这个端口进行访问,走错了门,会被直接乱棍打出门去的。与之类似,有名管道期望在两个进程间游走,就需要拿一个两个进程都能接受的进门暗号,这个就是有名管道的名字。在Chrome中(windows下…),有名管道的名字格式都是:\\.\pipe\chrome.ID。其中的ID,自然是要求独一无二,比如:进程ID.实例地址.随机数。通常,这个ID是由一个Process生成(往往是BrowserProcess),然后在创建另一个进程的时候,作为命令行参数传进去,从而完成名字的协商。
如果不了解并期待了解有关Windows下有名管道和信号量的知识,建议去看一些专业的书籍,比如圣经级别的《Windows核心编程》和《深入解析Windows操作系统》,当然也可以去查看SDK,你需要了解的API可能包括:CreateNamedPipe,CreateFile, ConnectNamedPipe, WaitForMultipleObjects,WaitForSingleObject, SetEvent, 等等。
图5 Chrome的IPC处理流程图
温柔的消息循环
其实,Chrome的很多消息循环,也不是都那么霸道,也是会被阻塞在某些信号量或者某种场景上的,毕竟客户端不是它家的服务器,CPU不能被全部归在它家名下。
比如IO线程,当没有消息来到,又没有信号量被激活的时候,就会被阻塞,具体实现可以去看MessagePumpForIO的WaitForWork方法。
不过这种阻塞是集中式的,可随时修改策略的,比起Channel直接阻塞在信号量上,停工的时间更短。
2.进程间的跨线程通信和同步通信
3.Chrome中的IPC消息格式
消息的序列化
前不久读了GoogleProtocolBuffers的源码,是用在服务器端,用做内部机器通信协议的标准、代码生成工具和框架。它主要的思想是揉合了key/value的内容到二进制中,帮助生成更为灵活可靠的二进制协议。
在Chrome中,没有使用这套东西,而是用到了纯二进制流作为消息序列化的方式。我想这是由于应用场景不同使然。在服务端,我们更关心协议的稳定性,可扩展性,并且,涉及到的协议种类很多。但在一个Chrome中,消息的格式很统一,这方面没有扩展性和灵活性的需求,而在序列化上,虽然key/value的方式很好很强大,但是在Chrome中需要的不是灵活性而是精简性,因此宁可不用ProtocolBuffers造好的轮子,而是另立炉灶,花了好一把力气提供了一套纯二进制的消息机制。
4.定义IPC消息
class SomeMessage: public IPC::Message
{
};
enum SomeChannel_MsgType
{
};
IPC_BEGIN_MESSAGES(PluginProcess, 3)
IPC_MESSAGE_CONTROL2(PluginProcessMsg_CreateChannel,
int ,
HANDLE )
IPC_MESSAGE_CONTROL1(PluginProcessMsg_ShutdownResponse,
bool )
IPC_MESSAGE_CONTROL1(PluginProcessMsg_PluginMessage,
std::vector<uint8> )
IPC_MESSAGE_CONTROL0(PluginProcessMsg_BrowserShutdown)
IPC_END_MESSAGES(PluginProcess)
多次展开宏的技巧
这是Chrome中用到的一个技巧,定义一次宏,展开多段代码,我孤陋寡闻,第一次见,一个类似的例子,如下:
首先,定义一个macro.h,里面放置宏的定义:
#undef SUPER_MACRO
#if defined(FIRST_TIME)
#undef FIRST_TIME
#define SUPER_MACRO(label, type) \
enum IDs { \
};
#elif defined(SECOND_TIME)
#undef SECOND_TIME
#define SUPER_MACRO(label, type) \
class TestClass \
{ \
};
#endif
可以看到,这个头文件是可重入的,每一次先undef掉之前的定义,然后判断进行新的定义。然后,你可以创建一个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 <iostream>
int _tmain(int argc, _TCHAR* argv[])
{
}
这样,你就成功的实现,一次定义,生成多段代码了。
此外,当接收到消息后,你还需要处理消息。接收消息的函数,是IPC::Channel::Listener子类的OnMessageReceived函数。在这个函数中,会放置一坨的宏,这一套宏,一定能让你想起MFC的MessageMap机制(关于此消息机制原理更具体的介绍,可参考侯捷的深入浅出MFC一书。):
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()
苦力的宏和模板
不论是宏还是模板,为了实现这套机制,都需要写大量的类似代码,比如为了支持0~N个参数的Control消息,你就需要写N+1个类似的宏;为了支持各种基础数据结构的序列化,你就需要写上十来个类似的Write函数和Traits。
之所以做如此苦力的活,都是为了用这些东西的人能够尽可能的简单方便,符合DIY原则。规约到之前说的设计者的职责上来,这是一个典型的苦了我一个幸福千万人的负责任的行为。在Chrome中,如此的代码随处可见,光Tuple那一套拳法,我现在就看到了使了不下三次(我曾经做过一套,直接吐血…),如此兢兢业业,真是可歌可泣啊。
【三】Chrome的进程模型
1.基本的进程结构
2.Render进程
3.进程开销控制算法
进程的优先级
在windows中,进程是有优先级的,当然,这个优先级不是真实的调度优先级,而是该进程中,线程优先级计算的基准。在《WindowsviaC/C++》(也就是《windows核心编程》的第五版)中,有一张详细的表,表述了线程优先级和进程优先级的具体对应关系,感觉设计的很不错,在此就不再赘述了,有兴趣的自行动手翻书。
本文Chrome源码剖析、上,完。
转自: http://blog.csdn.net/orichisonic/article/details/50555513
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。