Chromium学习之路(四)----Chromium多进程架构简要介绍和学习计划

       因工作需求研究了一下Chromium的多进程架构,发现在webrtc当中也有使用到,只不过google这群人将其精简了许多。我们本着工欲善其事必先利其器的态度,先掌握好Chromium基础,然后再进军webrtc的庞杂知识领域,步步为营,为后续音视频研究打下坚实基础。

多进程架构概述

       Chromium以多进程架构著称,它主要包含四类进程,分别是Browser进程、Render进程、GPU进程和Plugin进程。之所以要将Render进程、GPU进程和Plugin进程独立出来,是为了解决它们的不稳定性问题。也就是说,Render进程、GPU进程和Plugin进程由于不稳定而引发的Crash不会导致整个浏览器崩溃。

       一个Chromium实例只有一个Browser进程和一个GPU进程,但是Render进程和Plugin进程可能有若干个。Browser进程负责合成浏览器的UI,包括标题栏、地址栏、工具栏以及各个TAB的网页内容。Render进程负责解析和渲染网页的内容。一般来说,一个TAB就对应有一个Render进程。但是我们也可以设置启动参数,让具有相同的域名的TAB都运行在同一个Render进程中。简单起见,我们就假设一个TAB就对应有一个Render进程。无论是Browser进程,还是Render进程,当启用了硬件加速渲染时,它们都是通过GPU进程来渲染UI的。不过Render进程是将网页内容渲染在一个离屏窗口的,例如渲染在一个Frame Buffer Object上,而Browser进程是直接将UI渲染在Frame Buffer上,也就是屏幕上。正因为如此,Render进程渲染好的网页UI要经过Browser进程合成之后,才能在屏幕上看到。Plugin进程,就是用来运行第三方开发的Plugin,以便可以扩展浏览器的功能。例如,Flash就是一个Plugin,它运行在独立的Plugin进程中。注意,为了避免创建过多的Plugin进程,同一个Plugin的不同实例都是运行在同一个Plugin进程中的。也就是说,不管是在同一个TAB的网页创建的同类Plugin,还是在不同TAB的网页创建的同类Plugin,它们都是运行在同一个Plugin进程中。

chrome

进程间通信

       从上面的分析就可以知道,虽然每一个进程的职责不同,但是它们不是相互孤立的,而是需要相同协作,这样就需要执行进程间通信(IPC)。例如,Render进程渲染好自己负责解析的网页之后,需要通知GPU进程离屏渲染已经解析好的网页的UI,接着还要通知Browser进程合成已经离屏渲染好的网页UI。同样,Browser进程也需要通过GPU进程合成标题栏、地址栏、工具栏和各个网页的离屏UI。对于Plugin进程,Render进程需要将一些网页的事件发送给它处理,这样Render进程就需要与Plugin进程进行通信。反过来,Plugin进程也需要通过SDK接口向Render进程请求一些网页相关的信息,以便可以扩展网页的内容。更进一步地,如果Plugin进程需要绘制自己的UI,那么它也需要通过Render进程间接地和GPU进程进行通信。

       以上分析的Browser进程、Render进程、GPU进程和Plugin进程,以及它们之间的通信方式,如下所示:

multiprocess

进程间通信Unix Socket

       从上图可以看到,每一个进程除了具有一个用来实现各自职责的主线程之外,都具有一个IO线程。这个IO线程不是用来执行读写磁盘文件之类的IO的,而是用来负责执行IPC的。它们之所以称为IO线程,是因为它们操作的对象是一个文件描述符。即然操作的对象是文件描述符,当然也可以称之类IO。当然,这些是特殊的IO,具体来说,就是一个UNIX Socket。UNIX Socket是用来执行本地IPC的,它的概念与管道是类似的。只不过管道的通信是单向的,一端只能读,另一端只能写,而UNIX Socket的通信是双向的,每一端都既可读也可写。

       关于IO线程的实现,可以参考前面Chromium多线程模型设计和实现分析一文。简单来说,就是我们创建了一个UNIX Socket之后,就可以获得两个文件描述符。其中一个文件描述符作为Server端,增加到Server端的IO线程的消息循环中去监控,另一个文件描述符作为Client端,增加到Client端的IO线程的消息循环中去监控。对这些文件描述符的读写操作都封装在一个Channel对象。因此,Server端和Client端都有一个对应的Channel对象。

       当一个进程的主线程执行某个操作需要与另一个进程进行通信时,它的主线程就会将一个消息发送到IO线程的消息循环去。IO线程在处理这个消息的时候,就会通过前面已经创建好的UNIX Socket转发给目标进程处理。目标进程在其IO线程接收到消息之后,一般也会通过其主线程的消息循环通知主线程执行相应的操作。这就是说,在Chromium里面,线程间通过消息循环进行通信,而进程间通过UNIX Socket进行通信的。

Browser进程和Render进程之间的通信

       我们先来看Browser进程和Render进程之间的通信。Browser进程每启动一个Render进程,都会创建一个RenderProcessHost对象。Render进程启动之后,会创建一个RenderProcess对象来描述自己。这样,Browser进程和Render进程之间的通信就通过上述的RenderProcessHost对象和RenderProcess对象进行。

Browser进程和GPU进程之间的通信

       我们再来看Browser进程和GPU进程之间的通信。Browser进程会创建一个GpuProcessHost对象来描述它启动的GPU进程,GPU进程启动之后,会创建一个GpuProcess进程。这样,Browser进程和GPU进程之间的通信就通过上述的GpuProcessHost对象和GpuProcess对象进行。注意,这两个对象之间的Channel是用来执行信令类通信的。例如,Browser进程通过上述Channel可以通知GPU进程创建另外一个Channel,专门用来执行OpenGL命令。这个专门用来执行OpenGL命令的Channel称为Gpu Channel。

       我们知道,GPU进程需要同时为多个进程执行OpenGL命令,而OpenGL命令又是具有状态的,因此,GPU进程就需要为每一个Client进程创建一个OpenGL上下文,也就是一个GLContext对象。GPU进程在为某一个Client进程执行OpenGL命令之前,需要找到之前为该Client进程创建的GLContext对象,并且将该GLContext对象描述的OpenGL上下文设置为当前的OpenGL上下文。

Render进程也需要与GPU进行通信

       前面提到,Render进程也需要与GPU进行通信,这意味着它们也像Browser进程一样,需要与GPU进程建立一对Gpu Channel。不过,Render进程不能像Browser进程一样,直接请求GPU进程创建一对Gpu Channel。Render进程首先要向Browser进程发送一个创建Gpu Channel的请求,Browser进程收到这个请求之后,再向GPU进程转发。GPU接收到创建Gpu Channel的请求后,就会创建一个UNIX Socket,并且将Server端的文件描述符封装在一个GpuChannel对象中,而将Client端的文件描述符返回给Browser进程,Browser进程再返回到Render进程,这样Render进程就可以创建一个Client端的Gpu Channel了。除了创建一个Client端的Gpu Channel,Render进程还会创建一个WebGrahpicsContext3DCommandBufferImpl对象,用来描述一个Client端的OpenGL上下文,这个OpenGL上下文与GPU进程里面的GLContext对象描述的OpenGL上下文是对应的。

Render进程与Plugin进程之间的通信

       最后我们再来看Render进程与Plugin进程之间的通信。Chromium支持两种类型的插件,一种是NPAPI插件,另一种是PPAPI插件。NPAPI插件是来自于Mozilla的一种插件机制,它被很多浏览器所支持,Chromium也不例外。不过由于运行在NPAPI插件中的代码不能利用完全利用Chromium的沙箱技术和其他安全防护技术,现在NPAPI插件已经不被支持。因此这里我们就只关注PPAPI插件机制。

       Render进程在解析网页的过程中发现需要创建一个PPAPI插件实例时,就会通知Browser进程创建一个Plugin进程。当然,如果对应的Plugin进程已经存在,就会利用它,而不是再启动一个。Browser进程每启动一个Plugin进程,都会创建一个PpapiPluginProcessHost对象描述它。Plugin进程启动完成后,也会创建一个ChildProcess对象描述自己。这样,以后Browser进程和Plugin进程就可以通过PpapiPluginProcessHost对象和ChildProcess对象之间的Channel进行通信。但是Render进程和Plugin之间的通信需要另外一个Channel。因此,Browser进程会进一步请求Plugin进程创建另外一个Channel,用来在Render进程和Plugin进程之间进行通信。有了这个Channel之后,Render进程会创建一个HostDispatcher对象,而Plugin进程会创建一个PluginDispatcher对象,以后Render进程和Plugin进程之间的通信就通过上述两个对象进行。

       前面提到,Plugin进程有可能也需要渲染UI,因此,PPAPI插件机制提供了一个Graphics3D接口,PPAPI插件可以通过该接口与GPU进行通信。注意,Plugin进程和GPU进程之间的通信,不同于Render进程和GPU进程之间的通信,前者没有一个专门的Channel用来执行IPC通信。不过,Plugin进程却可以利用之前它已经与Render进程建立好的Channel进行通信。这意味着,Plugin进程和GPU进程之间的通信是要通过Render进程间接进行的。具体来说,就是Plugin进程首先要将OpenGL命令发送给Render进程,然后再由Render进程通过Gpu Channel发送给GPU进程执行。

消息处理

       在Chromium的运行过程中,进程之间需要发送很多IPC消息。不同类型的IPC消息会被不同的模块处理。为了能够快速地对这些IPC消息进行分发处理,Chromium提供了一套灵活的消息发分机制。这套分发机制规定每一个IPC消息都具有一个32位的Routing ID和一个也是32位的Type,其中,Type的高16位描述的是IPC消息类别,低16位没有特殊的意义。

       基于IPC消息的类别信息,我们可以在IO线程中注册一系列的MessageFilter,每一个Filter都可以指定自己所支持的IPC消息类别。IO线程接收到一个IPC消息的时候,首先就会根据它的类别查找有没有注册相应的MessageFilter。如果有的话,就快速地在IO线程分发给它处理。

       此外,我们还可以IO线程中注册一个Listener。当一个IPC消息没有相应的MessageFilter可以处理时,那么它接下来就会分发给上述注册的Listener处理。注意,这时候Listener处理代码运行在注册时的线程中。这个线程一般就是主线程。Listener首先根据IPC消息的Type进行分发给相应的Handler进行处理。如果没有相应的Handler可以处理,并且Listener支持注册Router,那么就会再根据IPC消息的Routing ID分发相应的Router进行处理。

沙箱

       最后,还有一点需要注意的是,在Android平台中,Chromium的Browser进程就是Android应用程序的主进程,它具有Android应用程序申请的所有权限,Render进程、GPU进程和Plugin进程是Android应用程序的Service进程。这些Service在AndroidManifest文件中被配置为运行在的孤立进程中,也就是它的android:isolatedProcess属性被设置为true。这类进程在启动的时候,将不会被赋予Android应用程序申请的权限,也就是它们运行在一个非常受限的进程中。这一点我们可以通过分析Android应用程序进程启动的过程源代码看到。

       从前面Android应用程序进程启动过程的源代码分析一文可以知道,Android应用程序进程是由ActivityManagerService启动的。具体来说,在启动一个Service的时候,如果发现需要为它创建一个单独的进程时,就会调用ActivityManagerService类的以下成员函数startProcessLocked创建一个新的进程,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
public final class ActivityManagerService extends ActivityManagerNative
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
......

private final void startProcessLocked(ProcessRecord app,
String hostingType, String hostingNameStr)
{

startProcessLocked(app, hostingType, hostingNameStr, null /* abiOverride */,
null /* entryPoint */, null /* entryPointArgs */);
}

......
}

       这个函数定义在文件frameworks/base/services/core/java/com/adroid/server/am/ActivityManagerService.java中。
       ActivityManagerService类三个参数版本的成员函数startProcessLocked调用了另外一个重载版本的成员函数startProcessLocked,后者的实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public final class ActivityManagerService extends ActivityManagerNative
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
......

private final void startProcessLocked(ProcessRecord app, String hostingType,
String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs)
{

......

try {
......

int[] gids = null;
......

if (!app.isolated) {
int[] permGids = null;
try {
......
final PackageManager pm = mContext.getPackageManager();
permGids = pm.getPackageGids(app.info.packageName);

......
} catch (PackageManager.NameNotFoundException e) {
......
}

......

if (permGids == null) {
gids = new int[2];
} else {
gids = new int[permGids.length + 2];
System.arraycopy(permGids, 0, gids, 2, permGids.length);
}
gids[0] = UserHandle.getSharedAppGid(UserHandle.getAppId(uid));
gids[1] = UserHandle.getUserGid(UserHandle.getUserId(uid));
}

......

Process.ProcessStartResult startResult = Process.start(entryPoint,
app.processName, uid, uid, gids, debugFlags, mountExternal,
app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,
app.info.dataDir, entryPointArgs);
......
} catch (RuntimeException e) {
......
}
}

......
}

       这个函数定义在文件frameworks/base/services/core/java/com/adroid/server/am/ActivityManagerService.java中。
       从这里可以看到,只有当参数app描述的一个ProcessRecord对象的成员变量isolated等于false的时候,ActivityManagerService类的成员函数startProcessLocked才会请求PackageManagerService返回要启动的Service所属的Android应用程序申请的权限,也就是一系列GID。而当一个Service在AndroidManifest.xml中将属性android:isolatedProcess属性设置为true的时候,这里的参数app描述的ProcessRecord对象的成员变量isolated也是等于true,这时候就相当于是不给该Service所运行在的进程赋予任何权限,因此它就运行在一个沙箱中。

多进程架构细节

       关于Chromium的多进程架构,我们就介绍到这里。在接下来的一系列文章,我们再结合源代码详细分析它的实现细节。具体来说,包括以下几个情景分析:

  1. Render进程的启动过程分析;
  2. IPC消息分发机制分析;
  3. GPU进程的启动过程分析;
  4. Plugin进程的启动过程分析;

       这里我们有意略过Browser进程的启动过程分析,这是因为Browser进程实际上就是Android应用程序进程,但是会涉及到一些Chromium相关库的加载过程,而且有些Chromium相关库会由Zygote进程执行预加载处理,等到后面我们分析WebView的启动过程时,再详细分析Browser进程的启动过程。

       同时,我们在分析Render进程的启动过程之后,并没有马上连贯分析GPU进程的启动过程,而是先分析一下前面描述过的IPC消息分发机制的具体实现,这是因为理解了Render进程的启动过程之后,我们就可以以它与Browser进程间的通信过程为情景,更好地理解Chromium的IPC消息分发机制。

GPU硬件加速概述

       当分析完成上述四个情景之后,我们还有一个重要任务,那就是分析Render进程和Plugin进程是如何通过GPU进程进行硬件加速渲染UI的。硬件加速渲染是智能设备获得流畅UI的必要条件。可以说,没有硬件加速渲染的支持,设备UI动画要达到60fps,是非常困难的。Chromium使用硬件加速渲染的方式非常独特,具有以下特点:

  1. Render进程和Plugin进程虽然调用了OpenGL的API,但是这些API仅仅是一个代理接口,也就是这些API调用仅仅是将对应的命令发送给GPU进程处理而已。
  2. 有些OpenGL API,例如glBufferSubData,除了要发送对应的命令给GPU进程之外,还需要发送该命令关联的数据给GPU进程。这些数据往往很大的,这样就涉及到如何正确有效地传输它们给GPU进程。
  3. GPU进程只有一个用来处理Client端发送过来的OpenGL命令的线程,但是该线程却要同时服务多个Client端,也就是要同时为Render进程、Plugin进程以及Browser进程服务,每一个Client端都相当于有一个OpenGL上下文,这将会涉及到如何为每一个OpenGL上下文进行调度的问题。
  4. GPU进程同时服务的多个Client端,它们并不是相互孤立的,它们有时候存在一定的关联性。例如,Render进程渲染好的离屏UI,需要交给Browser进程合成,以便可以最终显示在屏幕中。也就是说,Browser进程需要等待Render进程渲染完成之后,才可以进行合成,否则就会得到不完整的UI。对于GPU进程来说,涉及到的问题就是如何在两个不同的OpengGL上下文中进行同步。

       对于上面提到的这些特点,在完成了Chromium的多进程架构分析之后,我们再通过另外一个系列的文章进行详细分析。

meizi

坚持技术分享,您的支持将鼓励我继续创作!