qemu中的eventfd——ioeventfd

eventfd是內核實現的高效線程通信機制,還適合於內核與用戶態的通信,KVM模塊利用eventfd實現了KVM和qemu的高效通信機制ioeventfd,這一節主要將ioeventfd的實現,分KVM的實現和qemu的使用兩部分。

KVM ioeventfd

數據結構

  • ioeventfd是內核kvm模塊向qemu提供的一個vm ioctl命令字KVM_IOEVENTFD,對應調用kvm_vm_ioctl函數,原型如下:
int kvm_ioeventfd(struct kvm *kvm, struct kvm_ioeventfd *args)
  • 這個命令字的功能是將一個eventfd綁定到一段客戶機的地址空間,這個空間可以是mmio,也可以是pio。當guest寫這段地址空間時,會觸發EPT_MISCONFIGURATION缺頁異常,KVM處理時如果發現這段地址落在了已註冊的ioeventfd地址區間裏,會通過寫關聯eventfd通知qemu,從而節約一次內核態到用戶態的切換開銷。用戶態傳入的參數如下:
struct kvm_ioeventfd {
    __u64 datamatch;		/* 1 */
    __u64 addr;        /* legal pio/mmio address */
    __u32 len;         /* 0, 1, 2, 4, or 8 bytes    */
    __s32 fd;				/* 2 */
    __u32 flags;
    __u8  pad[36];
};
1. 如果flags設置了KVM_IOEVENTFD_FLAG_DATAMATCH,只有當客戶機向addr地址寫入的值時datamatch,纔會觸發event
2. eventfd關聯的fd,eventfd_ctx的結構在初始化時被放在了file結構的private_data中,首先通過fd從進程的fd表中可以找到file結構,順藤摸瓜可
以找到eventfd_ctx,從這裏能夠看出,eventfd的用法是用戶態註冊好evenfd並獲得fd,然後將fd和感興趣的地址區間封裝成kvm_ioeventfd結構體
作爲參數,調用ioctl KVM_IOEVENTFD命令字,註冊到kvm
  • 用戶態信息kvm_ioeventfd需要轉化成內核態存放,當guest寫地址時找到對應的結構體,觸發event,ioeventfd內核態結構體基於eventfd,如下:
/*          
 * --------------------------------------------------------------------
 * ioeventfd: translate a PIO/MMIO memory write to an eventfd signal.
 *          
 * userspace can register a PIO/MMIO address with an eventfd for receiving
 * notification when the memory has been touched.
 * --------------------------------------------------------------------
 */     
struct _ioeventfd {
    struct list_head     list;
    u64                  addr; 								/* 3 */
    int                  length;							/* 4 */
    struct eventfd_ctx  *eventfd;							/* 5 */
    u64                  datamatch;							/* 6 */
    struct kvm_io_device dev;								/* 7 */		
    u8                   bus_idx;							/* 8 */
}; 
3. 客戶機PIO/MMIO地址,當客戶機向該地址寫入數據時觸發event
4. 客戶機向該地址寫入數據時數據的長度,當length爲0時,忽略寫入數據的長度
5. 關聯的eventfd
6. 用戶態程序設置的match data,當ioeventfd被設置了KVM_IOEVENTFD_FLAG_DATAMATCH,只有滿足客戶機寫入的值等於datamatch的條件時才觸發event
7. VM-Exit退出時調用dev->ops的write操作,對應ioeventfd_write
8. 客戶機的地址被分爲了4類,MMIO,PIO,VIRTIO_CCW_NOTIFY,FAST_MMIO,bus_idx用來區分註冊的地址是哪一類
  • 用戶下發KVM_IOEVENTFD命令字最終會生成_ioeventfd結構體,存放在內核中,由於它是和一個虛機關聯的,因此被放到了kvm結構體中維護,kvm結構體中有兩個字段,存放了ioeventfd相關的信息,如下:
struct kvm {
	......
	struct kvm_io_bus *buses[KVM_NR_BUSES];			/* 9 */
	struct list_head ioeventfds;					/* 10 */
	......
}
struct kvm_io_bus {
    int dev_count;
    int ioeventfd_count;
    struct kvm_io_range range[];
}; 
enum kvm_bus {
    KVM_MMIO_BUS,
    KVM_PIO_BUS,
    KVM_VIRTIO_CCW_NOTIFY_BUS,
    KVM_FAST_MMIO_BUS,
    KVM_NR_BUSES 
};
9. kvm中將ioeventfd註冊的地址分爲4類,每類地址可以認爲有獨立的地址空間,它們被抽象成4個bus上的地址。分別是kvm_bus所列出的
 MMIO,PIO,VIRTIO_CCW_NOTIFY,FAST_MMIO,MMIO和FAST_MMIO的區別是,MMIO需要檢查寫入地址的值長度是否和ioeventfd
 指定的長度相等,FAST_MMIO不需要檢查。ioeventfd的地址信息被放在kvm_io_bus的range中
10. ioeventfd的整個信息通過list成員被鏈接到了ioeventfds中

註冊流程

-KVM_IOEVENTFDioctl命令字的主要功能是在kvm上註冊這個ioevent,最終目的是將ioevenfd信息添加到kvm結構的buses和ioeventfds兩個成員中,註冊流程如下:

static int kvm_assign_ioeventfd(struct kvm *kvm, struct kvm_ioeventfd *args)
{ 
	......
	bus_idx = ioeventfd_bus_from_flags(args->flags);						/* 1 */
	kvm_assign_ioeventfd_idx(kvm, bus_idx, args);							/* 2 */
    ......
}
1. 首先根據地址所屬總線類型,找到總線索引,方便在kvm->buses數組中找到kvm_io_bus結構
2. 註冊ioeventfd,將用戶態的信息拆解,封裝成內核態的kvm_io_bus和_ioeventfd結構,保存到kvm結構體的對應成員
  • 跟蹤kvm_assign_ioeventfd_idx主要流程:
static int kvm_assign_ioeventfd_idx(struct kvm *kvm,
                enum kvm_bus bus_idx,
                struct kvm_ioeventfd *args)
{
	......
	eventfd = eventfd_ctx_fdget(args->fd);									/* 3 */
	p = kzalloc(sizeof(*p), GFP_KERNEL);									/* 4 */
	INIT_LIST_HEAD(&p->list);
    p->addr    = args->addr;
    p->bus_idx = bus_idx;
    p->length  = args->len;
    p->eventfd = eventfd; 
    
    kvm_iodevice_init(&p->dev, &ioeventfd_ops);								/* 5 */
    kvm_io_bus_register_dev(kvm, bus_idx, p->addr, p->length, &p->dev);		/* 6 */
    kvm->buses[bus_idx]->ioeventfd_count++;									/* 7 */
    list_add_tail(&p->list, &kvm->ioeventfds);							
    ......      
}
static const struct kvm_io_device_ops ioeventfd_ops = {
    .write      = ioeventfd_write,
    .destructor = ioeventfd_destructor,
}; 
3. 根據fd從進程的描述符表中找到struct file結構,從file->private_data取出eventfd_ctx
4. 使用用戶態傳入的kvm_ioeventfd初始化一個_ioeventfd結構
5. 設置_ioeventfd的鉤子函數,當虛機寫內存缺頁時,KVM首先嚐試觸發p->dev中的write函數,檢查缺頁地址是否滿足ioeventfd觸發條件
6. 將_ioeventfd中的地址信息和鉤子函數封裝成kvm_io_range,放到kvm->buses的range[]數組中。之後kvm在處理缺頁就可以查詢到缺頁地址是否在已註冊的ioeventfd的地址區間
7. 更新ioeventfd的計數,將ioeventfd放到kvm的ioeventfds鏈表中,維護起來
  • 註冊ioeventfd的數據結構如下圖所示:
    在這裏插入圖片描述

觸發流程

  • 當kvm檢查VM-Exit退出原因,如果是缺頁引起的退出並且原因是EPT misconfiguration,首先檢查缺頁的物理地址是否落在已註冊ioeventfd的物理區間,如果是,調用對應區間的write函數。虛機觸發缺頁的流程如下:
vmx_handle_exit
	kvm_vmx_exit_handlers[exit_reason](vcpu)
		handle_ept_misconfig
  • 分析缺頁流程處理函數:
static int handle_ept_misconfig(struct kvm_vcpu *vcpu)
{
	......
	gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS);								/* 1 */
    if (!kvm_io_bus_write(vcpu->kvm, KVM_FAST_MMIO_BUS, gpa, 0, NULL)) {	/* 2 */
		skip_emulated_instruction(vcpu);
        trace_kvm_fast_mmio(gpa);
        return 1; 
    }
    handle_mmio_page_fault(vcpu, gpa, true);								/* 3 */
    ......
}
1. 從VMCS結構中讀取引發缺頁的虛機物理地址
2. 首先嚐試觸發ioeventfd,如果成功,eventfd會通知用戶態qemu,因此不需要退到用戶態,觸發ioeventfd之後再次進入客戶態就行了
3. 如果引發缺頁的地址不再ioeventfd監聽範圍內,進行缺頁處理,這裏的實現我們跳過,重點分析觸發ioeventfd的情形
  • 分析如何觸發eventfd流程:
int kvm_io_bus_write(struct kvm *kvm, enum kvm_bus bus_idx, gpa_t addr,
             int len, const void *val)
{
	......
	range = (struct kvm_io_range) {									/* 4 */
        .addr = addr,
        .len = len,
    };  
    bus = srcu_dereference(kvm->buses[bus_idx], &kvm->srcu);		/* 5 */
    r = __kvm_io_bus_write(bus, &range, val);						/* 6 */
    ......
4. 將引發缺頁的物理地址封裝成kvm_io_range格式,方便與以在總線上註冊的range進行對比
5. 根據物理地址類型找到該類型所在的地址總線,這個總線上記錄這所有在該總線上註冊的ioeventfd的地址區間
6. 對比總線上的地址區間和引發缺頁的物理地址區間,如果缺頁地址區間落在的總線上的地址區間裏,調用對應的write函數觸發eventfd
  • 最終,客戶機的缺頁流程會調用kvm_io_device->write()函數觸發eventfd,看下它的具體實現:
/* MMIO/PIO writes trigger an event if the addr/val match */
static int 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);					/* 7 */
    if (!ioeventfd_in_range(p, addr, len, val))					/* 8 */
        return -EOPNOTSUPP;
    eventfd_signal(p->eventfd, 1);								/* 9 */
    return 0;
} 
7. 根據從bus總線上取下的range[],取出其dev成員,由於dev結構體是_ioeventfd的一個成員,通過container轉化可以取出_ioeventfd
8. 檢查缺頁物理地址和range中註冊的地址是否匹配
9. 取出_ioeventfd中的eventfd_ctx結構體,往它維護的計數器中加1
  • eventfd_signal實現如下:
/**     
 * eventfd_signal - Adds @n to the eventfd counter.
 * @ctx: [in] Pointer to the eventfd context.
 * @n: [in] Value of the counter to be added to the eventfd internal counter.
 *          The value cannot be negative.
 *
 * This function is supposed to be called by the kernel in paths that do not
 * allow sleeping. In this function we allow the counter to reach the ULLONG_MAX
 * value, and we signal this as overflow condition by returning a POLLERR
 * to poll(2).
 *
 * Returns the amount by which the counter was incremented.  This will be less
 * than @n if the counter has overflowed.
 */
__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
{   
	......
    if (ULLONG_MAX - ctx->count < n)				/* 10 */
        n = ULLONG_MAX - ctx->count;
    ctx->count += n;								/* 11 */
    if (waitqueue_active(&ctx->wqh))				/* 12 */
        wake_up_locked_poll(&ctx->wqh, POLLIN);
	......
}
10. 首先判斷下計數器是否即將溢出。如果計數器加上1之後溢出了,讓計數器直接等於最大值,內核態寫eventfd與用戶態有所區別,它不允許阻
塞,因此當計數器溢出時直接設置其爲最大值
11. 增加計數器的值
12. 喚醒阻塞在eventfd上的讀線程,如果計數器原來爲0,有讀線程阻塞在這個eventfd上,那麼此時計數器加1後,就可以喚醒這些線程

QEMU ioeventfd

  • 傳統的QEMU設備模擬,當虛機訪問到pci的配置空間或者BAR空間時,會觸發缺頁異常而VM-Exit,kvm檢查到虛機訪問的是用戶QEMU模擬的用戶態設備的空間,這是IO操作,會退出到用戶態交給QEMU處理。梳理整個流程,需要經過兩次cpu模式切換,一次是非根模式到根模式,一次是內核態到用戶態。分析內核態切換到用戶態的原因,是kvm沒有模擬這個設備,處理不了這種情況才退出到用戶態讓QEMU處理,但還有一種解決方法就是讓kvm通知QEMU,把要處理IO這件事情通知到QEMU就可以了,這樣就節省了一個內核態到用戶態的開銷。
  • 如果以這樣的方式實現,假設虛機所有IO操作kvm都一股腦兒通知QEMU,那QEMU也不知道到底是哪個設備需要處理,所以需要一個東西作爲QEMU和KVM之間通知的載體,當QEMU模擬一個設備時,首先將這個設備的物理地址空間信息摘出來,對應關聯一個回調函數,然後傳遞給KVM,其目的是告知KVM,當虛機內部有訪問該物理地址空間的動作時,KVM調用QEMU關聯的回調函數通知QEMU,這樣就能實現針對具體物理區間的通知。這個實現就是ioeventfd。

數據結構

  • 以virtio磁盤爲例,virtio磁盤是一個pci設備,它有pci空間,這些空間的內存都是QEMU模擬的,當虛機寫這些pci空間時,QEMU需要做對應的處理,在virtio磁盤初始化成功後,它就會將自己的地址空間信息提取出來,封裝成ioeventfd,通過ioctl命令字傳遞給KVM,ioeventfd中包含一個QEMU提前通過eventfd創建好的fd,KVM通知QEMU是就往這個fd中寫1。以下就是這個ioeventfd的結構,具體含義見前面的分析。
struct kvm_ioeventfd {
    __u64 datamatch;		
    __u64 addr;        /* legal pio/mmio address */
    __u32 len;         /* 0, 1, 2, 4, or 8 bytes    */
    __s32 fd;				
    __u32 flags;
    __u8  pad[36];
};
  • QEMU是通過MemoryRegion來進行虛機內存管理的,一個MR可以對應一段虛機的內存區間,MR結構中有兩個成員與ioeventfd相關:
struct MemoryRegion {
	......
	unsigned ioeventfd_nb;							/* 1 */
	MemoryRegionIoeventfd *ioeventfds;				/* 2 */
	......
}
1. 表示MR對應的地址區間中有多少個ioeventfd
2. MR中包含的ioeventfds數組,每註冊一個ioeventfd,不只KVM會有記錄,QEMU也會有記錄
  • MemoryRegionIoeventfd具體結構如下:
struct MemoryRegionIoeventfd {
    AddrRange addr;						/* 3 */
    bool match_data;					/* 4 */
    uint64_t data;						
    EventNotifier *e;					/* 5 */
};
3. 虛機物理地址空間,用戶告知KVM,當虛機寫這個空間時通知QEMU
4. 是否匹配寫入的值,如果match_data這個爲真,只要當寫入addr地址的值爲data時,KVM才通知QEMU
5. 當滿足上面的所有條件後,KVM通過增加EventNotifier->wfd對應的內核計數器,通知QEMU

註冊流程

  • QEMU註冊ioeventfd的時間點是在virtio磁盤驅動初始化成功之後,流程如下:
static void virtio_ioport_write(void *opaque, uint32_t addr, uint32_t val)
{
	switch (addr) {
	case VIRTIO_PCI_STATUS:
		if (val & VIRTIO_CONFIG_S_DRIVER_OK) {			/* 前端驅動想device_status字段寫入DRIVER_OK,表明驅動初始化完成 */
            virtio_pci_start_ioeventfd(proxy);
        }
  	......
}
  • virtio_pci_start_ioeventfd之後走到virtio_blk_data_plane_start,這個函數會針對virtio磁盤的每個隊列,都設置一個ioeventfd,從這裏可以看出,virtio數據到達的通知針對每個隊列,都是獨立的:
virtio_pci_start_ioeventfd
	virtio_bus_start_ioeventfd(&proxy->bus)
		vdc->start_ioeventfd(vdev)													
			vdc->start_ioeventfd = virtio_blk_data_plane_start
int virtio_blk_data_plane_start(VirtIODevice *vdev)
{
	......
 	/* Set up virtqueue notify */
    for (i = 0; i < nvqs; i++) {					/* 爲virtio磁盤的每個隊列都設置一個ioeventfd */
        r = virtio_bus_set_host_notifier(VIRTIO_BUS(qbus), i, true);
    }
	......
}
  • virtio_bus_set_host_notifier具體實現如下:
int virtio_bus_set_host_notifier(VirtioBusState *bus, int n, bool assign)
{ 
	......
	EventNotifier *notifier = virtio_queue_get_host_notifier(vq);			/* 1 */
	r = event_notifier_init(notifier, 1);									/* 2 */
	k->ioeventfd_assign(proxy, notifier, n, true);							/* 3 */
	......
}
1. 每個virtio隊列關聯一個eventfd,eventfd的fd被存放到VirtQueue->host_notifier中,這裏把它取出來,用於初始化並傳遞給KVM
2. 初始化EventNotifier,將eventfd對應的計數器設置爲1
3. 註冊ioeventfd,最終會通過ioctl命令字KVM_IOEVENTFD註冊到KVM
  • ioeventfd關聯一段內存空間,由於QEMU每有地址空間變化,都會影響全部的地址空間,QEMU要統一更新地址空間的視圖,包括flatview和ioeventfd的更新。ioeventfd_assign對應virtio_pci_ioeventfd_assign,流程如下:
virtio_pci_ioeventfd_assign
	memory_region_add_eventfd
void memory_region_add_eventfd(MemoryRegion *mr,
                               hwaddr addr,
                               unsigned size,
                               bool match_data,
                               uint64_t data,
                               EventNotifier *e)
{ 
	......
    MemoryRegionIoeventfd mrfd = {					/* 4 */
        .addr.start = int128_make64(addr),
        .addr.size = int128_make64(size),
        .match_data = match_data,             
        .data = data,
        .e = e,
    }; 
    mr->ioeventfds[i] = mrfd;						
	memory_region_transaction_commit()				/* 5 */
	......
}
4. 封裝地址信息並將其添加到MR的ioeventfds數組上
5. 檢查根MR下面的每個子MR,蒐集所有的ioeventfd,統一註冊
  • memory_region_transaction_commit最終走到系統調用,下發ioctl命令字給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);
				kvm_io_ioeventfd_add
					kvm_set_ioeventfd_pio
						kvm_vm_ioctl(kvm_state, KVM_IOEVENTFD, &kick)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章