qemu-kvm的ioeventfd機制
Guest一個完整的IO流程包括從虛擬機內部到KVM,再到QEMU,並由QEMU最終進行分發,IO完成之后的原路返回。這樣的一次路徑稱為同步IO,即指Guest需要等待IO操作的結果才能繼續運行,但是存在這樣一種情況,即某次IO操作只是作為一個通知事件,用於通知QEMU/KVM完成另一個具體的IO,這種情況下沒有必要像普通IO一樣等待數據完全寫完,只需要觸發通知並等待具體IO完成即可。
ioeventfd正是為IO通知提供機制的東西,QEMU可以將虛擬機特定地址關聯一個eventfd,對該eventfd進行POLL,並利用ioctl(KVM_IOEVENTFD)向KVM注冊這段特定地址,當Guest進行IO操作exit到kvm后,kvm可以判斷本次exit是否發生在這段特定地址中,如果是,則直接調用eventfd_signal發送信號到對應的eventfd,導致QEMU的監聽循環返回,觸發具體的操作函數,進行普通IO操作。這樣的一次IO操作相比於不使用ioeventfd的IO操作,能節省一次在QEMU中分發IO請求和處理IO請求的時間。
QEMU注冊ioeventfd
注冊EventNotifier
struct EventNotifier {
#ifdef _WIN32
HANDLE event;
#else
int rfd;
int wfd;
#endif
};
QEMU進行ioeventfd注冊的時候需要一個EventNotifier,該EventNotifier由event_notifier_init()初始化,event_notifier_init中判斷系統是否支持EVENTFD機制,如果支持,那么EventNotifier中的rfd和wfd相等,均為eventfd()系統調用返回的新建的fd,並根據event_notifier_init收到的參數active決定是否喚醒POLLIN事件,即直接觸發eventfd/EventNotifer對應的handler。
如果系統不支持EVENTFD機制,則QEMU會利用pipe模擬eventfd,略過不看。
關聯IO地址&注冊進KVM
在注冊了EventNotifier之后,需要將EventNotifier中含有的fd(ioeventfd)與對應的Guest IO地址關聯起來。
核心函數為memory_region_add_eventfd。
void memory_region_add_eventfd(MemoryRegion *mr,
hwaddr addr,
unsigned size,
bool match_data,
uint64_t data,
EventNotifier *e);
參數中:
- mr指IO地址所在的MemoryRegion
- addr表示IO地址(GPA)
- size表示IO地址的大小
- match_data是一個bool值,表示的是Guest向addr寫入的值是否要與參數data完全一致才讓KVM走ioeventfd路徑,如果match_data為true,那么需要完全一致才讓KVM走ioeventfd路徑,如果為false。則不需要完全一致。
- data與match_data共同作用,用於限制Guest向addr寫入的值
- e指前面注冊的EventNotifier
memory_region_add_eventfd
=> memory_region_ioeventfd_before // 尋找本次要處理的ioeventfd應該在ioeventfd數組中的什么位置
=> g_realloc // 分配原ioeventfd數組大小+1的空間,用於將新的ioeventfd插入到ioeventfd數組中
=> memmove // 將第一步找到的位置之后的ioeventfd從ioeventfd數組中后移一位
=> mr->ioeventfds[i] = mrfd // 將新的ioeventfd插入到MemoryRegion的ioeventfd數組中
=> // 設置ioeventfd_update_pending
=> memory_region_transaction_commit // 該函數中會調用address_space_update_ioeventfds對KVM的ioeventfd布局進行更新
MemoryRegion中有很多ioeventfd,他們以地址從小到大的順序排列,ioeventfd_nb是MemoryRegion中ioeventfd的數量,通過for循環找到本次要添加的ioeventfd應該放在ioeventfd數組中的什么位置,為ioeventfd數組分配原大小+sizeof(ioeventfd)的空間,然后將之前找到的ioeventfd數組中位置之后的ioeventfd向后移動一個位置,然后將新的ioeventfd插入到ioeventfd數組中。最后設置ioevetfd_update_pending標志,調用memory_region_transaction_commit更新KVM中的ioeventfd布局。
memory_region_transaction_commit
=> address_space_update_ioeventfds
=> address_space_add_del_ioeventfds
=> MEMORY_LISTENER_CALL(as, eventfd_add, Reverse, §ion,
fd->match_data, fd->data, fd->e)
即memory_region_add_eventfd最終會調用memory_region_transaction_commit,而后者會調用eventfd_add函數,該eventfd_add函數在qemu中的定義如下:
// PIO
static MemoryListener kvm_io_listener = {
.eventfd_add = kvm_io_ioeventfd_add,
.eventfd_del = kvm_io_ioeventfd_del,
.priority = 10,
};
// MMIO
if (kvm_eventfds_allowed) {
s->memory_listener.listener.eventfd_add = kvm_mem_ioeventfd_add;
對於MMIO和PIO,最終調用的eventfd_add函數不同,MMIO對應的是kvm_mem_ioeventfd_add,而PIO調用的是kvm_io_ioeventfd_add。KVM對MMIO和PIO注冊的ioeventfd進行分辨,靠的是在調用kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd)中的iofd->flags進行辨認,如果flag為0,則為MMIO,如果flag為2,則為PIO。
接下來分別看這兩個不同的eventfd_add函數。
kvm_io_ioeventfd_add
kvm_io_ioeventfd_add
=> fd = event_notifier_get_fd(e) // 獲取之前注冊的EventNotifier中的eventfd的fd
=> kvm_set_ioeventfd_pio(fd, section->offset_within_address_space,
data, true, int128_get64(section->size),match_data);
=> // 定義一個kvm_ioeventfd結構類型變量kick,將要注冊的ioeventfd的data_match,io地址,flags(MMIO/PIO),io地址范圍,fd填充進去
=> // 確定flags中是否要設置KVM_IOEVENTFD_FLAG_DATAMATCH,表明需要全匹配才讓kvm走irqfd路徑
=> // 確定flags中是否要設置KVM_IOEVENTFD_FLAG_DEASSIGN,該flag在ioctl后告知kvm,需要將某地址和該ioeventfd解除關聯
=> kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick) // 將ioeventfd注冊進kvm
kvm_io_ioeventfd_add的邏輯很簡單,就是先獲取本次要注冊到kvm的ioeventfd的相關信息,然后調用ioctl注冊進kvm。
kvm_mem_ioeventfd_add
kvm_mem_ioeventfd_add
=> fd = event_notifier_get_fd(e) // 獲取之前注冊的EventNotifier中的eventfd的fd
=> kvm_set_ioeventfd_mmio(fd, section->offset_within_address_space,
data, true, int128_get64(section->size),match_data);
=> // 定義一個kvm_ioeventfd結構類型變量iofd,將要注冊的ioeventfd的data_match,io地址,flags(MMIO/PIO),io地址范圍,fd填充進去
=> // 確定flags中是否要設置KVM_IOEVENTFD_FLAG_DATAMATCH,表明需要全匹配才讓kvm走irqfd路徑
=> // 確定flags中是否要設置KVM_IOEVENTFD_FLAG_DEASSIGN,該flag在ioctl后告知kvm,需要將某地址和該ioeventfd解除關聯
=> kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &iofd);// 將ioeventfd注冊進kvm
可以看到,kvm_mem_ioeventfd_add與kvm_io_ioeventfd_add的處理步驟幾乎完全一樣,只是在kvm_ioeventfd結構中將flags置為0,標志這是一個MMIO ioeventfd注冊。
KVM注冊ioeventfd
在QEMU調用了kvm_vm_ioctl(KVM_IOEVENTFD)之后,kvm會對該ioctl做出反應。
kvm_vm_ioctl
=> case KVM_IOEVENTFD:{
copy_from_user(&data, argp, sizeof(data))
kvm_ioeventfd(kvm, &data)
}
kvm在獲得了QEMU傳入的參數,也就是kvm_ioeventfd結構的值之后,會調用kvm_ioeventfd。
kvm_ioeventfd
=> // 判斷flags中是否含有KVM_IOEVENTFD_FLAG_DEASSIGN,如果有則調用解除io地址和ioeventfd關聯的函數kvm_deassign_ioeventfd
// 如果沒有,則調用將io地址和ioeventfd關聯起來的函數---kvm_assign_ioeventfd
kvm_assign_ioeventfd中首先從kvm_ioeventfd->flags中提取出該eventfd是MMIO還是PIO,並獲得相應的總線號,也就是代碼中的bus_idx,然后對kvm_ioeventfd結構中的flags進行一些檢查,最終調用kvm_assign_ioeventfd_idx進行實際關聯。
kvm_assign_ioeventfd_idx(kvm, bus_idx, kvm_ioeventfd)
=> eventfd = eventfd_ctx_fdget(args->fd) // 獲取內核態的eventfd
=> kzalloc // 分配一個_ioeventfd結構p,用於表示eventfd和io地址之間的關聯
=> p->addr = args->addr;
p->bus_idx = bus_idx;
p->length = args->len;
p->eventfd = eventfd;
=> // 判斷kvm_ioeventfd結構中的flags是否含有datamatch,如果有,則置p->datamatch為true
=> ioeventfd_check_collision // 判斷本次與地址關聯的ioeventfd是否之前存在
=> kvm_iodevice_init(&p->dev, &ioeventfd_ops); // 初始化_ioevetfd結構中的kvm_io_device成員,將該設備的IO操作設置為ioevetfd_ops
=> kvm_io_bus_register_dev(kvm, bus_idx, p->addr, p->length,
&p->dev) // 將_ioevetfd結構中的kvm_io_device 設備注冊到Guest上
=> // 增加該bus上的ioeventfd_count數量
=> // 將該ioeventfd添加進ioeventfd鏈表中
以上代碼段為kvm_assign_ioeventfd_idx,即,將ioeventfd和具體IO地址進行關聯的主要過程。其中的核心數據為_ioeventfd,具體結構如下:
struct _ioeventfd {
struct list_head list;
u64 addr;
int length;
struct eventfd_ctx *eventfd;
u64 datamatch;
struct kvm_io_device dev;
u8 bus_idx;
bool wildcard;
};
list用於將當前ioeventfd鏈接到kvm的ioeventfd鏈表中去.
addr是ioeventfd對應的IO地址.
Length指的是eventfd關聯的長度.
eventfd即指的是該ioeventfd對應的eventfd.
datamatch用於確認Guest訪問該io地址是否需要全匹配才走ioeventfd路徑.
dev用於將該ioeventfd與Guest關聯起來(通過注冊該dev到Guest實現).
bus_idx指的是該ioeventfd要注冊到kvm的MMIO總線還是PIO總線.
wildcard與datamatch互斥,如果kvm_ioeventfd中datamatch為false,則_ioeventfd->wildcard設備true.
所以_ioeventfd描述符了一個ioeventfd要注冊到kvm中的所有信息,其中包含了ioeventfd信息和需要注冊到Guest的總線和設備信息。
所以整個KVM注冊ioeventfd的邏輯是:
- 將一個ioeventfd與一個虛擬設備dev聯系起來
- 該虛擬設備dev擁有寫函數
- 當Guest訪問ioeventfd對應的io地址時,則調用虛擬設備的write方法。
static const struct kvm_io_device_ops ioeventfd_ops = {
.write = ioeventfd_write,
.destructor = ioeventfd_destructor,
};
需要注意的是,ioeventfd對應的文件操作只有write操作,而沒有read操作。
write操作對應Guest中寫入ioeventfd對應的IO地址時觸發的操作,也就是Guest執行OUT類匯編指令時觸發的操作,相反read操作就是Guest執行IN類匯編指令時觸發的操作,OUT類指令只是簡單向外部輸出數據,無需等待QEMU處理完成即可繼續運行Guest,但IN指令需要從外部獲取數據,必須要等待QEMU處理完成IO請求再繼續運行Guest。
ioeventfd設計的初衷就是節省Guest運行OUT類指令時的時間,IN類指令執行時間無法節省,因此這里的ioeventfd 文件操作中只有write而沒有read。
ioeventfd對應的虛擬dev的操作(write)
ioeventfd_write(struct kvm_vcpu *vcpu, struct kvm_io_device *this, gpa_t addr,int len, const void *val)
{
struct _ioeventfd *p = to_ioeventfd(this);
if (!ioeventfd_in_range(p, addr, len, val))
return -EOPNOTSUPP;
eventfd_signal(p->eventfd, 1);
return 0;
}
可以看到ioeventfd_write函數首先從kvm_io_device得到了_ioeventfd,然后檢查訪問的地址和長度是否符合ioeventfd設置的條件,如果符合,則觸發eventfd_signal,后者增加了eventfd_ctx->count的值,並喚醒等待隊列中的EPOLLIN進程。
虛擬機進行IO操作時QEMU-kvm的處理
當虛擬機向注冊了ioeventfd的地址寫數據時,與所有IO操作一樣,會產生vmexit,接下來的函數處理流程為:
handle_io
=> kvm_fast_pio
=> kvm_fast_pio_out
=> emulator_pio_out_emulated
=> emulator_pio_in_out
=> kernel_pio
=> kvm_io_bus_write
int kvm_io_bus_write(struct kvm_vcpu *vcpu, enum kvm_bus bus_idx, gpa_t addr,
int len, const void *val)
{
struct kvm_io_bus *bus;
struct kvm_io_range range;
int r;
range = (struct kvm_io_range) {
.addr = addr,
.len = len,
};
bus = srcu_dereference(vcpu->kvm->buses[bus_idx], &vcpu->kvm->srcu);
if (!bus)
return -ENOMEM;
r = __kvm_io_bus_write(vcpu, bus, &range, val);
return r < 0 ? r : 0;
}
kvm_io_bus_write首先構造了一個kvm_io_range結構,其中記錄了本次Guest操作的IO地址和長度,然后調用__kvm_io_bus_write.
static int __kvm_io_bus_write(struct kvm_vcpu *vcpu, struct kvm_io_bus *bus,
struct kvm_io_range *range, const void *val)
{
int idx;
idx = kvm_io_bus_get_first_dev(bus, range->addr, range->len);
if (idx < 0)
return -EOPNOTSUPP;
while (idx < bus->dev_count &&
kvm_io_bus_cmp(range, &bus->range[idx]) == 0) {
if (!kvm_iodevice_write(vcpu, bus->range[idx].dev, range->addr,
range->len, val))
return idx;
idx++;
}
return -EOPNOTSUPP;
}
在__kvm_io_bus_write中,kvm_io_bus_get_first_dev用於獲得bus上由kvm_io_range指定的具體地址和長度范圍內的第一個設備的id,然后在bus的這個地址范圍內,針對每一個設備調用kvm_iodevice_write,該函數會調用每個設備之前注冊好的kvm_io_device_ops操作函數,對於ioeventfd”設備”來說,就是我們上面提到的ioeventfd_write,該函數檢查訪問的地址和長度是否符合ioeventfd設置的要求,如果符合則調用eventfd_signal觸發一次POLLIN事件,如果QEMU有對該eventfd的檢測,便會在QEMU中進行本次IO的處理,與此同時,kvm中的kernel_pio會返回0,表示成功完成了IO請求。
總結
整個ioeventfd的邏輯流程如下:
- QEMU分配一個eventfd,並將該eventfd加入KVM維護的eventfd數組中
- QEMU向KVM發送更新eventfd數組內容的請求
- QEMU構造一個包含IO地址,IO地址范圍等元素的ioeventfd結構,並向KVM發送注冊ioeventfd請求
- KVM根據傳入的ioeventfd參數內容確定該段IO地址所屬的總線,並在該總線上注冊一個ioeventfd虛擬設備,該虛擬設備的write方法也被注冊
- Guest執行OUT類指令(包括MMIO Write操作)
- VMEXIT到KVM
- 調用虛擬設備的write方法
- write方法中檢查本次OUT類指令訪問的IO地址和范圍是否符合ioeventfd設置的要求
- 如果符合則調用eventfd_signal觸發一次POLLIN事件並返回Guest
- QEMU監測到ioeventfd上出現了POLLIN,則調用相應的處理函數處理IO