流程介紹
虛機向virtio磁盤寫入數據後,走到塊設備層提交bio,最終會往virtio-blk隊列的環上添加寫入數據的物理地址,整個流程如下:
submit_bio
generic_make_request /* 將bio提交到塊設備的工作隊列上去 */
blk_mq_dispatch_rq_list /* 工作隊列處理函數 */
q->mq_ops->queue_rq() /* 調用多隊列入隊請求的具體實現 */
virtio_queue_rq /* virtio磁盤的入隊請求實現 */
__virtblk_add_req /* 將IO數據地址添加到virtio的隊列上 */
virtqueue_kick_prepare /* 判斷是否要通知後端 */
virtqueue_notify /* 通知 */
vq->notify() /* virtio隊列的通知實現 */
vp_notify() /* 對於基於pci的virtio設備,最終調用該函數實現通知 */
vp_notify的具體實現如下:
/* the notify function used when creating a virt queue */
bool vp_notify(struct virtqueue *vq)
{
/* we write the queue's selector into the notification register to
* signal the other end */
iowrite16(vq->index, (void __iomem *)vq->priv);
return true;
}
從這裏看,notify的動作就是往隊列的一個priv成員中寫入隊列的idx。這個priv成員在哪兒初始化的?看下面:
static struct virtqueue *setup_vq(struct virtio_pci_device *vp_dev,
struct virtio_pci_vq_info *info,
unsigned index,
void (*callback)(struct virtqueue *vq),
const char *name,
u16 msix_vec)
{
......
/* create the vring */
vq = vring_create_virtqueue(index, num, /* 1 */
SMP_CACHE_BYTES, &vp_dev->vdev,
true, true, vp_notify, callback, name);
vq->priv = (void __force *)vp_dev->notify_base + off * vp_dev->notify_offset_multiplier; /* 2 */
......
}
1. 創建virtio磁盤列隊,爲vring分配空間,並將其掛載到隊列上。函數傳入了一個vp_notify回調函數,這個函數就是在Guest添加buffer後要調用的
通知後端的notify函數
2. 設置notify函數中要寫入的pci地址,這個地址的計算依據是virtio規範
硬件基礎
virtio設備的PCI空間中,virtio_pci_cap_notify_cfg是專門用作前端通知的cap,通過讀取這個配置空間的信息,可以計算出通知後端時前端寫入的地址,整個virtio-pci的配置空間如下:
virtio中關於notify寫入地址的計算方法介紹如下:
從規範的介紹來看,notify地址是notify cap在bar空間內的偏移,加上common cap的queue_notify_off字段與notify cap的notify_off_multiplier的乘積。再看一次之前的地址計算公式,就是規範裏面介紹的計算方法
vq->priv = (void __force *)vp_dev->notify_base + off * vp_dev->notify_offset_multiplier
VM-exit
前端往notify地址寫入數據後,由於這是外設的空間,寫操作被認爲是敏感指令,觸發VM-exit,首先查看前端notify virtio磁盤時要寫的地址區間。
- 後端查看virtio磁盤的pci配置空間物理地址,notify的cap位於BAR4,BAR4的物理區間是0xfe008000~0xfe00bfff
virsh qemu-monitor-command vm --hmp info pci
- 後端查看notify配置空間在BAR4佔用的物理區間0xfe00b000~0xfe00bfff
virsh qemu-monitor-command vm --hmp info mtree
- 從上面可以知道,前端如果往virtio磁盤上添加buffer之後,notify要寫入的地址區間是0xfe00b000~0xfe00bfff,使用gdb跟蹤vp_notify的流程,可以看到最終notify會往地址爲0xffffc900003b8000的內存空間寫0,該地址就是pci總線域notify地址通過ioremap映射到存儲器域的地址,理論上,訪問這個地址就是訪問pci的notify地址
- 繼續單指令調試,下面這條mov指令是真正的寫io空間的操作,執行的時候觸發VM-Exit
- 在執行mov指令之前,打開主機上的kvm的兩個trace點,獲取VM-Exit中KVM的日誌打印
echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_exit/enable
echo 1 > /sys/kernel/debug/tracing/events/kvm/kvm_fast_mmio/enable
- 兩個trace點輸出信息在內核的定義如下:
/*
* Tracepoint for kvm guest exit:
*/
TRACE_EVENT(kvm_exit,
TP_PROTO(unsigned int exit_reason, struct kvm_vcpu *vcpu, u32 isa),
TP_ARGS(exit_reason, vcpu, isa),
......
TP_printk("reason %s rip 0x%lx info %llx %llx", /* 1 */
(__entry->isa == KVM_ISA_VMX) ?
__print_symbolic(__entry->exit_reason, VMX_EXIT_REASONS) :
__print_symbolic(__entry->exit_reason, SVM_EXIT_REASONS),
__entry->guest_rip, __entry->info1, __entry->info2)
);
/*
* Tracepoint for fast mmio.
*/
TRACE_EVENT(kvm_fast_mmio,
TP_PROTO(u64 gpa),
TP_ARGS(gpa),
......
TP_printk("fast mmio at gpa 0x%llx", __entry->gpa) /* 2 */
);
1. kvm_exit的輸出信息分別是:退出原因,引發退出的指令地址,VM-Exit退出時VMCS VM-Exit相關信息,如下:
info1:EXIT_QUALIFICATION,記錄觸發VM-Exit的指令或者異常
info2:VM_EXIT_INTR_INFO,記錄觸發VM-Exit的中斷信息
2. kvm_exit如果是EPT violation或者EPT misconfiguration引起的,會將引起退出的物理地址放到VMCS VM-Exit的GUEST_PHYSICAL_ADDRESS
字段,這裏就是打印這個字段裏面的值
- 追蹤VM-Exit流程,可以知道notify引發的退出,被歸類爲EPT misconfiguration,是由於訪問內存異常產生的退出。如果虛機EPT頁不存在,觸發的是EPT violation異常,KVM會進行缺頁處理。如果頁存在但訪問權限不對,才觸發EPT misconfiguration,這裏顯然是虛機沒有這個地址的訪問權限。從而觸發VM-Exit。
KVM缺頁處理
當客戶機因爲缺頁異常而退出後,KVM有兩種處理方式,第一種是常規的方式,針對引起缺頁的GPA,填充其對應的EPT頁表並將信息同步給qemu進程的頁表,這時KVM處理客戶機缺頁的最常見流程。另外一種,就是本文討論的情況,虛機訪問的物理地址不是普通的內存,而是一個PCI的配置空間,這種類型的內存,在qemu的分類中是MMIO(Memory-map ),當虛機讀寫這類內存的時候,會觸發qemu的callback。本小節就是介紹KVM對這種缺頁類型的處理機制——ioeventfd。
- TODO