返回博客
·8 分钟阅读·productdevbook

为什么辅助进程让 macOS 带宽监控变得混乱

辅助进程把一个面向用户的应用拆成十几条网络记录。本文解释 macOS 上为何如此,以及如何看穿这种现象。

  • macOS
  • Bandwidth
  • Network monitoring
  • Deep dive

你在安静的下午打开活动监视器,按网络字节数排序,列表头部看起来是这样:Google Chrome Helper (Renderer)Google Chrome Helper (GPU)Google Chrome HelperSlack HelperSlack Helper (Renderer)Slack Helper (GPU)。没有 "Chrome" 或 "Slack" 那一行——只是一团辅助五彩纸屑,每个每秒拉几百 KB。哪个应用实际在通信?这种困惑就是 Mac 辅助进程带宽归因为什么是 macOS 上构建网络监控最难的部分。

这篇解释为什么应用首先要派生辅助进程、为什么内核分别报告它们的网络用量、以及当一个工具试图给你一个干净的按应用数字时"归并"是什么意思。

为什么应用派生辅助进程

现代 macOS 应用几乎从不作为单一进程跑。三大原因。

沙盒和权限分隔

Apple 的 App Sandbox 是按进程的边界。一个解析不可信 HTML 或执行 JavaScript 的渲染器以与拥有你麦克风权限的父进程不同的 entitlement 集运行。如果渲染器被恶意页面攻陷,爆炸半径止于那个进程——它不能突然开始录音或读你的"文稿"文件夹。这就是 Chrome 和 Safari 都为每个标签或站点带独立进程、Slack、Discord 和 Notion(都是 Electron 应用)跟同样模式的原因。

崩溃隔离

如果 Chrome 里一个标签耗尽内存或踩到 JavaScript 引擎 bug,你不想浏览器里每个标签都跟着死。辅助进程意味着一个坏页面只让那个标签显示"哎呀"画面,别的没事。同样逻辑适用于 Slack 渲染一个频道,或 Discord 渲染一次语音通话。

Electron 和 Chromium 架构

如果应用基于 Electron——Slack、Discord、VS Code、Notion、Linear、Figma 桌面、1Password 8、Microsoft Teams、Postman——它整体继承 Chromium 的多进程模型。一个"主"进程、一个"GPU"进程、一个"网络服务"进程(常叫 Utility),以及每个浏览器窗口或重要视图一个渲染进程。渲染器自己从不开 socket。它通过 IPC 跟网络服务说话,网络服务做实际的 TCP 握手。

最后这个细节比听起来重要。

为什么 Mac 辅助进程带宽归因难

macOS 内核把字节归到调用 socket(2) 然后调用 connect(2)/sendto(2)/recvfrom(2) 的那个 PID。对内核那是对的答案——它不知道作为产品的 "Slack" 是什么。它只知道 PID 4711 开了一个到 34.117.x.x:443 的 socket。

所以当你问 nettoplsof -i 或原始 proc_pidinfo API "谁用了网络字节",你拿回一个扁平 PID 列表。每个 PID 有一个进程名(磁盘上可执行文件名),那个名字几乎总是这种:

  • Google Chrome Helper
  • Google Chrome Helper (GPU)
  • Google Chrome Helper (Renderer)
  • Google Chrome Helper (Plugin)
  • Slack Helper
  • Slack Helper (Renderer)
  • Slack Helper (GPU)
  • com.docker.backend

一个朴素的监控——只是把内核视图 dump 到屏幕——给你看的就是那个。你看到七个 Chrome 行,得脑内加和才能回答"Chrome 这小时用了多少?"。更糟,渲染器短寿。关一个标签,渲染器死。开新的,新的渲染器带新 PID 出现。在一小时上加和,答案碎散在几十个死 PID 之间。这就是 Mac 辅助进程带宽归因的核心挑战:内核暴露的单位跟人想思考的单位不匹配。

"归并"是什么意思

归并就是看一个辅助进程——通过其 bundle 路径、父 PID 或两者——并把它的字节归到它属于的用户可见应用。做对了,你不再看到十一行 Chrome,而开始看到一行叫 "Google Chrome" 的行带合计。

有几种做法,每种都有取舍。

按 bundle 路径

每个辅助住在父应用的 bundle 内。对 Chrome,那个路径类似:

/Applications/Google Chrome.app/Contents/Frameworks/
  Google Chrome Framework.framework/Versions/.../Helpers/
  Google Chrome Helper.app/Contents/MacOS/Google Chrome Helper

如果一个进程的可执行路径在树更上面包含另一个 .app bundle,外层 bundle 几乎肯定是父。这是最可靠的归并信号,因为它跨 PID 变化保持——开新的 Chrome 标签,新渲染器的路径仍在 Google Chrome.app 下。

按父 PID

你可以沿进程树往上走,直到碰到一个住在 /Applications 里或其 bundle 标识符匹配"真实"应用的父进程。这能用但更脆——launchd 给孤儿进程重新认父,一些辅助是由 XPC 服务而不是直接由父启动的。

按 bundle 标识符前缀

Slack 的 bundle ID 是 com.tinyspeck.slackmacgap。辅助的 bundle ID 是 com.tinyspeck.slackmacgap.helpercom.tinyspeck.slackmacgap.helper.renderer。对已装应用表做前缀匹配能抓到多数情况。这是 Apple 内部为某些用户面向报告用的技术。

实操中,好的监控用所有三个信号,并在某一个缺失时优雅回退。

看 ova 实战

一眼可瞄的菜单栏带宽监控——本地、签名、约 3 MB。

下载 macOS 版

Electron 特例

Electron 应用值得单独一段,因为它们这么常见。一个叫 Slack Helper (Renderer) 的 Electron 辅助实际不开 socket——开的是网络服务,按 Electron 版本通常叫 Slack Helper(无后缀)或 Slack Helper (Plugin)。渲染器通过 Chromium 的 Mojo IPC 总线跟网络服务说话。

所以如果你只看到光秃秃的 Slack Helper 上的流量,那不是 bug——是架构。渲染器在发请求,但内核看到的是网络服务在做 I/O。从用户角度,两者都该归到 "Slack" 下。从调试角度,知道这一点能在你纳闷为什么渲染器没亮起来时省你一下午。

同样适用于 Chrome 自己:在近期 Chrome 构建里,几乎所有网络流量来自 Google Chrome Helper (Plugin) 或网络服务辅助,不来自每标签的渲染器。

为什么不能信简单合计

理解辅助之后,几个常见问题更容易回答。

"为什么我 Chrome 用量看着低?" 因为字节散在十几个辅助上,你的监控没在求和。每个单独的辅助看着不大。

"为什么 200 MB 突然出现在我不认识的进程下?" 很可能是视频流的渲染器、处理同步的 XPC 服务(CloudKit、iCloud、Dropbox)、或做后台工作的系统守护进程。看可执行路径,不只是名字。

"为什么同一个应用跨重启显示在两个不同名字下?" 一些辅助在进程名里包含版本号或 UUID。多数监控会去掉,但不是全部。

实操中归并——以及什么时候撤销它

一个归并辅助的监控给你看的是这样:

Google Chrome    412 MB ↓   18 MB ↑
Slack             89 MB ↓    4 MB ↑
Spotify           62 MB ↓  120 KB ↑
Dropbox           34 MB ↓   12 MB ↑

…而不是:

Google Chrome Helper (Renderer)   38 MB ↓
Google Chrome Helper (GPU)         2 KB ↓
Google Chrome Helper (Plugin)    220 MB ↓
Google Chrome Helper             140 MB ↓
Google Chrome Helper (Renderer)   12 MB ↓
Slack Helper (Renderer)           41 MB ↓
Slack Helper                      48 MB ↓
... (后面还有 30 行)

第一个是你实际想要的。ova 自动做这种归并——它把每个辅助 PID 归到父 bundle 下,所以你读 "Slack" 而不是七行辅助。

辅助进程归并
ova 把每个辅助 PID 归到它的父应用下,所以你读 "Slack" 而不是七行辅助。Chrome、Slack、Discord、Telegram、VS Code、Figma——都自动整合。

你确实想看辅助的时候

有一种情况归并挡道:调试一个行为不当的应用。如果你是开发者,想搞清楚哪个 Chromium 进程在漏 socket,或者你的渲染器是不是绕过网络服务,你要原始视图。

有用的监控让你能点开归并的行看下面的辅助——每辅助字节、当前 PID、完整可执行路径。这样默认视图干净,细节一点之遥。

这对诊断崩溃也有用:一个每 30 秒重启并重建 TLS 会话的辅助能积累令人意外的后台流量,只有当你能看到辅助级别历史时它才可见。

走完一个:调查一个 Slack 谜题

假设 Slack 显示今天下载 600 MB 而你没看任何视频。这是我会查的顺序。

  1. 检查时段规律。 早 9 点的尖峰大概是早上追账同步。平的 24 小时曲线更有意思。
  2. 检查辅助分解。 如果 90% 在 Slack Helper (Plugin)(网络服务),那是"真正"的应用流量。如果以奇怪方式散在渲染器之间,你可能有个泄漏的标签开着。
  3. 检查工作区数量。 每个 Slack 工作区加一个长 WebSocket 和它自己的头像/文件缓存。三个工作区约是一个空闲流量的 3 倍。
  4. 检查屏幕共享或 huddle 历史。 Huddle 用 WebRTC,活跃时每秒 1–3 MB。

这些都不需要抓包。你需要一个归并辅助、保留一小时历史的按应用监控。

收尾

辅助进程不是要绕过的怪癖——是现代 macOS 应用保持安全稳定的方式。代价是内核对"谁用了网络"的视图读起来像一份晦涩的扁平列表,这就是 Mac 辅助进程带宽计数会绊倒每个通用工具的原因。任何像样的 macOS 上的带宽监控都得做辅助归并,没做的会让你余生在脑内加和行。

如果你当前工具给你看原始辅助行,你受够了第一千次读 "Google Chrome Helper (Renderer)",装 ova——它驻在菜单栏约 3 MB,约 1 Hz 采样,自动归并辅助。所有数据留在你 Mac 上。macOS 14 及以上,Apple Silicon 和 Intel。