3. x86 特性标志

3.1. 简介

/proc/cpuinfo 中的特性标志列表并不完整,它代表了很久以前将特性标志放在用户空间容易找到的地方这一不幸尝试。

然而,特性标志的数量随着每一代 CPU 的增长而增加,导致 /proc/cpuinfo 变得无法解析且笨重不堪。

更何况,这些特性标志甚至不需要出现在该文件中,因为用户空间并不关心它们——glibc 等已经使用 CPUID 来弄清楚目标机器支持什么、不支持什么。

并且即使它没有显示某个特定的特性标志——尽管 CPU 确实支持相应的硬件功能并且该 CPU 支持 CPUID 故障机制——用户空间也可以简单地探测该特性并弄清楚它是否被支持,而不管它是否在某处被宣告。

此外,这些标志字符串一经出现就成为了 ABI,而当根本没有任何东西使用它们时还要永远维护它们,这是一种巨大的精力浪费。

因此,当前 /proc/cpuinfo 的用途是显示内核已启用支持的特性。即:CPUID 特性标志存在,内核在引导时进行了额外的设置,并且该功能已准备好使用。这方面的一个完美例子是 “user_shstk”,内核中存在额外的代码启用以支持用户程序的影子堆栈(shadow stack)。

因此,如果用户想知道某个特性在给定系统上是否可用,他们会尝试在 /proc/cpuinfo 中查找该标志。如果给定的标志存在,则意味着

  • 内核对该特性有足够的了解,以至于拥有一个 X86_FEATURE 位

  • 内核支持该特性,并且目前正将其提供给用户空间或内核的其他部分

  • 如果该标志代表一个硬件特性,则硬件支持它。

/proc/cpuinfo 中缺少某个标志本身对最终用户来说几乎没有任何意义。

一方面,像 “vaes” 这样的特性对于未定义 X86_FEATURE_VAES 的内核上的用户应用程序来说可能是完全可用的,因此 /proc/cpuinfo 中没有 “vaes”。

另一方面,运行在非 VAES 硬件上的新内核在 /proc/cpuinfo 中也不会有 “vaes”。应用程序或用户无法分辨这两者的区别。

最终结果是,/proc/cpuinfo 中的 flags 字段对于内核调试仅有一点微弱的用处,但对于其他方面则没什么实际用处。应用程序应该改用诸如 glibc 用于查询 CPU 支持的设施。用户应该依赖诸如 tools/arch/x86/kcpuid 和 cpuid(1) 之类的工具。

关于实现,出现在 /proc/cpuinfo 中的标志在 arch/x86/include/asm/cpufeatures.h 中有一个 X86_FEATURE 定义。这些标志既代表硬件特性,也代表软件特性。

如果内核关心某个特性,或者 KVM 想要将该特性暴露给 KVM 客户机,它应该仅在客户机需要解析 /proc/cpuinfo 时才将其暴露给客户机。然而如上所述,这几乎是不可能的。KVM 可以合成 CPUID 位,而 KVM 客户机可以简单地查询 CPUID 并弄清楚虚拟机监控程序支持什么、不支持什么。正如已经陈述过的,/proc/cpuinfo 不是无用特性标志的倾倒场。

3.2. 特性标志是如何创建的?

3.2.1. 特性标志可以从 CPUID 叶(leaf)的内容中派生出来

这些特性定义的组织方式镜像了 CPUID 叶的布局,并按照 cpufeatures.h 中 enum cpuid_leafs 映射的带有偏移量的字(words)进行分组(详见 arch/x86/include/asm/cpufeatures.h)。如果某个特性在 cpufeatures.h 中使用 X86_FEATURE_<name> 定义,并且如果在运行时检测到它,则该标志将相应地显示在 /proc/cpuinfo 中。例如,“avx2” 标志来自 cpufeatures.h 中的 X86_FEATURE_AVX2。

3.2.2. 标志可以来自分散的基于 CPUID 的特性

在稀疏填充的 CPUID 叶中枚举的硬件特性会获得软件定义的值。尽管如此,仍需要查询 CPUID 以确定是否存在给定的特性。这在 init_scattered_cpuid_features() 中完成。例如,X86_FEATURE_CQM_LLC 被定义为 11*32 + 0,其存在性在运行时的相应 CPUID 叶 [EAX=f, ECX=0] 的 EDX[1] 位中进行检查。

分散 CPUID 叶的意图是不必要地膨胀 struct cpuinfo_x86.x86_capability[]。例如,CPUID 叶 [EAX=7, ECX=0] 有 30 个特性且是密集的,但 CPUID 叶 [EAX=7, EAX=1] 只有一个特性,这将在 x86_capability[] 数组中浪费 31 位的空间。由于每个可能的 CPU 都有一个 struct cpuinfo_x86,因此浪费的内存是不容忽视的。

3.2.3. 在特定条件下,可以为硬件特性综合创建标志

条件示例包括某些特性是否存在于 MSR_IA32_CORE_CAPS 中,或者识别出特定的 CPU 型号。如果满足所需的条件,则通过 set_cpu_cap 或 setup_force_cpu_cap 宏启用这些特性。例如,如果在 MSR_IA32_CORE_CAPS 中设置了第 5 位,则特性 X86_FEATURE_SPLIT_LOCK_DETECT 将被启用,并且将显示 “split_lock_detect”。标志 “ring3mwait” 仅在运行于 INTEL_XEON_PHI_[KNL|KNM] 处理器上时才会显示。

3.2.4. 标志可以纯粹代表软件特性

这些标志不代表硬件特性。相反,它们代表内核中实现的软件特性。例如,内核页表隔离(Kernel Page Table Isolation)纯粹是一个软件特性,其特性标志 X86_FEATURE_PTI 也定义在 cpufeatures.h 中。

3.3. 标志的命名

脚本 arch/x86/kernel/cpu/mkcapflags.sh 处理来自 cpufeatures.h 的 #define X86_FEATURE_<name>,并生成 kernel/cpu/capflags.c 中的 x86_cap/bug_flags[] 数组。生成的 x86_cap/bug_flags[] 中的名称用于填充 /proc/cpuinfo。x86_cap/bug_flags[] 中标志的命名如下

3.3.1. 标志默认不会出现在 /proc/cpuinfo 中

特性标志默认从 /proc/cpuinfo 中省略,因为在大多数情况下,将该特性暴露给用户空间是没有意义的。例如,X86_FEATURE_ALWAYS 在 cpufeatures.h 中定义,但该标志是替代运行时修补(alternative runtime patching)功能中使用的内部内核特性。因此该标志不会出现在 /proc/cpuinfo 中。

3.3.2. 如果绝对需要,请指定标志名称

如果 #define X86_FEATURE_* 这一行的注释以双引号字符 (“”) 开头,则双引号字符内的字符串将成为标志的名称。例如,“sse4_1” 标志来自 X86_FEATURE_XMM4_1 定义后面的注释 “sse4_1”。

在某些情况下,需要覆盖标志的显示名称。例如,/proc/cpuinfo 是一个用户空间接口,必须保持不变。如果由于某种原因,X86_FEATURE_<name> 的命名发生了变化,则应该用 /proc/cpuinfo 中已经使用的名称来覆盖新的命名。

3.4. 当发生以下一种或多种情况时,标志会缺失

3.4.1. 硬件未枚举对它的支持

例如,当新内核运行在旧硬件上,或者该特性未被引导固件启用时。即使硬件是新的,在运行时启用该特性也可能会出现问题,此时该标志将不会显示。

3.4.2. 内核不知道该标志

例如,当旧内核运行在新硬件上时。

3.4.3. 内核在编译时禁用了对它的支持

例如,如果在构建时未启用线性地址掩码(LAM)(即未选择 CONFIG_ADDRESS_MASKING),则标志 “lam” 不会显示。尽管仍会通过 CPUID 检测到该特性,但内核会通过调用 setup_clear_cpu_cap(X86_FEATURE_LAM) 清除它来禁用该特性。

3.4.4. 该特性在引导时被禁用

可以使用命令行参数或由于启用失败来禁用某个特性。可以使用命令行参数 clearcpuid=,通过使用 /arch/x86/include/asm/cpufeatures.h 中定义的特性编号来禁用特性。例如,可以使用 clearcpuid=514 禁用用户模式指令保护(UMIP)。数字 514 是由 #define X86_FEATURE_UMIP (16*32 + 2) 计算得出的。

请勿在生产环境中使用此命令行选项——它仅用作一种快速且粗糙的调试辅助手段,以排除特性启用代码是否是罪魁祸首。如果你使用它,它将污染内核。

此外,还存在各种用于禁用特定特性的自定义命令行参数。参数列表包括但不限于 nofsgsbase、nosgx、noxsave 等。5 级分页也可以使用 “no5lvl” 来禁用。

3.4.5. 已知该特性无法正常工作

由于在运行时缺少某个依赖项,已知该特性无法正常工作。例如,如果禁用了 XSAVE 特性,则 AVX 标志将不会显示,因为它们依赖于 XSAVE 特性。另一个例子是有缺陷的 CPU 且它们缺少微代码补丁。正因如此,内核决定不启用某个特性。