用户问题
客户报障磁盘卸载不了,客户反馈他们这个镜像是从阿里云拿过来的,在其他云可以卸载,在我们的云上一直卸载不了。用户用这个镜像创建了不少机器,都出现了卸载磁盘失败的报警,导致收到不少报警。
只要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 - 知乎
博客详细记录了一位工程师排查用户在使用阿里云镜像时遇到的磁盘卸载问题。问题源于grub内核启动参数`pcie_aspm=off`导致的中断注册缺失,影响了PCIe设备的热插拔。通过删除用户的crontab任务,确认了问题与中断注册有关。解决方案是修改grub配置,启用相关中断。此外,还探讨了不同云厂商间虚拟化技术的差异,并通过更换主板型号验证了解决方案。
1202

被折叠的 条评论
为什么被折叠?



