2018-01-24編程
佔個坑,準備下寫vhost-user的東西數組
vhost-user是vhost-kernel又回到用戶空間的實現,其基本思想和vhost-kernel很相似,不過以前在內核的部分如今有另一個用戶進程代替,多是snapp或者dpdk等。在網上看相關資料較少,就簡單介紹下。雖然和vhost-kernel實現的目標一致,可是具體的實現方式卻有所不一樣。vhost-user下,UNIX本地socket代替了以前kernel模式下的設備文件進行進程間的通訊(qemu和vhost-user app),而經過mmap的方式把ram映射到vhost-user app的進程空間實現內存的共享。其餘的部分和vhost-kernel原理基本一致。這種狀況下通常qemu做爲client,而vhost-user app做爲server如DPDK。而本文對於vhost-user server端的分析主要也是基於DPDK源碼。本文主要分析涉及到的三個重要機制:qemu和vhost-user app的消息傳遞,guest memory和vhost-user app的共享,guest和vhost-user app的通知機制。數據結構
1、qemu和vhost-user app的消息傳遞app
qemu和vhost-user app的消息傳遞是經過UNIX本地socket實現的,對應於kernel下每一個ioctl的實現,這裏vhost-user app必須對每一個ioctl 提供本身的處理,DPDK下在vhost-user.c文件下的vhost_user_msg_handler函數,這裏有一個核心的數據結構:VhostUserMsg,該結構是消息傳遞的載體,整個結構並不複雜socket
typedef struct VhostUserMsg { union { VhostUserRequest master;//qemu VhostUserSlaveRequest slave;//dpdk } request; #define VHOST_USER_VERSION_MASK 0x3 #define VHOST_USER_REPLY_MASK (0x1 << 2) #define VHOST_USER_NEED_REPLY (0x1 << 3) uint32_t flags; uint32_t size; /* the following payload size */ union { #define VHOST_USER_VRING_IDX_MASK 0xff #define VHOST_USER_VRING_NOFD_MASK (0x1<<8) uint64_t u64; struct vhost_vring_state state; struct vhost_vring_addr addr; VhostUserMemory memory; VhostUserLog log; struct vhost_iotlb_msg iotlb; } payload; int fds[VHOST_MEMORY_MAX_NREGIONS]; } __attribute((packed)) VhostUserMsg;
既然是傳遞消息,其中必須包含消息的種類、消息的內容、消息內容的大小。而這些也是該結構的主要部分,首個union便標誌該消息的種類。接下來的Flags代表該消息自己的一些性質,如是否須要回覆等。size就是payload的大小,接下來的union是具體的消息內容,最後的fds是關聯每個memory RAM的fd數組。消息種類以下:函數
typedef enum VhostUserRequest { VHOST_USER_NONE = 0, VHOST_USER_GET_FEATURES = 1, VHOST_USER_SET_FEATURES = 2, VHOST_USER_SET_OWNER = 3, VHOST_USER_RESET_OWNER = 4, VHOST_USER_SET_MEM_TABLE = 5, VHOST_USER_SET_LOG_BASE = 6, VHOST_USER_SET_LOG_FD = 7, VHOST_USER_SET_VRING_NUM = 8, VHOST_USER_SET_VRING_ADDR = 9, VHOST_USER_SET_VRING_BASE = 10, VHOST_USER_GET_VRING_BASE = 11, VHOST_USER_SET_VRING_KICK = 12, VHOST_USER_SET_VRING_CALL = 13, VHOST_USER_SET_VRING_ERR = 14, VHOST_USER_GET_PROTOCOL_FEATURES = 15, VHOST_USER_SET_PROTOCOL_FEATURES = 16, VHOST_USER_GET_QUEUE_NUM = 17, VHOST_USER_SET_VRING_ENABLE = 18, VHOST_USER_SEND_RARP = 19, VHOST_USER_NET_SET_MTU = 20, VHOST_USER_SET_SLAVE_REQ_FD = 21, VHOST_USER_IOTLB_MSG = 22, VHOST_USER_MAX } VhostUserRequest;
到目前爲止並不複雜,咱們下面看下消息自己的初始化機制,socket-file的路徑會做爲參數傳遞進來,在main函數中examples/vhost/,調用us_vhost_parse_socket_path對參數中的socket-fiile參數進行解析,保存在靜態數組socket_files中,然後在main函數中有一個for循環,針對每一個socket-file,會調用rte_vhost_driver_register函數註冊vhost 驅動,該函數的核心功能就是爲每一個socket-fie建立本地socket,經過create_unix_socket函數。vhost中的socket結構經過create_unix_socket描述。在註冊驅動以後,會根據具體的特性設置features。在最後會經過rte_vhost_driver_start啓動vhost driver,該函數卻是值得一看:佈局
int rte_vhost_driver_start(const char *path) { struct vhost_user_socket *vsocket; static pthread_t fdset_tid; pthread_mutex_lock(&vhost_user.mutex); vsocket = find_vhost_user_socket(path); pthread_mutex_unlock(&vhost_user.mutex); if (!vsocket) return -1; /*建立一個線程監聽fdset*/ if (fdset_tid == 0) { int ret = pthread_create(&fdset_tid, NULL, fdset_event_dispatch, &vhost_user.fdset); if (ret < 0) RTE_LOG(ERR, VHOST_CONFIG, "failed to create fdset handling thread"); } if (vsocket->is_server) return vhost_user_start_server(vsocket); else return vhost_user_start_client(vsocket); }
函數參數是對應的socket-file的路徑,進入函數內部,首先即是根據路徑經過find_vhost_user_socket函數找到對應的vhost_user_socket結構,全部的vhost_user_socket以一個數組的形式保存在vhost_user數據結構中。接下來若是該socket確實存在,就建立一個線程,處理vhost-user的fd,這個做用咱們後面再看,該線程綁定的函數爲fdset_event_dispatch。這些工做完成後,就啓動該socket了,起始qemu和vhost能夠互作server和client,通常狀況下vhsot是做爲server存在。因此這裏就調用了vhost_user_start_server。這裏就是咱們常見的socket編程操做了,調用bind……而後listen……,沒什麼好說的。後面調用了fdset_add函數,這是就是vhost處理消息fd的一個單獨的機制,ui
int fdset_add(struct fdset *pfdset, int fd, fd_cb rcb, fd_cb wcb, void *dat) { int i; if (pfdset == NULL || fd == -1) return -1; pthread_mutex_lock(&pfdset->fd_mutex); i = pfdset->num < MAX_FDS ? pfdset->num++ : -1; if (i == -1) { fdset_shrink_nolock(pfdset); i = pfdset->num < MAX_FDS ? pfdset->num++ : -1; if (i == -1) { pthread_mutex_unlock(&pfdset->fd_mutex); return -2; } } fdset_add_fd(pfdset, i, fd, rcb, wcb, dat); pthread_mutex_unlock(&pfdset->fd_mutex); return 0; }
簡單來講就是該函數爲對應的fd註冊了一個處理函數,當該fd有信號時,就調用該函數,這裏就是vhost_user_server_new_connection。具體是如何實現的呢?看下fdset_add_fd函數spa
static void fdset_add_fd(struct fdset *pfdset, int idx, int fd, fd_cb rcb, fd_cb wcb, void *dat) { struct fdentry *pfdentry = &pfdset->fd[idx]; struct pollfd *pfd = &pfdset->rwfds[idx]; pfdentry->fd = fd; pfdentry->rcb = rcb; pfdentry->wcb = wcb; pfdentry->dat = dat; pfd->fd = fd; pfd->events = rcb ? POLLIN : 0; pfd->events |= wcb ? POLLOUT : 0; pfd->revents = 0; }
這裏分紅了兩部分,一個是fdentry,一個是pollfd。前者保存具體的信息,後者用做poll操做,方便線程監聽fd。參數中函數指針爲第三個參數,因此這裏pfd->events就是POLLIN。那麼在會處處理線程的處理函數fdset_event_dispatch中,該函數會監聽vhost_user.fdset中的rwfds,當某個fd有信號時,則進入處理流程線程
if (rcb && pfd->revents & (POLLIN | FDPOLLERR)) rcb(fd, dat, &remove1); if (wcb && pfd->revents & (POLLOUT | FDPOLLERR)) wcb(fd, dat, &remove2);
這裏的rcb即是前面針對fd註冊的回調函數。再次回到vhost_user_server_new_connection函數中,當某個fd有信號時,這裏指對應socket-file的fd,則該函數被調用,創建鏈接,而後調用vhost_user_add_connection函數。既然鏈接已經創建,則須要對該鏈接進行vhost的一些設置了,包括建立virtio_net設備附加到鏈接上,設置device名字等等。而關鍵的一步是爲該fd添加回調函數,剛纔的回調函數用於創建鏈接,在鏈接創建後就須要設置函數處理socket的msg了,這裏即是vhost_user_read_cb。到這裏正式進入msg的部分。該函數中調用了vhost_user_msg_handler,而該函數正是處理socket msg的核心函數。到這裏消息處理的部分便介紹完成了。
2、guest memory和vhost-user app的共享
雖然qemu和vhost經過socket創建了聯繫,可是這信息量畢竟有限,重點是要傳遞的數據,難不成經過socket傳遞的??固然不是,若是這樣模式切換和數據複製估計會把系統撐死……這裏主要也是用到共享內存的概念。核心機制和vhost-kernel相似,qemu也須要把guest的內存佈局經過MSG傳遞給vhost-user,那麼咱們就從這裏開始分析,在函數vhost_user_msg_handler中
case VHOST_USER_SET_MEM_TABLE: ret = vhost_user_set_mem_table(dev, &msg); break;
在分析函數以前咱們先看下幾個數據結構
/*對應qemu端的region結構*/ typedef struct VhostUserMemoryRegion { uint64_t guest_phys_addr;//GPA of region uint64_t memory_size; //size uint64_t userspace_addr;//HVA in qemu process uint64_t mmap_offset; //offset } VhostUserMemoryRegion; typedef struct VhostUserMemory { uint32_t nregions;//region num uint32_t padding; VhostUserMemoryRegion regions[VHOST_MEMORY_MAX_NREGIONS];//All region } VhostUserMemory;
在vhsot端,對應的數據結構爲
struct rte_vhost_mem_region { uint64_t guest_phys_addr;//GPA of region uint64_t guest_user_addr;//HVA in qemu process uint64_t host_user_addr;//HVA in vhost-user uint64_t size;//size void *mmap_addr;//mmap base Address uint64_t mmap_size; int fd;//relative fd of region };
意義都比較容易理解就不在多說,在virtio_net結構中保存有指向當前鏈接對應的memory結構rte_vhost_memory
struct rte_vhost_memory { uint32_t nregions; struct rte_vhost_mem_region regions[]; };
OK,下面看代碼,代碼雖然較多,可是意義都比較容易理解,只看核心部分吧:
dev->mem = rte_zmalloc("vhost-mem-table", sizeof(struct rte_vhost_memory) + sizeof(struct rte_vhost_mem_region) * memory.nregions, 0); if (dev->mem == NULL) { RTE_LOG(ERR, VHOST_CONFIG, "(%d) failed to allocate memory for dev->mem\n", dev->vid); return -1; } /*region num*/ dev->mem->nregions = memory.nregions; for (i = 0; i < memory.nregions; i++) { /*fd info*/ fd = pmsg->fds[i]; reg = &dev->mem->regions[i]; /*GPA of specific region*/ reg->guest_phys_addr = memory.regions[i].guest_phys_addr; /*HVA in qemu address*/ reg->guest_user_addr = memory.regions[i].userspace_addr; reg->size = memory.regions[i].memory_size; reg->fd = fd; /*offset in region*/ mmap_offset = memory.regions[i].mmap_offset; mmap_size = reg->size + mmap_offset; /* mmap() without flag of MAP_ANONYMOUS, should be called * with length argument aligned with hugepagesz at older * longterm version Linux, like 2.6.32 and 3.2.72, or * mmap() will fail with EINVAL. * * to avoid failure, make sure in caller to keep length * aligned. */ alignment = get_blk_size(fd); if (alignment == (uint64_t)-1) { RTE_LOG(ERR, VHOST_CONFIG, "couldn't get hugepage size through fstat\n"); goto err_mmap; } /*對齊*/ mmap_size = RTE_ALIGN_CEIL(mmap_size, alignment); /*執行映射,這裏就是本進程的虛擬地址了,爲什麼能映射另外一個進程的文件描述符呢?*/ mmap_addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, 0); if (mmap_addr == MAP_FAILED) { RTE_LOG(ERR, VHOST_CONFIG, "mmap region %u failed.\n", i); goto err_mmap; } reg->mmap_addr = mmap_addr; reg->mmap_size = mmap_size; /*region Address in vhost process*/ reg->host_user_addr = (uint64_t)(uintptr_t)mmap_addr + mmap_offset; if (dev->dequeue_zero_copy) add_guest_pages(dev, reg, alignment); }
首先就是爲dev分配mem空間,由此咱們也能夠獲得該結構的佈局
下面一個for循環對每一個region先進行對應信息的複製,而後對該region的大小進行對其操做,接着經過mmap的方式對region關聯的fd進行映射,這裏便獲得了region在vhost端的虛擬地址,可是region中GPA對應的虛擬地址還須要在mmap獲得的虛擬地址上加上offset,該值也是做爲參數傳遞進來的。到此,設置memory Table的工做基本完成,看下地址翻譯過程呢?
/* Converts QEMU virtual address to Vhost virtual address. */ static uint64_t qva_to_vva(struct virtio_net *dev, uint64_t qva) { struct rte_vhost_mem_region *reg; uint32_t i; /* Find the region where the address lives. */ for (i = 0; i < dev->mem->nregions; i++) { reg = &dev->mem->regions[i]; if (qva >= reg->guest_user_addr && qva < reg->guest_user_addr + reg->size) { return qva - reg->guest_user_addr + reg->host_user_addr; } } return 0; }
至關簡單把,核心思想是先使用QVA肯定在哪個region,而後取地址在region中的偏移,加上該region在vhost-user映射的實際有效地址即reg->host_user_addr字段。這部分還有一個核心思想是fd的使用,vhost_user_set_mem_table直接從MSG中獲取到了fd,而後直接把FD進行mmap映射,這點一時間讓我難以理解,FD不是僅僅在進程內部有效麼?怎麼也能夠共享了??經過向開源社區請教,感嘆本身的知識面實在狹窄,這是Unix下一種通用的傳遞描述符的方式,怎麼說呢?就是進程A的描述符能夠經過特定的調用傳遞給進程B,進程B在本身的描述符表中分配一個位置給該描述符指針,所以實際上進程B使用的並非A的FD,而是本身描述符表中的FD,可是兩個進程的FD卻指向同一個描述符表,就像是增長了一個引用而已。後面會專門對該機制進行詳解,本文僅僅瞭解該做用便可。
3、vhost-user app的通知機制。
這裏的通知機制和vhost kernel基本一致,都是經過eventfd的方式。所以這裏就比較簡單了
qemu端的代碼:
file.fd = event_notifier_get_fd(virtio_queue_get_host_notifier(vvq));
r = dev->vhost_ops->vhost_set_vring_kick(dev, &file);
static int vhost_user_set_vring_kick(struct vhost_dev *dev, struct vhost_vring_file *file) { return vhost_set_vring_file(dev, VHOST_USER_SET_VRING_KICK, file); } static int vhost_set_vring_file(struct vhost_dev *dev, VhostUserRequest request, struct vhost_vring_file *file) { int fds[VHOST_MEMORY_MAX_NREGIONS]; size_t fd_num = 0; VhostUserMsg msg = { .request = request, .flags = VHOST_USER_VERSION, .payload.u64 = file->index & VHOST_USER_VRING_IDX_MASK, .size = sizeof(msg.payload.u64), }; if (ioeventfd_enabled() && file->fd > 0) { fds[fd_num++] = file->fd; } else { msg.payload.u64 |= VHOST_USER_VRING_NOFD_MASK; } if (vhost_user_write(dev, &msg, fds, fd_num) < 0) { return -1; } return 0; }
能夠看到這裏實質上也是把eventfd的描述符傳遞給vhost-user。再看vhost-user端,在vhost_user_set_vring_kick中,關鍵的一句
vq->kickfd = file.fd;
其實這裏的通知機制和kernel下沒什麼區別,不過是換到用戶空間對eventfd進行操做而已,這裏暫時不討論了,後面有時間在補充!
以馬內利!
參考資料:
qemu 2.7 源碼
DPDK源碼