之前的文章介绍了在虚拟机内通过用户态程序访问 ivshmem
设备的共享内存。在虚拟机之间或者宿主机与虚拟机之间通过共享内存进行通信的情形下,共享内存的两端必须依赖轮询方式来实现通知机制。这种方式是 ivshmem
提供的 ivshmem-plain
的使用方式。除此之外, ivshmem
还提供了 ivshmem-doorbell
的使用方式,它提供了基于中断的通知机制。
配置空间
PCI 配置空间是标准化的,用于识别和配置设备;而 BAR 空间(下文)是每个设备不同的,是设备自定义的,用于实际访问和操作设备。
下面简单介绍一下配置空间的常用寄存器:
Vendor ID、Device ID:标记了一个设备的生产厂商和具体的设备
Status:设备状态字
Base Address Registers: PCI 设备空间映射到的系统空间具体位置首地址的寄存器,该值在系统初始化时被写入,供后续使用
BAR 空间
ivshmem 在虚拟机内部表现为 PCI
设备,共享的内存区域则以 PCI BAR
的形式存在。ivshmem
PCI设备提供3个 BAR
:
BAR0
: 设备寄存器
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Offset Size Access On reset Function 0 4 read/write 0 Interrupt Mask bit 0: peer interrupt (rev 0) reserved (rev 1) bit 1..31: reserved 4 4 read/write 0 Interrupt Status bit 0: peer interrupt (rev 0) reserved (rev 1) bit 1..31: reserved 8 4 read-only 0 or ID IVPosition 12 4 write-only N/A Doorbell bit 0..15: vector bit 16..31: peer ID 16 240 none N/A reserved
|
Interrupt Mask:INTx
中断掩码,控制相应的中断是否被允许,这里只有 0
号中断可以使用
Interrupt Status:INTx
中断状态,表示中断请求状态,当这两个寄存器的相应位都为 1 时,INTx
中断才会被触发。
IVPosition:如果设备未配置中断,则为零。否则,这表示设备的ID(介于0和65535之间)
Doorbell:请求中断 ,写入值的高16位是要中断的对 peer ID,其低 16 位选择中断向量。
BAR1
: MSI-X
表
MSI/MSI-X
与 INTx
中断的区别
INTx
中断:
MSI/MSI-X
中断:
对于多个 MSI-X 向量,可以使用不同的向量来表示发生了不同的事件。中断向量的语义留给应用程序处理。
BAR2
: 共享内存区域
无它,仅存放了内存区域的首地址。
中断
ivshmem-doorbell
提供了两种中断方式:
一种是传统的基于 INTx
的中断, 它主要使用 BAR0
的 Interrupt Mask
和 Interrupt Status
两个寄存器;
一种是基于 MSI-X
的中断,它主要使用 BAR0
的 IVPosition
和 Doorbell
两个寄存器。
- 共享的设备端叫做
peer
。 IVPosition
寄存器存储该 peer
的数字标识符(0-65535), 称做 peer_id
。该寄存器为只读寄存器。
Doorbell
寄存器为只写寄存器。 ivshmem-doorbell
支持多个中断向量,写入 Doorbell
寄存器则触发共享该内存的某个 peer
的某个中断。 Doorbell
为 32 位,低16 位为peer_id
,而高 16 位为中断向量号(这里是从0
开始的顺序号,而非PCI
驱动在Guest虚拟机内部所申请的向量号)。
Server
使用 ivshmem-doorbell
机制需要运行 ivshmem-server。ivshmem-server 根据参数创建共享内存,并通过监听本地 UNIX DOMAIN SOCKET 等待共享内存的 peer 来连接。添加了 ivshmem-doorbell
设备的 QEMU 进程会连接该 socket, 从而获取 ivshmem-server 所分配的一个 peer_id。
ivshmem-doorbell
支持多个中断向量,ivshmem-server 会为 ivshmem 虚拟 PCI 设备支持的每个中断向量创建一个 eventfd,并将共享内存以及为所有客户端中断向量所创建的 eventfd 都通过 CM_RIGHTS
机制传递给所有客户端进程。这样所有的 peer 便都具备了独立的两两之间的通知通道。之后在虚拟机内通过触发 ivshmem 虚拟 PCI 设备的 DOORBELL
寄存器的写入,虚拟机的QEMU 进程便会通过 DOORBELL
寄存器中的 peer_id 和中断向量号来找到相应的 eventfd,从而通知到对端的 QEMU 进程来产生相应的 PCI 中断。
PCI 设备驱动
要使用中断机制,用户态程序是无能为力的,需要编写相应的 PCI
驱动来实现。本文通过一个简单的 PCI
驱动示例来说明 ivshmem-doorbell
的 MSI-X
中断机制的使用。
模块入口 & 出口
模块入口函数注册 PCI 设备驱动,出口函数注销
1 2 3 4 5 6 7 8 9 10 11 12
| static int __init pci_init(void) { return pci_register_driver(&pci_drv); }
static void __exit pci_exit(void) { pci_unregister_driver(&pci_drv); }
module_init(pci_init); module_exit(pci_exit);
|
设备探测和移除
首先是驱动对象以及设备匹配表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| static struct pci_device_id ids[] = { { .vendor = PCI_VENDOR_ID_IVSHMEM, .device = PCI_DEVICE_ID_IVSHMEM, }, { 0 }, }; MODULE_DEVICE_TABLE(pci, ids);
static struct pci_driver pci_drv = { .name = MODULE_NAME, .id_table = ids, .probe = iv_probe, .remove = iv_remove, };
|
然后是设备探测函数 probe
,分为下面几个部分:
使能设备 & 预留空间
关键函数:pci_enable_device(pdev)
,pci_request_regions(pdev, name)
1 2 3 4 5 6 7 8 9 10 11
| retval = pci_enable_device(pdev); if (retval < 0) { dev_err(&pdev->dev, "can't enable PCI device\n"); return -ENODEV; }
retval = pci_request_regions(pdev, MODULE_NAME); if (retval < 0) { dev_err(&pdev->dev, "pci_request_regions failed\n"); return retval; }
|
申请重要设备信息保存空间
1 2 3 4 5 6
| pci_iv = kzalloc(sizeof(struct pci_ivshmem_t), GFP_KERNEL); if (!pci_iv) { dev_err(&pdev->dev, "can't alloc memory for pci device\n"); return -ENOMEM; } pci_iv->pdev = pdev;
|
申请 MSI 中断
关键函数:pci_alloc_irq_vectors(pcde, min, max, flags)
,pci_irq_vector(pdev, idx)
request_irq(irq,irq_handler_t,flags,name,cookie)
1 2 3 4 5 6 7 8
| pci_read_config_byte(pdev, PCI_REVISION_ID, &reversion); pr_debug("Revision: %d\n", reversion);
if (reversion == 1) { retval = irq_alloc(pci_iv); if (retval < 0) goto irq_alloc_err; }
|
irq_alloc
函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| static int irq_alloc(struct pci_ivshmem_t *pci_iv) { int retval = 0, i; int nvec, irq; struct pci_dev *pdev = pci_iv->pdev;
nvec = pci_alloc_irq_vectors(pdev, 1, PCI_VEC_NUMBER, PCI_IRQ_MSI | PCI_IRQ_MSIX); if (nvec < 0) { dev_err(&pdev->dev, "pci_alloc_irq_vectors failed\n"); retval = nvec; goto exit_err; }
for (i = 0; i < nvec; i++) { irq = pci_irq_vector(pdev, i); retval = request_irq(irq, ivshmem_msi_isr, 0, MODULE_NAME, pci_iv); if (retval < 0) { dev_err(&pci_iv->pdev->dev, "request_irq failed\n"); goto req_irq_err; } pci_iv->irqs[i] = irq; pci_iv->irq_nr++; }
return 0;
req_irq_err: irq_free(pci_iv); exit_err: return retval; }
|
映射 BAR 空间
关键函数 pci_resource_start/len/end(pdev,idx)
,pci_iomap(pdev, idx, barlen)
,pci_iounmap(pdev, void*)
映射完成后,可以像访问内存一样,来访问 PCI 的 BAR 空间,实际上保存的是映射后的物理地址
1 2 3 4 5
| retval = map_bars(pci_iv); if (retval < 0) { dev_err(&pdev->dev, "can't map bars\n"); goto map_bars_err; }
|
map_bars
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| static int map_sigal_bar(struct pci_ivshmem_t *pci_iv, int idx) { struct pci_dev *pdev = pci_iv->pdev; resource_size_t bar_start; resource_size_t bar_len; void *__iomem bar_map;
bar_start = pci_resource_start(pdev, idx); bar_len = pci_resource_len(pdev, idx); if (bar_len == 0) return 0;
bar_map = pci_iomap(pdev, idx, bar_len);
pr_info("BAR%d at 0x%llx mapped at 0x%p, length=%llu(0x%llx)\n", idx, (u64)bar_start, \ bar_map, (u64)bar_len, (u64)bar_len);
pci_iv->bars[pci_iv->bar_nr++] = bar_map; pci_iv->barlens[pci_iv->bar_nr] = bar_len;
return (int)bar_len; }
static void unmap_bars(struct pci_ivshmem_t *pci_iv) { int i;
for (i = 0; i < pci_iv->bar_nr; i++) pci_iounmap(pci_iv->pdev, pci_iv->bars[i]); }
static int map_bars(struct pci_ivshmem_t *pci_iv) { int retval = 0; int i;
for (i = 0; i < PCI_BAR_NUMBER; ++i) { retval = map_sigal_bar(pci_iv, i); if (retval == 0) { continue; } else if (retval < 0) { retval = -EINVAL; goto fail; } } fail: unmap_bars(pci_iv); return retval; }
|
创建字符设备
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| static void destory_cdev(struct pci_ivshmem_t *pci_iv) { cdev_del(&pci_iv->cdev); unregister_chrdev_region(pci_iv->devt, 1); }
static int create_cdev(struct pci_ivshmem_t *pci_iv) { int retval = 0;
retval = alloc_chrdev_region(&pci_iv->devt, 0, 1, MODULE_NAME); if (retval < 0) return retval;
cdev_init(&pci_iv->cdev, &pci_fops); retval = cdev_add(&pci_iv->cdev, pci_iv->devt, 1); if (retval < 0) goto cdev_add_err;
cdev_add_err: unregister_chrdev_region(pci_iv->devt, 1); return retval; }
|
移除
1 2 3 4 5 6 7 8 9 10 11
| static void iv_remove(struct pci_dev *pdev) { struct pci_ivshmem_t *pci_iv = dev_get_drvdata(&pdev->dev);
destory_cdev(pci_iv); unmap_bars(pci_iv); irq_free(pci_iv); pci_release_regions(pdev); pci_disable_device(pdev); kfree(pci_iv); }
|
file_operations 文件操作
1 2 3 4 5 6 7 8 9
| static struct file_operations pci_fops = { .owner = THIS_MODULE, .open = iv_open, .read = iv_read, .write = iv_write, .mmap = iv_mmap, .unlocked_ioctl = iv_ioctl, .release = iv_release, };
|
open
1 2 3 4 5 6 7 8 9 10
| static int iv_open(struct inode *node, struct file *filp) { struct cdev *cdev = node->i_cdev; struct pci_ivshmem_t *pci_iv = container_of(cdev, struct pci_ivshmem_t, cdev);
filp->private_data = pci_iv;
pr_err("%s() is invoked\n", __FUNCTION__); return 0; }
|
read
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| static ssize_t iv_read(struct file *filp, char *__user buf, size_t count, loff_t *pos) { size_t offset, byte_copy; struct pci_ivshmem_t *pci_iv = filp->private_data;
BUG_ON(pci_iv == NULL); BUG_ON(pci_iv->shmem == NULL);
offset = *pos; if (count > pci_iv->barlens[2] - offset) count = pci_iv->barlens[2] - offset;
byte_copy = copy_to_user(buf, pci_iv->shmem + offset, count); if (byte_copy > 0) return -EFAULT; *pos += count; return count; }
|
write
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| static ssize_t iv_write(struct file *filp, const char *__user buf, size_t count, loff_t *pos) { size_t offset, not_written; struct pci_ivshmem_t *pci_iv = filp->private_data;
BUG_ON(pci_iv == NULL); BUG_ON(pci_iv->shmem == NULL);
offset = *pos; if (count > pci_iv->barlens[2] - offset) count = pci_iv->barlens[2] - offset;
not_written = copy_from_user(pci_iv->shmem + offset, buf, count); if (not_written > 0) return -EFAULT;
*pos += count; return count; }
|
mmap
关键函数:io_remap_pfn_range(vma, vstart, pfn, vlen, prot)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| static int iv_mmap(struct file *filp, struct vm_area_struct *vma) { struct pci_ivshmem_t *pci_prv = filp->private_data; unsigned long vstart, vend, voff; unsigned long start, end, len;
BUG_ON(pci_prv == NULL); BUG_ON(pci_prv->shmem == NULL);
vstart = vma->vm_start; vend = vma->vm_end;
WARN_ON(offset_in_page(vstart)); WARN_ON(offset_in_page(vend));
voff = vma->vm_pgoff << PAGE_SHIFT;
start = (unsigned long)pci_prv->shmem; len = pci_prv->barlens[2];
end = PAGE_ALIGN(start + len); start = PAGE_ALIGN(start); len = end - start;
if (vend - vstart + voff > len) { dev_err(&pci_prv->pdev->dev, "mmap overflow\n"); return -EINVAL; }
voff += start; vma->vm_pgoff = voff >> PAGE_SHIFT; vma->vm_flags |= VM_IO | VM_SHARED | VM_DONTEXPAND | VM_DONTDUMP; if (io_remap_pfn_range(vma, vstart, voff >> PAGE_SHIFT, vend - vstart, vma->vm_page_prot)) { dev_err(&pci_prv->pdev->dev, "mmap bar2 failed\n"); return -ENXIO; }
return 0; }
|
ioctl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| static long iv_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int rv = 0; struct pci_ivshmem_t *pci_iv = filp->private_data; uint32_t value; uint32_t ivposition;
BUG_ON(pci_iv == NULL); BUG_ON(pci_iv->shmem == NULL);
switch (cmd) { case IOCTL_WAIT: rv = wait_event_interruptible(ivshmem_wait, atomic_read(&g_ring) == 1); if (rv == 0) { dev_info(&pci_iv->pdev->dev, "wakeup!\n"); atomic_set(&g_ring, 0); } else if (rv == -ERESTARTSYS) { dev_info(&pci_iv->pdev->dev, "wakeup by signal\n"); return rv; } else { dev_info(&pci_iv->pdev->dev, "unkown fault\n"); return rv; } break;
case IOCTL_IVPOSITION: ivposition = pci_iv->ivposition; if (copy_to_user((void*)arg, &ivposition, sizeof(uint32_t))) rv = -ENXIO; break; case IOCTL_RING: if(copy_from_user(&value, (void*)arg, sizeof(uint32_t))) { rv = -ENXIO; break; } writel(value & 0xFFFFFFFF, pci_iv->bars[0] + Doorbell); break; default: dev_info(&pci_iv->pdev->dev, "bad ioctl command\n"); }
return rv; }
|