一、vdpa中virtio寄存器读写的初始化
ifcvf_init_hw
// 读取pci配置空间中cap起始指针的偏移,大小为一个字节的内容,即首个capability的位置
// 1. PCI_CAPABILITY_LIST为0x34,实际读取的时候,会将这个0x34(基于pci config空间的偏移),
// 加上vfio config region基于device fd的偏移,从device fd读取该偏移位置的内容.
// 2. pos返回的内容,就是在上面计算的偏移的位置读取的内容。与内核态驱动读取这个pos的位置的内容,应该是相同的。
// 用户态这里的读取经过了vfio的转换。得到的都是基于pci配置空间开始的偏移。
// 3. 后面迭代读取各个capability的时候,使用的pos(cap.cap_next)都会经过上面1中偏移的计算,然后去读取pci配置空间的cap内容。
PCI_READ_CONFIG_BYTE(dev, &pos, PCI_CAPABILITY_LIST);
rte_pci_read_config(dev, val, 1, where)
pci_vfio_read_config(device, buf, len, offset);
// 这里返回的是device fd
fd = rte_intr_dev_fd_get(dev->intr_handle);
// VFIO_PCI_CONFIG_REGION_INDEX为7,表示第8个region,固定为pci配置空间
// 1. 初始化的时候会通过系统调用:
// ioctl(vfio_dev_fd, VFIO_DEVICE_GET_REGION_INFO, ri)
// 来读取各个region的信息初始化pdev->region[]. 其中ri->index会先初始化为要读取的region的index
// 2. 这里直接返回初始化过的该region的size和该region从device fd开始的偏移
pci_vfio_get_region(dev, VFIO_PCI_CONFIG_REGION_INDEX, &size, &offset)
*size = pdev->region[index].size;
*offset = pdev->region[index].offset;
// 从device fd开始指定偏移,读取len大小的数据到buf。
// 1. 这里buf结构是struct ifcvf_pci_cap,即virtio cap的结构。
// 2. 指定偏移,包括region基于device fd的偏移offset,和基于pci配置空间开始位置的偏移offs
pread64(fd, buf, len, offset + offs)
// 迭代读取pci配置空间中的各个cap内容,根据cap的offset返回virtio各个cap在相应bar偏移后的地址。
// 1. 根据cap.cfg_type筛选virtio特定的cap,得到对应cap的bar和bar中的offset
// 2. 基于已经初始化的bar的起始地址,即hw->mem_resource[bar].addr,再根据1中得到的offset偏移,得到该cap在bar中的偏移地址。
// 3. 后面读取virtio各个capability中的寄存器的时候,直接基于这里初始化的比如hw->common_cfg记录的起始地址,
// 再根据寄存器在bar中的偏移,去读取指定寄存器。下面的lm_cfg和mq_cfg也是如此。
// 4. hw->common_cfg等cap指针变量,起始就是对应cap的数据结构,里面包含了各个寄存器,读写各个寄存器的时候直接变量引用即可。
// cap的结构指针在下面。
while (pos) {
PCI_READ_CONFIG_RANGE(dev, (u32 *)&cap, sizeof(cap), pos);
// 这个函数跟上面首次读取cap指针使用的函数是一样的。都是基于device fd开始,
// vfio config region偏移加上这里指定的where偏移,当做偏移总和,读取指定size大小的内容到val
rte_pci_read_config(dev, val, size, where);
switch (cap.cfg_type) {
case IFCVF_PCI_CAP_COMMON_CFG:
hw->common_cfg = get_cap_addr(hw, &cap);
u8 bar = cap->bar;
u32 offset = cap->offset;
return hw->mem_resource[bar].addr + offset;
case IFCVF_PCI_CAP_NOTIFY_CFG:
case IFCVF_PCI_CAP_ISR_CFG:
case IFCVF_PCI_CAP_DEVICE_CFG:
}
pos = cap.cap_next;
}
// 获取热迁移相关的cap在bar中的偏移地址。
// 1. 这两个cap没有通过pci配置空间中的迭代cap查找,而是直接写死的bar index和offset。
hw->lm_cfg = hw->mem_resource[4].addr;
hw->mq_cfg = hw->mem_resource[4].addr + IFCVF_MQ_OFFSET;
// 至此,virtio寄存器读取需要的初始化完成。vdpa中这部分是在vdpa启动,调用ifcvf_probe初始化virtio-pci设备层的时候执行。
下面是涉及到的数据结构
struct ifcvf_hw {
u64 req_features;
u8 notify_region;
u32 notify_off_multiplier;
struct ifcvf_pci_common_cfg *common_cfg;
union {
struct ifcvf_net_config *net_cfg;
struct virtio_blk_config *blk_cfg;
void *dev_cfg;
};
u8 *isr;
u16 *notify_base;
u16 *notify_addr[IFCVF_MAX_QUEUES * 2];
u8 *lm_cfg;
u8 *mq_cfg;
struct vring_info vring[IFCVF_MAX_QUEUES * 2];
u8 nr_vring;
int device_type;
struct ifcvf_pci_mem_resource mem_resource[IFCVF_PCI_MAX_RESOURCE];
};
struct ifcvf_pci_common_cfg {
/* About the whole device. */
u32 device_feature_select;
u32 device_feature;
u32 guest_feature_select;
u32 guest_feature;
u16 msix_config;
u16 num_queues;
u8 device_status;
u8 config_generation;
/* About a specific virtqueue. */
u16 queue_select;
u16 queue_size;
u16 queue_msix_vector;
u16 queue_enable;
u16 queue_notify_off;
u32 queue_desc_lo;
u32 queue_desc_hi;
u32 queue_avail_lo;
u32 queue_avail_hi;
u32 queue_used_lo;
u32 queue_used_hi;
};
二、vdpa对virtio寄存器的读写操作
update_datapath
vdpa_ifcvf_start
ifcvf_start_hw(&internal->hw);
// 1. reset、IFCVF_CONFIG_STATUS_ACK、IFCVF_CONFIG_STATUS_DRIVER、IFCVF_CONFIG_STATUS_FEATURES_OK、IFCVF_CONFIG_STATUS_DRIVER_OK,
// 均是通过直接写hw->common_cfg->device_status变量,该变量映射的硬件设备该寄存器。
// 2. feature和其他commoncfg里的寄存器也是通过相同的方式,直接写的变量。
// 3. 对于mq_cfg则是直接以变量u8的方式直接读写它映射的地址。
// 根据num queue,设置给mq_cfg。如果是blk,则直接写入num queue,如果是net则写入queue pair数
// 4. 对于cfg->queue_desc_lo、cfg->queue_size、lm_cfg的last_avail_idx和last_used_idx、cfg->queue_enable等等,
// 则是先从vhost_virtqueue中获取到,再设置给硬件寄存器。
// 5. 对于每个队列的queue_msix_vector,则是从1写入queue0, 2写入queue1依次类推,不区分blk和net。最后一个ctrlq的queue_msix_vector应该是没有写。
// 内核态驱动中,会将这里填入queue_msix_vector的值跟实际设备的中断号关联起来。
// 6. 对于每个队列的cfg->queue_notify_off,则是读取到notify_off中,然后进行下面计算得到当前队列的notify地址:
// hw->notify_addr[i] = (void *)((u8 *)hw->notify_base + notify_off * hw->notify_off_multiplier);
//
// hw->notify_off_multiplier是从notify的pci配置空间的capability中读取出来的,如下:
// rte_pci_read_config(dev, &hw->notify_off_multiplier, 4, pos + sizeof(cap));
// 即,从notify这个cap的起始位置pos,往后偏移一个cap结构的偏移,读取4个字节内容赋值给hw->notify_off_multiplier
//
// hw->notify_base是根据notify cap在pci配置空间中指示的bar和offset,从对应映射的bar内存地址加偏移得到的地址,即:
// hw->notify_base = get_cfg_addr(dev, &cap);
ifcvf_reset(hw);
ifcvf_set_status(hw, 0);
IFCVF_WRITE_REG8(status, &hw->common_cfg->device_status);
rte_write8((val), (reg))
rte_write8_relaxed(value, addr);
*(volatile uint8_t *)addr = value;
ifcvf_add_status(hw, IFCVF_CONFIG_STATUS_ACK);
status |= ifcvf_get_status(hw);
ifcvf_set_status(hw, status);
ifcvf_add_status(hw, IFCVF_CONFIG_STATUS_DRIVER);
ifcvf_config_features(hw)
host_features = ifcvf_get_features(hw); // 获取hw的feature
hw->req_features &= host_features; // 跟当前软件的feature取交集
ifcvf_set_features(hw, hw->req_features); // 设置给hw的feature
struct ifcvf_pci_common_cfg *cfg = hw->common_cfg;
IFCVF_WRITE_REG32(0, &cfg->guest_feature_select);
IFCVF_WRITE_REG32(features & ((1ULL << 32) - 1), &cfg->guest_feature);
IFCVF_WRITE_REG32(1, &cfg->guest_feature_select);
IFCVF_WRITE_REG32(features >> 32, &cfg->guest_feature);
ifcvf_add_status(hw, IFCVF_CONFIG_STATUS_FEATURES_OK);
ifcvf_hw_enable(hw)
IFCVF_WRITE_REG16(0, &cfg->msix_config);
cfg = hw->common_cfg;
lm_cfg = hw->lm_cfg;
ifcvf_enable_mq(hw);
// 根据num queue,设置给mq_cfg。如果是blk,则直接写入num queue,如果是net则写入queue pair数
mq_cfg = hw->mq_cfg;
*(u32 *)mq_cfg = nr_queue;
*(u32 *)mq_cfg = nr_queue / 2;
for (i = 0; i < hw->nr_vring; i++) {
IFCVF_WRITE_REG16(i, &cfg->queue_select);
io_write64_twopart(hw->vring[i].desc, &cfg->queue_desc_lo, &cfg->queue_desc_hi);
io_write64_twopart(hw->vring[i].avail, &cfg->queue_avail_lo, &cfg->queue_avail_hi);
io_write64_twopart(hw->vring[i].used, &cfg->queue_used_lo, &cfg->queue_used_hi);
IFCVF_WRITE_REG16(hw->vring[i].size, &cfg->queue_size);
if (lm_cfg) {
// 对于blk和net,分别将后端的avail 和used idx写到lm_cfg中。
// 对于blk,每个vq的两个idx,独占一个IFCVF_LM_CFG_SIZE
// 对于net,rx和tx一对vq,共享一个IFCVF_LM_CFG_SIZE,rxvq占前4个字节,txvq占靠后4个字节
*(u32 *)(lm_cfg + IFCVF_LM_RING_STATE_OFFSET + i * IFCVF_LM_CFG_SIZE) =
(u32)hw->vring[i].last_avail_idx |
((u32)hw->vring[i].last_used_idx << 16);
*(u32 *)(lm_cfg + IFCVF_LM_RING_STATE_OFFSET + (i / 2) * IFCVF_LM_CFG_SIZE +
(i % 2) * 4) =
(u32)hw->vring[i].last_avail_idx |
((u32)hw->vring[i].last_used_idx << 16)
IFCVF_WRITE_REG16(i + 1, &cfg->queue_msix_vector);
notify_off = IFCVF_READ_REG16(&cfg->queue_notify_off);
hw->notify_addr[i] = (void *)((u8 *)hw->notify_base + notify_off * hw->notify_off_multiplier);
IFCVF_WRITE_REG16(1, &cfg->queue_enable);
ifcvf_add_status(hw, IFCVF_CONFIG_STATUS_DRIVER_OK)
// 将每个队列的notify对应的eventfd,即kickfd,添加到poll轮询中,等待qemu端触发notify。可读的时候,表示收到了notify,
// 然后根据触发notify的队列,得到硬件设备的notify地址,并将队列index写入该地址,通知硬件设备。
ret = setup_notify_relay(internal);
rte_ctrl_thread_create(&internal->tid, name, NULL, notify_relay,
// 创建线程notify_relay,里面poll监听所有kickfd的notify事件,
// 如果有,则提取对应的qid,并写一下该queue的notify,通知给硬件
// 将每个vq的kickfd添加到
q_num = rte_vhost_get_vring_num(internal->vid);
for (qid = 0; qid < q_num; qid++) {
ev.events = EPOLLIN | EPOLLPRI;
rte_vhost_get_vhost_vring(internal->vid, qid, &vring);
ev.data.u64 = qid | (uint64_t)vring.kickfd << 32;
epoll_ctl(epfd, EPOLL_CTL_ADD, vring.kickfd, &ev)
}
for (;;) {
nfds = epoll_wait(epfd, events, q_num, -1);
for (i = 0; i < nfds; i++) {
qid = events[i].data.u32;
kickfd = (uint32_t)(events[i].data.u64 >> 32);
nbytes = read(kickfd, &buf, 8); // while (1)循环检测是否有返回值
ifcvf_notify_queue(hw, qid);
IFCVF_WRITE_REG16(qid, hw->notify_addr[qid]);
}
// 至此virtio设备就初始化完成了,后面可以进行正常的io和收发包,这里省去了每个queue的callfd的中断初始化,
// 这部分初始化的核心位于内核态vfio-pci的实现中,所以先跳过,简单来说,就是将每个queue的callfd填充到一个irq_set->data[]中,
// 然后调用下面函数通知给vfio:
// ioctl(internal->vfio_dev_fd, VFIO_DEVICE_SET_IRQS, irq_set);
// 需要留意的是irq_set->data[]的第一个,存放的是config中断的callfd,后面依次是queue0、queue1等等的callfd。
// 这个顺序跟内核态驱动,注册中断的顺序是一致的,中断申请的时候,第一个中断分配给了config中断,后面依次分配给了queue0、queue1等。
// 这里通过vfio-pci绑定中断eventfd,也是这个顺序。内核态初始化的时候,按照config、queue0、queue1...顺序先申请好了中断号,
// 然后这里按照同样顺序,根据传入的irq_set->data[]的index顺序,将各个callfd eventfd绑定到同样顺序位置对应的中断,即调用request_irq函数,
// 将中断号绑定中断处理函数vfio_msihandler和对应的传入的eventfd,这样,当设备触发中断的时候,在vfio_msihandler处理函数中,会调用
// eventfd_signal(trigger, 1)通知用户态poll对应的eventfd,进而通过qemu触发到guest。
// device的msi_list链接的各个msi_desc的先后顺序,跟这里的irq_set->data[]的index先后顺序是一一对应的。根据这个可以把vector和irq关联起来。