virtio
Virtio是IO虛擬化中的一個(gè)優(yōu)化方案,屬于para-virtulization的一種實(shí)現(xiàn)柒室,即Guest OS中需要運(yùn)行virtio的驅(qū)動(dòng)程序,通過virtio設(shè)備和后端(KVM/QEMU)進(jìn)行交互逗宜。
Virtio設(shè)備可以視為QEMU為Guest模擬的一個(gè)PCI設(shè)備雄右,因此可以像普通PCI設(shè)備一樣配置、使用中斷和DMA機(jī)制纺讲,這對設(shè)備驅(qū)動(dòng)開發(fā)者來說很方便擂仍。
Virtio 使用 virtqueue 來實(shí)現(xiàn)其 I/O 機(jī)制,每個(gè) virtqueue 就是一個(gè)承載大量數(shù)據(jù)的 queue熬甚。vring 是virtqueue的具體實(shí)現(xiàn)方式逢渔,后面會(huì)詳細(xì)介紹vring的實(shí)現(xiàn)。
Virtio-blk
QEMU為虛擬機(jī)指定一個(gè)Virtio-blk設(shè)備 乡括,使得Guest中能看到一個(gè)”/dev/vda”設(shè)備
-drive file=../sdb.img,cache=none,if=virtio
Virtio-blk前端驅(qū)動(dòng)
Guest系統(tǒng)中涉及的Virtio-blk drivers包括(按照執(zhí)行的先后順序):
- virtio.c
- 注冊virtio_bus
- virtio_pci.c
- 注冊pci_driver到pci總線(pci_bus_type)
- probe函數(shù)會(huì)根據(jù)pci_dev創(chuàng)建virtio_pci_device肃廓,并將virtio_pci_device添加到virtio_bus
- virtio_blk.c
- 注冊virtio_driver到virtio_bus下
- probe函數(shù)完成virtio-blk設(shè)備具體的初始化:
- 創(chuàng)建塊設(shè)備"/dev/vda"及其request_queue
- 創(chuàng)建和Host通信需要的virtqueue和vring
從Linux設(shè)備驅(qū)動(dòng)的框架來看,virtio-blk涉及到:
- 兩個(gè)bus:pci_bus_type, virtio_bus
- 兩個(gè)driver:virtio_pci_driver, virtio_blk
- 兩個(gè)device:pci_dev, virtio_pci_device
Virtio-blk前端IO流程
virtblk_probe函數(shù)中為gendisk分配了request_queue诲泌,內(nèi)核從v3.13開始盲赊,virtio開始使用multi-queue。(multi-queue的設(shè)計(jì)犧牲了全局范圍的request合并敷扫;認(rèn)為大部分相鄰的訪問都集中在同一個(gè)進(jìn)程哀蘑,所以request只在本CPU的軟件隊(duì)列處理,因而不需要加鎖呻澜。)
“/dev/vda”和讀寫普通的磁盤一樣递礼,VFS的讀寫請求在到達(dá)塊設(shè)備之前會(huì)經(jīng)過一個(gè)漫長的旅程
user memory --> page --> buffer_head --> bio --> request
最終構(gòu)造成request提交給塊設(shè)備的請求隊(duì)列:
submit_bh(write_op, bh);
submit_bio(rw, bio);
generic_make_request
q->make_request_fn(q, bio); /* blk_sq_make_request */
blk_mq_run_hw_queue
__blk_mq_run_hw_queue
q->mq_ops->queue_rq /* virtio_queue_rq */
對于一個(gè)讀寫請求惨险,最終需要交給后端的信息有:
- page/offset/len Guest的物理內(nèi)存地址
- sector 虛擬塊設(shè)備的地址
- type 讀還是寫
virtio_queue_rq()
blk_rq_map_sg
__blk_bios_map_sg
__virtblk_add_req(vblk->vqs[qid].vq, vbr, vbr->sg, num);
sg_init_one(&hdr, &vbr->out_hdr, sizeof(vbr->out_hdr))
sgs[num_out + num_in++] = data_sg;
virtqueue_add_sgs(vq, sgs, num_out, num_in, vbr, GFP_ATOMIC)
virtqueue_add /* 將sg填入到vring中去 */
desc[i].addr = sg_phys(sg);
desc[i].len = sg->length;
virtqueue_kick_prepare
virtqueue_notify(vblk->vqs[qid].vq);
我們可以看到向vring中寫了多個(gè)scatterlist:
- out_hdr 用來向后端描述這次請求羹幸,包括type, sector, ioprio
- Data 一個(gè)或者多個(gè)Guest OS的一個(gè)物理地址
-
Status Guest OS準(zhǔn)備好的一個(gè)字節(jié),后端在IO完成后填寫
寫完vring之后通過virtqueue_notify來通知QEMU
virtqueue_notify
vq->notify(_vq) <-- vp_notify
iowrite16(vq->index, vp_dev->ioaddr + VIRTIO_PCI_QUEUE_NOTIFY)
其實(shí)質(zhì)是Guest寫io寄存器辫愉,從而觸發(fā)VM exit到KVM中處理栅受,KVM檢查退出的返回值,無法處理就一步步返回到最初的入口kvm_vcpu_ioctl,然后返回到用戶態(tài)也就是QEMU進(jìn)程空間屏镊。
Vring
Vring由一個(gè)freelist和兩個(gè)ring組成:
desc數(shù)組構(gòu)造了一個(gè)freelist依疼,每一片里存放著Guest和Host之間傳輸?shù)臄?shù)據(jù):
- addr/len Guest的物理地址和長度
- flags next是否有效?讀 or 寫而芥? INDIRECT 律罢?
- next
avail->ring[]是發(fā)送端(Guest)維護(hù)的環(huán)形隊(duì)列,指向需要host處理的desc(一次用了多片desc棍丐,但ring[]里只寫入了一個(gè)idx误辑;這多片desc通過鏈表組織起來)
used->ring[]是接收端(Host/QEMU)維護(hù)的環(huán)形隊(duì)列,指向自己已經(jīng)處理過了的desc
- 發(fā)送端(Guest)更新
- vring.avail->idx
- vring_virtqueue.free_head歌逢,它指向desc數(shù)組里freelist的頭
- vring_virtqueue.last_used_idx巾钉,它表示Guest下一次檢查used ring[]的位置
- Host更新
- vring.used->idx
- VirtQueue.last_avail_idx,它表示Host下一次檢查avail ring[]的位置
- 這四個(gè)計(jì)數(shù)會(huì)一直遞增下去
QEMU
KVM退出到QEMU之后進(jìn)入kvm_handle_io函數(shù)秘案,通過write eventfd將等待在ppoll系統(tǒng)調(diào)用上的QEMU的主線程喚醒
int kvm_cpu_exec(CPUArchState *env)
{
do {
run_ret = kvm_vcpu_ioctl(env, KVM_RUN, 0);
switch (run->exit_reason) { /* Qemu根據(jù)退出的原因進(jìn)行處理 */
case KVM_EXIT_IO:
kvm_handle_io();
...
main線程處理vring的主要流程:調(diào)用vq的回調(diào)函數(shù)砰苍,從vring中讀取Guest的物理地址,并轉(zhuǎn)化為自己的虛擬地址后構(gòu)造成QEMU的request
main() main_loop() main_loop_wait ()
os_host_main_loop_wait()
glib_pollfds_poll()
g_main_context_dispatch ()
aio_ctx_dispatch aio_dispatch
virtio_queue_host_notifier_read
virtio_queue_notify_vq
virtio_blk_handle_output
Vring的處理函數(shù)
Vring注冊的處理函數(shù)virtio_blk_handle_output阱高,從vring中讀取請求赚导,然后構(gòu)造成QEMU的request,然后創(chuàng)建協(xié)程赤惊,在協(xié)程中完成IO的提交辟癌。
QEMU協(xié)程
如果指定了aio=native
-drive if=none,id=drive0,cache=none,aio=native,format=qcow2,file=path/to/disk.img \
-device virtio-blk,drive=drive0,scsi=off
那么IO主流程和協(xié)程的交互過程大致如下圖所示:
要理解協(xié)程,上圖有幾個(gè)關(guān)鍵跳轉(zhuǎn)需要注意:
- 原線程調(diào)用qemu_coroutine_enter進(jìn)入?yún)f(xié)程荐捻;
- 協(xié)程submit_io后通過qemu_coroutine_yield直接“退出”協(xié)程黍少,返回到原線程調(diào)用enter處,而不是“返回”到調(diào)動(dòng)yield處处面,此時(shí)協(xié)程的代碼邏輯是沒有執(zhí)行完的厂置;原線程可以繼續(xù)在循環(huán)中創(chuàng)建新的協(xié)程來不斷的提交io;
- io完成后main_loop中再次調(diào)用qemu_coroutine_enter再次進(jìn)入?yún)f(xié)程魂角,協(xié)程的代碼邏輯好像是調(diào)用yield返回一樣昵济,然后開始執(zhí)行yield之后的代碼,一步步返回到上層函數(shù)野揪;
- 協(xié)程調(diào)用blk_aio_complete
QEMU block driver
上圖協(xié)程的部分里的回調(diào)函數(shù)需要關(guān)注
- 在協(xié)程的IO棧里bdrv_aligned_preadv被調(diào)用了兩次访忿,但兩次調(diào)用drv->bdrv_co_readv是不一樣的,第一次的drv是bdrv_qcow2斯稳,第二次的drv是bdrv_file
- 對于本例中的塊設(shè)備IO海铆,QEMU協(xié)程中實(shí)際上分了兩步:QCOW2處理和file處理,分別對應(yīng)兩個(gè)struct BlockDriverState挣惰,它們有不同的drv
- bs->drv->bdrv_aio_readv卧斟,這是不同drv提交IO的函數(shù)殴边,對于本地文件系統(tǒng)就是raw_aio_submit,最終選擇io_submit或者pread/pwrite系統(tǒng)調(diào)用珍语;而對于其它類型的存儲锤岸,比如Ceph rbd就參考bdrv_rbd中的實(shí)現(xiàn)。
如果qemu參數(shù)沒有指定aio=native板乙,那么協(xié)程中將會(huì)使用線程池來模擬異步IO是偷,paio_submit會(huì)從線程池中找一個(gè)worker線程,然后在worker線程中調(diào)用pread/pwrite:
| start_thread
| worker_thread
| req->func(req->arg) /* aio_worker */
| handle_aiocb_rw
| handle_aiocb_rw_linear
| pwrite/pread /* syscall */
| qemu_bh_schedule
| aio_notify(ctx) /* 寫main_loop中阻塞的fd */
main_loop線程被qemu_bh_schedule喚醒之后:
| main_loop -- > glib_pollfds_poll -- > thread_pool_completion_bh -- > ...
| bdrv_co_io_em_complete < -- 調(diào)用drv->bdrv_aio_readv時(shí)指定的回調(diào)函數(shù)
| qemu_coroutine_enter(co->coroutine, NULL)
| qemu_coroutine_switch /* 再次進(jìn)入?yún)f(xié)程 */
對于不同的BlockBackend募逞,其對應(yīng)的BlockDriver也不相同晓猛,我們需要的就是實(shí)現(xiàn)自己的BlockDriver中的各種函數(shù),比如. bdrv_file_open和.bdrv_aio_readv
Vhost
Virtio-vring實(shí)現(xiàn)了一套Guest和Host之間基于PCI設(shè)備的標(biāo)準(zhǔn)接口凡辱,同時(shí)將原來多次的IO寄存器的訪問改為vring的讀寫戒职,從而減少了VM Exit和Resume的次數(shù)。
但是Virtio避免不了Host上內(nèi)存的拷貝:
QEMU仍然是一個(gè)普通的進(jìn)程透乾,QEMU也需要通過syscall發(fā)起IO請求洪燥,Host內(nèi)核正常情況下會(huì)將數(shù)據(jù)讀/寫到內(nèi)核的page中,然后從內(nèi)核page拷貝到QEMU的虛擬地址中乳乌。
Vhost可以實(shí)現(xiàn)Guest和Host Kernel直接進(jìn)行數(shù)據(jù)交換捧韵,從而避免syscall和數(shù)據(jù)拷貝的性能消耗。
vhost和kvm是兩個(gè)獨(dú)立的運(yùn)行模塊汉操,用戶態(tài)程序通過“/dev/vhost-net”來訪問再来,對于Guest來說,vhost并沒有模擬一個(gè)完整的PCI適配器磷瘤。它內(nèi)部只涉及了virtqueue-vring的操作芒篷,而virtio設(shè)備的適配模擬仍然由Qemu來負(fù)責(zé)。
vhost與kvm的事件通信通過eventfd機(jī)制來實(shí)現(xiàn)采缚,主要包括兩個(gè)方向的event针炉,一個(gè)是Guest到Vhost方向的kick event,通過ioeventfd承載扳抽;另一個(gè)是Vhost到Guest方向的call event篡帕,通過irqfd承載。