參考資料: > [kvm-host](https://github.com/sysprog21/kvm-host) > [kvm-host 的改進](https://hackmd.io/@ray90514/kvm-host#kvm-host-%E7%9A%84%E6%94%B9%E9%80%B2) > [Linux 核心專題: 系統虛擬機器開發和改進](https://hackmd.io/@sysprog/rkro_FeSh#Linux-%E6%A0%B8%E5%BF%83%E5%B0%88%E9%A1%8C-%E7%B3%BB%E7%B5%B1%E8%99%9B%E6%93%AC%E6%A9%9F%E5%99%A8%E9%96%8B%E7%99%BC%E5%92%8C%E6%94%B9%E9%80%B2) > [linux-riscv-dev/exercises2/kvm/kvm.md](https://github.com/magnate3/linux-riscv-dev/blob/main/exercises2/kvm/kvm.md) > [Virtio-networking series](https://www.redhat.com/en/virtio-networking-series) > [KVM: Linux 虛擬化基礎建設](https://hackmd.io/@sysprog/linux-kvm#KVM-Linux-%E8%99%9B%E6%93%AC%E5%8C%96%E5%9F%BA%E7%A4%8E%E5%BB%BA%E8%A8%AD) > [Virtio: An I/O virtualization framework for Linux](https://www.cs.cmu.edu/~412/lectures/Virtio_2015-10-14.pdf) > [Introduction to VirtIO](https://blogs.oracle.com/linux/post/introduction-to-virtio#:~:text=For%20the%20VM%20to%20access%20the%20host%E2%80%99s%20NIC%2C,network%20data%20between%20the%20host%20and%20the%20guest.) > [virtio-v1.2-cs01.pdf](https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.pdf) > [Universal TUN/TAP device driver](https://www.kernel.org/doc/html/latest/networking/tuntap.html) > [Linux 核心專題: RISC-V 系統模擬器](https://hackmd.io/@sysprog/Skuw3dJB3#Linux-%E6%A0%B8%E5%BF%83%E5%B0%88%E9%A1%8C-RISC-V-%E7%B3%BB%E7%B5%B1%E6%A8%A1%E6%93%AC%E5%99%A8) > [semu](https://github.com/sysprog21/semu) # TODO: 嘗試實作出 virtio-net ## Virtio Linux 支援多種不同的虛擬化系統,比如說 * Xen * KVM * VMWare 而每個系統都會有其特有的驅動裝置,比如說 block, console, network 等等,而 virtio 作為一個虛擬化的 io,其功能在於作為前端與後端的通訊,而此處所提之前端即為虛擬化系統的驅動裝置,後端則為由虛擬化系統 KVM 所模擬的裝置。 ![](https://hackmd.io/_uploads/H1ErUhqK2.png) ## Linux 中的前端驅動裝置 Front-end drivers 在 Linux 中,virtio 的前端的驅動裝置 Front-end drivers 都分別被寫為核心模組 kernel modules,並且可以直接通過 virtio 來通訊。 ![](https://hackmd.io/_uploads/SkF3Z1iF3.png) 比如說 virtio-net 就是通過將其寫為核心模組的形式來啟動。 ```c ... static __init int virtio_net_driver_init(void) { int ret; ret = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN, "virtio/net:online", virtnet_cpu_online, virtnet_cpu_down_prep); if (ret < 0) goto out; virtionet_online = ret; ret = cpuhp_setup_state_multi(CPUHP_VIRT_NET_DEAD, "virtio/net:dead", NULL, virtnet_cpu_dead); if (ret) goto err_dead; ret = register_virtio_driver(&virtio_net_driver); if (ret) goto err_virtio; return 0; err_virtio: cpuhp_remove_multi_state(CPUHP_VIRT_NET_DEAD); err_dead: cpuhp_remove_multi_state(virtionet_online); out: return ret; } module_init(virtio_net_driver_init); static __exit void virtio_net_driver_exit(void) { unregister_virtio_driver(&virtio_net_driver); cpuhp_remove_multi_state(CPUHP_VIRT_NET_DEAD); cpuhp_remove_multi_state(virtionet_online); } module_exit(virtio_net_driver_exit); MODULE_DEVICE_TABLE(virtio, id_table); MODULE_DESCRIPTION("Virtio network driver"); MODULE_LICENSE("GPL"); ``` 而這些前端的任務為 * 接收來自使用者的請求 request * 將前端的請求傳輸至相應的後端 * 從後端接收已完成請求的指令 從以上所述可以了解到,virtio-net 屬於前端的驅動裝置,並且透過 virtio 傳輸資料,但是在 [kvm-host](https://github.com/sysprog21/kvm-host) 中,驅動裝置都是建立在 virtio-pci 上(後面會介紹),並不是透過註冊核心模組的方式來啟動。 ## Virtio 傳輸資料 virtio 傳輸資料透過結構體 virtqueue,virtqueue 主要分為兩種形式,一種為 Split virtqueue,另一種為 Packed virtqueue,在 [kvm-host](https://github.com/sysprog21/kvm-host) 中實作的形式為 Packed virtqueue,所以這裡主要討論的是 Packed virtqueue。 ### Virtio 裝置 Virtio 裝置為提供一個虛擬界面來交換資訊,而一個 Virtio 虛擬界面應該包含以下幾個部份 * Device status field * Feature bits * Notifications * 一個或多個的 virtqueues #### Device status field Device status field 為指示此裝置的一些裝態,比如說 * `ACKNOWLEDGE` (0x1) 為此裝置已被系統承認 * `DRIVER` (0x2) 為表示此裝置已經被初始化 * `DRIVER_OK` (0x4),`FEATURES_OK` (0x8) 代表著此驅動或裝置已經可以開始進行通訊 * `DRIVER_NEEDS_RESET` (0x40),`FAILED` (0x80) 代表此裝置遇到錯誤 ![](https://hackmd.io/_uploads/HycJylat3.png) Packed virtqueue 跟 Split virtqueue 的主要區別在 Split virtqueue 將 descriptor ring,available ring 和 used ring 分開來實作,而 Packed virtqueue 則是結合在一起,合併的優點在 ![](https://hackmd.io/_uploads/SJPR5yaKn.png) 在 Packed virtqueue 的形式中,首先要理解的是 descriptor ring,available ring 和 used ring。 ### descriptor ring descriptor ring 由四個變數組成,分別為 `addr`,`len`,`id` 和 `flags`,其主要目的是為了描述從 guest 傳遞的資料並且在完成 guest 的請求後由 used buffer 通知已完成。 * `addr` 為資料在 guest 中的地址 * `len` 為資料的大小 * `flags` 為此資料的運作方式,選項有 write-only 或 read-only ```c struct vring_packed_desc { /* Buffer Address. */ __le64 addr; /* Buffer Length. */ __le32 len; /* Buffer ID. */ __le16 id; /* The flags depending on descriptor type. */ __le16 flags; }; ``` ### available ring & used ring available ring 的功能是將 available descriptor 寫入 descriptor ring 當中並且完成 io 的請求,used ring 則是在完成請求後會向驅動發起 notification。 ![](https://hackmd.io/_uploads/rJHRhfTt2.png) ## PCI bus 上的 Virtio 由於在 [kvm-host](https://github.com/sysprog21/kvm-host) 中的 driver 都是建立在 virtio-pci 上,所以有必要理解 virtio-pci 的實作和原理。 ### PCI ![](https://hackmd.io/_uploads/SJMssWH5h.png) 一個 PCI 的架構如上,在此專案中每個 PCI 裝置都被設定為需要通過 PCI host bridge 來存取資訊,繼而透過 PCI host bridge 來與 cpu 和記憶體溝通。 此專案中,每個 Virtio 裝置都實作為 PCI 裝置,啟動時也會以 PCI 裝置啟動,比如說 virtio-blk ```c ... struct virtio_blk_req { uint32_t type; uint32_t reserved; uint64_t sector; uint8_t *data; uint16_t data_size; uint8_t *status; }; struct virtio_blk_dev { struct virtio_pci_dev virtio_pci_dev; struct virtio_blk_config config; struct virtq vq[VIRTIO_BLK_VIRTQ_NUM]; int irqfd; int ioeventfd; int irq_num; pthread_t vq_avail_thread; pthread_t worker_thread; struct diskimg *diskimg; bool enable; }; void virtio_blk_init(struct virtio_blk_dev *virtio_blk_dev); void virtio_blk_exit(struct virtio_blk_dev *dev); void virtio_blk_init_pci(struct virtio_blk_dev *dev, struct diskimg *diskimg, struct pci *pci, struct bus *io_bus, struct bus *mmio_bus); ``` PCI 裝置分配了 256 個 bytes 來進行設定,前 64 個 bytes 為共通設定如下圖所示,後 128 個 bytes 為自行設定,而前 64 個 bytes 則是需要從 [virtio-v1.2-cs01.pdf](https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.pdf) 來得知其中訊息 ![](https://hackmd.io/_uploads/rkprazS52.png) * 所有 PCI 裝置的 Vendor ID 皆為 0x1AF4 * Device ID 的起始值為 0x1040,不同裝置有不同的 Device ID,比如說 Virtio-net 的 Device ID 即為 0x1041,Virtio-block 的 Device ID 即為 0x1042 | Transitional PCI Device ID | Virtio Device | | -------------------------- |:----------------:| | 0x1000 | network card | | 0x1001 | block device | | 0x1002 | memory balloning | | 0x1003 | console | | 0x1004 | SCSI host | | 0x1005 | entropy source | | 0x1009 | 9P transport | ::: info :question: 從上方 pci 裝置上可以看到有 class code 這個設定,那下面定義又是從何而來? 查閱 [virtio-v1.2-cs01.pdf](https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.pdf) 卻沒看到相關設定。 ```c #define VIRTIO_BLK_PCI_CLASS 0x018000 ``` ::: ### PCI 裝置初始化 首先先來觀察 kvm-host 是如何定義 PCI 裝置 ```c struct pci_dev { uint8_t cfg_space[PCI_CFG_SPACE_SIZE]; void *hdr; uint32_t bar_size[6]; bool bar_active[6]; bool bar_is_io_space[6]; struct dev space_dev[6]; struct dev config_dev; struct bus *io_bus; struct bus *mmio_bus; struct bus *pci_bus; }; ``` 從結構體 `pci_dev` 可以看到此結構體提供了一塊區域設定 PCI 裝置,但是如何去設定此 PCI 裝置 ? 答案為結構體的第二個成員 `hdr`。 ```c #define PCI_HDR_READ(hdr, offset, width) (*((uint##width##_t *) (hdr + offset))) #define PCI_HDR_WRITE(hdr, offset, value, width) \ ((uint##width##_t *) (hdr + offset))[0] = value ``` 從上方的定義可以看出兩個訊息,一是 `PCI_HDR_READ` 為讀取 `hdr + offset` 中的資料,一是 `PCI_HDR_WRITE` 為對 `hdr + offset` 中的資料進行寫入,在 PCI 的裝置中的成員 `hdr` 就是指向 PCI 裝置地址的起點,也就是 `cfg_space` 的起始位置。 ```c void pci_dev_init(struct pci_dev *dev, struct pci *pci, struct bus *io_bus, struct bus *mmio_bus) { memset(dev, 0x00, sizeof(struct pci_dev)); dev->hdr = dev->cfg_space; dev->pci_bus = &pci->pci_bus; dev->io_bus = io_bus; dev->mmio_bus = mmio_bus; } ``` 初始化完 PCI 裝置後,必須要針對 virtio-pci 介面進行初始化,首先第一行的 0x40 為 PCI 裝置設定區域的起始地址,接著將個別裝置的設定以 PCI 裝置中的 `hdr` 寫入 PCI 中。 ```c void virtio_pci_init(struct virtio_pci_dev *dev, struct pci *pci, struct bus *io_bus, struct bus *mmio_bus) { /* The capability list begins at offset 0x40 of pci config space */ uint8_t cap_list = 0x40; memset(dev, 0x00, sizeof(struct virtio_pci_dev)); pci_dev_init(&dev->pci_dev, pci, io_bus, mmio_bus); PCI_HDR_WRITE(dev->pci_dev.hdr, PCI_VENDOR_ID, VIRTIO_PCI_VENDOR_ID, 16); PCI_HDR_WRITE(dev->pci_dev.hdr, PCI_CAPABILITY_LIST, cap_list, 8); PCI_HDR_WRITE(dev->pci_dev.hdr, PCI_HEADER_TYPE, PCI_HEADER_TYPE_NORMAL, 8); PCI_HDR_WRITE(dev->pci_dev.hdr, PCI_INTERRUPT_PIN, 1, 8); pci_set_status(&dev->pci_dev, PCI_STATUS_CAP_LIST | PCI_STATUS_INTERRUPT); pci_set_bar(&dev->pci_dev, 0, 0x100, PCI_BASE_ADDRESS_SPACE_MEMORY, virtio_pci_space_io); virtio_pci_set_cap(dev, cap_list); dev->device_feature |= (1ULL << VIRTIO_F_RING_PACKED) | (1ULL << VIRTIO_F_VERSION_1); } ``` ### Virtio 裝置初始化 Virtio 裝置初始化需要透過 virtio-pci 將裝置建立到 pci 上, ## TUN/TAP TUN/TAP 為 Linux 核心模擬出來的虛擬網路裝置,並且提供可以使用 userspace 來接收跟傳輸數據包。 TUN/TAN 為完全由軟體支援的網路設備,差別在於 TUN 位於 Network layer 運行專門處理 IP 封包,而 TAP 在 Data Link layer 運行專門處理 Ethernet 封包。 ![](https://hackmd.io/_uploads/ry4YBHAq2.png) TUN/TAP 是如何與外界溝通,可以從下面這張圖得知。 ![](https://hackmd.io/_uploads/B1AabICc3.png) 圖中 APP 為如 Firefox 等網路瀏覽器將網路封包由 Read 將封包資訊經過在 kernel 中模擬出來的 character device 送到 Process 中,而 Write 則是將封包傳輸出去。 ### 開啟 TUN/TAP 作為 Virtio-net 傳輸資料裝置 從 [Universal TUN/TAP device driver](https://www.kernel.org/doc/html/latest/networking/tuntap.html) 可以知道如何開啟 TUN/TAP 作為虛擬網路裝置,以下簡單解釋各成員。 * `virtio_pci_dev` 將 virtio-net 驅動裝置以 virtio-pci 的形式開啟。 * 在虛擬機中,前端和後端的溝通是透過結構體 `virtq` 來溝通,而這裡考慮到一個封包會有接收端 RX 和傳輸端 TX,所以將結構體 `virtq` 的陣列長度乘上 2。 ```c struct virtio_net_dev { struct virtio_pci_dev virtio_pci_dev; struct virtq vq[VIRTIO_NET_VIRTQ_NUM * 2]; int tap_fd; int ioeventfd; bool enable; int irq_num; }; bool virtio_net_init(struct virtio_net_dev *virtio_net_dev); void virtio_net_exit(struct virtio_net_dev *dev); void virtio_net_init_pci(struct virtio_net_dev *dev, struct pci *pci, struct bus *io_bus, struct bus *mmio_bus); ``` 下方程式碼為將網卡開啟的方法,並且在 `vm_arch_init_platform_device` 開啟此網卡,之後的封包資料會透過此檔案來讀取和寫入。 ```c virtio-net.c ... #define DEVICE_NAME "tap%d" bool virtio_net_init(struct virtio_net_dev *virtio_net_dev) { virtio_net_dev->tap_fd = open("/dev/net/tun", O_RDWR); if (virtio_net_dev->tap_fd < 0) { fprintf(stderr, "failed to open TAP device: %s\n", strerror(errno)); return false; } struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = IFF_TAP | IFF_NO_PI; strcpy(ifr.ifr_name, DEVICE_NAME); if (ioctl(virtio_net_dev->tap_fd, TUNSETIFF, &ifr) < 0) { fprintf(stderr, "failed to allocate TAP device: %s\n", strerror(errno)); return false; } fprintf(stderr, "allocated TAP interface: %s\n", ifr.ifr_name); } x86/vm.c ... int vm_arch_init_platform_device(vm_t *v) { ... virtio_net_init(&v->virtio_net_dev); return 0; } ``` ::: info :question: 在 main.c 和 src/vm.c 中可以看到 `vm_load_diskimg` 此一函式為將一映像檔來作為 pci 裝置上的 virtio-blk 開啟的地方,那如果是 virtio-net 呢,virtio-net 應該在哪裡開啟?記憶體上? ```c int vm_load_diskimg(vm_t *v, const char *diskimg_file) { if (diskimg_init(&v->diskimg, diskimg_file) < 0) return -1; virtio_blk_init_pci(&v->virtio_blk_dev, &v->diskimg, &v->pci, &v->io_bus, &v->mmio_bus); return 0; } ``` ::: ### 透過 PCI 啟動 virtio-net 透過 PCI 啟動虛擬網卡需要設定一些有關 PCI 的參數,以下為我所設定的參數,首先為 virtqueue 的數量和 virtio-net 在 PCI 裝置上的 class code。 ```c #define VIRTIO_NET_VIRTQ_NUM 2 #define VIRTIO_NET_PCI_CLASS 0x019000 ``` 接著定義 PCI 裝置上的 device ID,從 [virtio-v1.2-cs01.pdf](https://docs.oasis-open.org/virtio/virtio/v1.2/cs01/virtio-v1.2-cs01.pdf) 中可得知為 0x1041。 ```c #define VIRTIO_PCI_DEVICE_ID_NET 0x1041 ``` 接著將這些設定註冊至 PCI 中 ```c void virtio_net_init_pci(struct virtio_net_dev *virtio_net_dev, struct pci *pci, struct bus *io_bus, struct bus *mmio_bus) { struct virtio_pci_dev *dev = &virtio_net_dev->virtio_pci_dev; virtio_net_setup(virtio_net_dev); virtio_pci_init(dev, pci, io_bus, mmio_bus); virtio_pci_set_pci_hdr(dev, VIRTIO_PCI_DEVICE_ID_NET, VIRTIO_NET_PCI_CLASS, virtio_net_dev->irq_num); virtio_pci_set_virtq(dev, virtio_net_dev->vq, VIRTIO_NET_VIRTQ_NUM); virtio_pci_add_feature(dev, 0); virtio_pci_enable(dev); pthread_create(&virtio_net_dev->worker_thread, NULL, (void *) virtio_net_thread, (void *) virtio_net_dev); fprintf(stderr, "Initialize net device through pci \n"); } ``` 直接在 `main.c` 中啟動虛擬機並且將 virtio-net 註冊到 pci 上,應該可以看到以下畫面 ```shell script Initialize net device through pci ... PCI host bridge to bus 0000:00 pci_bus 0000:00: root bus resource [io 0x0000-0xffff] pci_bus 0000:00: root bus resource [mem 0x00000000-0x7fffffffff] pci_bus 0000:00: No busn resource found for root bus, will use [bus 00-ff] pci 0000:00:00.0: [1af4:1042] type 00 class 0x018000 pci 0000:00:00.0: reg 0x10: [mem 0x00000000-0x000000ff] pci 0000:00:01.0: [1af4:1041] type 00 class 0x019000 pci 0000:00:01.0: reg 0x10: [mem 0x00000000-0x000000ff] pci_bus 0000:00: busn_res: [bus 00-ff] end is updated to 00 clocksource: Switched to clocksource tsc-early pci 0000:00:00.0: BAR 0: assigned [mem 0x40000000-0x400000ff] pci 0000:00:01.0: BAR 0: assigned [mem 0x40000100-0x400001ff] ... virtio-pci 0000:00:00.0: enabling device (0000 -> 0002) virtio-pci 0000:00:01.0: enabling device (0000 -> 0002) virtio-pci 0000:00:01.0: virtio_pci: bad capability len 0 (>0 expected) ... ``` 上面的畫面可以看出 pci 開啟了一塊區域 `0000:00:01.0` 給 virtio-net,至於 `0000:00:00.0` 則是給 virtio-blk,而 1041 則是上面所定義的 device ID。 :::success ``` pci 0000:00:01.0: [1af4:1041] type 00 class 0x019000 ``` ::: ::: info :question: 下方顯示 bad capability 的原因為我沒有設定 virtio-net 的 capability,在結構體中 virtio_net_dev 有一個成員為 virtio_net_config ,但是在 [linux/virtio_net.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/virtio_net.h#L90) 卻沒有可以填入 capability 的選項? ```shell virtio-pci 0000:00:01.0: virtio_pci: bad capability len 0 (>0 expected) ``` ::: ### 虛擬網卡傳輸資料 要透過虛擬網卡傳輸資料首先需要在 Linux 或 Busybox 的設定檔中加入可傳送封包的指令如 ip 和 ping,目的在於讓虛擬機和外界中可以互傳網路封包