Linux内核热插拔机制深度实战:从CPU到内存再到PCIe的动态资源管理
在现代数据中心和云计算场景中,服务器的可用性和资源弹性至关重要。一台运行中的Linux服务器能否在不关机的情况下添加CPU核心、扩展内存容量或更换PCIe设备,直接决定了业务的连续性和运维效率。本文将深入Linux内核热插拔(Hotplug)机制的实现细节,涵盖CPU热插拔、内存热插拔和PCIe热插拔三大核心子系统,并结合实战代码和调试技巧帮助读者构建系统性的理解。
一、热插拔机制的内核基础设施
Linux内核的热插拔不是某个单一模块的能力,而是多个子系统协作的结果。其核心设计围绕"设备树变更通知"这一模型展开。
1.1 uevent机制:用户空间与内核的桥梁
当内核检测到硬件变动(如CPU上线、内存块添加、PCIe设备插入)时,通过kobject_uevent()向用户空间发送NETLINK uevent事件。用户空间的udev/mdadm等守护进程监听这些事件并执行相应策略。
// 内核源码:lib/kobject_uevent.c
int kobject_uevent(struct kobject *kobj, enum kobject_action action)
{
return kobject_uevent_env(kobj, action, NULL);
}
// uevent 通过 NETLINK_KOBJECT_UEVENT 协议族广播
// 用户空间通过 socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT) 接收
1.2 内存屏障与热插拔的同步原语
热插拔操作涉及多核之间的状态一致性。内核使用stop_machine()机制暂停所有CPU以确保在执行关键热插拔操作时没有其他处理器访问相关数据结构:
// stop_machine: 让所有CPU执行指定函数
int stop_machine(int (*fn)(void *), void *data, const struct cpumask *cpus)
{
// 1. 发送IPI中断暂停所有目标CPU
// 2. 各CPU执行fn回调
// 3. 恢复CPU运行
}
stop_machine()是热插拔同步的核心手段——它提供了"伪原子"的上下文,保证在热插拔期间数据结构的修改不会被并发访问破坏。
二、CPU热插拔工程实战
2.1 CPU热插拔的状态机模型
Linux通过cpu_online_mask和cpu_active_mask两个位图管理CPU状态。CPU热插拔遵循严格的状态转换:
CPU_OFFLINE → CPUHP_TEARDOWN_CPU → CPUHP_BRINGING_CPU_UP → CPUHP_AP_ONLINE → CPU_ONLINE
CPU_ONLINE → CPUHP_AP_OFFLINE → CPUHP_TEARDOWN_CPU → CPU_OFFLINE
这些状态通过cpuhp_cpu_state枚举定义(位于include/linux/cpuhotplug.h),是CPUHP_AP_ONLINE_DYN动态分配的状态槽位的关键API。
2.2 CPU热插拔回调注册实战
设备驱动需要注册cpuhp_setup_state()来响应CPU热插拔事件:
#include <linux/cpuhotplug.h>
static int my_driver_cpu_online(unsigned int cpu)
{
struct my_data *data = per_cpu_ptr(&my_percpu_data, cpu);
// 为新上线的CPU分配本地数据
data->buffer = kzalloc_node(BUFF_SIZE, GFP_KERNEL, cpu_to_node(cpu));
if (!data->buffer)
return -ENOMEM;
// 初始化该CPU的定时器和队列
timer_setup(&data->poll_timer, my_poll_callback, 0);
pr_info("MyDriver: CPU%u online, NUMA node %d\n",
cpu, cpu_to_node(cpu));
return 0;
}
static int my_driver_cpu_offline(unsigned int cpu)
{
struct my_data *data = per_cpu_ptr(&my_percpu_data, cpu);
// 迁移待处理任务到其他CPU
synchronize_rcu();
del_timer_sync(&data->poll_timer);
kfree(data->buffer);
pr_info("MyDriver: CPU%u offline\n", cpu);
return 0;
}
static int __init my_driver_init(void)
{
int ret;
// 注册CPU热插拔状态回调
// CPUHP_AP_MY_DRIVER_ONLINE: 在arch级别初始化完成后回调
ret = cpuhp_setup_state(CPUHP_AP_ONLINE_DYN,
"my_driver:online",
my_driver_cpu_online,
my_driver_cpu_offline);
if (ret < 0)
return ret;
driver_online_state = ret;
return 0;
}
2.3 CPU热插拔的性能陷阱与实践建议
陷阱一:per-CPU数据访问与CPU离线竞态
CPU离线时,若其他代码路径仍访问该CPU的per-CPU数据,会导致use-after-free。正确做法是使用get_cpu()/put_cpu()配对或rcu_read_lock()保护:
// 错误做法:直接访问其他CPU的per-CPU数据
void buggy_access(unsigned int target_cpu)
{
struct my_data *data = per_cpu_ptr(&my_percpu_data, target_cpu);
// 如果target_cpu此刻已离线,data可能被释放!
process(data);
}
// 正确做法:使用CPU热插拔通知链
static int my_cpu_notifier(struct notifier_block *nb,
unsigned long action, void *hcpu)
{
unsigned int cpu = (unsigned long)hcpu;
switch (action) {
case CPU_DEAD:
// 清理该CPU相关的RCU回调
flush_all_rcux(cpu);
break;
}
return NOTIFY_OK;
}
陷阱二:中断路由与CPU离线
对于MSI-X中断,需要在CPU离线前将目标中断重新路由到其他在线CPU,否则中断会丢失:
// 使用 irq_set_affinity_hint 迁移中断
static void migrate_irqs_from_cpu(unsigned int offlining_cpu)
{
struct irq_desc *desc;
unsigned int irq;
for_each_irq_desc(irq, desc) {
struct irq_data *data = irq_desc_get_irq_data(desc);
const struct cpumask *affinity = data->common->affinity;
if (cpumask_test_and_clear_cpu(offlining_cpu, affinity)) {
cpumask_copy(affinity, cpu_active_mask); // 迁移到活跃CPU
irq_set_affinity(irq, affinity);
}
}
}
三、内存热插拔机制深度解析
3.1 内存热插拔的硬件前提
内存热插拔需要ACPI(通常是ACPI 5.0+的_HID:EISAID(PNP0C80)内存设备描述)或设备树中正确的#address-cells/#size-cells配置。内核配置必须开启CONFIG_MEMORY_HOTPLUG和CONFIG_MEMORY_HOTREMOVE。
3.2 内存块与Section架构
Linux将物理内存划分为Section(通常为128MB或2GB粒度),称为"memory block"。热插拔的最小单位是一个memory block:
# 查看内存块状态
cat /sys/devices/system/memory/memory0/state # "online" 或 "offline"
# 查看内存块的物理地址范围
cat /sys/devices/system/memory/memory0/phys_index # 物理section编号
cat /sys/devices/system/memory/memory0/valid_zones # 所属NUMA zone
# 手动触发内存热移除
echo offline > /sys/devices/system/memory/memory5/state
3.3 内存热插拔的页表与映射管理
当内存上线时,内核需要建立页表映射。核心流程在memory_block_online()中:
// mm/memory_hotplug.c - 简化版核心流程
static int memory_block_online(struct memory_block *mem)
{
unsigned long start_pfn = section_nr_to_pfn(mem->start_section_nr);
unsigned long nr_pages = PAGES_PER_SECTION;
struct zone *zone;
int ret;
// 1. 验证zone约束:确保内存与相邻block属于可兼容的zone
zone = page_zone(pfn_to_page(start_pfn));
ret = zone_spans_memory(start_pfn, nr_pages, zone);
if (ret)
return -EINVAL;
// 2. 迁移页面类型(如果有现有页面)
ret = online_pages(start_pfn, nr_pages, zone);
if (ret)
return ret;
// 3. 设置页框状态:将struct page标记为可分配
// 4. 将zone的managed_pages增加
// 5. 通知buddy allocator新页面可用
// 6. 触发uevent通知用户空间
memory_notify(MEM_ONLINE, NULL);
return 0;
}
3.4 内存热移除的关键挑战:页面迁移
内存热移除是三个热插拔中风险最大的操作——必须保证被移除内存上无任何活跃页面:
// 内存热移除的核心:页面迁移
static int offline_pages(unsigned long start_pfn, unsigned long nr_pages)
{
// 1. 隔离内存区域:设置PG_reserved,阻止新分配
// 2. 迁移所有LRU页面到目标zone
// 3. 处理不可迁移页面(mlocked、per-CPU、page table等)
// 4. 处理失败时回滚(re-online)
// 核心迁移逻辑:
do {
ret = migrate_pages(&source_pages, alloc_migration_target,
NULL, 0, MIGRATE_SYNC, MR_MEMORY_HOTPLUG);
} while (ret);
// 如果有不可移动页面,则hot-remove失败
}
实战提示:内存热移除失败通常因为存在mlock()锁定的页面或KASAN shadow内存。建议:
- 在线内存前使用madvise(addr, len, MADV_DONTNEED)释放不必要页面
- 避免在可热插拔内存区域使用MAP_LOCKED的mmap
- /proc/sys/vm/compact_memory可在热插拔前手动压缩减少碎片
四、PCIe热插拔子系统
4.1 PCIe热插拔的硬件信号
PCIe热依赖Slot Attention Button(按钮)、Presence Detect(在位检测)、Attention LED(指示灯)等硬件信号。内核通过pciehp(PCI Express Native Hotplug)驱动与硬件交互。
关键信号时序:
1. 用户按下Slot Attention Button → 触发SERR/NMI中断
2. 内核pciehp驱动捕获中断,确认设备在位状态
3. 点亮Attention LED,等待操作窗口
4. 根据写入状态执行:power on / power off
4.2 pciehp驱动工作流程
// drivers/pci/hotplug/pciehp_hpc.c - 中断处理核心
static irqreturn_t pciehp_isr(int irq, void *dev_id)
{
struct controller *ctrl = (struct controller *)dev_id;
u32 slot_status;
// 读取Slot状态寄存器
pcie_readw(ctrl, PCI_EXP_SLTSTA, &slot_status);
// 检测事件类型
if (slot_status & PCI_EXP_SLTSTA_PDC) // Presence Detect Changed
ctrl->button_press = true;
if (slot_status & PCI_EXP_SLTSTA_PDC) // Data Link Layer State Changed
ctrl->link_active_changed = true;
// 唤醒工作队列处理热插拔事件
queue_work(ctrl->wq, &ctrl->handle_button);
return IRQ_HANDLED;
}
// 工作队列执行实际的on/off操作
static void handle_button(struct work_struct *work)
{
if (presence_detect_changed()) {
if (card_present()) {
// 上电、训练链路、枚举设备
pciehp_power_on_slot(ctrl);
pciehp_check_link_active(ctrl);
pci_scan_child_bus(ctrl->pcie->port->bus);
} else {
// 从总线移除设备
pci_stop_and_remove_bus_device(card);
pciehp_power_off_slot(ctrl);
}
}
}
4.3 NVMe SSD热插拔实战示例
以NVMe SSD热插拔为例,完整流程如下:
# 1. 查看当前NVMe设备
ls /sys/block/ | grep nvme
# nvme0n1
# 2. 触发PCIe热移除(通过sysfs)
echo 1 > /sys/bus/pci/devices/0000:3c:00.0/remove
# 3. 内核执行:pci_stop_bus_device → nvme_remove → 销毁队列
# 4. 更换物理SSD(硬件操作)
echo 1 > /sys/bus/pci/devices/0000:3c:00.0/rescan # 重新扫描
# 触发config space读取 → 设备枚举 → NVMe驱动probe → 新命名空间注册
五、内核通知链(Notifier Chain)的统一治理
三大热插拔子系统通过Linux内核的Notifier Chain机制向驱动和子系统广播事件。
5.1 CPU热插拔通知链
// 注册CPU事件通知
static struct notifier_block my_cpu_nb = {
.notifier_call = my_cpu_callback,
.priority = 0, // 优先级,高优先级先执行
};
// 注册到CPU通知链
register_cpu_notifier(&my_cpu_nb);
5.2 内存热插拔通知链
// 内存热插拔通知链注册
hotplug_memory_notifier(my_memory_callback, MY_MEMORY_PRIO);
static int my_memory_callback(struct notifier_block *self,
unsigned long action, void *arg)
{
switch (action) {
case MEM_GOING_ONLINE:
// 为新上线内存预分配资源
return mem_reserve_region(arg);
case MEM_GOING_OFFLINE:
// 释放特定于该内存区域的资源
case MEM_OFFLINE:
// 内存已下线,更新本地映射表
mem_remove_from_map(arg);
}
return NOTIFY_OK;
}
5.3 PCI设备热插拔通知
// 通过pci_register_driver的error_handlers回调
static const struct pci_error_handlers my_err_handler = {
.error_detected = my_error_detected, // D-state错误
= my_slot_reset, // 槽位复位
= my_mmio_enabled, // MMIO重新使能
= my_link_reset, // 链路复位
};
六、调试与性能分析
6.1 ftrace追踪热插拔事件
# 追踪CPU热插拔事件
echo 1 > /sys/kernel/debug/tracing/events/cpuhp/enable
cat /sys/kernel/debug/tracing/trace_pipe
# 输出示例:
# cpuhp_enter: cpu=0x1 cpuhp_state=0x30 (CPUHP_TEARDOWN_CPU)
# cpuhp_exit: cpu=0x1 cpuhp_state=0x30 error=0
# 追踪内存热插拔
echo 1 > /sys/kernel/debugtracing/events/memory_hotplug/enable
6.2 使用crash工具分析热插拔故障
# 查看CPU热插拔状态
crash> p cpuhp_tasks
# 查看各状态的任务队列
# 查看内存block状态
crash> p mem_section
crash> memory_block.state
6.3 BPF监控热插拔延迟
// BPF程序:测量CPU热插拔各阶段延迟
SEC("tp_brfm/cpuhp_enter")
int trace_cpuhp_enter(struct trace_event_raw_cpuhp_enter *ctx)
{
u32 cpu = bpf_get_smp_processor_id();
u64 ts = bpf_ktime_get_ns();
u32 state = ctx->state;
bpf_map_update_elem(&start_timestamps, &state, &ts, BPF_ANY);
return 0;
}
SEC("tp_btf/cpuhp_exit")
int trace_cpuhp_exit(struct trace_event_raw_cpuhp_exit *ctx)
{
u32 state = ctx->state;
u64 *start_ts = bpf_map_lookup_elem(&start_timestamps, &state);
if (start_ts) {
u64 delta = bpf_ktime_get_ns() - *start_ts;
// 记录到histogram map,输出直方图
hist_record(&cpuhp_latency, delta / 1000); // 微秒
}
return 0;
}
七、总结与工程实践建议
Linux内核热插拔机制涉及跨子系统的复杂协作,关键要点总结如下:
| 操作 | 风险等级 | 关键API | 关键约束 |
|---|---|---|---|
| CPU离线 | 中 | cpuhp_setup_state() |
消除所有per-CPU资源依赖 |
| 内存上线 | 低 | online_pages() |
保证zone连续性 |
| 内存离线 | 高 | offline_pages() |
必须迁移或回收所有页面 |
| PCIe插拔 | 中 | pciehp驱动 |
需要硬件支持,链路训练时间不确定 |
核心设计原则:
1. 失败安全:所有热插拔操作必须支持回滚,half-hotplugged状态不能泄露给用户
2. NUMA感知:资源分配和迁移必须考虑NUMA拓扑,优先在本地节点操作
3. 最小化停机窗口:使用stop_machine()的时间尽量短,长时间操作切出stop_machine上下文
4. uevent一致性:热插拔成功后必须向用户空间发送uevent,确保mdadm/lvm/multipath等层同步
通过理解这些底层机制,系统工程师可以设计更可靠的在线运维方案,实现真正意义上的"零停机"资源管理。

发表评论 取消回复