谷歌chrome浏览器源码分析Word格式.docx
《谷歌chrome浏览器源码分析Word格式.docx》由会员分享,可在线阅读,更多相关《谷歌chrome浏览器源码分析Word格式.docx(22页珍藏版)》请在冰豆网上搜索。
ok,激动人心的Chrome源码剖析旅程,即刻开始。
Chrome源码剖析【序】
此序成于08年末,Chrome刚刚推出之际。
duguguiyu:
"
有的人一看到Chrome用到多进程就说垃圾废物肯定低能。
拜托,大家都是搞技术的,你知道多进程的缺点,Google也知道,他们不是政客,除了搞个噱头扯个蛋就一无所知了,人家也是有脸有皮的,写一坨屎一样的开源代码放出来遭世人耻笑难道会很开心?
所谓技术的优劣,是不能一概而论的,同样的技术在不同场合不同环境不同代码实现下,效果是有所不同的。
."
Chrome对我来说,有吸引力的地方在于(排名分先后…):
1、它是如何利用多进程(其实也会有多线程一起)做并发的,又是如何解决多进程间的一些问题的,比如进程间通信,进程的开销;
2、做为一个后来者,它的扩展能力如何,如何去权衡对原有插件的兼容,提供怎么样的一个插件模型;
3、它的整体框架是怎样,有没有很NB的架构思想;
4、它如何实现跨平台的UI控件系统;
5、传说中的V8,为啥那么快。
但Chrome是一个跨平台的浏览器,其Linux和Mac版本正在开发过程中,所以我把所有的眼光都放在了windows版本中,所有的代码剖析都是基于windows版本的。
有错误请指正。
关于Chrome的源码下载和环境配置,大家可自行查找资料,强调一点,一定要严格按照说明来配置环境,特别是vs2005的补丁和windowsSDK的安装,否则肯定是编译不过的。
最后,写这部分唯一不是废话的内容,请记住以下这幅图,这是Chrome最精华的一个缩影:
图1Chrome的线程和进程模型
Chrome源码剖析【一】--多线程模型
【一】Chrome的多线程模型
0.Chrome的并发模型
如果你仔细看了前面的图,对Chrome的线程和进程框架应该有了个基本的了解。
Chrome有一个主进程,称为Browser进程,它是老大,管理Chrome大部分的日常事务;
其次,会有很多Renderer进程,它们圈地而治,各管理一组站点的显示和通信(Chrome在宣传中一直宣称一个tab对应一个进程,其实是很不确切的…),它们彼此互不搭理,只和老大说话,由老大负责权衡各方利益。
它们和老大说话的渠道,称做IPC(Inter-ProcessCommunication),这是Google搭的一套进程间通信的机制,基本的实现后面自会分解。
Chrome的进程模型
Google在宣传的时候一直都说,Chrome是onetaboneprocess的模式,其实,这只是为了宣传起来方便如是说而已,基本等同广告,实际疗效,还要从代码中来看。
实际上,Chrome支持的进程模型远比宣传丰富,简单的说,Chrome支持以下几种进程模型:
1.Process-per-site-instance:
就是你打开一个网站,然后从这个网站链开的一系列网站都属于一个进程。
这是Chrome的默认模式。
2.Process-per-site:
同域名范畴的网站放在一个进程,比如(由于此文形成于08年,所以无法访问,你懂的)和process-per-site开启。
3.Process-per-tab:
这个简单,一个tab一个process,不论各个tab的站点有无联系,就和宣传的那样。
用–process-per-tab开启。
4.SingleProcess:
这个很熟悉了吧,即传统浏览器的模式:
没有多进程只有多线程,用–single-process开启。
关于各种模式的优缺点,官方有官方的说法,大家自己也会有自己的评述。
不论如何,至少可以说明,Google不是由于白痴而采取多进程的策略,而是实验出来的效果。
大家可以用Shift+Esc观察各模式下进程状况,至少我是观察失败了(每种都和默认的一样…),原因待跟踪。
不论是Browser进程还是Renderer进程,都不只是光杆司令,它们都有一系列的线程为自己打理各种业务。
对于Renderer进程,它们通常有两个线程:
一个是Mainthread,它负责与老大进行联系,有一些幕后黑手的意思;
另一个是Renderthread,它们负责页面的渲染和交互,一看就知道是这个帮派的门脸级人物。
相比之下,Browser进程既然是老大,小弟自然要多一些,除了大脑般的Mainthread,和负责与各Renderer帮派通信的IOthread,其实还包括负责管文件的filethread,负责管数据库的dbthread等等,它们各尽其责,齐心协力为老大打拼。
它们和各Renderer进程的之间的关系不一样,同一个进程内的线程,往往需要很多的协同工作,这一坨线程间的并发管理,是Chrome最出彩的地方之一了。
闲话并发
单进程单线程的编程是最惬意的事情,所看即所得,一维的思考即可。
但程序员的世界总是没有那么美好,在很多的场合,我们都需要有多线程、多进程、多机器携起手来一齐上阵共同完成某项任务,统称:
并发(非官方版定义…)。
在我看来,需要并发的场合主要是要两类:
1.为了更好的用户体验。
有的事情处理起来太慢,比如数据库读写、远程通信、复杂计算等等,如果在一个线程一个进程里面来做,往往会影响用户感受,因此需要另开一个线程或进程转到后台进行处理。
它之所以能够生效,仰仗的是单CPU的分时机制,或者是多CPU协同工作。
在单CPU的条件下,两个任务分成两拨完成的总时间,是大于两个任务轮流完成的,但是由于彼此交错,给人的感觉更自然一些。
2.为了加速完成某项工作。
大名鼎鼎的Map/Reduce,做的就是这样的事情,它将一个大的任务,拆分成若干个小的任务,分配个若干个进程去完成,各自收工后,再汇集在一起,更快地得到最后的结果。
为了达到这个目的,只有在多CPU的情形下才有可能,在单CPU的场合(单机单CPU…),是无法实现的。
在第二种场合下,我们会自然而然的关注数据的分离,从而很好的利用上多CPU的能力;
而在第一种场合,我们习惯了单CPU的模式,往往不注重数据与行为的对应关系,导致在多CPU的场景下,性能不升反降。
1.Chrome的线程模型
仔细回忆一下我们大部分时候是怎么来用线程的,在我足够贫瘠的多线程经历中,往往都是这样用的:
起一个线程,传入一个特定的入口函数,看一下这个函数是否是有副作用的(SideEffect),如果有,并且还会涉及到多线程的数据访问,仔细排查,在可疑地点上锁伺候。
Chrome的线程模型走的是另一个路子,即,极力规避锁的存在。
换更精确的描述方式来说,Chrome的线程模型,将锁限制了极小的范围内(仅仅在将Task放入消息队列的时候才存在…),并且使得上层完全不需要关心锁的问题(当然,前提是遵循它的编程模型,将函数用Task封装并发送到合适的线程去执行…),大大简化了开发的逻辑。
不过,从实现来说,Chrome的线程模型并没有什么神秘的地方,它用到了消息循环的手段。
每一个Chrome的线程,入口函数都差不多,都是启动一个消息循环(参见MessagePump类),等待并执行任务。
而其中,唯一的差别在于,根据线程处理事务类别的不同,所起的消息循环有所不同。
比如处理进程间通信的线程(注意,在Chrome中,这类线程都叫做IO线程)启用的是MessagePumpForIO类,处理UI的线程用的是MessagePumpForUI类,一般的线程用到的是MessagePumpDefault类(只讨论windows)。
不同的消息循环类,主要差异有两个,一是消息循环中需要处理什么样的消息和任务,第二个是循环流程(比如是死循环还是阻塞在某信号量上…)。
下图是一个完整版的Chrome消息循环图,包含处理Windows的消息,处理各种Task(Task是什么,稍后揭晓,敬请期待),处理各个信号量观察者(Watcher),然后阻塞在某个信号量上等待唤醒。
图2Chrome的消息循环
当然,不是每一个消息循环类都需要跑那么一大圈的,有些线程,它不会涉及到那么多的事情和逻辑,白白浪费体力和时间,实在是不可饶恕的。
因此,在实际中,不同的MessagePump类,实现是有所不同的,详见下表:
2.Chrome中的Task
从上面的表不难看出,不论是哪一种消息循环,必须处理的,就是Task(暂且遗忘掉系统消息的处理和Watcher,以后,我们会缅怀它们的…)。
刨去其它东西的干扰,只留下Task的话,我们可以这样认为:
Chrome中的线程从实现层面来看没有任何区别,它的区别只存在于职责层面,不同职责的线程,会处理不同的Task。
最后,在铺天盖地西红柿来临之前,我说一下啥是Task。
简单的看,Task就是一个类,一个包含了voidRun()抽象方法的类(参见Task类…)。
一个真实的任务,可以派生Task类,并实现其Run方法。
每个MessagePump类中,会有一个MessagePump:
Delegate的类的对象(MessagePump:
Delegate的一个实现,请参见MessageLoop类…),在这个对象中,会维护若干个Task的队列。
当你期望,你的一个逻辑在某个线程内执行的时候,你可以派生一个Task,把你的逻辑封装在Run方法中,然后实例一个对象,调用期望线程中的PostTask方法,将该Task对象放入到其Task队列中去,等待执行。
我知道很多人已经抄起了板砖,因为这种手法实在是太常见了,就不是一个简单的依赖倒置,在线程池,Undo\Redo等模块的实现中,用的太多了。
但,我想说的是,虽说谁家过年都是吃顿饺子,这饺子好不好吃还是得看手艺,不能一概而论。
在Chrome中,线程模型是统一且唯一的,这就相当于有了一套标准,它需要满足在各个线程上执行的几十上百种任务的需求,因此,必须在灵活行和易用性上有良好的表现,这就是设计标准的难度。
为了满足这些需求,Chrome在底层库上做了足够的功夫:
1.它提供了一大套的模板封装(参见task.h),可以将Task摆脱继承结构、函数名、函数参数等限制(就是基于模板的伪function实现,想要更深入了解,建议直接看鼻祖《ModernC++》和它的Loki库…);
2.同时派生出CancelableTask、ReleaseTask、DeleteTask等子类,提供更为良好的默认实现;
3.在消息循环中,按逻辑的不同,将Task又分成即时处理的Task、延时处理的Task、Idle时处理的Task,满足不同场景的需求;
4.Task派生自tracked_objects:
Tracked,Tracked是为了实现多线程环境下的日志记录、统计等功能,使得Task天生就有良好的可调试性和可统计性;
这一套七荤八素的都搭建完,这才算是一个完整的Task模型,由此可知,这饺子,做的还是很费功夫的。
3.Chrome的多线程模型
工欲善其事,必先利其器。
Chrome之所以费了老鼻子劲去磨底层框架这把刀,就是为了面对多线程这坨怪兽的时候杀的更顺畅一些。
在Chrome的多线程模型下,加锁这个事情只发生在将Task放入某线程的任务队列中,其他对任何数据的操作都不需要加锁。
当然,天下没有免费的午餐,为了合理传递Task,你需要了解每一个数据对象所管辖的线程,不过这个事情,与纷繁的加锁相比,真是小儿科了不知道多少倍。
图3Task的执行模型
如果你熟悉设计模式,你会发现这是一个Command模式,将创建于执行的环境相分离,在一个线程中创建行为,在另一个线程中执行行为。
Command模式的优点在于,将实现操作与构造操作解耦,这就避免了锁的问题,使得多线程与单线程编程模型统一起来,其次,Command还有一个优点,就是有利于命令的组合和扩展,在Chrome中,它有效统一了同步和异步处理的逻辑。
Command模式
Command模式,是一种看上去很酷的模式,传统的面向对象编程,我们封装的往往都是数据,在Command模式下,我们希望封装的是行为。
这件事在函数式编程中很正常,封装一个函数作为参数,传来传去,稀疏平常的事儿;
但在面向对象的编程中,我们需要通过继承、模板、函数指针等手法,才能将其实现。
应用Command模式,我们是期望这个行为能到一个不同于它出生的环境中去执行,简而言之,这是一种想生不想养的行为。
我们做Undo/Redo的时候,会把在任一一个环境中创建的Command,放到一个队列环境中去,供统一的调度;
在Chrome中,也是如此,我们在一个线程环境中创建了Task,却把它放到别的线程中去执行,这种寄居蟹似的生活方式,在很多场合都是有用武之地的。
在一般的多线程模型中,我们需要分清楚啥是同步啥是异步,在同步模式下,一切看上去和单线程没啥区别,但同时也丧失了多线程的优势(沦落成为多线程串行…)。
而如果采用异步的模式,那写起来就麻烦多了,你需要注册回调,小心管理对象的生命周期,程序写出来是嗷嗷恶心。
在Chrome的多线程模型下,同步和异步的编程模型区别就不复存在了,如果是这样一个场景:
A线程需要B线程做一些事情,然后回到A线程继续做一些事情;
在Chrome下你可以这样来做:
生成一个Task,放到B线程的队列中,在该Task的Run方法最后,会生成另一个Task,这个Task会放回到A的线程队列,由A来执行。
如此一来,同步异步,天下一统,都是Task传来传去,想不会,都难了。
图4Chrome的一种异步执行的解决方案
4.Chrome多线程模型的优缺点
一直在说Chrome在规避锁的问题,那到底锁是哪里不好,犯了何等滔天罪责,落得如此人见人嫌恨不得先杀而后快的境地。
《代码之美》的第二十四章"
美丽的并发"
中,Haskell设计人之一的SimonPeytonJones总结了一下用锁的困难之处,如下:
1.锁少加了,导致两个线程同时修改一个变量;
2.锁多加了,轻则妨碍并发,重则导致死锁;
3.锁加错了,由于锁和需要锁的数据之间的联系,只存在于程序员的大脑中,这种事情太容易发生了;
4.加锁的顺序错了,维护锁的顺序是一件困难而又容易出错的问题;
5.错误恢复;
6.忘记唤醒和错误的重试;
7.而最根本的缺陷,是锁和条件变量不支持模块化的编程。
比如一个转账业务中,A账户扣了100元钱,B账户增加了100元,即使这两个动作单独用锁保护维持其正确性,你也不能将两个操作简单的串在一起完成一个转账操作,你必须让它们的锁都暴露出来,重新设计一番。
好好的两个函数,愣是不能组在一起用,这就是锁的最大悲哀;
通过这些缺点的描述,也就可以明白Chrome多线程模型的优点。
它解决了锁的最根本缺陷,即,支持模块化的编程,你只需要维护对象和线程之间的职能关系即可,这个摊子,比之锁的那个烂摊子,要简化了太多。
对于程序员来说,负担一瞬间从泰山降成了鸿毛。
而Chrome多线程模型的一个主要难点,在于线程与数据关系的设计上,你需要良好的划分各个线程的职责,如果有一个线程所管辖的数据,几乎占据了大半部分的Task,那么它就会从多线程沦为单线程,Task队列的锁也将成为一个大大的瓶颈。
设计者的职责
一个底层结构设计是否成功,这个设计者是否称职,我一直觉得是有一个很简单的衡量标准的。
你不需要看这个设计人用了多少NB的技术,你只需要关心,他的设计,是否给其他开发人员带来了困难。
一个NB的设计,是将所有困难都集中在底层搞定,把其他开发人员换成白痴都可以工作的那种;
一个SB的设计,是自己弄了半天,只是为了给其他开发人员一个长达250条的注意事项,然后很NB的说,你们按照这个手册去开发,就不会有问题了。
从根本上来说,Chrome的线程模型解决的是并发中的用户体验问题而不是联合工作的问题(参见我前面喷的"
闲话并发"
),它不是和Map/Reduce那样将关注点放在数据和执行步骤的拆分上,而是放在线程和数据的对应关系上,这是和浏览器的工作环境相匹配的。
设计总是和所处的环境相互依赖的,毕竟,在客户端,不会和服务器一样,存在超规模的并发处理任务,而只是需要尽可能的改善用户体验,从这个角度来说,Chrome的多线程模型,至少看上去很美。
Chrome源码剖析【二】--进程通信
【二】Chrome的进程间通信
1.Chrome进程通信的基本模式
进程间通信,叫做IPC(Inter-ProcessCommunication)。
Chrome最主要有三类进程,一类是Browser主进程,我们一直尊称它老人家为老大;
还有一类是各个Render进程,前面也提过了;
另外还有一类一直没说过,是Plugin进程,每一个插件,在Chrome中都是以进程的形式呈现,等到后面说插件的时候再提罢了。
Render进程和Plugin进程都与老大保持进程间的通信,Render进程与Plugin进程之间也有彼此联系的通路,唯独是多个Render进程或多个Plugin进程直接,没有互相联系的途径,全靠老大协调。
进程与进程间通信,需要仰仗操作系统的特性,能玩的花着实不多,在Chrome中,用到的就是有名的管道(NamedPipe),只不过,它用一个IPC:
Channel类,封装了具体的实现细节。
Channel可以有两种工作模式,一种是Client,一种是Server,Server和Client分属两个进程,维系一个共同的管道名,Server负责创建该管道,Client会尝试连接该管道,然后双发往各自管道缓冲区中读写数据(在Chrome中,用的是二进制流,异步IO…),完成通信。
管道名字的协商
在Socket中,我们会事先约定好通信的端口,如果不按照这个端口进行访问,走错了门,会被直接乱棍打出门去的。
与之类似,有名管道期望在两个进程间游走,就需要拿一个两个进程都能接受的进门暗号,这个就是有名管道的名字。
在Chrome中(windows下…),有名管道的名字格式都是:
\.\pipe\chrome.ID。
其中的ID,自然是要求独一无二,比如:
进程ID.实例地址.随机数。
通常,这个ID是由一个Process生成(往往是BrowserProcess),然后在创建另一个进程的时候,作为命令行参数传进去,从而完成名字的协商。
如果不了解并期待了解有关Windows下有名管道和信号量的知识,建议去看一些专业的书籍,比如圣经级别的《Windows核心编程》和《深入解析Windows操作系统》,当然也可以去查看SDK,你需要了解的API可能包括:
CreateNamedPipe,CreateFile,ConnectNamedPipe,WaitForMultipleObjects,WaitForSingleObject,SetEvent,等等。
Channel中,有三个比较关键的角色,一个是Message:
Sender,一个是Channel:
Listener,最后一个是MessageLoopForIO:
Watcher。
Channel本身派生自Sender和Watcher,身兼两角,而Listener是一个抽象类,具体由Channel的使用者来实现。
顾名思义,Sender就是发送消息的接口,Listener就是处理接收到消息的具体实现,但这个Watcher是啥?
如果你觉得Watcher这东西看上去很眼熟的话,我会激动的热泪盈眶的,没错,在前面(第一部分第一小节…)说消息循环的时候,从那个表中可以看到,IO线程(记住,在Chrome中,IO指的是网络IO,*_*)的循环会处理注册了的Watcher。
其实Watcher很简单,可以视为一个信号量和一个带有OnObjectSignaled方法对象的对,当消息循环检测到信号量开启,它就会调用相应的OnObjectSignaled方法。
图5Chrome的IPC处理流程图
一图解千语,如上图所示,整个Chrome最核心的IPC流程都在图上了,期间,刨去了一些错误处理等逻辑,如果想看原汁原味的,可以自查Channel类的实现。
当有消息被Send到一个发送进程的Channel的时候,Channel会把它放在发送消息队列中,如果此时还正在发送以前的消息(发送端被阻塞…),则看一下阻塞是否解除(用一个等待0秒的信号量等待函数…),然后将消息队列中的内容序列化并写道管道中去。
操作系统会维护异步模式下管道的这一组信号量,当消息从发送进程缓冲区写到接收进程的缓冲区后,会激活接收端的信号量。
当接收进程的消息循环,循到了检查Watcher这一步,并发现有信号量激活了,就会调用该Watcher相应的OnObjectSignaled方法,通知接受进程的Channel,有消息来了!
Channel会尝试从管道中收字节,组消息,并调用Listener来解析该消息。
从上面的描述不难看出,Chrome的进程通信,最核心的特点,就是利用消息循环来检查信号量,而不是直接让管道阻塞在某信号量上。
这样就与其多线程模型紧密联系在了一起,用一种统一的模式来解决问题。
并且,由于是消息循环统一检查,线程不会随便就被阻塞了,可以更好的处理各种其他工作,从理论上讲,这是通过增加CPU工作时间,来换取更好的体验,颇有资本家的派头。
温柔的消息循环
其实,Chrome的很多消息循环,也不是都那么霸道,也是会被阻塞在某些信号量或者某种场景上的,毕竟客户端不是它家的服务器,CPU不能被全部归在它家名下。
比如IO线程,当没有消息来到,又没有信号量被激活的时候,就会被阻塞,具体实现可以去看MessagePumpForIO的WaitForWork方法。
不过这种阻塞是集中式的,可随时修改策略的,比起Channel直接阻塞在信号量上,停工的时间更短。
2.进程间的跨线程通信和同步通信
在Chrome中,任何底层的数据都是线程非安全的,Channel不是太上老君(抑或中国足球?
…),它也没有例外。
在每一个进程中,只能有一个线程来负责操作Channel,这个线程叫做IO线程(名不符实真是一件悲凉的事情…)。
其它线程要是企图越俎代庖,是会出大乱子的。
但是有时候(其实是大部分时候…),我们需要从非IO线程与别的进程相通信,这该如何是好?
如果,你有看过我前面写的线程模型,你一定可以想到,做法很简单,先将对Channel的操作放到Task中,将此Task放到IO线程队列里,让IO线程来处理即可。
当然,由于这种事情发生的太频繁,每次都人肉做一次颇为繁琐,于是有一个代理类,叫做ChannelProxy,来帮助你完成这一切。
从接口上看,ChannelProxy的接口和Channel没有大的区别(否则就不叫Proxy了…),你可以像用Channel一样,用ChannelPr