pcie设备卸载没有响应

用户问题

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

只要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查看出问题虚拟机的设备,发现磁盘块设备也已经有了

 查看设备对应的中断

pcie设备卸载没有响应_第1张图片

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

三、查看内核代码

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的博客-CSDN博客

PCI Express Port Bus Driver [LWN.net]

你可能感兴趣的:(linux,运维,服务器)