如何使能512個virtio_blk設備

一例virtio_blk設備中斷佔用分析

背景:這個是在客戶的centos8.4的環境上復現的,dpu是目前很多

雲服務器上的網卡標配了,在雲豹的dpu產品測試中,dpu實現的virtio_blk

設備在申請中斷時報錯,在排查這個錯誤的過程中,覺得某些部分還比較有

趣,故記錄之。本身涉及的背景知識有:irq,msi,irq_domain,

affinity,virtio_blk,irqbalance

下面列一下我們是怎麼排查並解決這個問題的。

一、故障現象

內核團隊接到測試組測試客戶前端內核拋棧:

[25338.485128] virtio-pci 0000:b3:00.0: virtio_pci: leaving for legacy driver
[25338.496174] genirq: Flags mismatch irq 0. 00000080 (virtio418) vs. 00015a00 (timer)
[25338.503822] CPU: 20 PID: 5431 Comm: kworker/20:0 Kdump: loaded Tainted: G           OE    --------- -  - 4.18.0-305.30.1.jmnd2.el8.x86_64 #1
[25338.516403] Hardware name: Inspur NF5280M5/YZMB-00882-10E, BIOS 4.1.21 08/25/2021
[25338.523881] Workqueue: events work_for_cpu_fn
[25338.528235] Call Trace:
[25338.530687]  dump_stack+0x5c/0x80
[25338.534000]  __setup_irq.cold.53+0x7c/0xd3
[25338.538098]  request_threaded_irq+0xf5/0x160
[25338.542371]  vp_find_vqs+0xc7/0x190
[25338.545866]  init_vq+0x17c/0x2e0 [virtio_blk]
[25338.550223]  ? ncpus_cmp_func+0x10/0x10
[25338.554061]  virtblk_probe+0xe6/0x8a0 [virtio_blk]
[25338.558846]  virtio_dev_probe+0x158/0x1f0
[25338.562861]  really_probe+0x255/0x4a0
[25338.566524]  ? __driver_attach_async_helper+0x90/0x90
[25338.571567]  driver_probe_device+0x49/0xc0
[25338.575660]  bus_for_each_drv+0x79/0xc0
[25338.579499]  __device_attach+0xdc/0x160
[25338.583337]  bus_probe_device+0x9d/0xb0
[25338.587167]  device_add+0x418/0x780
[25338.590654]  register_virtio_device+0x9e/0xe0
[25338.595011]  virtio_pci_probe+0xb3/0x140
[25338.598941]  local_pci_probe+0x41/0x90
[25338.602689]  work_for_cpu_fn+0x16/0x20
[25338.606443]  process_one_work+0x1a7/0x360
[25338.610456]  ? create_worker+0x1a0/0x1a0
[25338.614381]  worker_thread+0x1cf/0x390
[25338.618132]  ? create_worker+0x1a0/0x1a0
[25338.622051]  kthread+0x116/0x130
[25338.625283]  ? kthread_flush_work_fn+0x10/0x10
[25338.629731]  ret_from_fork+0x1f/0x40
[25338.633395] virtio_blk: probe of virtio418 failed with error -16
       

從堆棧看,是某個virtio_blk設備在probe的時候報錯,錯誤碼為-16。

二、故障現象分析

從堆棧信息看:

1、virtio418是一個virtio_blk設備,在probe過程中調用 __setup_irq 返回了-16。

2、[25338.496174] genirq: Flags mismatch irq 0. 00000080 (virtio418) vs. 00015a00 (timer),說明我們的virtio_blk

設備去申請了0號中斷,由於0號中斷被timer佔用,irq子系統在比較flags時發現不符合,則打印這行。

具體代碼為:


static int

__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{

......

mismatch:
        if (!(new->flags & IRQF_PROBE_SHARED)) {
                pr_err("Flags mismatch irq %d. %08x (%s) vs. %08x (%s)\n",
                       irq, new->flags, new->name, old->flags, old->name);

......
       ret = -EBUSY;

......

      return ret;

}

至於為什麼virtio_blk會去申請0號中斷,因為我們實現virtio_blk後端設備的時候,並沒有支持intx,即virtio_pci類虛擬的pci_dev設備的irq值為0,本文先不管這個。

從堆棧看,virtio 申請中斷是走了vp_find_vqs_intx流程,


crash> dis -l vp_find_vqs+0xc7
/usr/src/debug/linux/drivers/virtio/virtio_pci_common.c: 369---行號為369


    356 static int vp_find_vqs_intx(struct virtio_device *vdev, unsigned nvqs,
    357                 struct virtqueue *vqs[], vq_callback_t *callbacks[],
    358                 const char * const names[], const bool *ctx)
    359 {
 ......
    366
    367         err = request_irq(vp_dev->pci_dev->irq, vp_interrupt, IRQF_SHARED,
    368                         dev_name(&vdev->dev), vp_dev);

    369         if (err)----壓棧的返回地址

......

我們dpu卡實現的virtio設備,都是使能msix的,按照代碼流程,應該是先嘗試msix,既然能走到 vp_find_vqs_intx 流程,說明 vp_find_vqs_msix失敗了,而且按照如下代碼:


    395 int vp_find_vqs(struct virtio_device *vdev, unsigned nvqs,
    396                 struct virtqueue *vqs[], vq_callback_t *callbacks[],
    397                 const char * const names[], const bool *ctx,
    398                 struct irq_affinity *desc)
    399 {
    400         int err;
    401
    402         /* Try MSI-X with one vector per queue. */
    403         err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, true, ctx, desc);//caq:先嘗試單vqueue單中斷號
    404         if (!err)
    405                 return 0;
    406         /* Fallback: MSI-X with one vector for config, one shared for queues. */
    407         err = vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, false, ctx, desc);//caq:嘗試多個vq共享一個中斷號
    408         if (!err)
    409                 return 0;
    410         /* Finally fall back to regular interrupts. */
    411         return vp_find_vqs_intx(vdev, nvqs, vqs, callbacks, names, ctx);//caq:最後退化成intx模式
    412 }

說明vp_find_vqs_msix 失敗了兩次。第一次是 單vqueue單中斷號的非共享方式,第二次是多個vq共用一個中斷的方式。

通過打點發現,失敗兩次的原因是 __irq_domain_activate_irq 返回了-28


__irq_domain_activate_irq return=-28

0xffffffff8fb52f70 : __irq_domain_activate_irq+0x0/0x80 [kernel]
 0xffffffff8fb54bc5 : irq_domain_activate_irq+0x25/0x40 [kernel]
 0xffffffff8fb56bfe : msi_domain_alloc_irqs+0x15e/0x2f0 [kernel]
 0xffffffff8fa5e5e4 : native_setup_msi_irqs+0x54/0x90 [kernel]
 0xffffffff8feef69f : __pci_enable_msix_range+0x3df/0x5e0 [kernel]
 0xffffffff8feef96b : pci_alloc_irq_vectors_affinity+0xbb/0x130 [kernel]
 0xffffffff8ff7472b : vp_find_vqs_msix+0x1fb/0x510 [kernel]
 0xffffffff8ff74aad : vp_find_vqs+0x6d/0x190 [kernel]


查看具體的代碼:


static int __irq_domain_activate_irq(struct irq_data *irqd, bool reserve)
{
 int ret = 0;

 if (irqd && irqd->domain) {//caq:均不為NULL
 struct irq_domain *domain = irqd->domain;

 if (irqd->parent_data)
 ret = __irq_domain_activate_irq(irqd->parent_data,
 reserve);//caq:遞歸,將父domain的對應irq都active一下
 if (!ret && domain->ops->activate) {
 ret = domain->ops->activate(domain, irqd, reserve);//caq:parent 執行完activate 然後再兒子輩執行。
 /* Rollback in case of error */
 if (ret && irqd->parent_data)//caq:異常則回退
 __irq_domain_deactivate_irq(irqd->parent_data);
 }
 }
 return ret;
}

由於客戶 host kernel開啟了 CONFIG_IRQ_DOMAIN_HIERARCHY,根據irq_domain 級別 ,該系統的irq_domain 級聯如下:

不過上圖是arm常見的,盜用arm圖,本x86系統類似,irq_domain級別具體跟蹤如下:


crash> irq_domain.name,parent 0xffff9bff87d4dec0
  name = 0xffff9bff87c1fd60 "INTEL-IR-MSI-1-2"
  parent = 0xffff9bff87400000
crash> irq_domain.name,parent 0xffff9bff87400000
  name = 0xffff9bff87c24300 "INTEL-IR-1"
  parent = 0xffff9bff87c6c900
crash> irq_domain.name,parent 0xffff9bff87c6c900
  name = 0xffff9bff87c3ecd0 "VECTOR"-----------最高級的
  parent = 0x0---所以parent為空

根據返回-28,根據最高級的irq_domain定位到 調用鏈為:


//caq:類比於 dma_domain_ops,在x86內是最高級的irq_domain了,因為他的domain parent為NULL
static const struct irq_domain_ops x86_vector_domain_ops = {//caq:x86針對acpi實現的irq_domain_ops
 .alloc = x86_vector_alloc_irqs,//caq:分配中斷
 .free = x86_vector_free_irqs,
 .activate = x86_vector_activate,//caq:activate實現
 .deactivate = x86_vector_deactivate,
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
 .debug_show = x86_vector_debug_show,
#endif
};
調用鏈:

x86_vector_activate-->activate_managed-->assign_managed_vector-->irq_matrix_alloc_managed

查看 代碼如下:


int irq_matrix_alloc_managed(struct irq_matrix *m, const struct cpumask *msk,
      unsigned int *mapped_cpu)
{//caq:managed irq 分配
 unsigned int bit, cpu, end = m->alloc_end;
 struct cpumap *cm;

 if (cpumask_empty(msk))
 return -EINVAL;

 cpu = matrix_find_best_cpu_managed(m, msk);//caq:找最合適的cpu
 if (cpu == UINT_MAX)
 return -ENOSPC;//caq:說明沒找到
......
}

由於沒有開啟 CONFIG_GENERIC_IRQ_DEBUGFS,所以沒辦法直接看到 vector_matrix 具體的值,

藉助crash工具查看:


crash> p *vector_matrix
$82 = {
  matrix_bits = 256,
  alloc_start = 32,
  alloc_end = 236,
  alloc_size = 204,
  global_available = 0,//caq:只剩下了這麼多個irq
  global_reserved = 151,
  systembits_inalloc = 3,
  total_allocated = 1922,//caq:只分配了這麼多個irq
  online_maps = 80,
  maps = 0x2ff20,
  scratch_map = {18446744069952503807, 18446744073709551615, 18446744073709551615, 18446735277616529407, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
  system_map = {1125904739729407, 0, 1, 18446726481523507200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
}

一個疑問湧上心頭,為什麼總共才分配了1922 個中斷,全局的global_available就為0了呢?

然後繼續打點,發現virtio_blk設備的vq申請中斷時,走的是 apicd->is_managed 流程,而同屬於virtio協議的virtio_net設備卻不是。

而走managed流程,也就是申請中斷時,帶有了RQD_AFFINITY_MANAGED:


static int
assign_irq_vector_policy(struct irq_data *irqd, struct irq_alloc_info *info)
{
 if (irqd_affinity_is_managed(irqd))//caq:如果是 managed 的irq,也就是irq_data中有 IRQD_AFFINITY_MANAGED 標記
 return reserve_managed_vector(irqd);

我們回過來查看vector alloc時的調用鏈:

x86_vector_alloc_irqs–>assign_irq_vector_policy–>reserve_managed_vector–>irq_matrix_reserve_managed

對一個兩個隊列的virtio_blk申請中斷時,打點發現如下:


m->global_available=15296 

0xffffffff87158300 : irq_matrix_reserve_managed+0x0/0x130 [kernel]---從15296減少到15256
m->global_available=15256

call vdev=0xffff8b781ce17000,index=0,callback=0xffffffffc0448000,ctx=0,msix_vec=1----------容量減少了40

由於已經縮小到是因為virtio_blk設備的中斷申請流程,使用熱插拔確認一下:


118:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0         53          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0  IR-PCI-MSI 94371841-edge      virtio3-req.0
 119:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0         49          0          0          0  IR-PCI-MSI 94371842-edge      virtio3-req.1

熱拔前:
crash> p *vector_matrix
$2 = {
  matrix_bits = 256,
  alloc_start = 32,
  alloc_end = 236,
  alloc_size = 204,
  global_available = 15215,
  global_reserved = 150,
  systembits_inalloc = 3,
  total_allocated = 553,
  online_maps = 80,
  maps = 0x2ff20,
  scratch_map = {1179746449752063, 0, 1, 18446726481523507200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
  system_map = {1125904739729407, 0, 1, 18446726481523507200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
}

熱拔後:
crash> p *vector_matrix
$3 = {
  matrix_bits = 256,
  alloc_start = 32,
  alloc_end = 236,
  alloc_size = 204,
  global_available = 15296,---增長了81個,一個config中斷+兩個分別為40的req中斷,此時req.0和req.1是非共享模式
  global_reserved = 150,
  systembits_inalloc = 3,
  total_allocated = 550,
  online_maps = 80,
  maps = 0x2ff20,
  scratch_map = {481036337152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
  system_map = {1125904739729407, 0, 1, 18446726481523507200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
}

說明 對於mask內的cpu號,都需要一個中斷容量,兩個40就是因為80核的服務器,兩個vq,平分中斷,感興趣的同學可以去查看irq_build_affinity_masks這個函數的實現。這樣一個vector,因為開啟了 IRQD_AFFINITY_MANAGED 屬性,導致需要佔用80個中斷容量。而我們系統由於有512個virtio_blk設備,所以申請到部分設備的時候,就把總的

vector容量耗光了,但其實分配的irq總數才不到2000.

那麼,virtio_blk設備什麼時候開啟的 IRQD_AFFINITY_MANAGED 屬性的呢?查看git記錄:


6abd6e5a44040 (Amit Shah                 2011-12-22 16:58:29 +0530  507) static int init_vq(struct virtio_blk *vblk)
6abd6e5a44040 (Amit Shah                 2011-12-22 16:58:29 +0530  508) {
......
6a27b656fc021 (Ming Lei                  2014-06-26 17:41:48 +0800  515)        struct virtio_device *vdev = vblk->vdev;
ad71473d9c437 (Christoph Hellwig         2017-02-05 18:15:25 +0100  516)        struct irq_affinity desc = { 0, };----會導致blk申請中斷時,使用內核managed方式來申請,一個dev會佔用cpu核數這麼多的容量。

看起來是 ad71473d9c437 這個 commit引入了這個問題。

但是根據virtio_blk驅動,第一遍中斷申請的時候才有affinity_managed 設置,第二遍應該並沒有設置,具體 vp_find_vqs_msix 如下:


//caq:vq申請中斷,msix 模式,per_vq_vectors 決定是個vq共享中斷還是獨佔中斷
static int vp_find_vqs_msix(struct virtio_device *vdev, unsigned nvqs,
 struct virtqueue *vqs[], vq_callback_t *callbacks[],
 const char * const names[], bool per_vq_vectors,
 const bool *ctx,
 struct irq_affinity *desc)
{
 struct virtio_pci_device *vp_dev = to_vp_device(vdev);
 u16 msix_vec;
 int i, err, nvectors, allocated_vectors;

 vp_dev->vqs = kcalloc(nvqs, sizeof(*vp_dev->vqs), GFP_KERNEL);
 if (!vp_dev->vqs)
 return -ENOMEM;

 if (per_vq_vectors) {//caq:單個vq 單個vector 中斷號
 /* Best option: one for change interrupt, one per vq. */
 nvectors = 1;//caq:這個是config的中斷,注意要和virtio_net的 ctrl vq區分
 for (i = 0; i < nvqs; ++i)
 if (callbacks[i])//caq:由於ctrl vq是不設置callbacks的,所以它沒有中斷
 ++nvectors;
 } else {
 /* Second best: one for change, shared for all vqs. */
 nvectors = 2;
 }
    //caq:中斷總數最少為2,最多為vq個數+1,1為config的中斷,另外單個vq單個vector才具備帶親核屬性
 err = vp_request_msix_vectors(vdev, nvectors, per_vq_vectors,
       per_vq_vectors ? desc : NULL);//caq:nvectors 為總中斷數,注意desc的配置取決於 per_vq_vectors

//caq:virtio_pci申請msix的vector
static int vp_request_msix_vectors(struct virtio_device *vdev, int nvectors,
    bool per_vq_vectors, struct irq_affinity *desc)
{
......
 vp_dev->msix_affinity_masks
 = kcalloc(nvectors, sizeof(*vp_dev->msix_affinity_masks),
   GFP_KERNEL);//caq:中斷掩碼內存的分配
......
 if (desc) {//caq:要求帶親核屬性
 flags |= PCI_IRQ_AFFINITY;//caq:帶上親核屬性
 desc->pre_vectors++; /* virtio config vector *///caq:細節,相當於指定了config中斷不要設置親核,走系統默認
 }
......

原因,因為前面很多virtio_blk設備因為一個vector佔用了80個中斷容量,導致整體中斷數不夠了,

而後面的virtio_blk設備,第一遍使用vp_find_vqs_msix帶 managed_affinity屬性申請中斷時失敗,第二遍儘管使用vq共享中斷模式,由於os連一個中斷都分配不出來, 也會失敗,導致走入了第三個流程,也就是 vp_find_vqs_intx 流程。

在另外一個virtio_blk單vq的環境上,具體查看如下:


[root@localhost config_json]#  cat /proc/interrupts |grep req |tail -1
 986:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0        114          0          0  IR-PCI-MSI 93687809-edge      virtio180-req.0
[root@localhost config_json]# cat /proc/irq/986/smp_affinity
ffff,ffffffff,ffffffff

[root@localhost config_json]#  cat /proc/interrupts |grep queues |tail -1
1650:          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0        120          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0          0  IR-PCI-MSI 94369793-edge      virtio512-virtqueues


[root@localhost config_json]# cat /proc/irq/1650/smp_affinity
ffff,ffffffff,ffffffff
[root@localhost config_json]# ls /sys/bus/virtio/devices/virtio180/block/
vdsh
[root@localhost config_json]# ls /sys/bus/virtio/devices/virtio512/block/
vdafj

以上可以看出,virtio180,也就是 vdsh 的block設備,走了 vp_find_vqs_msix 第一遍流程,分配了帶 managed_affinity 的vector,所以他的中斷名字是req結尾的,

而另外一個 virtio512 ,也就是 vdafj 的block設備,走了 vp_find_vqs_msix 第二遍流程,沒有分配 帶 managed_affinity 的vector,所以它的中斷名字是 virtqueues 結尾的,

而後面的設備,只能走第三個流程,報錯了,所以打點發現,除了 activate的時候會報容量不足,在alloc階段,也會在 irq_matrix_alloc 報容量不足。


int irq_matrix_alloc(struct irq_matrix *m, const struct cpumask *msk,
      bool reserved, unsigned int *mapped_cpu)
{

......
 cpu = matrix_find_best_cpu(m, msk);
 if (cpu == UINT_MAX)//caq:在msk內,沒找到合適的cpu來記賬
 return -ENOSPC;

 cm = per_cpu_ptr(m->maps, cpu);
 bit = matrix_alloc_area(m, cm, 1, false);//caq:獲取一個
 if (bit >= m->alloc_end)
 return -ENOSPC;//caq:沒有資源了
......

打點記錄如下:

Returning from:  0xffffffffb7158300 : irq_matrix_reserve_managed+0x0/0x130 [kernel]
Returning to  :  0xffffffffb705adb7 : x86_vector_alloc_irqs+0x2d7/0x3c0 [kernel]
 0xffffffffb75dc42c : intel_irq_remapping_alloc+0x7c/0x6d0 [kernel]
 0xffffffffb7156438 : msi_domain_alloc+0x68/0x120 [kernel]
 0xffffffffb715457d : __irq_domain_alloc_irqs+0x12d/0x300 [kernel]
 0xffffffffb7156b38 : msi_domain_alloc_irqs+0x98/0x2f0 [kernel]
 0xffffffffb705e5e4 : native_setup_msi_irqs+0x54/0x90 [kernel]
 0xffffffffb74ef69f : __pci_enable_msix_range+0x3df/0x5e0 [kernel]
 0xffffffffb74ef96b : pci_alloc_irq_vectors_affinity+0xbb/0x130 [kernel]
 0xffffffffb70635e0 : kretprobe_trampoline+0x0/0x50 [kernel]
 0x1b7574a40
 0x1b7574a40 (inexact)
irq_matrix_reserve_managed return -28

三、故障復現

1、只要是一開始各個核中斷的aviable 容量相當,然後熱插拔足夠多virtio_blk設備,則必現。

2、如果各個核的中斷的available容量相差很多,比如常見的numa節點的第一個cpu的中斷佔用過多,

使得走第一分支時因為某個核容量不夠而reserve_managed 失敗,然後

則會使得後面大量的virtio_blk走第二個分支,此時不帶managed_affinity,反而能分配成功。

四、故障規避或解決

可能的解決方案之一:


static int init_vq(struct virtio_blk *vblk)//caq:初始化關於vq相關的內容
{
 int err;
 int i;
 vq_callback_t **callbacks;
 const char **names;
 struct virtqueue **vqs;
 unsigned short num_vqs;
 struct virtio_device *vdev = vblk->vdev;
 struct irq_affinity desc = { 0, };//caq:去掉這行代碼

解決方案之二:

開啟irqbalance,並讓服務器進入 Power-save mode 時,irqbalance 會將中斷集中分配給numa節點的第一個 CPU,這樣慢慢地,各個核

的available的irq 容量就相差比較大了,當然這種不太靠譜。

解決方案之三:

手工調整中斷親核,使得某些核的容量接近於0,然後再加載virtio_blk設備。

五、作者簡介

陳安慶,目前在dpu廠商 雲豹智能 負責linux內核及虛擬化方面的工作,

聯繫方式:微信與手機同號:18752035557。