返回知识中心
指纹

hardwareConcurrency 如何暴露你的真实 CPU 核心数

了解浏览器如何通过 navigator.hardwareConcurrency 和 Worker 基准测试报告 CPU 核心数,以及这一信号可能暴露什么。

BotBrowser Team

文档中心

想直接看维护中的产品文档?

这篇文章对应的主题已经有文档中心页面。需要规范流程、当前参数和长期参考时,优先看 docs。

navigator.hardwareConcurrency 只是一个数字,却是任何脚本都能在不弹出权限提示的情况下读取的硬件线索之一。它报告浏览器可以使用多少个逻辑处理器,在主线程和所有类型的 worker 中都可用。想判断设备描述是否自洽的网站,可以把这个数字与它看到的其他硬件值,以及并行任务在实际中的表现进行对照。下面的各节依次说明这个值的含义、它在不同机器上为何不同、它对识别的贡献有多大,以及基于 Profile 的核心数需要满足哪些条件。BotBrowser 可以在受控测试的各个上下文中一致地报告同一份配置的核心数,但不能改变宿主机器实际拥有的核心数,也不能改变站点如何使用该值。

示意图:Profile 的核心数报告给主线程、Worker、SharedWorker 和 ServiceWorker,并在 Linux 与 Windows 上让并行扩展表现保持一致

观看视频

这段英文视频在受控实验页中检验关于所报告核心数的四个说法:控制台读数、Worker 上下文、在一台 Ubuntu 主机上的计时扩展测试,以及文章对配置档的说明。它只展示两台主机,而不是用户总体。

摘要

  • navigator.hardwareConcurrency 报告逻辑处理器,不保证实际吞吐量。
  • 虚拟机、容器、浏览器策略和混合 CPU 都可能改变报告值。
  • 把主线程和 worker 中的值与 Profile 的平台及内存提示对照。
  • BotBrowser 会保持 Profile 核心数一致;Linux 和 Windows 还可以让 Worker 的 CPU 亲和性与之对齐。

目录

navigator.hardwareConcurrency 可以反映被限制为 2 vCPU 的客户机,而不是其主机的处理器数量。

该属性返回一个整数,表示可用于运行线程的逻辑处理器数量。逻辑处理器等于物理核心数乘以每个核心的硬件线程数,因此带同时多线程的四核处理器会报告 8。MDN 把这个属性描述为浏览器可用的逻辑处理器数量,WHATWG HTML 规范则补充说,用户代理应倾向于暴露真实数字,只有在存在限制或希望降低指纹风险时才使用更小的值。

// 主线程
console.log(navigator.hardwareConcurrency);
// 在专用 worker、共享 worker 或 service worker 内
console.log(self.navigator.hardwareConcurrency);

这个数字是规划用的提示,不是速度的度量。它不说明单个核心有多快,不说明核心是能效核心还是性能核心,不说明机器此刻有多忙,也不说明操作系统实际会为页面调度多少线程。两台都报告 8 的机器,在同样的并行负载下表现可能大不相同。

应用读取这个值有正当理由,例如选择 worker 池的大小,所以浏览器不能直接移除它,否则会破坏正常工作的代码。它还不需要权限,并且对所有类型的 worker 都可见。这种组合解释了为什么这个数字一直可见,也解释了为什么实际目标是各上下文之间保持一致,而不是移除它。

对应用代码来说,稳妥的做法是把这个值当作 worker 池的上限,给主线程留出余量,并在确定大小之前测量真实吞吐量。与报告数字相等的池可能和渲染、输入处理争抢资源,而比这个数字更大的池很少能帮助受 CPU 限制的任务,因为多出来的线程只是在等处理器。这个数字也不应该用来做授权决定,或作为收集更详细硬件信息的理由。

这个数字为何因机器而异

在 Linux 上,容器可以给浏览器提供 cpuset.cpus 限额,例如 2 vCPU,而不是主机总共的 16-core。

有几个因素决定浏览器报告什么。

  • 处理器型号。 核心数和线程数由 CPU 决定。许多常见的桌面处理器报告 8、12 或 16 个逻辑处理器,而较旧或低端的机器可能报告 2 或 4。Apple silicon 和其他混合设计把能效核心与性能核心组合在一起,总数取决于芯片。
  • 虚拟机。 虚拟机报告由虚拟机监控程序分配的虚拟 CPU。一台 16 核的主机如果给客户机分配 2 个虚拟 CPU,客户机内就显示 2,与底层硬件无关。
  • 容器与操作系统限制。 容器运行环境和调度器设置可以限制进程能使用的处理器数量。浏览器能否看到这种限制,取决于限制的应用方式。
  • 浏览器策略。 规范允许用户代理在设置了限制或希望降低指纹风险时报告更小的值,一些浏览器也尝试过对数字设置上限或分桶。Chromium 目前报告操作系统暴露的数量。

这种差异首先影响开发者。在 CI 容器或云实例内读到的值,常常和同一浏览器在笔记本上的值大不相同,所以假设某个固定数字的测试可能在一处通过而在另一处失败。请在运行时读取这个数字,把它当作输入,而不是常量。

典型取值跟随硬件市场。桌面端最常见的是 4 和 8,6、12 和 16 出现在特定的处理器系列上。超过 16 的情况多见于工作站和服务器。手机通常报告 4、6 或 8,取决于芯片设计。7 或 13 这样的奇数在在售处理器中很少见,当有人考虑手动挑选数字时,这一点就变得重要。

当工作负载在不同环境之间迁移时,请比较报告出来的数字,而不是宣传的实例规格。以某个虚拟 CPU 数量出售的云实例,在容器内可能报告不同的值,带能效核心的笔记本报告的逻辑处理器数量,也可能比随意看一眼规格表所暗示的更多。把观察到的值和每次基准测试结果记录在一起,可以消除两次运行结果不同时的常见困惑。在虚拟机中,报告的值对下文所述的扩展性检查同样重要,因为只有 2 个虚拟 CPU 的客户机确实无法同时运行超过两个线程。一致的环境,是报告的数字与可用处理器相符的环境。

这一信号对识别的贡献

指纹研究衡量每个属性能把人群缩小多少。AmIUnique 的数据把 navigator.hardwareConcurrency 的熵估计为约 4.5 比特,相当于区分约 23 个等概率的分组。真实分布远不均匀:少数常见取值覆盖了大多数浏览器,其余都很少见,因此实际的区分度比这个数字所暗示的要粗。

单独来看,这个值是一个起辅助作用的信号,不是唯一标识符。属性相互独立时,熵会累加。每多一个比特,匹配的浏览器集合大致减半,所以核心数与屏幕尺寸、时区和字体列表结合后,人群会很快缩小。相关的属性贡献更少,因为点名某款手机的用户代理,本身就已经隐含了一个很短的合理核心数列表。

罕见取值会缩小得更多。报告 24、32 或 64 暗示这是工作站或服务器,在日常浏览人群中并不常见。这个数字并不说明任何个人,但它可能让一次会话在其他报告 4 或 8 的会话中更容易被区分出来。

移动设备增加了另一层。某些芯片系列会把特定数字与其他属性搭配,例如平台字符串和屏幕。报告 8 并带有特定用户代理模式的手机,会把设备缩小到一个很短的型号列表,这比任何一个值单独提供的信息都多。桌面设备也是同样的逻辑:一个本身常见的数字,与不常见的内存值或平台搭配后,会更有区分度。

注重隐私的浏览器以不同方式处理这一取舍。对数字设置上限或分桶,会减少它携带的信息,代价是让应用在确定 worker 池大小时得到的提示不够准确。两种选择都合理。对开发者来说,有用的问题是应用有多依赖准确的数字;对隐私评审者来说,则是它旁边还暴露了多少其他硬件线索。

这些数字是需要谨慎的理由,而不是定论。报告常见数字的浏览器并不能免于其他信号的影响,报告不常见数字的浏览器也不会被自动标记。这个数字只是众多输入之一,它的分量取决于页面还能看到什么。

核心数如何被交叉核对,以及手动改写为何失败

比较从 navigator.hardwareConcurrency 开始,也可以纳入同一页面创建的 Worker 返回的值。

除了数字本身,网站还可以问这个数字是否合理。报告核心数时,有三种一致性很重要。

  1. 与其他值的合理性。 核心数应与平台、用户代理和设备内存提示相互匹配。2 个核心配 8 GB 内存,描述的是一台普通的小型笔记本或虚拟机。64 核配手机用户代理,描述的则是并不存在的硬件。
  2. 跨上下文的一致性。 主线程、专用 worker、共享 worker 和 service worker 应报告同一个值。页面中的一个脚本就能读取所有这些值,所以上下文之间的差异很容易被注意到。
  3. 与并行行为的一致性。 脚本可以用逐渐增多的 Worker 运行同一个任务,观察完成时间在哪里不再改善。有 4 个逻辑处理器的机器在大约四个 Worker 处趋于平缓,而有 16 个的机器会继续改善更久。

值得检查的组合很具体。Windows 桌面用户代理搭配 8 或 16 个逻辑处理器和报告的 8 GB 内存是常见的。Linux x86_64 平台配 4 个核心,指向普通桌面或服务器硬件,而 ARM 平台字符串配 8 个核心,则暗示手机或特定的 ARM 服务器。这些组合单独都证明不了什么;每一个只需要避免与描述的其余部分相矛盾。

跨上下文的一致性值得关注,因为 worker 由页面创建并共享其来源,但每个 worker 都有自己的全局作用域。专用 worker、被多个标签页使用的共享 worker,以及比页面存活更久的 service worker,都会独立读取该属性,所以为页面所做的任何改动都必须在每一个中应用,并分别验证。

第三项检查更多是一种可能性,而不是常规测量。浏览器会降低计时器精度,而 SharedArrayBuffer 这类共享内存计时工具要求页面处于跨源隔离状态,并设置 COOP 和 COEP 标头。这些要求限制了页面测量扩展性的精度,所以应把这项检查视为让报告的数字与真实行为保持一致的又一个理由,而不是一个确定的结果。

某个具体网站如何权衡这些检查,从外部无法观察到。它们是一致性目标,而不是对某个特定网站会得出什么结论的预测。

当核心数出错时,分歧会按固定顺序出现。首先要看主线程,因为注入的页面脚本通常只改动这一作用域。其次是由 Blob URL 创建的专用 worker,它只需三行代码,就能暴露没有到达 worker 作用域的覆盖。共享 worker 和 service worker 排在后面,因为它们需要更多准备,但仍从各自的全局作用域读取该属性。设备内存与平台字符串放在最后,用来对四个上下文一致返回的数字做合理性检查。

评审者可以把上面的顺序当成四次读取。第一步取页面里的 navigator.hardwareConcurrency,随后依次取 self.navigator.hardwareConcurrency 于 Blob 专用 worker、共享 worker 和 service worker 之中,最后把四个数字并排对比。同一会话里再取 navigator.deviceMemory 与 navigator.platform,用来判断核心数是否与描述的其余部分吻合。下文所述的扩展性运行是第五轮,需要 Linux 或 Windows 主机。

匹配的结果很平淡:四个上下文都返回 4,设备内存显示 8,平台字符串与 Profile 相符,基准测试在接近四个 Worker 时停止改善。不匹配同样容易解读。页面返回 4 而专用 worker 返回 32,指向的是页面级覆盖,而不是硬件。四个上下文一致,但数字与平台或内存提示相矛盾,指向的是被单独修改的 Profile;而上下文一致却仍然超出声明核心数继续扩展,指向的是 macOS 上缺少 CPU 亲和性。

在页面中覆盖 Navigator.prototype.hardwareConcurrency,不会更新独立 worker 全局作用域中的 WorkerNavigator.hardwareConcurrency。

改变这个值的常见办法,是通过扩展或注入脚本覆盖该属性。这会改变主线程返回的数字,同时留下三个缺口。

  • 扩展性不匹配。 如果属性说 4,而机器有 16 个核心,并行任务会比 4 核机器所允许的更早完成。只要有人测量,声明与行为就会不一致。
  • 上下文缺口。 只作用于页面 navigator 对象的覆盖,不会到达 worker 作用域,除非也注入到那里,所以 worker 仍可能返回真实值。
  • 设备描述不完整。 一个没有与用户代理、平台和内存提示协调的新数字,可能与它们相矛盾。

随机值会让第三个缺口更严重。7 或 13 这样的数字很少与在售硬件吻合。即使是 8 这样固定的常见值,也会与点名某款 4 核芯片手机的用户代理相矛盾。单独改动的一个属性,其可信度只相当于周围其他值中最薄弱的那一个。

三个缺口中,上下文缺口最容易发现。测试页面可以用 Blob URL 创建一个专用 worker,把值发回来,并用几行代码与主线程比较。共享 worker 和 service worker 需要多一点准备,但比较方式相同,任何差异都是覆盖实现的缺陷,而不是硬件的属性。

因此要求是一致性:一个值,在每个上下文中报告,既与设备描述的其余部分相符,也与并行任务的实际扩展方式相符。要在所有这些层面上手动保持这种一致性,正是风险所在。

如何检查基于 Profile 的核心数

第一项检查会记录主线程和每个 Worker 中的 navigator.hardwareConcurrency。

基于 Profile 的配置从完整的设备 Profile 中取得数字,而不是来自手动设置的覆盖。没有单独的核心数参数,因为这个值来自 Profile。四项检查可以说明结果是否自洽。

  • 值。 读取该属性,确认它等于 Profile 的核心数,而不是主机的核心数。
  • 上下文。 在专用 worker、共享 worker 和 service worker 内读取它,确认每一个都与主线程一致。
  • 合理性。 确认这个数字对 Profile 的平台、用户代理和设备内存来说是现实的。
  • 稳定性。 用同一个 Profile 在多个会话中读取它,确认它从不变化。
检查读取位置4 核 Profile 的预期结果
主线程的值页面中的 navigator.hardwareConcurrency4,而不是主机的 32
worker 的值专用 worker 中的 self.navigator.hardwareConcurrency4,与主线程一致
共享 worker 与 service worker各自 worker 全局作用域中的同一属性两者都是 4,没有上下文缺口
合理性把核心数与平台、内存提示放在一起4 符合移动 Profile,32 不符合
const source = 'postMessage(self.navigator.hardwareConcurrency)';
const blob = new Blob([source], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = event => console.log('worker:', event.data, 'main:', navigator.hardwareConcurrency);

如果某项检查失败,常见原因不多。

  • 值显示的是主机的核心数。 Profile 没有加载,或其中没有预期的核心数。该值来自 Profile,所以请先检查 Profile 文件。
  • 并行任务在 macOS 上仍然很快。 macOS 不提供 Linux 和 Windows 所用的 CPU 亲和性接口,所以 Worker 会在所有核心上运行。报告的值仍然来自 Profile,但并行耗时可能与之不一致。当扩展行为很重要时,请使用 Linux 或 Windows。
  • Worker 看起来很慢。 当 Profile 声明的核心数少于主机时,这是预期现象,因为 Worker 被有意限制在声明的数量内。

扩展性是第五项检查,并且取决于平台。在核心数多于 Profile 声明的主机上,把并行任务分配给超过声明数量的 Worker,加速应在声明数量附近停止。当多个浏览器实例共用一台主机时,每个实例由各自的 Profile 独立限制,所以同一台服务器上的 4 核 Profile 和 8 核 Profile,会显示与各自声明相符的不同扩展曲线。

一个实际的例子可以说明这一效果。一台 32 核的构建服务器运行声明 4 个核心的移动 Profile。该 Profile 在每个上下文中都报告 4,Worker 被限制在四个物理核心上,所以用 8 个 Worker 的并行基准测试,在大约四个 Worker 之后不再加速,呈现的是 4 核机器的扩展曲线,而不是主机本来能达到的曲线。绝对速度仍取决于主机的单核性能,通常高于手机。

同一台 32 核主机可以同时为多个浏览器上下文提供一个 4 核 Profile。在同一个 Profile 下打开三个或四个页面,在每个页面中读取 navigator.hardwareConcurrency,并从每个页面打开一个 worker 读取其中的 self.navigator.hardwareConcurrency。每个页面和每个 worker 都应返回 4,而在受控浏览器之外读取主机本身会返回 32。如果某个上下文返回 32,说明该上下文是在没有该 Profile 的情况下创建的,而不是由该 Profile 创建的,所以在把问题归到 Profile 之前,先比较每个上下文的启动方式。

好习惯能让结果保持可预测。使用完整的 Profile,而不是覆盖单个属性;选择核心数与其平台和内存相符的 Profile;每次都测试 worker 上下文;避免手动调整这个数字。如果报告的数字需要改变,请更换 Profile,因为设备的其他值属于同一个描述。更多细节见 navigator 属性保护指南、设备内存 API 指南、worker 规划指南以及浏览器指纹概览。

请为每次检查保留一份简短的书面记录,以便把之后的变化与它对照:使用的 Profile、主机平台、在主线程和每种 worker 类型中读到的值,以及在 Linux 或 Windows 上的扩展性结果。如果记录中还写明浏览器版本和 Profile 版本,就很容易判断之后出现的差异来自 Profile、浏览器还是主机。

BotBrowser 能够让 Profile 中的 navigator.hardwareConcurrency 在主线程和 worker 中保持一致,并且在 Linux 和 Windows 上对 Worker 线程应用 CPU 亲和性,使并行基准测试的扩展表现与声明的核心数相符。当较大的主机运行核心数更少的 Profile 时,这会有帮助,因为报告的数字与并行任务的扩展方式保持一致。它不能增加真实的 CPU 核心,也不会改变主机的实际硬件,在 macOS 上不会通过 CPU 亲和性约束 Worker,因此并行耗时可能与 Profile 不一致,并且不保证任何网站会如何解读这个信号。

实践结论

把报告值当作有边界的规划输入。在每个 worker 上下文中读取它,与 Profile 的平台对照;当测试包含 CPU 亲和性扩展时使用 Linux 或 Windows。需要改变数字时更换 Profile,不要手动修补单个属性。

公开来源

#Cpu#浏览器指纹识别#hardwareConcurrency#隐私

本文属于指南: 什么是浏览器指纹?原理、风险、信号与隐私边界

让 BotBrowser 从研究走向生产

先用这些指南理解模型,再进入跨平台验证、隔离上下文和面向规模化的浏览器部署。