第4章:NVMe热插拔实战:驱动处理流程与监控脚本
好,咱们今天来点真格的。前面几章讲了热插拔的原理和硬件信号,这一章我们直接杀进Linux内核源码,看看NVMe驱动到底是怎么处理热插拔的。我会带着你一步步分析代码路径,最后再写个实用的监控脚本。
4.1 NVMe驱动的热插拔处理流程
先说说整体流程。NVMe热插拔,说白了就是两个动作:热添加和热移除。驱动层怎么感知到这两个动作?嗯,这里有两个关键路径。
路径一:PCIe热插拔控制器中断
当你在机器上插拔NVMe盘时,PCIe槽位上的热插拔控制器会检测到状态变化。这个控制器会触发一个中断,然后PCIe核心层开始处理。流程大概是这样的:
- PCIe热插拔驱动检测到槽位状态变化
- 调用pciehp_handle_presence_or_link_change()
- 最终触发pci_bus_add_device()或pci_bus_remove_device()
- NVMe驱动收到通知,开始处理
路径二:NVMe驱动自己的轮询机制
有些场景下,PCIe热插拔控制器可能不靠谱。我记得有一次在某个服务器平台上,热插拔中断就是没触发,结果盘插上去系统完全没反应。所以NVMe驱动还保留了一个轮询机制——定期检查设备是否还活着。
核心要点:NVMe热插拔依赖两层机制——底层PCIe热插拔框架 + 上层NVMe驱动自身的健康检查。两层都靠谱,才能保证不掉盘。
4.2 热添加的代码路径分析
咱们直接看代码。NVMe驱动中,热添加的入口函数是nvme_probe()。但在这之前,PCIe核心已经帮我们做了很多事。
// drivers/nvme/host/pci.c
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct nvme_dev *dev;
int result;
// 1. 分配设备结构体
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
// 2. 初始化PCI设备
result = pcim_enable_device(pdev);
if (result)
goto out_free;
// 3. 设置DMA掩码
result = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (result)
goto out_free;
// 4. 映射BAR空间
dev->bar = pcim_iomap(pdev, 0, 0);
if (!dev->bar) {
result = -ENOMEM;
goto out_free;
}
// 5. 初始化队列
result = nvme_init_queue(dev, 0);
if (result)
goto out_free;
// 6. 注册字符设备
result = nvme_register_device(dev);
if (result)
goto out_free;
// 7. 创建内核线程处理IO
dev->thread = kthread_run(nvme_worker, dev, "nvme%d", dev->instance);
return 0;
out_free:
kfree(dev);
return result;
}
你看,这个流程其实挺清晰的。我个人习惯把nvme_probe()分成三个阶段来看:
- 硬件初始化阶段:使能PCI设备、设置DMA、映射BAR空间。这一步如果失败,后面就不用看了。
- 队列初始化阶段:创建管理队列和IO队列。NVMe协议的核心就是队列,队列建不起来,盘就是废的。
- 设备注册阶段:注册字符设备、创建内核线程。这时候用户空间才能看到/dev/nvme0n1。
避坑指南:我曾经遇到过一个问题——热添加时nvme_probe()执行成功了,但lsblk就是看不到盘。查了半天发现是udev规则没触发,导致设备节点没创建。所以热添加后,别忘了检查udev是否正常工作。
4.3 热移除的代码路径分析
热移除比热添加要复杂一些,因为涉及到数据安全。你不能直接拔盘,得先让驱动知道盘要走了。
// drivers/nvme/host/pci.c
static void nvme_remove(struct pci_dev *pdev)
{
struct nvme_dev *dev = pci_get_drvdata(pdev);
// 1. 设置移除标志,停止新的IO请求
set_bit(NVME_CTRL_REMOVING, &dev->flags);
// 2. 取消所有未完成的IO请求
nvme_cancel_requests(dev);
// 3. 销毁IO队列
nvme_free_queues(dev);
// 4. 注销字符设备
nvme_unregister_device(dev);
// 5. 停止内核线程
kthread_stop(dev->thread);
// 6. 释放资源
pcim_iounmap(pdev, dev->bar);
pci_disable_device(pdev);
kfree(dev);
}
这里有个关键点——NVME_CTRL_REMOVING标志。一旦这个标志被设置,所有新来的IO请求都会被拒绝。为什么要这样做?你想想看,如果盘已经物理上拔掉了,但还有IO请求在排队,那系统就会卡死。
我记得有一次调试一个热移除问题,发现盘拔掉后系统直接panic了。追查下来,是因为nvme_cancel_requests()没处理好,有个请求一直卡在那里,导致后续流程走不下去。后来我加了个超时机制,超过5秒还没取消的请求直接强制完成。
警告:热移除时,如果盘上有未刷入的数据,这些数据会丢失。所以生产环境中,一定要确保文件系统已经umount,或者使用带电容的NVMe盘(比如企业级盘),它们能在掉电瞬间把缓存数据刷入NAND。
4.4 实战:编写热插拔监控脚本
好了,理论说完了,咱们来写个实用的监控脚本。这个脚本可以实时监控NVMe盘的热插拔事件,并记录日志。
#!/bin/bash
# nvme_hotplug_monitor.sh
# 监控NVMe热插拔事件
LOG_FILE="/var/log/nvme_hotplug.log"
UDEV_RULES_FILE="/etc/udev/rules.d/99-nvme-hotplug.rules"
# 创建udev规则
create_udev_rules() {
cat > $UDEV_RULES_FILE << EOF
# NVMe热添加事件
ACTION=="add", SUBSYSTEM=="nvme", RUN+="/usr/local/bin/nvme_hotplug_handler.sh add %k"
# NVMe热移除事件
ACTION=="remove", SUBSYSTEM=="nvme", RUN+="/usr/local/bin/nvme_hotplug_handler.sh remove %k"
EOF
echo "udev规则已创建: $UDEV_RULES_FILE"
udevadm control --reload-rules
echo "udev规则已重载"
}
# 创建处理脚本
create_handler_script() {
cat > /usr/local/bin/nvme_hotplug_handler.sh << 'EOF'
#!/bin/bash
ACTION=$1
DEVICE=$2
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
LOG_FILE="/var/log/nvme_hotplug.log"
# 获取设备信息
if [ "$ACTION" = "add" ]; then
# 等待设备稳定
sleep 2
# 获取设备详细信息
if [ -e "/dev/$DEVICE" ]; then
MODEL=$(nvme id-ctrl /dev/$DEVICE | grep "mn" | awk -F: '{print $2}')
SERIAL=$(nvme id-ctrl /dev/$DEVICE | grep "sn" | awk -F: '{print $2}')
SIZE=$(nvme id-ctrl /dev/$DEVICE | grep "nsze" | awk -F: '{print $2}')
echo "[$TIMESTAMP] 热添加: $DEVICE" >> $LOG_FILE
echo " 型号: $MODEL" >> $LOG_FILE
echo " 序列号: $SERIAL" >> $LOG_FILE
echo " 容量: $SIZE 扇区" >> $LOG_FILE
# 检查是否已经挂载
if ! mount | grep -q "/dev/$DEVICE"; then
echo " 设备未挂载,建议手动挂载" >> $LOG_FILE
fi
else
echo "[$TIMESTAMP] 热添加: $DEVICE (设备节点未就绪)" >> $LOG_FILE
fi
elif [ "$ACTION" = "remove" ]; then
echo "[$TIMESTAMP] 热移除: $DEVICE" >> $LOG_FILE
# 检查是否有未完成的IO
if [ -e "/sys/block/$DEVICE/stat" ]; then
IO_INFLIGHT=$(cat /sys/block/$DEVICE/inflight)
echo " 未完成IO: $IO_INFLIGHT" >> $LOG_FILE
fi
fi
EOF
chmod +x /usr/local/bin/nvme_hotplug_handler.sh
echo "处理脚本已创建: /usr/local/bin/nvme_hotplug_handler.sh"
}
# 启动监控
start_monitoring() {
echo "开始监控NVMe热插拔事件..."
echo "日志文件: $LOG_FILE"
echo "按Ctrl+C停止监控"
# 使用inotify监控sysfs
while true; do
if [ -d /sys/class/nvme ]; then
CURRENT_DEVICES=$(ls /sys/class/nvme/ 2>/dev/null | wc -l)
if [ "$CURRENT_DEVICES" -ne "$PREVIOUS_DEVICES" ] 2>/dev/null; then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] NVMe设备数量变化: $PREVIOUS_DEVICES -> $CURRENT_DEVICES" >> $LOG_FILE
fi
PREVIOUS_DEVICES=$CURRENT_DEVICES
fi
sleep 5
done
}
# 主菜单
case "$1" in
install)
create_udev_rules
create_handler_script
echo "安装完成"
;;
start)
start_monitoring
;;
status)
echo "当前NVMe设备列表:"
ls -la /dev/nvme* 2>/dev/null || echo "没有发现NVMe设备"
echo ""
echo "最近的热插拔事件:"
tail -20 $LOG_FILE 2>/dev/null || echo "日志文件不存在"
;;
*)
echo "用法: $0 {install|start|status}"
exit 1
;;
esac
这个脚本有三个主要功能:
- install:创建udev规则和处理脚本,这样每次热插拔都会自动记录日志
- start:启动实时监控,每5秒检查一次设备数量变化
- status:查看当前设备状态和最近的热插拔事件
使用建议:我个人习惯把这个脚本加到crontab里,每5分钟执行一次status检查。如果发现设备数量异常,就发告警邮件。这样即使热插拔事件没被udev捕获,也能通过轮询发现异常。
4.5 知识体系总览
为了让你对整个热插拔流程有个直观的认识,我画了张图:
这张图把整个热插拔流程分成了四个层次。从底层的物理检测,到PCIe核心层的事件分发,再到NVMe驱动的具体处理,最后到用户空间的监控响应。每一层都有各自的职责,缺一不可。
好了,这一章的内容就到这里。代码路径分析清楚了,监控脚本也写好了,你可以直接拿去用。下一章我们会深入NVMe的电源管理,讲讲怎么在性能和功耗之间找到平衡点。
公众号:蓝海资料掘金营,微信deep3321