qemu-kvm的ioeventfd機制


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, &section,
                                 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的邏輯是:

  1. 將一個ioeventfd與一個虛擬設備dev聯系起來
  2. 該虛擬設備dev擁有寫函數
  3. 當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的邏輯流程如下:

  1. QEMU分配一個eventfd,並將該eventfd加入KVM維護的eventfd數組中
  2. QEMU向KVM發送更新eventfd數組內容的請求
  3. QEMU構造一個包含IO地址,IO地址范圍等元素的ioeventfd結構,並向KVM發送注冊ioeventfd請求
  4. KVM根據傳入的ioeventfd參數內容確定該段IO地址所屬的總線,並在該總線上注冊一個ioeventfd虛擬設備,該虛擬設備的write方法也被注冊
  5. Guest執行OUT類指令(包括MMIO Write操作)
  6. VMEXIT到KVM
  7. 調用虛擬設備的write方法
  8. write方法中檢查本次OUT類指令訪問的IO地址和范圍是否符合ioeventfd設置的要求
  9. 如果符合則調用eventfd_signal觸發一次POLLIN事件並返回Guest
  10. QEMU監測到ioeventfd上出現了POLLIN,則調用相應的處理函數處理IO


免責聲明!

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



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