pcie设备卸载没有响应

博客详细记录了一位工程师排查用户在使用阿里云镜像时遇到的磁盘卸载问题。问题源于grub内核启动参数`pcie_aspm=off`导致的中断注册缺失,影响了PCIe设备的热插拔。通过删除用户的crontab任务,确认了问题与中断注册有关。解决方案是修改grub配置,启用相关中断。此外,还探讨了不同云厂商间虚拟化技术的差异,并通过更换主板型号验证了解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

用户问题

客户报障磁盘卸载不了,客户反馈他们这个镜像是从阿里云拿过来的,在其他云可以卸载,在我们的云上一直卸载不了。用户用这个镜像创建了不少机器,都出现了卸载磁盘失败的报警,导致收到不少报警。

只要qemu收到卸载请求了,如果没有卸载成功,基本都是guest内部没有响应,可是这个用户反馈这个镜像在阿里云可以正常卸载。

排查过程

一、查看系统日志

        /var/log/messages 日志里没有找到卸载磁盘相关的日志,说明虚拟机完全没有响应卸载磁盘的请求。

Jan 10 02:41:40 instance-2ro0jdws kernel: pciehp 0000:00:02.2:pcie004: Slot(0-2): Attention button pressed
Jan 10 02:41:40 instance-2ro0jdws kernel: pciehp 0000:00:02.2:pcie004: Slot(0-2): Powering off due to button press
Jan 10 02:41:47 instance-2ro0jdws kernel: pciehp 0000:00:02.2:pcie004: Slot(0-2): Link Up
Jan 10 02:41:47 instance-2ro0jdws kernel: pciehp 0000:00:02.2:pcie004: Slot(0-2): No adapter

二、查看中断信息

 客户虚拟机中断 cat /proc/interrupts 

 对比正常虚拟机中断 

能明显发现客户的虚拟机少了24-35号中断

lspci查看出问题虚拟机的设备,发现磁盘块设备也已经有了

 查看设备对应的中断

所以现在的问题是有块设备,但是对应的块设备中断没有注册。

三、查看内核代码

1、查看 /proc/interrupts 对应的内核代码

对应的函数主要  show_interrupts,我需要从这个函数里查看中断信息是从哪个变量里取出来的。

这个函数的意思大概是:获取每个中断号的信息并显示; 对于 0 <= i < NR_IRQS 的中断,调用 irq_to_desc() 获取中断的信息,并打印每个 CPU 对应的统计数量 kstat_irqs_cpu().

遍历0到NR_IRQS,然后从 irq_to_desc函数里获取注册的中断信息,所以接下来看 irq_to_desc 

int show_interrupts(struct seq_file *p, void *v)
{
	int i = *(loff_t *) v, j;
	unsigned long flags;
	...
	if (i < NR_IRQS) {
		struct irq_desc *desc = irq_to_desc(i);
		struct irqaction *action;

		raw_spin_lock_irqsave(&desc->lock, flags);
		action = desc->action;
		if (!action)
			goto skip;
		seq_printf(p, "%3d: ", i);
#ifdef CONFIG_SMP
		for_each_online_cpu(j)
			seq_printf(p, "%10u ", kstat_irqs_cpu(i, j));
#else
		seq_printf(p, "%10u ", kstat_irqs(i));
#endif

		seq_printf(p, " %14s", irq_desc_get_chip(desc)->name);
#ifndef PARISC_IRQ_CR16_COUNTS
		seq_printf(p, "  %s", action->name);
        ...
#endif
		seq_putc(p, '\n');
 skip:
		raw_spin_unlock_irqrestore(&desc->lock, flags);
	}
    ...

2、irq_to_desc函数比较简单,就是从 irq_desc_tree 这个 radix 树里索引数据,所以接下来看 irq_desc_tree 是怎么添加的,找到对应的插入函数函数是 irq_insert_desc 。

struct irq_desc *irq_to_desc(unsigned int irq)
{
	return radix_tree_lookup(&irq_desc_tree, irq);
}

3、irq_insert_desc 只在函数 early_irq_init 和 alloc_descs 里有调用, early_irq_init 虽然有调用,但是执行不到,因为通过dmesg 能看到 initcnt变量为0,所以只用看 alloc_descs。

alloc_descs 被调用地方挺多的,一个一个看哪里调用了这个相关函数,困难比较大,于是切换思路,多下往上看比较麻烦,那就从上往下看,从注册pcie port 往下看。

pcie 首先注册一个标准的pci_driver即pcie_portdrv。pcie_portdrv是所有pci桥设备的驱动,在pcie_portdrv提供的probe(pcie_portdrv_probe)函数中,探测桥是否为pcie桥,然后创建pice 设备

/*
 * pcie_portdrv_probe - Probe PCI-Express port devices
 * @dev: PCI-Express port device being probed
 *
 * If detected invokes the pcie_port_device_register() method for
 * this port device.
 *
 */
static int pcie_portdrv_probe(struct pci_dev *dev,
					const struct pci_device_id *id)
{
	int status;

	if (!pci_is_pcie(dev) ||
	    ((pci_pcie_type(dev) != PCI_EXP_TYPE_ROOT_PORT) &&
	     (pci_pcie_type(dev) != PCI_EXP_TYPE_UPSTREAM) &&
	     (pci_pcie_type(dev) != PCI_EXP_TYPE_DOWNSTREAM)))
		return -ENODEV;

	status = pcie_port_device_register(dev);       // 关键接口pcie_port_device_register, 为高级服务Hotplug、AER、DPC、PME申请中断,
	if (status)
		return status;

4、pcie_port_device_register 这块代码通过函数名也能知道个大概,但是这里日志比较少,具体流程怎么走,需要添加日志,看下为什么没有走到下面的中断注册。最后通过日志,知道是在 "capabilities = get_port_device_capability(dev); " 没有获取到pcie port 的capabilities 直接返回了。

/**
 * pcie_port_device_register - register PCI Express port
 * @dev: PCI Express port to register
 *
 * Allocate the port extension structure and register services associated with
 * the port.
 */
int pcie_port_device_register(struct pci_dev *dev)
{
	int status, capabilities, i, nr_service;
	int irqs[PCIE_PORT_DEVICE_MAXSERVICES];

	/* Enable PCI Express port device */
	status = pci_enable_device(dev);
	if (status)
		return status;


	/* Get and check PCI Express port services */
	capabilities = get_port_device_capability(dev);
	if (!capabilities)
		return 0;

	pci_set_master(dev);

	status = pcie_init_service_irqs(dev, irqs, capabilities);
    ...
}

5、继续添加日志,发现host->native_pcie_hotplug 这个变量是false,所以现在要看下这个为什么设置成false了。

static int get_port_device_capability(struct pci_dev *dev) {
	struct pci_host_bridge *host = pci_find_host_bridge(dev->bus);
	int services = 0;


	if (dev->is_hotplug_bridge &&
	    (pcie_ports_native || host->native_pcie_hotplug)) {
		services |= PCIE_PORT_SERVICE_HP;

		/*
		 * Disable hot-plug interrupts in case they have been enabled
		 * by the BIOS and the hot-plug service driver is not loaded.
		 */
		pcie_capability_clear_word(dev, PCI_EXP_SLTCTL,
			  PCI_EXP_SLTCTL_CCIE | PCI_EXP_SLTCTL_HPIE);
	}

6、搜索代码只有acpi_pci_root_create 函数里才会设置 host_bridge→native_pcie_hotplug,当没有 OSC_PCI_EXPRESS_NATIVE_HP_CONTROL 时才会设置  host_bridge->native_pcie_hotplug = 0

struct pci_bus *acpi_pci_root_create(struct acpi_pci_root *root,
				     struct acpi_pci_root_ops *ops,
				     struct acpi_pci_root_info *info,
				     void *sysdata)
{
	...
	host_bridge = to_pci_host_bridge(bus->bridge);
	if (!(root->osc_control_set & OSC_PCI_EXPRESS_NATIVE_HP_CONTROL))
		host_bridge->native_pcie_hotplug = 0;
    ...

7、OSC_PCI_EXPRESS_NATIVE_HP_CONTROL 在 negotiate_os_control 里设置的,  从下图的dmesg日志能看到,是supoort变量有问题。到这里就简单了,看support是怎么生成的,最后发现是grub里设置了 pcie_aspm=off 导致的。

static void negotiate_os_control(struct acpi_pci_root *root, int *no_aspm)
{
	...
	support = OSC_PCI_SEGMENT_GROUPS_SUPPORT;
	if (pci_ext_cfg_avail())
		support |= OSC_PCI_EXT_CONFIG_SUPPORT;
	if (pcie_aspm_support_enabled())
		support |= OSC_PCI_ASPM_SUPPORT | OSC_PCI_CLOCK_PM_SUPPORT;
	if (pci_msi_enabled())
		support |= OSC_PCI_MSI_SUPPORT;
    ...
	if ((support & ACPI_PCIE_REQ_SUPPORT) != ACPI_PCIE_REQ_SUPPORT) {
		decode_osc_support(root, "not requesting OS control; OS requires",    从下图的dmesg日志能看到,在这里就return了,support值有问题,所以后面没有设置了。
				   ACPI_PCIE_REQ_SUPPORT);
		return;
	}

	control = OSC_PCI_EXPRESS_CAPABILITY_CONTROL
		| OSC_PCI_EXPRESS_PME_CONTROL;

	if (IS_ENABLED(CONFIG_HOTPLUG_PCI_PCIE))
		control |= OSC_PCI_EXPRESS_NATIVE_HP_CONTROL;
    ...

8、用户把pcie 中断注册都跳过了,那热插磁盘是不是也应该有问题?

      确实热插磁盘是有问题的,但是用户可能发现了这个问题,所以加了个crontab,每两分钟扫描一次pci总线,这样就能发现磁盘了,只是没有注册中断。

      在用户镜像里把这个crontab删掉,这时用户镜像热插磁盘也是会没有反应,lspci和lsblk也会看不到热插的磁盘块设备。

*/2 * * * * echo 1 > /sys/bus/pci/rescan

四、其他云厂商为什么这个镜像能正常卸载

       各厂商的虚拟化原理基本一致,差异不大,而且还是同一个镜像,为什么会有这种差异?

       xx云和其他云虚拟机差异我最先想到的可能就是主板不一致,于是征得用户同意,使用用户镜像创建一个i440fx虚拟机,测试发现i440fx是能正常挂卸载的。

       用户镜像里只是没有注册pcie中断,pci的中断还是正常注册的,所以不影响pci形式的设备。

       xx云使用的是q35主板,磁盘和网卡默认都是插在pcie root上,属于pcie 设备,当用户镜像把pcie 相关功能关闭后会影响到虚拟机设备的使用。

结论

     用户通过修改grub里的 pcie_aspm=off 即可解决问题。

     查清楚问题后,想了下,通过对grub里内核启动参数的增删,来定位是哪个启动参数的问题也可以,后续如果对内核不熟悉的话,可以通过修改grub的cmdline方式来定位问题。

参考

/proc/interrupts 的数值是如何获得的? – 肥叉烧 feichashao.com

PCI Express Port Bus Driver - 知乎

2.1 Linux驱动设备模型_pwl999的博客-优快云博客

PCI Express Port Bus Driver [LWN.net]

### 如何为 PCIe 设备开发或添加驱动程序 #### 硬件与软件环境准备 为了开发或添加 PCIe 设备的驱动程序,首先需要准备好合适的硬件和软件环境。硬件部分包括一块支持 PCIe 的开发板或虚拟机以及目标 PCIe 设备(如网卡、SSD 或其他终端设备)。软件方面则需安装 Linux 操作系统(推荐 Ubuntu),因为它提供了强大的 PCIe 支持功能[^1]。 #### 理解 PCIe Endpoint 和 Switch 的基本概念 在开始编写驱动之前,了解 PCIe 架构中的核心组件至关重要。PCIe Endpoint 是连接到根复合体(Root Complex)或交换机(Switch)上的终端设备,负责执行具体的功能。每个 PCIe 设备都拥有独立的配置空间,其中包含了设备信息和控制寄存器。对于更复杂的场景,可能还需要考虑 PCIe Switch 的使用,它能够通过多个端口扩展连接能力[^4]。 #### 设置开发工具链 确保已安装 GCC 编译器、Make 工具以及其他必要的开发工具。这些可以通过标准包管理器轻松获取,在基于 Debian 的发行版中可利用如下命令完成安装: ```bash sudo apt-get update && sudo apt-get install build-essential vim git ``` #### 编写驱动结构 Linux 内核提供了一套完善的机制来管理和操作 PCI 总线及其附属设备。以下是构建一个简单 PCIe 驱动的关键要素: - **`struct pci_driver`**: 描述整个 PCI 驱动的行为特征。 - **`struct pci_device_id`**: 列举该驱动所支持的具体设备型号列表。 - 使用 `pci_register_driver()` 函数向系统注册新定义好的驱动实例;相对应地,在卸载模块时调用 `pci_unregister_driver()` 进行清理工作[^2]。 #### 创建字符设备接口供用户态访问 为了让高层应用得以操控底层硬件资源,通常会借助于 `/dev` 下面建立起来的特殊文件节点作为桥梁。这一步骤涉及到了几个重要环节:分配主次号码、初始化 cdev 结构体成员变量并将其关联至实际方法集最后公布出去等待请求到来[^5]。 #### 测试与调试阶段 完成初步编码之后进入验证过程尤为重要。可以采用 dmesg 查看内核日志输出确认消息是否按预期流转;另外也可以尝试手动触发某些事件比如模拟热插拔动作观察响应情况等等。 ```c #include <linux/module.h> #include <linux/kernel.h> #include <linux/pci.h> static int __init my_pci_init(void){ printk(KERN_INFO "My PCI Driver Initialized\n"); return 0; } static void __exit my_pci_exit(void){ printk(KERN_INFO "Exiting My PCI Driver\n"); } module_init(my_pci_init); module_exit(my_pci_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Your Name Here"); MODULE_DESCRIPTION("A Simple PCI Device Driver Example."); ``` 以上代码片段展示了最基础版本的一个 PCI 类型驱动模板框架样例[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值