Linux 内核 | CPU 热插拔(Hotplug)


引言

CPU 热插拔[63] 是 Linux 中一种广泛使用于支持处理器集缩放的机制。它们被设计用于两种用途:移除出现故障的 CPU 或动态增加 CPU 计算资源和动态更改虚拟机配置的 CPU 数量。

现代先进的系统架构已经引进了具有先进的错误报告和纠错能力的处理器。有一些原始设备生产商(Original Equipment Manufacturer,OEM)也支持可热插拔的NUMA硬件,其中物理节点的插入与移除要求 CPU 热插拔的支持。这样的升级需要删除 kernel 当前可用的 CPU,要么是出于配置(Provisioning)的原因,要么是出于RAS(Reliability、Availability、Serviceability)的目的,以使有问题的 CPU 远离系统执行路径。因此,Linux内核需要CPU热插拔支持。

CPU热插拔支持在系统启动后,关闭任意一个secondary cpu(在 ARM64 架构下,CPU0 为boot cpu,不能被关闭),并在需要时重新启动它。例如,在内核选项CONFIG_HOTPLUG_CPU使能的前提下,通过如下命令可以将 CPUx 移除(offline)或加入(online)到当前内核的管理下。

echo 0 > /sys/devices/system/cpu/cpu1/online
echo 1 > /sys/devices/system/cpu/cpu1/online

毕业设计涉及到 Linux 内核 CPU 热插拔的实现修改,所以当时在 CPU 热插拔花了许多时间,现将基本的知识总结一下。

CPU 热插拔的基本功能

具体来说,CPU 热插拔需要完成以下基本功能。

状态迁移。当 CPU 被移除时,与该 CPU 相关的重要软件状态必须迁移到另一个活跃的 CPU 上。软件状态通常包括软中断以及 CPU 运行队列中的线程。系统中的软中断和当前 CPU 上运行的线程都保存在 per-CPU 队列中, 软中断需要被移动到其他活跃的 CPU 以进行处理,运行队列中的线程也会同步 迁移以避免运行程序的性能下降。但在 per-CPU 的实现下,每个 CPU 都存在一份副本,当发生 CPU 热插拔时就需要为目标 CPU 重新分配或释放 per-CPU 结构 实例,这一定程度上增大了 CPU 热插拔的时间开销。

硬件管理与中断迁移。在 CPU 热插拔事件期间,需要禁用或启用某些硬件 相关功能。例如,CPU 进入离线状态前必须禁用机器检查(例如,停止看门狗线程的执行),因为离线 CPU 中的任何故障都不应影响其余系统。同时,当 CPU 被移除时,关联到当前 CPU 的中断需要被移动到其他活跃的 CPU 上进行处理以 保证系统的正常运行。

系统状态更新。

  • 关键全局变量的更新。例如,Linux 维护着一些重要的 CPU 状态全局位掩码,它们在系统中被频繁访问。这些掩码应在缩放事件期间 立即更新,以确保系统正常运行。
  • 系统文件系统(sysfs)的更新。sysfs 和 procfs 等易失性文件系统通过 Linux 中的虚拟文件系统接口提供对内核配置和获 取内核的状态。sysfs 中包含的基于 CPU 核的文件或目录节点在 CPU 集缩放期间 需要被删除或创建。
  • 内存管理。多数内核子系统在收到缩放事件通知后会释 放或分配内存结构,例如 per-CPU 变量或缓冲区队列。
  • 线程停放(Parking)。 新版本的 Linux 使用线程停放来无限期地挂起线程。在 CPU 缩放期间,系统线 程(例如看门狗线程)会被置为停放状态。

CPU 的移除过程

CPU 热插拔子系统为其他内核子系统提供接口来订阅处理器集更改的通知。当 CPU 热插拔事件发生时,其他子系统注册的通知函数将被调用从而进行相应的处理以保证系统在 CPU 移除或加入时系统能感知并维持正常的运行。通过读取 proc 文件/sys/devices/system/cpu/hotplug/states的内容可查看系统内所有注册的回调函数。

tail /sys/devices/system/cpu/hotplug/states

一次 CPU 移除过程需要经过 CPU_DOWN_PREPARE 、CPU_DYING 、CPU_DEAD 和 CPU_POST_DEAD 总共四个阶段。具体来说,CPU 热插拔过程由各个内核子系统注册的回调函数组成。当 CPU 移除事件发生时,各子系统注册的回调函数被调用来为系统 CPU 集的变化做相 应的操作。CPU 热插拔每个阶段对应多个步骤,每个步骤主要由子系统注册回 调函数构成。不同平台的硬件差异性和不同 Linux 内核版本可能导致系统内各子 系统注册的回调函数存在一些区别。在本文的实验环境下(硬件平台树莓派 4B 和 Linux 内核 5.10 版本),CPU 热插拔各阶段所对应的回调函数按执行顺序(从左至右再顺序向下)排列如下图所示。

内核源码分析

热插拔线程

CPU 热插拔的关键实现源码分为两个部分:

  • 通用实现

    • include/linux/cpuhotplug.h
    • kernel/smpboot.c
    • kernel/smpboot.h
    • kernel/cpu.c
  • 处理器相关

    • arch/arm64/kernel/smp.c

前面提到,CPU 热插拔执行回调过程部分回调函数由待移除的目标 CPU 完成。该实现的原理是在系统启动时,内核调用cpuhp_threads_init()函数在每个 CPU 上都创建了一个热插拔线程。

void __init cpuhp_threads_init(void)
{
    BUG_ON(smpboot_register_percpu_thread(&cpuhp_threads));
    kthread_unpark(this_cpu_read(cpuhp_state.thread));
}

具体来说,smpboot_register_percpu_thread() 函数调用kthread_create_on_cpu 创建了名如cpuhp/%u的线程,如下所示:

pi@raspberrypi:~ $ ps -ef | grep cpuhp
root          15       2  0 Oct07 ?        00:00:00 [cpuhp/0]
root          16       2  0 Oct07 ?        00:00:00 [cpuhp/1]
root          21       2  0 Oct07 ?        00:00:00 [cpuhp/2]
root          26       2  0 Oct07 ?        00:00:00 [cpuhp/3]

CPU Down

接下来分析一下 CPU offline 的过程。如上所示,我们可以通过在用户态写入 proc 文件/sys/devices/system/cpu/cpu1/online来触发 CPU offline 事件。对应的内核函数调用为cpu_down()->cpu_down_maps_locked()-> _cpu_down()_cpu_down()的函数定义如下:

  • 首先唤醒对应 CPU 热插拔线程完成CPUHP_TEARDOWN_CPU之前的所有回调函数,通过设定热插拔 target 为CPUHP_TEARDOWN_CPU实现。CPU_DOWN_PREPARE和CPU_DYING 阶段对应的回调函数。

    • 若目标 CPU 执行发生错误就返回错误码;
    • 目标 CPU 执行其需要做的回调函数成功则继续;
  • 由当前 CPU 完成剩余的 CPU 热插拔的回调函数,CPU_DEAD 和 CPU_POST_DEAD 阶段对应的回调函数。

    static int __ref _cpu_down(unsigned int cpu, int tasks_frozen,
                 enum cpuhp_state target)
    {
      struct cpuhp_cpu_state *st = per_cpu_ptr(&cpuhp_state, cpu);
      int prev_state, ret = 0;
      cpus_write_lock();
      cpuhp_tasks_frozen = tasks_frozen;
      prev_state = cpuhp_set_state(st, target);
      
      // 如果当前状态在目标CPU待执行热插拔线程的范围内,我们就需要唤醒 CPU 热插拔线程
      if (st->state > CPUHP_TEARDOWN_CPU) {
          st->target = max((int)target, CPUHP_TEARDOWN_CPU);
          ret = cpuhp_kick_ap_work(cpu);
          // 目标 CPU 执行发生错误的回滚,就返回错误码
          if (ret)
              goto out;
          // 可能已经停止在 AP hotplug 线程范围内,即 target 设置为大于 CPUHP_TEARDOWN_CPU 步骤
          if (st->state > CPUHP_TEARDOWN_CPU)
              goto out;
          st->target = target;
      }
      // 目标 CPU 将自己带到了 CPUHP_TEARDOWN_CPU 状态了,我们需要进一步cleanups
      ret = cpuhp_down_callbacks(cpu, st, target);
      if (ret && st->state == CPUHP_TEARDOWN_CPU && st->state < prev_state) {
          cpuhp_reset_state(st, prev_state);
          __cpuhp_kick_ap(st);
      }
    
    out:
      ...
    }

    Linux 内核 CPU 热插拔的实现中,前两个阶段(CPU_DOWN_PREPARE 和 CPU_DYING)由待移除的目标 CPU 完成,然后目标 CPU 进入到离线状态。系统中其他在线的 CPU 完成剩余的两个阶段来结束 CPU 移除的完整过程。

参考链接

  1. CPU hotplug in the Kernel
  2. 图解 CPU hot plug 流程
  3. CPU hotplug in the Kernel
  4. Linux 内核 | CPU 状态管理
  5. Linux 内核 | Per CPU 变量

声明:一丁点儿的网络日志|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Linux 内核 | CPU 热插拔(Hotplug)


勿在浮沙筑高台,每天进步一丁点儿!