排查 CPU 总线锁定问题

本文档介绍了如何在 Linux 客户操作系统中识别和排查 CPU 总线锁定问题。本文档介绍了 CPU 总线锁的症状、如何使用内核日志消息诊断这些问题、如何找到有问题的代码,以及如何应用缓解措施或修复。

概览

当处理器必须断言硬件 LOCK# 信号才能获得对整个系统内存总线的独占访问权时,就会发生 CPU 总线锁定。此问题通常会在以下情况之一下发生:

  • 原子指令对跨越缓存行边界(即拆分锁)的未对齐内存进行操作。
  • 原子指令对指定为 uncacheable (UC) 的内存(例如 Memory-Mapped I/O [MMIO])进行操作。

由于 CPU 会断言全局总线锁定,因此在内存操作完成之前,客户操作系统中的所有其他处理器和设备都必须等待。总线锁定率过高可能会严重降低 CPU 性能。

虽然旧处理器不跟踪总线锁定,但现代 x86 处理器(例如 Intel Sapphire Rapids 及更高版本,或 AMD Zen 5 及更高版本)包含可检测 CPU 总线锁定的硬件功能。当指令触发 CPU 总线锁定后,CPU 会在指令完成后立即发出调试异常 (#DB)。

从 Linux 内核版本 5.13(在 Intel 上)和 6.13(在 AMD 上)开始,Linux 内核会拦截此 #DB 异常并应用缓解措施,通常是通过对有问题的进程进行速率限制。通过有意强制线程休眠,内核可防止单个应用使内存总线饱和,从而以牺牲故障应用的性能为代价,为计算实例的其余部分保留系统性能。

表现

如果 Linux 客户机中的某个进程触发了 CPU 总线锁定,您可能会遇到以下症状:

  • 应用性能下降:CPU 总线锁定可能会导致应用出现意外延迟。
  • 系统范围内的负载峰值:整体系统响应能力可能会下降。
  • 意外的应用崩溃:如果您将内核配置为严格处理拆分或总线锁定 (split_lock_detect=fatal),则有缺陷的应用可能会因 SIGBUS 错误而崩溃。

识别 CPU 总线锁定

如需确定计算实例是否遇到 CPU 总线锁定,请执行以下操作之一:

CPU 总线锁定轨迹示例

x86/split lock detection: #DB: <process_name>/<pid> took a bus_lock trap at address: 0x<address>

如需检测未来的 CPU 总线锁定,请执行以下操作:

  1. 启用串行端口输出日志记录。
  2. 针对以下日志创建基于日志的提醒政策:

    resource.type="gce_instance" log_id("serialconsole.googleapis.com/serial_port_1_output") textPayload=~"took a bus_lock trap"

    此日志条目会提供负责 CPU 总线锁的进程名称 (<process_name>) 和进程 ID (<pid>),以及发生故障的指令指针地址。

排查 CPU 总线锁定问题

如果您正在开发或编译有问题的应用,可以使用特定的 C 或 C++ 编译器警告来识别可能导致拆分锁定的变量和结构。

编译器警告

如果您使用 GCC 或 Clang,请使用以下标志编译代码,以帮助识别对齐问题:

  • -Wcast-align 或 -Wcast-align=strict:当指针转换增加目标所需的对齐方式时,这些标志会发出警告。将通用 char* 缓冲区强制转换为 uint64_t* 并对其执行原子操作是导致拆分锁的经典原因。
  • -Waddress-of-packed-member:此标志会在您获取打包结构体成员的地址时(例如,使用 #pragma pack(1) 或 __attribute__((packed)))发出警告。由于打包结构体忽略了自然内存对齐,因此对打包结构体成员执行的任何原子操作都有很高的概率会跨越 64 字节的缓存行边界。

捕获不可缓存 (UC) 的内存锁定

如果对不可缓存内存的原子操作导致 CPU 总线锁定,编译器警告不会捕获到此问题。此问题通常在与设备内存互动时发生:

  • 审核内存映射:检查您的代码,看看是否使用了带有 O_SYNC 等标志的 mmap,或者直接访问 /dev/mem 或 /dev/uio。
  • 避免在 MMIO 上使用原子操作:不要在映射到设备寄存器或不可缓存内存缓冲区的内存区域上使用 __sync_fetch_and_add 或 std::atomic 等原子操作

修复 CPU 总线锁定

您可以通过修正应用源代码中的内存对齐问题来解决 CPU 总线锁定问题。

  • 避免在包含原子变量、互斥锁或自旋锁的结构上使用 #pragma pack 或 __attribute__((packed))。
  • 使用标准对齐指令(例如 C++11 中的 alignas(64) 或 C 中的 __attribute__((aligned(64))))强制使在原子操作中大量使用的变量与缓存行边界对齐。
  • 确保编译期间没有与对齐相关的警告。
  • 请确保仅在标准可缓存 RAM 上使用标准锁定机制(互斥锁、自旋锁)或原子指令,切勿在 MMIO 或 UC 内存上使用。

如果问题排查步骤未能解决问题,请与 Cloud Customer Care 联系,并提供您在问题排查过程中收集的所有信息。