如何使能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。