本文在解读 https://lwn.net/Articles/790464/ 的基础上,加入自己理解,与原文存在差异。

从地址不对齐访问到split lock

Intel CPU微架构允许不对齐的内存访问,但ARM、RISC-V等架构却不允许。在众多的不对齐中,一个特殊的场景是:原子操作的操作数(由于地址不对齐)跨越两个cache lines,Intel将之叫做split lock。它有两个特征:

  1. 原子操作,即汇编指令包含Lock前缀;
  2. 操作数地址不对齐,还跨越两个cache lines;

其实大部分吃瓜群众都不知道这个特性,但是它却对应用性能影响极大。最近,Intel工程师Fenghua Yu同学正在开发一组内核补丁,用于检测和处理split lock,现在已经发出了第8版code review。阿里巴巴在多年前就意识到split lock的危害,在线上实施了大规模监控,并采取必要隔离措施。

下面通过一小段代码给吃瓜群众一个感性的认识:

  1 #include<stdio.h>
  2 #include <sys/mman.h>
  3 
  4 #pragma pack(push,2)
  5 struct counter
  6 {
  7     char buf[62];
  8     long long c;
  9 };
 10 #pragma pack(pop)
 11 
 12 int main () {
 13     struct counter *p;
 14     int size = sizeof(struct counter);
 15     int prot = PROT_READ | PROT_WRITE;
 16     int flags = MAP_PRIVATE | MAP_ANONYMOUS;
 17 
 18     p = (struct counter *) mmap(0, size, prot, flags, -1, 0);
 19 
 20     while(1) {
 21         __sync_fetch_and_add(&p->c, 1);
 22     }
 23 
 24     return 0;
 25 }

Intel cpu中,一个cache line 只有64个字节,struct counter中的成员 c 占8个字节,buf填充了62个字节。因此,一旦访问成员c,就涉及两个cache lines的内容的拼接;代码21行中,执行原子操作 __sync_fetch_and_add()会触发split lock。

split lock 的危害

Intel处理器很贴心,自动处理了不对齐的内存访问,甚至能保证split lock操作的正确性;但是也给程序的性能埋下一颗很难被发现的雷。

学过体系结构的同学都应该知道,缓存一制性协议MESI只能保证cache line粒度的一致性。同时访问两个cache lines不是常见操作,为保证split lock的原子性,设计硬件时使用特殊逻辑(冷路径)来处理:锁住整个访存总线,阻止其它逻辑cpu访存。

从原理出发,我们很容易想到,锁住总线将导致其它core上访存操作受阻,宏观表现为平均访存延时显著上升。为不让各位看官白走一趟,小编在自己的skylake机器上测了一组数据,随着split lock速率的增加,访存延迟呈指数恶化。

云场景下,在Guest中执行一个用户态进程,就可以阻塞物理机上其它核的访存操作,导致整机性能下降;极端情况下,甚导致整机的拒绝服务(DoS)攻击。这是一个严重的故障隔离问题。

解决方案

Intel 即将发布的下一代处理器 (Tremont和Ice Lake) 新增了Alignment Check (#AC)异常,并提供一个硬件开关。当开关使能的情况下,cpu产生split lock时,可以触发异常。至于采取什么动作就取决于注册的异常处理程序了,这正是Fenghua同学要做的事。而在早期的cpu上,只能通过perf监控sq_misc.split_lock 事件的数量,用于调试目的。

接下来,我们来看看这组内核补丁是如何处理#AC异常的。从整体看,可以将一个机器上运行的代码分为三类:固件、内核、用户态软件。

如果split lock出现在内核中,只打印warning,然后关闭当前cpu的检测功能。报出warning之后,重新执行被打断的指令,系统继续运行。是打印warning还是直接panic,是社区当前讨论的一个焦点。比较合理的解释是:内核中产生split lock是bug,但是该bug没有严重到需要直接panic。

如果split lock出现在用户态程序中,默认会发送一个SIGBUS信号,导致进程异常退出。只有当开发人员修复后才能正常运行程序。

当split lock出现在系统固件代码中,系统将会夯(hang)住。之所以这么设计,是内核开发人员担心,如果不暴露问题,固件厂商永远不会关注修复这类问题。

处理split lock的方式,有warning,sigbus, hang,都不是省油的灯。我们可能会担心,代码我改不了,一旦出现了split lock怎么办?好在设计者提供了关闭接口:

  1. 在内核启动参数中加入nosplit_lock_detect
  2. echo 0 > /sys/devices/system/cpu/split_lock_detect

这组内核补丁经过多轮review,社区的反馈都比较积极,包括Thomas Gleixner和Ingo Molnar等大佬。虽然还剩下一些小问题,预计不久将被合入内核主线。

号外

如何在现有的cpu上检测split lock呢?

perf stat -e cpu/event=0xf4,umask=0x10/ -a -I 1000  

其中 event=0xf4, umask=0x10 对应PMU事件sq_misc.split_lock。