Linux 内核热插拔深度实战:CPU、内存、PCIe 动态资源管理全景
在线弹性伸缩是云原生基础设施的核心能力。本文从内核源码层面拆解 CPU 热插拔、内存热插拔、PCIe 热插拔三大子系统的实现原理,并给出生产环境中的观测、排障与调优指南。
一、为什么需要热插拔?
数据中心运维中,硬件故障、资源扩容、节能降本是持续挑战。传统做法依赖整机重启来变更物理资源配置,这意味着:
- 服务中断:关键业务(数据库、AI 训练)无法接受分钟级停机
- 资源利用率低:为了避免重启,通常过度配置(over-provision),平均 CPU 利用率仅 15%-30%
- 故障恢复慢:坏内存条、故障 CPU 需要上架换件
热插拔技术(Hotplug)让系统在不重启的前提下,动态增减物理资源。Linux 内核在 2.6 时代引入 CPU/内存热插拔,如今已成为云服务器的标配能力。
二、CPU 热插拔
2.1 基本原理
CPU 热插拔本质上是控制调度器(scheduler)何时开始/停止调度任务到某个 CPU,以及该 CPU 上的中断路由。
内核将 CPU 状态分为:
| 状态 | 含义 |
|---|---|
| CPU_DOWN_PREPARE | 准备下线,停止新任务调度 |
| CPU_DYING | 停止中断、迁移任务 |
| CPU_DEAD | 完全离线,可物理移除 |
| CPU_UP_PREPARE | 准备上线 |
| CPU_UP_CANCELED | 上线失败回滚 |
| CPU_ONLINE | 在线可用 |
2.2 操作接口
通过 sysfs 操作 CPU 热插拔:
# 查看在线 CPU
cat /sys/devices/system/cpu/online
# 输出:0-31(32核在线)
# 下线 CPU 15
echo 0 > /sys/devices/system/cpu/cpu15/online
# 上线 CPU 15
echo 1 > /sys/devices/system/cpu/cpu15/online
# 查看 CPU 状态
cat /sys/devices/system/cpu/cpu15/online
# 0 = 离线,1 = 在线
2.3 内核源码流程
CPU 下线的核心调用链(简化版):
echo 0 > .../online
└─ cpu_down() # kernel/cpu.c
└─ _cpu_down(cpu, 0)
└─ cpuhp_invoke_callback(CPU_DEAD)
└─ 通知调度器停止任务
└─ 迁移中断路由
└─ 关闭本地 APIC
关键函数 take_cpu_down() 是架构相关的回调(x86 实现于 arch/x86/kernel/smpboot.c),它会:
1. 通知 SMT sibling 同步 L1/L2 cache 状态
2. 关闭本地 APIC timer
3. 跳转到 idle 的 MWAIT 指令进入 C3/C6 低功耗状态
2.4 NUMA 拓扑感知
在 NUMA 系统中,CPU 热迁移还涉及内存节点的本地性变化。当 CPU 下线时,其关联的本地内存可能也需要迁移:
# 查看 CPU 所在 NUMA 节点
cat /sys/devices/system/cpu/cpu15/topology/physical_package_id
# 0
lscpu | grep NUMA
# NUMA node0 CPU(s): 0-15,32-47
# NUMA node1 CPU(s): 16-31,48-63
2.5 实战建议
- 生产环境注意:离线 CPU 时,per-CPU 变量(per-CPU runqueue 统计、softirq 计数)会被清零,监控工具可能显示锯齿状波动
- 性能影响:在线/离线 CPU 涉及全局调度器锁争用,建议避开业务峰值
- qemu/kvm 场景:支持 CPU hot-unplug,配合
virsh setvcpus使用
三、内存热插拔
3.1 两种机制
Linux 内核提供两种内存热插拔机制:
| 机制 | 接口 | 粒度 | 典型场景 |
|---|---|---|---|
| Memory Hotplug | sysfs | 内存块(通常 128MB) | 物理 DIMM 热插拔 |
| Memory Ballooning | virtio-balloon | 可变粒度 | 虚拟机动态调整内存 |
3.2 sysfs 接口
# 查看内存块状态
ls /sys/devices/system/memory/memory*/
# 查看状态
cat /sys/devices/system/memory/memory52/state
# "online"、"offline"、"going offline"
# 离线内存块
echo offline > /sys/devices/system/memory/memory52/state
# 在线内存块
echo online > /sys/devices/system/memory/memory52/state
# 查看内存块大小
cat /sys/devices/system/memory/memory52/block_size_bytes
# 134217728 = 128MB
3.3 内部流程
内存在线的关键步骤:
/*
* 简化版 memory_online 流程 (drivers/base/memory.c)
*/
memory_block->state = MEM_GOING_ONLINE;
└─ memory_block->online_callback() // 架构实现
└─ add_memory() # 添加物理内存到 buddy
└─ create_section_mem_map()
└─ __add_pages() # 真正添加到 buddy
└─ online_page() # 标记 PG_active
关键的疑难问题在于:内存下线前必须先迁移页面。
# 生产环境挂载 hugetlbfs 后大页内存无法 offline
# 排查:是否有进程持有大页
cat /proc/meminfo | grep HugePages_
# HugePages_Total: 1024
# HugePages_Free: 0 ← 被全部占用,无法 offline
3.4 自映射(self-mapping)与 vmemmap
x86_64 内核使用 direct mapping(线性映射区)访问物理内存。当热插新增物理内存时,内核需要扩展:
- vmemmap 稀疏内存映射:记录 struct page 的虚拟地址
- direct mapping:将新物理页加入 PAGE_OFFSET 开始的线性映射区
- zone 扩展:更新 zone 的 spanned_pages / present_pages
# 查看 zone 状态
cat /proc/zoneinfo grep -E "Node|zone|spanned|present|managed"
3.5 生产故障排查
内存离线失败通常有以下原因:
| 错误码 | 含义 | 解决方案 |
|---|---|---|
| -EBUSY | 内存被固定(pinned) | 检查是否有内核模块持有、是否有 MAP_FIXED 映射 |
| -EINVAL | 内存已离线 | 无需处理 |
| -ENOMEM | 无法迁移页面 | 检查碎片化、NUMA 绑定 |
| -EAGAIN | 正在尝试 | 稍后重试 |
四、PCIe 热插拔
4.1 硬件基础
PCIe 热插拔依赖硬件支持:
- Attention Button:物理按钮通知控制器拔出请求
- Power Indicator / Attention Indicator:LED 状态指示
- Presence Detect 信号线:检测插卡存在
- Power Controller:控制插槽电源
4.2 内核驱动
Linux PCI 子系统(drivers/pci/hotplug/)提供多种热插拔驱动:
| 驱动 | 适用 |
|---|---|
| pciehp | 标准 PCIe 热插拔控制器 |
| shpchp | 兼容 SHPC 标准(PCI/PCIe) |
| acpiphp | ACPI 方式的热插拔(服务器常用) |
| rpaphp | IBM Power 平台 |
| pnv_php | IBM PowerNV 平台 |
4.3 操作接口
# 查看 PCI 桥插槽
ls /sys/bus/pci/slots/
# 查看插槽状态
cat /sys/bus/pci/slots/0-1/power
cat /sys/bus/pci/slots/0-1/attention
# 上电
echo 1 > /sys/bus/pci/slots/0-1/power
# 下电
echo 0 > /sys/bus/pci/slots/0-1/power
4.4 拔出流程
用户按下 Attention Button 后:
Attention Button Pressed
└─ pciehp_handle_button_press()
└─ pcie_handle_button_press() # pciehp
└─ 通知用户态
└─ pciehp_disable_slot()
└─ 卸载绑定的驱动
└─ 释放 BAR 资源
└─ 关闭插槽电源
4.5 NVMe SSD 热插拔实践
服务器环境中最常见的热插拔是 NVMe U.2/U.3 SSD:
# 列出 NVMe 设备
nvme list
# 识别新插入的 NVMe
dmesg | tail
# nvme nvme1: pci function 0000:82:00.0
# nvme nvme1: 31 model ...
# 安全拔出流程(关键!)
# 1. 卸载文件系统
umount /data
# 2. 解除块设备绑定(通知多路径)
echo 'remove' > /sys/bus/pci/devices/0000:82:00.0/driver/unbind
# 或
echo 1 > /sys/bus/pci/devices/0000:82:00.0/remove
# 3. 确认移除
dmesg | tail
# nvme nvme1: Shutdown timeout set to 10 seconds
4.6 SR-IOV 热插拔与 PF/VF 联动
SR-IOV(Single Root I/O Virtualization)场景下,VF(Virtual Function)的热插拔会影响 PF(Physical Function):
# 查看 PF 的 VF 数量
cat /sys/bus/pci/devices/0000:3b:00.0/sriov_numvfs
# 减少 VF 数量(如 DPDK 场景需要释放 VF)
echo 0 > /sys/bus/pci/devices/0000:3b:00.0/sriov_numvfs
# 增加 VF 数量
echo 8 > /sys/bus/pci/devices/0000:3b:00.0/sriov_numvfs
# 注意:减少 VF 时,已绑定到 vm 的 VF 会触发错误
# 必须先关闭对应虚拟机再调整
五、ACPI / 设备树与上层层通知
5.1 ACPI 热插拔事件
服务器平台通过 ACPI(Advanced Configuration and Power Interface)标准传递热插拔事件:
# 通过 acpid 监听事件
acpi_listen
# 输出示例:
# cpuinden00 00000080 00000001 ← CPU 插入事件
# meminden34 00000080 00000000 00000081 ← 内存事件
内核侧的 ACPI 热插拔通知链路:
ACPI Notify(0x80)
└─ acpi_bus_notify()
└─ acpi_device_hotplug()
└─ 解析 _EJ0(Eject)/_OST(OST 状态)方法
└─ 调用通用 device core 热插拔链
5.2 设备树(Device Tree)热插拔
嵌入式/LoongArch/RISC-V 平台使用设备树传递热插拔:
/* 设备树中定义热插拔节点 */
@memory@20000000 {
device_type = "memory";
reg = <0x20000000 0x4000000>; /* 起始地址 512MB, 大小 64MB */
status = "okay";
hotpluggable;
};
热插拔时,内核调用 of_add_memory() / of_remove_memory()。
六、生产环境最佳实践
6.1 观测工具
# 查看 CPU 在线状态
lscpu | grep -i "on-line\|off-line"
# On-line CPU(s) list: 0-15,32-47
# Off-line CPU(s) list: 16-31,48-63
# 查看内存块
lsmem
# ADDR-RANGE STATE REMOVABLE BLOCK
# 0x0000000000000000-0x00000001fffffff offline 0 yes
# 0x0000000200000000-0x00000003fffffff online 1 yes
# 查看 PCI 拓扑
lspci -tv
# └─08:00.0-Xilinx Corporation Device
6.2 性能陷阱
| 陷阱 | 原因 | 解决方案 |
|---|---|---|
| CPU 下线后 numactr 仍统计 | 残留统计信息 | 清除 /sys 对应节点 |
| 内存下线失败 -EBUSY | DMA buffer 或内核模块 pin 住 | 重启前 echo 3 > /proc/sys/vm/drop_caches |
| PCIe 电源时序异常 | 固件未合规 AC 掉电延迟 | 更新 BIOS/iLO/iDRAC |
| 内存热插 NUMA 失衡 | 新内存被分配到错误节点 | 使用 mempolicy 或 libnumactl 显式绑定 |
6.3 容器/Kubernetes 协同
在 Kubernetes 集群中,热插拔与 kubelet 联动:
# Kubelet 配置 --system-reserved
kubelet --system-reserved=cpu=1,memory=2Gi \
--kube-reserved=cpu=500m,memory=1Gi \
--eviction-hard=memory.available<500Mi
# 当物理内存热移除时,kubelet 自动驱逐 Pod
6.4 与云管平台的融合方案
公有云(AWS Nitro、阿里云神龙)使用自研热插拔框架:
- AWS Nitro:脱卡热拔插通过 Nitro Card Enclave 安全释放
- 阿里云 ACM:通过
memory_block_statesysfs 实现无感缩容 - OpenStack:Nova 调用 libvirt
virsh setmem/virsh setvcpus
七、工程速查表
┌─────────────────────────────────────────────────────────────────┐
│ Linux 热插拔工程速查 │
├─────────────────────────────────────────────────────────────────┤
│ CPU 在线 echo 1 > /sys/devices/system/cpu/cpuN/online │
│ CPU 离线 echo 0 > /sys/devices/system/cpu/cpuN/online │
│ 内存在线 echo online > /sys/devices/system/memory/memN/state │
│ 内存离线 echo offline > /sys/devices/system/memory/memN/state│
│ PCI 上电 echo 1 > /sys/bus/pci/slots/X/power │
│ PCI 下电 echo 0 > /sys/bus/pci/slots/X/power │
├─────────────────────────────────────────────────────────────────┤
│ lscpu | grep -i on/off 查看 CPU 在线状态 │
│ lsmem 查看内存块状态 │
│ dmesg \| tail 查看热插拔日志 │
│ acpi_listen ACPI 事件监听 │
├─────────────────────────────────────────────────────────────────┤
│ 注意:PCI 设备必须先卸载驱动再下电,避免 DMA 残留 │
│ 注意:内存 offline 前需迁移页面,碎片化严重时可能失败 │
│ 注意:KVM 环境需先关机再减少 VF 数量 │
└─────────────────────────────────────────────────────────────────┘
八、总结
Linux 热插拔是基础设施弹性的终极武器,但各子系统的实现复杂度远超日常使用:
- CPU 热插拔涉及调度器、中断控制器、APIC 状态机
- 内存热插拔要求 buddy 分配器线性扩展与页面迁移
- PCIe 热插拔依赖硬件控制器、ACPI 固件、驱动卸载链
在生产环境中,热插拔失败 90% 的原因不是内核不支持,而是资源仍被持有(DMA pin、大页锁定、中断亲和绑定)。排查时始终从 dmesg 和 -EBUSY 错误码入手,先理清谁在用手上的资源。
掌握热插拔不是炫技,而是在刀片上让硬件"活"起来——这是云原生时代对基础软件工程师的硬核要求。

发表评论 取消回复