0%

【虚拟化】ivshmem(二)基于中断的通信原理与实现--PCI设备驱动

之前的文章介绍了在虚拟机内通过用户态程序访问 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 MaskINTx 中断掩码,控制相应的中断是否被允许,这里只有 0 号中断可以使用

  • Interrupt StatusINTx 中断状态,表示中断请求状态,当这两个寄存器的相应位都为 1 时,INTx 中断才会被触发。

  • IVPosition:如果设备未配置中断,则为零。否则,这表示设备的ID(介于0和65535之间)

  • Doorbell:请求中断 ,写入值的高16位是要中断的对 peer ID,其低 16 位选择中断向量。

BAR1: MSI-X

MSI/MSI-XINTx 中断的区别

INTx 中断:

  • INTx 中断通过传统的物理引脚发送中断信号。这些引脚与特定的中断控制器(如 PIC 或 APIC)相连,CPU 通过这些控制器接收中断信号。

  • 每个设备通常只有一个中断引脚,因此多个设备需要共享中断线,可能会导致中断冲突和中断风暴问题,共享中断线也会导致额外的开销,因为系统需要检查所有可能的设备来确定哪个设备触发了中断

MSI/MSI-X 中断:

  • MSI 中断通过总线传输消息来触发中断,而不是通过物理引脚。这些消息在 PCI 或 PCIe 总线上传递,直接发送到 CPU 的中断控制器。

  • 每个设备可以拥有多个独立的中断消息,从而避免了中断线共享的问题。

对于多个 MSI-X 向量,可以使用不同的向量来表示发生了不同的事件。中断向量的语义留给应用程序处理。

BAR2: 共享内存区域

无它,仅存放了内存区域的首地址。

中断

ivshmem-doorbell 提供了两种中断方式:

一种是传统的基于 INTx 的中断, 它主要使用 BAR0Interrupt MaskInterrupt Status 两个寄存器;

一种是基于 MSI-X 的中断,它主要使用 BAR0IVPosition Doorbell 两个寄存器。

  • 共享的设备端叫做 peerIVPosition 寄存器存储该 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-doorbellMSI-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;

/* 自动分配 MSI 中断向量 */
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++) {
/* 将 MSI 中断向量转成 Linux irq 号 */
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;
}