客户报障磁盘卸载不了,客户反馈他们这个镜像是从阿里云拿过来的,在其他云可以卸载,在我们的云上一直卸载不了。用户用这个镜像创建了不少机器,都出现了卸载磁盘失败的报警,导致收到不少报警。
只要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的博客-CSDN博客
PCI Express Port Bus Driver [LWN.net]