浏览器进程架构深度解析
一句话概括
Chrome 的多进程架构通过将 Browser、GPU、Renderer、Plugin 等进程隔离运行,利用 IPC(Inter-Process Communication)进行通信,并借助 Site Isolation 实现跨站点安全隔离,是现代浏览器在稳定性、安全性、性能三者之间反复权衡后的最优工程解。
1. 背景与意义
1.1 单进程浏览器的困境
在 Chrome 诞生之前(2008 年以前),主流浏览器如 Internet Explorer 6、Firefox 2 等均采用单进程架构。这意味着:
- 一个标签页崩溃 → 整个浏览器崩溃。回想 2000 年代中后期,你正在填写一个超长的表单,”哐”一声整个窗口无响应,所有辛苦付诸东流。
- 一个插件卡死 → 整个浏览器卡死。Flash 插件(当时几乎无处不在)一旦异常,所有标签页跟着遭殃。
- 内存泄漏无法隔离。一个页面的 JS 泄漏会拖慢所有页面的响应速度。
1.2 演进驱动力
多进程架构的诞生并非凭空而来,而是由三个核心驱动力共同催生:
- 摩尔定律的尾声与多核 CPU 的普及:2005 年后桌面 CPU 逐步走向多核,单进程架构无法充分利用多核优势。
- Web 应用复杂度的指数级增长:Gmail 在 2004 年发布,开启了”Web 应用”时代。页面从纯文档演变为复杂的交互系统。
- 安全需求的根本性转变:跨站脚本、点击劫持等攻击面急剧扩大,进程级别隔离成为唯一可靠的防线。
1.3 现代浏览器的架构选择
2012 年,Chrome 率先将多进程架构推向成熟,后续 Firefox(Electrolysis 项目,2016)、Safari(2018)、Edge(转向 Chromium 内核,2020)全面跟进。多进程架构已成为浏览器的事实标准。
2. 概念与定义
2.1 进程与线程的区别
在浏览器语境下,我们需要严格区分两个概念:
| 维度 | 进程(Process) | 线程(Thread) |
|---|---|---|
| 资源拥有 | 独立的内存空间、文件句柄 | 共享进程内存空间 |
| 隔离性 | 完全隔离(一个进程崩溃不影响其他) | 不隔离(一个线程崩溃整个进程崩溃) |
| 通信方式 | IPC(成本高,需序列化) | 共享内存(成本低) |
| 启动开销 | 大 | 小 |
2.2 Chrome 四进程模型
Chrome 将浏览器划分为四个主要进程类型:
1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────────────────────────────┐
│ Chrome Browser │
├───────────────────┬───────────────────┬─────────────────────┤
│ Browser进程 │ GPU进程 │ Utility进程 │
│ (主控) │ (合成加速) │ (网络/设备等) │
├───────────────────┴───────────────────┴─────────────────────┤
│ Renderer进程 │ Renderer进程 │ Renderer进程 │
│ (标签页1) │ (标签页2) │ (标签页3) │
├───────────────────┬───────────────────┬─────────────────────┤
│ 插件进程 │ 扩展进程 │ ... │
└───────────────────┴───────────────────┴─────────────────────┘
Browser 进程(主进程)
- 只有一个全局实例
- 负责:地址栏、书签栏、前进/后退按钮、网络请求管理、文件访问
- 是浏览器”骨架”,所有标签页共享
Renderer 进程(渲染进程)
- 每个标签页通常有一个(或更多)
- 负责:HTML/CSS 解析、JavaScript 执行、页面渲染
- 运行在沙箱(Sandbox)中,权限受限
- 内部有多个线程:主线程、工作线程、Compositor 线程、Raster 线程
GPU 进程
- 独立进程,处理 GPU 相关任务
- 负责:合成加速(Compositing)、3D CSS 加速、WebGL
- 早期融合在 Browser 进程中,后因 GPU 驱动程序频繁崩溃而独立
插件进程 / 扩展进程
- 每个插件(如 Flash 时代)独立进程
- 扩展进程用于管理与渲染
- 2020 年后 Chrome 逐步取消对 NPAPI 插件的支持,此进程类型正在退场
2.3 IPC(Inter-Process Communication)
Chrome 使用 Chromium IPC 框架,基于命名管道(Windows)或 Unix Domain Socket(macOS/Linux):
1
Renderer 进程 ──→ [序列化] ──→ IPC Channel ──→ [反序列化] ──→ Browser 进程
IPC 消息结构:
1
2
3
4
5
6
7
// Chromium 中 IPC 消息的典型结构(伪代码)
struct IPC::Message {
int32 routing_id; // 路由目标
uint32 type; // 消息类型标识
base::Pickle payload; // 序列化后的负载
// 传输层自动处理
};
3. 最小示例
以下是用 Node.js 模拟浏览器多进程架构的核心机制:
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// multi-process-browser-simulation.js
const { fork } = require('child_process');
const http = require('http');
// ─── Browser 进程 ───
// 模拟浏览器主进程,管理渲染进程的创建与 IPC 通信
class BrowserProcess {
constructor() {
this.rendererProcesses = new Map(); // tabId -> child_process
this.tabCounter = 0;
this.setupIPCServer();
}
setupIPCServer() {
// 创建 HTTP 服务器模拟 IPC 通道
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
}
createNewTab(url) {
const tabId = ++this.tabCounter;
console.log(`[Browser] 创建新标签页 #${tabId},URL: ${url}`);
// fork 一个新进程模拟 Renderer 进程
const renderer = fork(__filename, ['--renderer', String(tabId)]);
renderer.on('message', (msg) => {
console.log(`[Browser] 收到来自标签页 #${tabId} 的消息:`, msg);
// 处理 IPC 消息,例如页面标题更新、导航请求等
});
renderer.on('exit', (code) => {
console.log(`[Browser] 标签页 #${tabId} 已退出,code: ${code}`);
this.rendererProcesses.delete(tabId);
});
this.rendererProcesses.set(tabId, renderer);
// 发送导航请求到渲染进程
renderer.send({ type: 'NAVIGATE', url, tabId });
return tabId;
}
crashTab(tabId) {
const renderer = this.rendererProcesses.get(tabId);
if (renderer) {
console.log(`[Browser] 模拟标签页 #${tabId} 崩溃`);
renderer.kill('SIGKILL');
}
}
simulateStable() {
// 启动 3 个标签页
this.createNewTab('https://example.com');
this.createNewTab('https://google.com');
this.createNewTab('https://github.com');
// 模拟标签页1崩溃 — 其他标签页不受影响
setTimeout(() => {
console.log('\n=== 模拟崩溃隔离测试 ===');
this.crashTab(1);
console.log(`[Browser] 存活标签页: ${this.rendererProcesses.size}`);
}, 1000);
}
handleRequest(req, res) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'running',
tabs: this.rendererProcesses.size,
processIds: Array.from(this.rendererProcesses.keys())
}));
}
start(port = 3000) {
this.server.listen(port, () => {
console.log(`[Browser] 主进程已启动,端口: ${port}`);
this.simulateStable();
});
}
}
// ─── Renderer 进程 ───
// 模拟渲染进程,在沙箱中运行
class RendererProcess {
constructor(tabId) {
this.tabId = tabId;
this.currentUrl = null;
this.setupIPCMessageHandler();
}
setupIPCMessageHandler() {
process.on('message', (msg) => {
switch (msg.type) {
case 'NAVIGATE':
this.navigate(msg.url);
break;
case 'EVALUATE_SCRIPT':
this.executeScript(msg.script);
break;
default:
console.log(`[Renderer #${this.tabId}] 未知消息类型:`, msg.type);
}
});
}
async navigate(url) {
console.log(`[Renderer #${this.tabId}] 开始导航到: ${url}`);
this.currentUrl = url;
// 模拟页面加载过程
await this.simulatePageLoad();
// 解析 HTML 模拟
const pageTitle = this.extractMockTitle(url);
// 通过 IPC 通知主进程
process.send({
type: 'PAGE_LOADED',
tabId: this.tabId,
title: pageTitle,
url: this.currentUrl
});
}
async simulatePageLoad() {
// 模拟 HTML 解析 + JS 执行 + 渲染
return new Promise((resolve) => {
setTimeout(() => {
console.log(`[Renderer #${this.tabId}] 页面渲染完成`);
// 模拟在沙箱中执行 JS
this.executeTrustedCode();
resolve();
}, 500);
});
}
executeTrustedCode() {
// 模拟 V8 执行
console.log(`[Renderer #${this.tabId}] JS 引擎执行完毕`);
}
executeScript(code) {
try {
// 在沙箱中执行脚本
const result = eval(`(function() { ${code} })()`);
process.send({
type: 'SCRIPT_RESULT',
tabId: this.tabId,
result: String(result)
});
} catch (err) {
process.send({
type: 'SCRIPT_ERROR',
tabId: this.tabId,
error: err.message
});
}
}
extractMockTitle(url) {
const titles = {
'https://example.com': 'Example Domain',
'https://google.com': 'Google',
'https://github.com': 'GitHub'
};
return titles[url] || 'Unknown Page';
}
}
// ─── 入口 ───
const argv = process.argv.slice(2);
if (argv[0] === '--renderer') {
// 作为 Renderer 进程启动
const tabId = parseInt(argv[1], 10);
new RendererProcess(tabId);
} else {
// 作为 Browser 进程启动
const browser = new BrowserProcess();
browser.start(3000);
}
运行方式:
1
node multi-process-browser-simulation.js
输出示例:
1
2
3
4
5
6
7
8
9
10
11
12
[Browser] 主进程已启动,端口: 3000
[Browser] 创建新标签页 #1,URL: https://example.com
[Browser] 创建新标签页 #2,URL: https://google.com
[Browser] 创建新标签页 #3,URL: https://github.com
[Renderer #1] 开始导航到: https://example.com
[Renderer #2] 开始导航到: https://google.com
[Renderer #3] 开始导航到: https://github.com
...
=== 模拟崩溃隔离测试 ===
[Browser] 模拟标签页 #1 崩溃
[Browser] 存活标签页: 2
4. 核心知识点拆解
4.1 Chrome 进程模型的演进(四个阶段)
阶段一:Process-per-site-instance(站点实例级)
- 每个导航到一个新站点时分配一个新进程
- 缺陷:
iframe嵌套不同站点时,所有 iframe 仍在同一进程,一个 iframe 崩溃可能拖垮整页
阶段二:Process-per-tab(标签页级)
- 每个标签页独立进程
- 优势:一个标签页崩溃不会影响其他
- 缺陷:同一标签页中跨站 iframe 仍然不隔离
阶段三:Process-per-site(站点级)
- 同一站点的标签页合并到同一进程(优化资源消耗)
- 优势:减少进程数,降低内存开销
- 缺陷:一个站点的崩溃影响该站点的所有标签页
阶段四:Site Isolation(站点隔离,Chrome 67+)
- 同一页面中,不同站点的 iframe 被分配到不同进程
- 这是目前最细粒度的隔离策略
- 也是防御 Spectre/Meltdown 类 CPU 漏洞的关键措施
4.2 各进程内部线程架构
Browser 进程内部线程
1
2
3
4
5
6
┌──────────────────────────────────────────────────────────────┐
│ Browser Process │
├──────────────────────────────────────────────────────────────┤
│ UI Thread │ IO Thread │ File Thread │ Database Thread │
│ (界面响应) │ (网络IO) │ (文件操作) │ (IndexedDB等) │
└──────────────────────────────────────────────────────────────┘
- UI Thread:处理用户交互(点击、输入)、绘制浏览器 UI(地址栏、书签栏)
- IO Thread:处理所有网络请求,解析 HTTP 响应头
- File Thread:文件读写操作(下载保存等)
- Database Thread:IndexedDB、WebSQL 等持久化存储
Renderer 进程内部线程
1
2
3
4
5
6
7
8
9
┌──────────────────────────────────────────────────────────────┐
│ Renderer Process │
├──────────────────────────────────────────────────────────────┤
│ Main Thread │ Worker Threads │ Compositor │ Raster │
│ (主线程) │ (Web Workers) │ (合成线程) │ (光栅化) │
├──────────────────────────────────────────────────────────────┤
│ JS Execution │ HTML Parser │ Layer Tree │ Bitmap │
│ DOM Update │ CSS Parser │ Tile Management │ Render │
└──────────────────────────────────────────────────────────────┘
Main Thread 是 Renderer 进程的灵魂:
- 负责 HTML 解析 → DOM 树构建
- CSS 解析 → Style 树 → Layout 树 → Paint 树
- JavaScript 执行(单线程事件循环)
- 所有 DOM 操作都必须在这里发生
Compositor Thread 是性能的关键:
- 独立于 Main Thread,即使主线程繁忙也不阻塞
- 负责将页面分层(Layer Tree),并管理 Tile
- 处理滚动、动画(transform、opacity 等 GPU 加速属性)
- 屏幕刷新率(60fps/120fps)的守护者
Worker Threads:
- Web Workers / Service Workers
- 真正的后台线程,无法访问 DOM
- 用于纯计算或离线缓存
4.3 IPC 通信机制详解
Chrome 的 IPC 基于 Mojo(Chromium 的跨语言绑定框架),取代了早期的 Chromium IPC:
1
2
3
Sender (C++/JS) ──→ Mojo Message ──→ Serialization ──→ Pipe
│
Receiver (C++/JS) ←── Mojo Message ←── Deserialization ←──┘
IPC 通信流程(以导航过程为例):
- 用户在地址栏输入 URL,点击回车
- Browser 进程的 UI Thread 收到输入,解析为 URL
- UI Thread 将导航请求通过 Mojo IPC 发送到 Browser 进程的 IO Thread
- IO Thread 发起 DNS 查询 + HTTP 请求
- 收到响应头(Content-Type: text/html)后,IO Thread 通过 IPC 通知 Renderer 进程
- Renderer 进程开始下载 HTML 响应体并解析渲染
- 渲染完成后,Renderer 进程通过 IPC 回传页面标题、URL 等元数据
4.4 内存占用与性能权衡
多进程架构的代价:
1
2
3
4
5
┌── 稳定性 ←── 多进程隔离
浏览器架构三角 ─────┤
├── 安全性 ←── 沙箱 + 站点隔离
│
└── 性能 ←── 内存开销 + 进程通信
| 架构 | 打开 10 个标签页内存开销 | 一个标签页崩溃 |
|---|---|---|
| 单进程 | ~300MB | 全部崩溃 |
| 多进程(10个Renderer) | ~1.2GB(每个约 40-80MB + 共享 ~400MB) | 仅崩溃一个 |
Chrome 的优化策略:
- 进程合并:同一站点标签页共享进程
- 内存压缩:后台标签页的 Renderer 进程会被压缩(Chrome 的
--renderer-process-limit) - 冻结:不可见标签页可能被冻结(Chrome 89+ 的
freeze策略) - Proactive Tab Discard:内存不足时自动丢弃最久未使用的标签页
5. 实战案例
案例一:基于 Chrome DevTools Protocol 监控进程状态
编写一个工具来观测浏览器多进程的实际行为:
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// process-monitor.js
const CDP = require('chrome-remote-interface');
const http = require('http');
const { execSync } = require('child_process');
class BrowserProcessMonitor {
constructor() {
this.initialProcesses = new Set();
this.currentPIDs = new Map(); // type -> [pid]
}
async start() {
// 启动 Chrome 并暴露 DevTools
console.log('启动 Chrome with remote debugging...');
const chromeCommand = [
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'--remote-debugging-port=9222',
'--user-data-dir=/tmp/chrome-profile-monitor',
'--no-first-run',
'--new-window',
'about:blank'
].join(' ');
// 连接到 DevTools
const client = await CDP({ port: 9222 });
const { Target, SystemInfo, Browser } = client;
// 获取进程信息(Chrome 88+ 支持)
const { processInfo } = await SystemInfo.getProcessInfo();
console.log('\n=== 当前 Chrome 进程信息 ===');
processInfo.forEach(p => {
console.log(`[${p.type}] PID: ${p.id}, CPU: ${p.cpuTime.toFixed(2)}s`);
});
// 监听新标签页创建
Target.setDiscoverTargets({ discover: true });
Target.on('targetCreated', async ({ targetInfo }) => {
if (targetInfo.type === 'page') {
console.log(`\n新页面创建: "${targetInfo.title}" (${targetInfo.url})`);
// 打开更多页面模拟多进程
await this.openNewTab(client, 'https://developer.mozilla.org');
await this.openNewTab(client, 'https://github.com');
// 再次获取进程信息
setTimeout(async () => {
const { processInfo: updatedInfo } = await SystemInfo.getProcessInfo();
console.log('\n=== 更新后 Chrome 进程信息 ===');
updatedInfo.forEach(p => {
console.log(`[${p.type}] PID: ${p.id}, CPU: ${p.cpuTime.toFixed(2)}s`);
});
// 统计
const types = {};
updatedInfo.forEach(p => {
types[p.type] = (types[p.type] || 0) + 1;
});
console.log('\n进程数量统计:', types);
client.close();
process.exit(0);
}, 2000);
}
});
await Target.setAutoAttach({ autoAttach: true, waitForDebuggerOnStart: false });
}
async openNewTab(client, url) {
const { Target } = client;
const { targetId } = await Target.createTarget({ url });
return targetId;
}
}
// 运行
const monitor = new BrowserProcessMonitor();
monitor.start().catch(console.error);
案例二:模拟站点隔离(Site Isolation)的效果
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// site-isolation-demo.js
// 演示同一个页面中跨站 iframe 在 Site Isolation 下的进程隔离
document.addEventListener('DOMContentLoaded', () => {
const demoContainer = document.getElementById('demo');
const infoPanel = document.getElementById('info');
// 创建三个 iframe,分别加载不同站点
const sites = [
{ name: '同一站点', url: '/same-origin-page.html' },
{ name: '跨站点 A', url: 'https://www.wikipedia.org' },
{ name: '跨站点 B', url: 'https://www.bing.com' }
];
sites.forEach((site, index) => {
const iframe = document.createElement('iframe');
iframe.src = site.url;
iframe.style.width = '300px';
iframe.style.height = '200px';
iframe.style.border = '1px solid #ccc';
iframe.dataset.siteName = site.name;
iframe.id = `iframe-${index}`;
demoContainer.appendChild(iframe);
});
// Site Isolation 检测辅助函数
async function checkSiteIsolation() {
const results = [];
// 方法1:检查 window.origin 隔离
try {
// 跨域 iframe 无法访问 contentWindow,否则会抛出 SecurityError
const iframe1 = document.getElementById('iframe-0');
const iframe2 = document.getElementById('iframe-1');
// 同源访问应成功
iframe1.contentWindow.document;
results.push('同源 iframe 可访问 ✅');
// 跨域访问应失败
try {
iframe2.contentWindow.location.href;
results.push('跨域 iframe 访问成功(未隔离 ⚠️)');
} catch (e) {
if (e.name === 'SecurityError') {
results.push('跨域 iframe 访问被阻止 ✅ - 浏览器跨域安全策略生效');
}
}
} catch (e) {
results.push(`检测失败: ${e.message}`);
}
// 方法2:检查 Performance API 中的进程信息
// 注:performance.memory 是非标准的,仅在 Chrome 中有
if (performance.memory) {
results.push(`JS 堆内存: ${Math.round(performance.memory.usedJSHeapSize / 1024 / 1024)}MB`);
}
// 方法3:通过 navigator.hardwareConcurrency 判断资源隔离
results.push(`逻辑 CPU 核数: ${navigator.hardwareConcurrency}`);
return results;
}
// 异步检查
setTimeout(async () => {
const isolationInfo = await checkSiteIsolation();
infoPanel.innerHTML = `
<h4>Site Isolation 检测结果</h4>
<ul>
${isolationInfo.map(r => `<li>${r}</li>`).join('')}
</ul>
<p class="note">
⚡ 注意:Chrome 67+ 启用 Site Isolation 后,不同站点 iframe
将在不同 Renderer 进程中渲染。可以通过 chrome://process-internals
查看实际进程分配。
</p>
`;
}, 2000);
});
6. 底层原理
6.1 Chromium 源码中的进程管理
在 Chromium 源码(src/content/browser/)中,进程管理由 BrowserProcessSubThread 和 RenderProcessHost 等类负责:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 源码简化版:src/content/browser/renderer_host/render_process_host_impl.cc
// 创建新的 Renderer 进程
int RenderProcessHostImpl::GetNextRoutingID() {
return next_routing_id_++; // 为每个 RenderView 分配唯一路由 ID
}
bool RenderProcessHostImpl::Init() {
// 1. 创建进程
child_process_launcher_ = std::make_unique<ChildProcessLauncher>(
/* ... */
peer, // ChildProcessLauncherHelper
std::move(file_data),
command_line,
/* ... */
);
// 2. 建立 IPC 通道
channel_->Init(this /* receiver */);
// 3. 等待进程启动确认
return true;
}
6.2 Site Isolation 的实现机制
Site Isolation 是 Chrome 67+ 的核心安全特性。其实现依赖于以下关键技术:
1. Document 级别的进程分配
在源码中,SiteInstance 是进程分配的原子单位:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/content/browser/site_instance_impl.cc
scoped_refptr<SiteInstance> SiteInstanceImpl::CreateForURL(
BrowserContext* browser_context,
const GURL& url) {
// 根据 URL 计算 Site URL
// 同一 Site 的页面共享同一 Renderer 进程
GURL site_url = DetermineSiteURL(browser_context, url);
// 查找或创建对应的 RenderProcessHost
RenderProcessHost* process =
RenderProcessHost::GetOrCreateProcessForSite(browser_context, site_url);
return new SiteInstanceImpl(browser_context, site_url, process);
}
2. 跨文档消息的进程间转发
当不同进程中的 iframe 需要通信时(如 postMessage),Chromium 使用 CrossProcessFrameConnector:
1
2
3
4
5
6
7
8
9
10
11
12
// src/content/browser/renderer_host/frame_tree_node.cc
void FrameTreeNode::SwapFrameRenderer(
RenderProcessHost* new_process) {
// 通知旧的 Renderer 进程卸载
old_render_frame_host_->Unload();
// 在新的 Renderer 进程中创建 Frame
new_render_frame_host_->CreateFrame();
// 建立跨进程连接
cross_process_frame_connector_->Initialize(new_render_frame_host_);
}
3. 对 Spectre 攻击的防御
2018 年 1 月公开的 Spectre 漏洞利用推测执行 + 缓存侧信道读取跨进程内存。Site Isolation 被证明是最有效的防御方案之一:
- 即使攻击者在 Renderer A 中利用 Spectre,最坏情况也只能读取 Renderer A 的内存
- 敏感数据(密码管理、Cookie、OAuth tokens)存储在 Browser 进程中,Renderer 进程根本无权访问
- Chrome 67 默认启用
--site-per-process标志
6.3 沙箱(Sandbox)技术
每个 Renderer 进程运行在一个受限的沙箱环境中:
| 沙箱维度 | macOS | Linux | Windows |
|---|---|---|---|
| 系统调用过滤 | Seatbelt sandbox | seccomp-bpf | Job object + Win32k lockdown |
| 文件系统访问 | ~/Downloads 只读 | 无 | AppContainer |
| 网络访问 | 通过 Browser 进程代理 | 通过 Browser 进程代理 | 通过 Browser 进程代理 |
| 进程创建 | 禁止 | 禁止 | 禁止 |
1
2
3
4
5
6
7
8
9
10
// macOS Sandbox 配置示例 (chromium/content/browser/sandbox_mac.mm)
// 使用 Seatbelt sandbox 限制 Renderer 进程的能力
const char* kSandboxPolicy = R"(
(version 1)
(deny default)
(allow file-read* (literal "/path/to/localized/resources"))
(allow sysctl-read (sysctl-name "hw.*") (sysctl-name "machdep.cpu.*"))
(deny file-write*)
(deny network-outbound*)
)";
6.4 进程生命周期管理
1
2
3
4
5
── 进程太多?──→ 同一站点进程合并
/
用户打开标签页 ──→ 分配 Renderer 进程 ──→ 空闲 5 分钟?──→ 进程压缩
│
└── 内存不足?──→ Discard 最久未用标签页
Chrome 内部使用 ProcessManager 类智能管理进程池,当进程数超过阈值(通常为 80 个)时,启动回收机制。
7. 高频面试题解析
面试题 1:Chrome 打开一个标签页时,背后至少启动了几个进程?
答案:至少启动 2 个(但实际上通常是 3-4 个)。
具体分析:
- Browser 进程:始终存在(用于管理窗口、网络请求等)
- Renderer 进程:负责渲染新标签页
- GPU 进程:Chrome 在启动时会创建一个 GPU 进程用于合成加速
- Utility 进程:网络服务进程(Chrome 88+ 默认启用网络服务化)
如果在 chrome://process-internals 中查看,通常会看到:
1
2
3
4
Browser: PID 1234
GPU: PID 1235
Utility: PID 1236 (Network Service)
Renderer (about:blank): PID 1237
如果安装了扩展,还会有:
Extension进程(如密码管理器扩展)
所以严格来说,新开一个空白标签页,最低启动 2 个进程(Browser + Renderer),正常情况 3-4 个。
面试题 2:什么是 Site Isolation?它能防御什么样的攻击?
答案:
Site Isolation(站点隔离)是 Chrome 67+ 引入的进程级安全机制。核心思想是:将不同站点的内容分配到不同的 Renderer 进程中。
它能做到的:
防御 Spectre 类型攻击:利用推测执行漏洞读取进程内存。在 Site Isolation 前,攻击页面通过 iframe 嵌入 bank.com,可以通过 Spectre 读取 bank.com 进程内的密码、token。Site Isolation 后,bank.com 的 iframe 在独立进程中,攻击页面所在进程根本不含敏感数据。
防御 Processor 侧信道攻击:如 Meltdown,原理同上。
限定进程内数据范围:即使 Renderer 进程被完全攻破,攻击者也只获得该站点数据,无法访问其他站点的 Cookie、LocalStorage、IndexedDB。
它不能做到的:
- 不能防御应用层的 XSS 攻击(那是 CSP 的职责)
- 不能防御网络层攻击
性能代价:
- 内存开销增加约 10-13%(Chrome 官方数据)
- 跨进程通信增加延迟
- 可通过
--disable-features=IsolateOrigins或--site-per-process控制
面试题 3:描述一下用户在地址栏输入 URL 并按回车后,Chrome 内部各进程的完整协作流程。
答案:这是一个经典的浏览器原理面试题,我们从进程协作的角度深入剖析:
阶段一:Browser 进程(UI Thread)处理用户输入
- 用户在地址栏输入 URL → Browser 进程的 UI Thread 捕获输入
- UI Thread 判断输入是搜索关键词还是 URL(Omnibox 算法检查是否符合 URL 格式)
- 假设是完整 URL(如
https://www.example.com),UI Thread 通过 Mojo IPC 将导航请求发送给 Browser 进程的 IO Thread
阶段二:Browser 进程(IO Thread)发起网络请求
- IO Thread 检查是否存在 Service Worker 注册
- 如果有 → Service Worker 可能拦截请求
- 如果没有 → 发起标准 HTTP 请求
- IO Thread 查找 DNS 缓存,如果没有则发起 DNS 解析
- 建立 TLS 连接(如果是 HTTPS)
- 发送 HTTP 请求
阶段三:处理响应
- IO Thread 收到响应头,检查 Content-Type
- 如果是 HTML → 寻找或创建 Renderer 进程
- 检查是否有可复用的 Same-site Renderer 进程
- 没有则创建新 Renderer 进程
- 通过 IPC 将响应数据流发送到 Renderer 进程
阶段四:Renderer 进程(Main Thread)渲染页面
- HTML 解析:Main Thread 将 HTML 解析为 DOM 树
- 遇到
<script>标签 → 暂停解析 → 下载并执行 JS(但 async/defer 和 preload scanner 有优化) - 遇到
<link>或<img>→ 通过 preload scanner 发起并行下载
- 遇到
- CSS 解析:构建 CSSOM 树
- Layout:根据 DOM + CSSOM 计算布局
- Paint:生成 Paint Records(绘制指令列表)
- Compositing:Main Thread 将绘制指令提交给 Compositor Thread
阶段五:Compositor Thread 合成
- Compositor Thread 将页面分层(Layer Tree)
- 每层被切分为 Tiles(瓦片)
- Raster Thread 将 Tiles 转换为 GPU 纹理(位图)
- 组合所有层发送到 GPU 进程进行最终渲染
阶段六:显示
- GPU 进程将最终帧显示在屏幕上
整个过程的核心在于:
- UI Thread 和 IO Thread 在 Browser 进程中协调,不阻塞 UI
- Renderer 进程在自己的沙箱中独立完成渲染
- Compositor Thread 是保证 60fps 滚动的关键
- 跨进程通信全程安全的——Mojo IPC 带类型安全校验
8. 总结与扩展
核心要点回顾
- Chrome 的多进程架构是稳定性、安全性、性能三者权衡的最优解
- 四个核心进程:Browser、Renderer、GPU、Plugin
- IPC 通信通过 Mojo 框架实现跨进程消息传递
- Site Isolation 将不同站点的内容隔离到不同 Renderer 进程中
- 每个 Renderer 进程内部有 Main/Compositor/Raster 等多个线程,各司其职
- 进程管理策略不断演进:从 per-site-instance 到 per-tab 再到 per-frame
值得继续深挖的方向
- Chrome 的 Memory Saver 模式:如何管理后台标签页内存
- WebAssembly + 多进程:Wasm 编译如何利用跨进程优化
- Isolated Web Apps:Chrome 正在试验的 IWA 架构
- Performance API 的 Process-level metrics:如何通过
performance.eventCounts监听跨进程事件 - Servo 浏览器的实验性架构:Mozilla 的 Servo 采用完全不同的组件化并行渲染引擎
思考题
- 如果一个标签页中有 10 个跨域 iframe,在启用了 Site Isolation 的情况下,Chrome 会启动多少个 Renderer 进程?
postMessage在跨进程 iframe 之间如何通信?性能代价是什么?- 为什么 Chrome 团队决定将网络请求从 Browser 进程的 IO Thread 迁移到独立的 Network Service(Utility)进程?
参考资源: