QEMU中VIRTIO實現


http://39.107.46.219/qemu%E8%99%9A%E6%8B%9F%E5%8C%96%E5%AE%89%E5%85%A8%EF%BC%88%E4%BA%8C%EF%BC%89/

 

 

VIRTIO設備

​ 了解QEMU和KVM交互的知道,客戶機的IO操作通過KVM處理后再交由QEMU,反饋也如此。這種純軟件的模擬IO設備,增加了IO的延遲。

​ 而Virtio卻為虛擬化的IO提供了另一種解決方案:

Virtio在虛擬機系統內核安裝前端驅動,在QEMU中實現后端驅動。前后端驅動通過Virtqueue直接通信,從而繞過了KVM內核模塊處理,提高了IO操作性能。

QEMU中VIRTIO實現

啟動配置設備
-device virtio-scsi-pci

在虛擬機里查看scsi設備lspci

Bm5Sf0.png

可以看到Virtio-pci設備的相關信息:IO/PORT: 0xc040 (size=64),MemoryAddress: 0xfebf1000(size=4k)

Virtqueue

​ Virtio使用Virtqueue實現IO機制,每個Virtqueue就是承載大量數據的queue。vring是Virtqueue實現的具體方式;virtio_ring是virtio傳出機制的實現,vring引入ving buffer作為數據的載體。

struct VirtQueue
{
  VRing vring; /* Next head to pop */
    uint16_t last_avail_idx;

    /* Last avail_idx read from VQ. */
    uint16_t shadow_avail_idx;

    uint16_t used_idx;

    /* Last used index value we have signalled on */
    uint16_t signalled_used;

    /* Last used index value we have signalled on */
    bool signalled_used_valid;

    /* Notification enabled? */
    bool notification;

    uint16_t queue_index;

    int inuse;

    uint16_t vector;
    void (*handle_output)(VirtIODevice *vdev, VirtQueue *vq);   // handle output
    void (*handle_aio_output)(VirtIODevice *vdev, VirtQueue *vq);
    VirtIODevice *vdev;
    EventNotifier guest_notifier;
    EventNotifier host_notifier;
    QLIST_ENTRY(VirtQueue) node;
};

vring
typedef struct VRing
{
    unsigned int num;       // 
    unsigned int num_default;
    unsigned int align;
    hwaddr desc;            // 關聯描述符數組 (buffer的描述)
    hwaddr avail;           // 表示客戶機可用的描述符
    hwaddr used;            // 表示宿主機已經使用的描述符
} VRing;
Vring Descriptor
typedef struct VRingDesc
{
    uint64_t addr;  // 指向guest端的物理地址, 一組buffer列表
    uint32_t len;   // buffer長度
    uint16_t flags; // 包含 3 個值,分別是 VRING_DESC_F_NEXT(1)、
                    // VRING_DESC_F_WRITE(2)、VRING_DESC_F_INDIRECT(4);
    uint16_t next;  //指向下一個描述符的index(鏈表結構)
} VRingDesc;

 

 

​ 由一組描述符構成描述符表

Available Vring
typedef struct VRingAvail
{
    uint16_t flags;
    uint16_t idx;  // 指向下一描述符表的入口
    uint16_t ring[0]; // 每一個值是一個索引,指向描述符表中的一個可用描述符
} VRingAvail;

VRingUsedElem
typedef struct VRingUsedElem
{
    uint32_t id;
    uint32_t len;
} VRingUsedElem;
VRingUsed
typedef struct VRingUsed
{
    uint16_t flags;
    uint16_t idx;
    VRingUsedElem ring[0];
} VRingUsed;

 

Virtqueue初始化(在Qemu端實現)
VirtQueue *virtio_add_queue(VirtIODevice *vdev, int queue_size,
                            void (*handle_output)(VirtIODevice *, VirtQueue *))
{                           //每個Device 維護一組Virtqueue
    int i;

    for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {    
        if (vdev->vq[i].vring.num == 0)
            break;
    }

    if (i == VIRTIO_QUEUE_MAX || queue_size > VIRTQUEUE_MAX_SIZE) 
        abort();                        // 每個Device最多1024Virtqueue
                                        // 每個Virtqueue最多1024 vring
    vdev->vq[i].vring.num = queue_size; // 初始化vring.num
    vdev->vq[i].vring.num_default = queue_size; // 初始化vring.num_default
    vdev->vq[i].vring.align = VIRTIO_PCI_VRING_ALIGN; //初始化vring.align
    vdev->vq[i].handle_output = handle_output;  // 初始化handle_output
    vdev->vq[i].handle_aio_output = NULL;   // handle_aio_output

    return &vdev->vq[i];
}

 

 

​ 在Guest端,virtio驅動中vm_setup_vq建立與queue對應的Virtqueue

num = readl(vm_dev->base + VIRTIO_MMIO_QUEUE_NUM_MAX);// 獲取vring.num

// vring_create_virtqueue
queue = vring_alloc_queue(vdev, vring_size(num, vring_align),
                      &dma_addr, GFP_KERNEL|__GFP_ZERO);// 分配Virtqueue空間

//vring_size計算方式
static inline unsigned vring_size(unsigned int num, unsigned long align)
{
    return ((sizeof(struct vring_desc) * num + sizeof(__virtio16) * (3 + num)
         + align - 1) & ~(align - 1))
        + sizeof(__virtio16) * 3 + sizeof(struct vring_used_elem) * num;
}

 

​ 從這里可以看出來vring的內存布局

Bm59pV.png

​ 接着Guest virtio驅動通知Qemu Queue的vring.num

writel(virtqueue_get_vring_size(vq), vm_dev->base + VIRTIO_MMIO_QUEUE_NUM); unsigned int virtqueue_get_vring_size(struct virtqueue *_vq) { struct vring_virtqueue *vq = to_vvq(_vq); return vq->vring.num; } 
 
Guest向虛擬設備提供buffer

在virtio驅動virtqueue_add實現

// buffer空間 DMA方式分配 dma_addr_t addr = vring_map_one_sg(vq, sg, DMA_TO_DEVICE); // 填充desc表 flags addr len desc[i].flags = cpu_to_virtio16(_vq->vdev, VRING_DESC_F_NEXT); desc[i].addr = cpu_to_virtio64(_vq->vdev, addr); desc[i].len = cpu_to_virtio32(_vq->vdev, sg->length); //更新可用ring頭 /* Put entry in available array (but don't update avail->idx until they * do sync). */ avail = vq->avail_idx_shadow & (vq->vring.num - 1); vq->vring.avail->ring[avail] = cpu_to_virtio16(_vq->vdev, head); //更新可用ring index vq->avail_idx_shadow++; vq->vring.avail->idx = cpu_to_virtio16(_vq->vdev, vq->avail_idx_shadow); //當Virtqueue添加次數達到64k時,flush vring內容到QEMU if (unlikely(vq->num_added == (1 << 16) - 1)) virtqueue_kick(_vq); bool virtqueue_kick(struct virtqueue *vq) { if (virtqueue_kick_prepare(vq)) // 修改 virtqueue notify 寄存器 return virtqueue_notify(vq); return true; } 
 
虛擬設備使用Buffer
    offset = 0;
while (offset < size) { 
        //從desc表中尋找available ring中添加的buffers,映射內存
        elem = virtqueue_pop(vrng->vq, sizeof(VirtQueueElement));

        if (!elem) {
            break;
        }
        // 讀取內容
        len = iov_from_buf(elem->in_sg, elem->in_num,
                           0, buf + offset, size - offset);
        // 更新讀取光標
        offset += len;
        virtqueue_push(vrng->vq, elem, len);
        trace_virtio_rng_pushed(vrng, len);
        g_free(elem);
    }

void virtqueue_push(VirtQueue *vq, const VirtQueueElement *elem,
                    unsigned int len)
{   // 取消內存映射,跟新usedVring字段
    virtqueue_fill(vq, elem, len, 0);
    virtqueue_flush(vq, 1);
}

 

 
QEMU-GUEST交互

所有設備的i/o操作都經由virtio_ioport_write處理

static void virtio_ioport_write(void *opaque, uint32_t addr, uint32_t val){
    .....
    switch (addr) {
    case VIRTIO_PCI_GUEST_FEATURES:
        /* Guest does not negotiate properly?  We have to assume nothing. */
        if (val & (1 << VIRTIO_F_BAD_FEATURE)) {
            val = virtio_bus_get_vdev_bad_features(&proxy->bus);
        }
        virtio_set_features(vdev, val);
        break;
        ....
    case VIRTIO_PCI_QUEUE_PFN:  // addr = 8
        pa = (hwaddr)val << VIRTIO_PCI_QUEUE_ADDR_SHIFT;    // 描述符表物理地址
        if (pa == 0) {
            virtio_pci_reset(DEVICE(proxy));
        }
        else
            virtio_queue_set_addr(vdev, vdev->queue_sel, pa); // 寫入描述符表物理地址
        break;
    case VIRTIO_PCI_QUEUE_SEL:  // addr = 14
        if (val < VIRTIO_QUEUE_MAX)
            vdev->queue_sel = val;  // 更新Virtqueue handle_output 序號 
        break;
    case VIRTIO_PCI_QUEUE_NOTIFY:   // addr = 16
        if (val < VIRTIO_QUEUE_MAX) {
            virtio_queue_notify(vdev, val); //根據val序號 觸發Virtqueue的描述符表
        }
        break;
        }
}

 

 

其中addr是相對於ioport端口地址的偏移, val是寫入的數據。

​ 在該函數下斷點,運行到vdev被初始化后

outl(0xaa, 0xc040+0x10)     // module_init執行

​ 斷下的狀態

Bm5km4.png

可以看到有三種handle_output:ctrl, event, cmd

而我們handle_output被觸發的路徑

virtio_ioprt_write ==> virtio_queue_notify(vdev, val) ==> virtio_queue_notify_vq(&vdev->vq[n]) ==> 

// 觸發VirtQueue->Handle_Output
static void virtio_queue_notify_vq(VirtQueue *vq)
{
    if (vq->vring.desc && vq->handle_output) {
        VirtIODevice *vdev = vq->vdev;

        trace_virtio_queue_notify(vdev, vq - vdev->vq, vq);
        vq->handle_output(vdev, vq);
    }
}

 

 
簡單的I/O交互示例
  VRingDesc *desc1;
    req * buffer;
    VRingAvail *avail;
    VRingUsed *used1;
    unsigned long mem;
    mem = kmalloc(0x3000, GFP_KERNEL);//align
    memset(mem,0,0x3000);
    //vring的內存布局
    desc1 = mem;
    // 因為設備默認最大有0x80個描述符表,一個描述符的大小為0x10
    // qemu實現中把avail表接在了描述符表之后,因此avail表=desc+0x80*0x10;`
    avail = mem + 0x800;
    // 而一個avail結構體為0x2*0x80+4=>0x104,而qemu做了一個4k對齊操作,因此變成了+0x1000
    used1 = mem + 0x1000;

    // 初始化desc
    desc1[0].addr = (u64)virt_to_phys(buffer);
    desc1[0].len = (u32)0x33;   // buffer的大小
    desc1[0].flags = (u16)0x2;  // VRING_DESC_F_WRITE,因為沒有VRING_DESC_F_NEXT標志,表示沒有                                // 下一個描述符
    desc1[0].next = (u16)0x2;   //這個字段無效了

    // buffer為scsi定義的結構體,詳見virtio-scsi.h的99行
    buffer = kmalloc(sizeof(req) * SIZE, GFP_KERNEL);
    buffer->cmd.cdb[0] = 0x28;
    buffer->cmd.lun[0] = 0x0;   //0x1
    buffer->cmd.lun[1] = 0x0;
    buffer->cmd.lun[2] = 0x0;   //0x40

    //初始一個avail表
    avail->idx = 0;
    avail->ring[0] = 0x0;

    // I/O 交互
    queue_sel(2);// 設定命令類型為2,代表 virtio_scsi_handle_cmd
    queue_pfn(mem>>12);// 設定描述符表
    queue_notify(2);// 觸發virtio_scsi_handle_cmd函數.

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM