背景

Aliyun Linux 2 是为云上应用程序特别优化的开源操作系统,上游包括 4.19 LTS 内核、CentOS 7.6 软件包,为阿里云基础设施深度优化,致力于为云上用户提供最佳体验。

Aliyun Linux 2 4.19.24 发布之后,使用 will-it-scale testbench 性能测试发现 poll1\poll2 等系统调用,相较于 Aliyun Linux 1 4.4.95 存在一定程度的性能下降。

will-it-scale 使用每秒钟完成的系统调用次数来衡量系统调用的性能,以下为各个内核版本下使用 will-it-scale 测试 poll1 性能的结果。

4.4 4.4.95 4.19.24
845 万次每秒 860 万次每秒 720 万次每秒

Aliyun Linux 2 4.19.24 下的 poll1 性能相较于 Aliyun Linux 1 4.4.95 存在 16.2% 的性能下降,此外 poll2 也存在 7.5% 的性能下降。

结论

git bisect 发现以下 commit 影响了 poll1 的性能变化:

  • 5b710f34e194 (x86/uaccess: Enable hardened usercopy)
  • 236222d39347 (x86/uaccess: Optimize copy_user_enhanced_fast_string() for short strings)
  • 21d375b6b34f (x86/entry/64: Remove the SYSCALL64 fast path)

will-it-scale 测得的 poll1 性能数据如下:

版本 万次/秒
4.4 845
5b710f34e194~ 836
5b710f34e194 750
236222d39347~ 739
236222d39347 771
21d375b6b34f~ 756
21d375b6b34f 724
4.19.24 720

分析

x86/uaccess: Enable hardened usercopy

5b710f34e194 (x86/uaccess: Enable hardened usercopy) 带来 10.29% 的性能下降。

该 commit 在 v4.8-rc2 合入主线,其在 copy_from_user()/copy_to_user() 路径中添加 check_object_size() 检查,以防止内核内存泄漏 (kernel memory exposure)或堆溢出 (heap overflow exploit) 等。该检查会提升系统的安全性,但也带来了一定的性能开销。

x86/uaccess: Enable hardened usercopy

Enables CONFIG_HARDENED_USERCOPY checks on x86. This is done both in
copy_*_user() and __copy_*_user() because copy_*_user() actually calls
down to _copy_*_user() and not __copy_*_user().

x86/uaccess: Optimize copy_user_enhanced_fast_string() for short strings

236222d39347 (x86/uaccess: Optimize copy_user_enhanced_fast_string() for short strings) 带来 4.44% 的性能提升。

该 commit 在 v4.13-rc1 合入主线。

当处理器支持 ERMS (Enhanced REP MOVSB) 特性时,可以使用 rep movsb 指令优化内存拷贝操作,但是依据 Intel 64 and IA-32 Architectures Optimization Reference Manual,该优化在拷贝的数据量较大时效果明显,而当拷贝的数据量较小时,rep movsb 指令本身存在的开销会导致其优化效果不明显。

因而该 commit 在 64 字节以下的内存拷贝中,将 rep movsb 替换为显式的循环操作,从而带来一定的性能提升。

x86/uaccess: Optimize copy_user_enhanced_fast_string() for short strings

commit 236222d39347e0e486010f10c1493e83dbbdfba8 upstream.

According to the Intel datasheet, the REP MOVSB instruction
exposes a pretty heavy setup cost (50 ticks), which hurts
short string copy operations.

This change tries to avoid this cost by calling the explicit
loop available in the unrolled code for strings shorter
than 64 bytes.

The 64 bytes cutoff value is arbitrary from the code logic
point of view - it has been selected based on measurements,
as the largest value that still ensures a measurable gain.

Micro benchmarks of the __copy_from_user() function with
lengths in the [0-63] range show this performance gain
(shorter the string, larger the gain):

- in the [55%-4%] range on Intel Xeon(R) CPU E5-2690 v4
- in the [72%-9%] range on Intel Core i7-4810MQ

Other tested CPUs - namely Intel Atom S1260 and AMD Opteron
8216 - show no difference, because they do not expose the
ERMS feature bit.

x86/entry/64: Remove the SYSCALL64 fast path

21d375b6b34f (x86/entry/64: Remove the SYSCALL64 fast path) 带来 4.23% 的性能下降。

该 commit 在 v4.16-rc2 合入主线。该 commit 为修复 spectre 漏洞移除了 x86 64 syscall 入口的 fast path,而全部走 slow path,从而造成了一定的性能损失。

spectre 漏洞是 2018 年爆出的处理器安全漏洞,其实际利用处理器的分支预测功能所潜在的问题,利用旁路攻击方式造成内核数据泄露。以下简单描述该攻击方式的原理。

当处理器执行间接跳转指令,例如执行 call *<mem> 指令时,<mem> 表示一个内存地址,该地址处的内存保存了一个绝对地址,call 指令需要跳转到该绝对地址。

此时 call 指令首先需要获取 <mem> 内存的值,当该地址处的内存不处于 cache 中时,处理器就必须执行读内存操作,由于读内存操作相当耗时,此时处理器硬件会执行分支预测功能。

处理器硬件内部使用 BTB (Branch Target Buffer) 来缓存 (间接跳转指令的原地址, 跳转指令的目标地址) 这一对数据,处理器硬件的分支预测即使用 BTB 来预测当前执行的间接跳转指令对应的目标跳转地址。

然而该机制存在的一个问题是,同一个处理器上的不同应用程序会共用同一个 BTB,因而 spectre 漏洞的原理即是 attacker 通过用户态程序执行特定的间接跳转指令来训练当前处理器的 BTB,从而使同一处理器上运行的 Linux 内核运行间接跳转指令时,分支预测功能就会被误导跳转到 attacker 设计的一个特定地址上。

attacker 可以使用 eBPF 机制将 attacker 编写的攻击代码注入到内核中,而通过之前描述的机制 attacker 可以使内核的间接跳转指令在预测执行过程中,跳转执行 attacker 注入的 eBPF 代码,这样通过 cache 旁路攻击就可以获取内核的内存数据,从而造成数据泄漏。

而 syscall fast path 的移除正是与 spectre 漏洞有关。Linux 内核中,x86 64 syscall 入口实现有两条路径以调用对应的系统调用。

第一条路径是用汇编写的 fast path,若当前没有开启 trace 相关的功能,则会根据系统调用号直接在 sys_call_table 中查找对应的 syscall handler 地址,并跳转执行该 syscall handler。

call    *sys_call_table(, %rax, 8)

另外一条是用 C 写的 slow path,当前开启 trace 相关功能时,只能执行 slow path 即 do_syscall_64() 函数。

由于 fast path 中使用了间接跳转指令,容易遭受 spectre 漏洞的影响,因而在经过一系列的讨论之后,社区暂时移除了 x86 syscall 的 fast path 入口,而全部走 slow paith, 而这也给系统调用的性能带来了一定的回退。

x86/entry/64: Remove the SYSCALL64 fast path
The SYCALLL64 fast path was a nice, if small, optimization back in the good
old days when syscalls were actually reasonably fast.  Now there is PTI to
slow everything down, and indirect branches are verboten, making everything
messier.  The retpoline code in the fast path is particularly nasty.

Just get rid of the fast path. The slow path is barely slower.