Chromium多进程机制解析

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 关于Chromium多进程分析的文章很多了,这篇尝试以浅显的方式解释Chromium多进程机制,解析IPC内部运作的基本机制。Chromium如何保证多进程的性能对于一个多进程应用,其核心要解决的是并发的问题.两个面: 线程 和 IPC.一个多进程交互程序和城市的交通管理是非常相似,我做个类比:交通管理的问题解决办法对应多进程应用的情况车多限购。

关于Chromium多进程分析的文章很多了,这篇尝试以浅显的方式解释Chromium多进程机制,解析IPC内部运作的基本机制

Chromium如何保证多进程的性能

对于一个多进程应用,其核心要解决的是并发的问题.两个面: 线程 和 IPC

一个多进程交互程序和城市的交通管理是非常相似,我做个类比:

交通管理的问题 解决办法 对应多进程应用的情况
车多 限购。不要有事没事就买车。 事务多。
路窄 限行。没能拓宽前,只能限制上路的汽车。 进程通讯负载有限。
临时事故
分流。划分不同功能的车道,避免相互间的干扰。 任务等待导致的阻塞
缺少路况信息 主动监听,错峰上路。 IPC不可用时尝试使用会导致等待。


Chromium为解决这些问题(即C10K问题),使用了 非阻塞式,多线程,配合状态监听的解决方案 主要运用了以下关键技术:

  • Unix Domain Socket (POSIX下使用的IPC机制)
  • libevent (轻型事件驱动的网络库,用于监听IPC中的端口(文件描述符))
  • ChannelProxy (为Channel提供线程安全的机制)
  • 闭包 (线程的运作方式)

让我们从头说起!

先修路! 建立通道

写一个单机下多进程应用的核心就是建立进程之间沟通的方式,可以称为channel 或者 pipe。操作系统会提供这样的基础机制,包括:named pipe, shared memory, socket。


Chromium在POSIX下使用Unix Domain Socket来实现。Unix Domain Socket通过复用网络socket的标准接口,提供轻量级的稳定的本机socket通讯。Unix Domain的命名则是来自于使用socket时将domain参数定义为: PF_UNIX (Mac OS)或AF_UNIX, 来标识为单机的通讯使用。socket的建立不以IP地址为目标了,而是由文件系统中的FD(文件描述符)。


socket API原本是为网络通讯设计的,但后来在socket的框架上发展出一种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于IPC更有效率:不需要经过网络协议栈,不需要打包拆包、计算校验和、维护序号和应答等,只是将应用层数据从一个进程拷贝到另一个进程。 UNIX域套接字与TCP套接字相比较,在同一台主机的传输速度前者是后者的两倍。这是因为,IPC机制本质上是可靠的通讯,而网络协议是为 不可靠的通讯设计的。UNIX Domain Socket也提供面向流和面向数据包两种API接口,类似于TCP和UDP,但是面向消息的UNIX Domain Socket也是可靠的,消息既不会丢失也不会顺序错乱。

使用UNIX Domain Socket的过程和网络socket十分相似,也要先调用socket()创建一个socket文件描述符,address family指定为AF_UNIX,type可以选择SOCK_DGRAM或SOCK_STREAM,protocol参数仍然指定为0即可。

UNIX Domain Socket与网络socket编程最明显的不同在于地址格式不同,用结构体sockaddr_un表示,网络编程的socket地址是IP地址加端口 号,而UNIX Domain Socket的地址是一个socket类型的文件在文件系统中的路径。


Chromium在POSIX下直接使用socketpair() API创建了已经连通的匿名管道, 逻辑结构如下:


可以看过IPC机制里区分了Server和Client, 其实通过socketpair()创建的匿名管道是全双工,实质上并不区分Server/Client。

这个IPC建立的过程是在主进程完成的,需要使用其它的机制通知子进程。在Android Chrome下,通过传递FD列表完成这个操作。这个稍后再解释 (可以查找kPrimaryIPCChannel学习)。

当子进程知道Server端的socket FD后,就可以进行连接,发送hello message,  认证后就可以开始通讯了。


通讯端口的管家

当端口打通后,效率成为关键。通常而言,一般这时候会考虑为了及时处理收到消息,要么轮询,要么实现回调。但事情并不是这么简单。

虽说Unix Domain Socket不走网络栈已经提升不了性能。但还有负载的问题需要解决。使用回调机制是必须的,更重要的是面临C10K问题:

C10K Problem 

C10K 问题的最大特点是:设计不够良好的程序,其性能和连接数及机器性能的关 系往往是非线性的。举个例子:如果没有考虑过 C10K 问题,一个经典的基于 select 的程序能在旧服务器上很好处理 1000 并发的吞吐量,它在 2 倍性能新服务器上往往处 理不了并发 2000 的吞吐量。


Chromium使用了大名鼎鼎的第三方并发网络库:libevent来完成这项工作 (另外还有ACE,自适应通信环境)。 下面是它的功能介绍:

Libevent是一个轻量级的开源高性能网络库.有几个显著的亮点:

a. 事件驱动(event-driven),

b. 高性能 轻量级,专注于网络,不如ACE那么臃肿庞大

c. 注册事件优先级

基本的socket编程是阻塞/同步的,每个操作除非已经完成或者出错才会返回,这样对于每一个请求,要使用一个线程或者单独的进程去处理,系统资源没法支撑大量的请求(所谓c10k problem),例如内存:默认情况下每个线程需要占用2~8M的栈空间。posix定义了可以使用异步的select系统调用,但是因为其采用了轮询的方式来判断某个fd是否变成active,效率不高[O(n)],连接数一多,也还是撑不住。于是各系统分别提出了基于异步/callback的系统调用,例如Linux的epoll,BSD的kqueue,Windows的IOCP。由于在内核层面做了支持,所以可以用O(1)的效率查找到active的fd。基本上,libevent就是对这些高效IO的封装,提供统一的API,简化开发。

简而言之,就是:"不要等!只要好了,我就会通知你!",就是典型的好莱坞法则。这相当于在通讯端口设了一位大管家,提升IPC交互的能力。


Libevent的核心是应用了解决并发问题中常用的基于事件驱动的Reactor模式。简单而言就是通过一个内部的循环,在事件触发时启动并进行响应,无事件时则挂起.它同样需要注意事件回调的处理不能做太多事情,避免拥塞.在Chromium里的代码体现IPC Channel实现如下接口:

// Used with WatchFileDescriptor to asynchronously monitor the I/O readiness

// of a file descriptor.

class Watcher {

 public:

  // Called from MessageLoop::Run when an FD can be read from/written to

  // without blocking

  virtual void OnFileCanReadWithoutBlocking(int fd) = 0;

  virtual void OnFileCanWriteWithoutBlocking(int fd) = 0;

 protected:

  virtual ~Watcher() {}

};

关于Reactor模式和另一类相似的模式的比对, 可以读一下这篇文章


专人接待! 排队,排队...


IPC通道已经建立好了,但有一大群的调用者。任何人都想在自己方便的时候进行IPC通讯,这样就存在并发问题了。Channel本身只想做好通道的管理工作,一心对外。 Chromium为此引入了Channel Proxy。 

     

Proxy从设计模式上来看,职能上就是服务上的秘书,安排访客与Channel见面的时间和方式。同线程的可以入队列等候,不同线程的,不好意思,出门右拐,再进来。总之Channel只在一个线程上做事,Channel Proxy负责将要处理的事务安排到指定的线程上。 

bool ChannelProxy::Send(Message* message) {

  context_->ipc_task_runner()->PostTask(

      FROM_HERE,

      base::Bind(&ChannelProxy::Context::OnSendMessage,

                 context_, base::Passed(scoped_ptr<Message>(message))));

  return true;

}


在Chromium中的线程只负责建立跑道,执行事务,它不会保存特定任务的上下文信息(有助于提高性能),也不关心执行的是什么事务。执行什么操作完全由上层的业务逻辑决定。在主干道上,任务的处理时间是非常宝贵的,特定是在UI线程上。进程里会指定Channel处理任务所在的线程,多线程情况以下线程任务的方式请求Channel发送及处理消息。在线程上其核心就是闭包的实现.

 

让线程更高效 - 裸奔的线程

提升应用程序的并发能力,两个要点:

  • 任务响应短,快.不要有耗时的事务。
  • 尽量少使用锁。
  • 避免频繁的上下文切换。


前面提到的libevent中应用的Reactor模式也是要达到相同的目的. 第一点是应用层的一个约定,第二层就是线程机制要保障的.


传统的线程在创建时就会指定一个入口,往往已经是一个具体功能的入口了,里面会一个循环,对所要监测的事件,以及对应的处理,这个循环体本身是清楚,甚至可以做一些逻辑判断工作.这些就是因为它掌握了当前任务的上下文信息. 


Chromium上的线程可以避免这类的上下文切换, 线程本身不保存任务的信息,任务对其是透明的,线程只负责调用其执行操作,可以视为裸奔的状态,没有任何负担.跑在线程上的任务以典型的Command模式实现。它必须解决两个问题:

  • 任务本身在一个线程运行,就可以由其自己保存上下文信息.
  • 任务的参数(也是一种上下文),则可以通过闭包的方式也由其自己保存.


闭包在C++有很多的尝试,Chromium中特别说明其也参考了tr1::bind的设计.关于Chromium的线程实现这里先不多做说明。

 

Chromium进程架构

从逻辑上来看,Chromium将UI所在的进程视为主进程,取名为Browser, 页面渲染所在的进程以及其它业务进程,都是子进程。包括Renderer, GPU等。

主进程本身除了初始化自身,还要负责创建子进程,同时通知建立channel的信息(文件描述符)。概念模型如下:

    


对于Chromium而言,Contents已经代表一个浏览器能力,在其上就是实现浏览器业务的Embedder了。但不同的Embedder或者在不同的平台进程的选择可能不同,比如启动一个浏览主进程的行为不同(比如在Android上就不需要启动新进程了,直接初化BrowserMainRunner。而Linux则可以要以Service Process的形式运行主进程。),初始化SandBox的方式不同。Chromium与是将所有进程的启动的操作集中起来供Embedder和主进程来启动新进程 (Embedder负责启动Browser进程, 主进程则再启动新进程,并在ContentMainRunner中根据参数启动不同的子进程。):

     

*在单进程模型下,主进程就不会直接启动子进程了,而是通过CreateInProcessRendererThread()创建新的线程(InProcessRendererThread),同样会传入channel描述符。


进一步看主进程和Renderer进程,两边负责接口的,则是两个兼有(继承)IPC::Sender和IPC::Listener功能的类:RenderProcessHost和RenderThread。没错,就是RenderThread! RenderProcess也存在,只是一个进程的逻辑表示,只有一小部分的代码。以单进程模式下调用InProcessRendererThread来初始化RenderThread为例,可以看到channel_id_是传入到RenderThread中处理的。

void InProcessRendererThread::Init() {

  render_process_.reset(new RenderProcessImpl());

  new RenderThreadImpl(channel_id_);

}

专线与公共线路 - 消息的分发

分发消息时,分为广播和专线两种方式。在Chromium中一个页面在不同线程,Browser和Renderer两端以routed id为标识彼此。如果要说悄悄话,就指定一个routed id, 这类消息称为Routed Message,是专线。 另一类消息,则是进行广播,不区分身份,这类消息为Control Message。

    


RenderView (继承自RenderWidget)用于向IPC Channel注册自己的代码在RenderWidget::DoInit():

bool result = RenderThread::Get()->Send(create_widget_message);

if (result) {

  RenderThread::Get()->AddRoute(routing_id_, this);

  ......

}

另外在Browser和Render Thread初始化时,都会在IPC Channel上增加一组Filters,以便供其它功能使用。

 

参考资料


目录
相关文章
|
16天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2
|
2月前
|
存储 缓存 算法
分布式锁服务深度解析:以Apache Flink的Checkpointing机制为例
【10月更文挑战第7天】在分布式系统中,多个进程或节点可能需要同时访问和操作共享资源。为了确保数据的一致性和系统的稳定性,我们需要一种机制来协调这些进程或节点的访问,避免并发冲突和竞态条件。分布式锁服务正是为此而生的一种解决方案。它通过在网络环境中实现锁机制,确保同一时间只有一个进程或节点能够访问和操作共享资源。
80 3
|
19天前
|
存储 消息中间件 算法
深入探索操作系统的心脏——内核机制解析
本文旨在揭示操作系统核心——内核的工作原理,通过剖析其关键组件与机制,为读者提供一个清晰的内核结构图景。不同于常规摘要的概述性内容,本文摘要将直接聚焦于内核的核心概念、主要功能以及其在系统管理中扮演的角色,旨在激发读者对操作系统深层次运作原理的兴趣与理解。
|
26天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
66 4
|
27天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
28天前
|
消息中间件 存储 Linux
|
1月前
|
存储 缓存 安全
🌟Java零基础:深入解析Java序列化机制
【10月更文挑战第20天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
25 3
|
2月前
|
Java 开发者 UED
Java编程中的异常处理机制解析
在Java的世界里,异常处理是确保程序稳定性和可靠性的关键。本文将深入探讨Java的异常处理机制,包括异常的类型、如何捕获和处理异常以及自定义异常的创建和使用。通过理解这些概念,开发者可以编写更加健壮和易于维护的代码。
中断处理机制解析
【10月更文挑战第5天】中断处理需定义中断处理函数`irq_handler_t`,参数包括中断信号`irq`和通用指针`dev_id`。返回值`IRQ_NONE`表示非本设备中断,`IRQ_HANDLED`表示已处理,`IRQ_WAKE_THREAD`表示需唤醒等待进程。处理程序常分上下半部,关键部分在中断处理函数中完成,延迟部分通过工作队列处理。注册中断处理函数需调用`request_irq`,参数包括中断信号、处理函数、标志位、设备名和通用指针。
|
2月前
|
JavaScript 前端开发 开发者
原型链深入解析:JavaScript中的核心机制
【10月更文挑战第13天】原型链深入解析:JavaScript中的核心机制
33 0

推荐镜像

更多