轉自 https://virtio-fs.gitlab.io/howto-qemu.html
https://heychenbin.github.io/post/kata_with_virtiofs/
https://kernel.taobao.org/2019/11/virtio-fs-intro-and-perf-optimize/
1. 當前安全容器存儲領域的問題
在介紹virtio-fs之前,我們先來了解一下當前安全容器存儲領域遇到的問題,因為只有在理解了所要解決的問題才能更好的理解解決問題的方案。
在當前安全容器領域,Kata Containers可以說是最被廣泛應用的容器技術了。Kata Containers使用輕量級虛擬機和硬件虛擬化技術來提供更強隔離,以構建安全的容器運行時。但也正是因為使用了虛擬機,容器的根文件系統(容器rootfs)無法像runc那樣直接使用主機上構建好的目錄,而需要有一種方法把host上的目錄共享給guest。
目前基本上只有兩種流派的方法能夠透傳host目錄/數據給guest,一種是基於文件的方案,一個是基於塊設備的方案。而這兩種流派又都有各自有自身的優缺點,這里分別以9pfs和devicemapper為例來說明。
- 基於文件的9pfs
- 優點:因為使用host上overlayfs,能充分利用host pagecache;部署簡單,不需要額外的組件
- 缺點:基於網絡的協議,性能差;POSIX語義兼容性方面不好(主要體現在mmap(2)的支持上,在特殊情況下有數據丟失的風險)
- 基於塊設備的devicemapper
- 優點:良好的性能;很好的POSIX語義兼容性
- 缺點:無法充分利用host pagecache,部署運維復雜(需要維護lvm volume)
可以看到,這兩種流派優缺點基本互補。那么有沒有一種解決方案能夠結合兩者的優點,同時能夠克服兩者的缺點呢?virtio-fs正是這種新的方案,為我們帶來了新的曙光。
2. virtio-fs介紹
2.1 基本信息
virtio-fs是紅帽在2018年12月10號在kata社區提出的一種在guest之間共享文件系統的方案。 項目主頁:https://virtio-fs.gitlab.io/ 代碼倉庫:https://gitlab.com/virtio-fs 郵件列表:https://www.redhat.com/mailman/listinfo/virtio-fs
其主要設計目標為:
- 在不同guest之間,以快速、一致、安全的方式共享同一個host目錄樹結構
- 擁有較好的性能和跟本地文件系統(如ext4)一樣的語義
主要使用場景:
- 在kata-container場景中替換9p,作為容器rootfs
- 為虛擬機在host和guest之間共享數據 (shared file system for virtual machines)
- File-system-as-a-Service,更安全
2.2 原理與架構設計
virtio-fs方案使用FUSE協議在host和guest之間通信。在host端實現一個fuse server操作host上的文件,然后把guest kernel當作fuse client在guest內掛載fuse,server和client之間使用virtio來做傳輸層來承載FUSE協議,而不是傳統結構上的/dev/fuse設備。為了支持在不同guest中同時mmap(MAP_SHARED)同一個文件,virtio-fs把文件mmap進qemu的進程地址空間並讓不同guest使用DAX訪問該內存空間,這樣就繞過了guest pagecache達到不同guest都訪問同一份數據的目的,同時也在多個guest之間共享了內存,節省了內存資源。
簡要架構圖:
從圖中我們可以了解到,virtio-fs主要由以下幾個組件組成:
- guest kernel:作為fuse client來掛載host上導出的目錄
- qemu:新添加的vhost-user-fs-pci設備用於在guest kernel和virtiofsd之間建立起vhost-user連接
- virtiofsd(同樣在qemu倉庫中):host上運行的基於libfuse開發的fuse daemon,用於向guest提供fuse服務
下圖是更詳細的架構圖
virtio-fs跟其他基於文件的方案(比如9pfs或者NFS)相比,其獨特之處在於virtio-fs充分利用了虛擬機和hypervisor同時部署在一個host上的特點以避免昂貴的VMEXITS。具體來說就是,DAX數據訪問和元數據的共享內存訪問都是通過共享內存的方式避免不必要的VM/hypervisor之間通信(在元數據沒有改變的情況下),而共享內存訪問也比基於網絡文件系統協議訪問要更輕量級也有更好的本地文件系統語義和一致性。這也是基於FUSE設計virtio-fs而不是基於其他網絡文件系統協議做改進的原因。
了解virtio-fs原理與設計架構之后,我們可以總結出virtio-fs的幾條設計要點,看其是如何同時擁有9pfs和devicemapper方案的優點,同時克服它們的缺點的:
- FUSE協議而不是基於網絡的協議:更快的性能,更好的POSIX語義兼容性
- 獨立的virtiofsd進程:更安全,且不需要維護單獨的塊設備(相比9pfs多了一個組件,但維護成本比devicemapper還是要小很多)
- Host/guest之間共享內存(DAX):進一步提升性能,同時節省內存資源
整體架構
virtio-fs整體架構如下圖所示:
總體來說, virtio-fs主要依賴於FUSE文件系統, FUSE文件系統本來是作為用戶態文件系統的內核實現, 但是virtio-fs對其進行了一些改造, 在此基礎上拓展成了virtio-fs文件系統.
在主機端, virtio-fs使用libfuse實現了一個用戶態文件系統, 該程序名為virtiofsd, 每個虛擬機容器對應一個該進程. virtiofsd與GuestOS中的virtio-fs使用vhost-user協議進行通信. vhost-user包含兩條通道, 一條通道為控制通道, 一條通道為數據通道. 控制通道通過Unix Socket進行通信, 數據通道按照virtio方式, 使用共享內存的方式進行通信.
為了提高效率, 在GuestOS中, virtio-fs還實現了DAX(Direct Access), 將用戶態空間中的文件通過內存映射的方式映射到GuestOS中的塊設備地址中, 達到提升讀取效率的目的. 在測試數據中也可以看到, 讀取效率還是有一定提升, 特別是對小文件的讀寫相比9pfs有了比較大的提高.
GuestOS端實現
在GuestOS端主要是對內核的修改, 主要代碼都集中在fs/fuse/文件夾下, 初始化代碼為fs/fuse/virtio_fs.c:
1. virtio-fs在執行probe操作時, 調用virtio_fs_setup_vqs初始化virtqueue, 還會注冊virtio中斷處理函數, 用於接收由virtfsd中發來的數據.
1 static int virtio_fs_probe(struct virtio_device *vdev) 2 { 3 struct virtio_fs *fs; 4 int ret; 5 6 fs = devm_kzalloc(&vdev->dev, sizeof(*fs), GFP_KERNEL); 7 if (!fs) 8 return -ENOMEM; 9 vdev->priv = fs; 10 11 ret = virtio_fs_read_tag(vdev, fs); 12 if (ret < 0) 13 goto out; 14 15 ret = virtio_fs_setup_vqs(vdev, fs); 16 if (ret < 0) 17 goto out; 18 19 /* TODO vq affinity */ 20 /* TODO populate notifications vq */ 21 22 ret = virtio_fs_setup_dax(vdev, fs); 23 if (ret < 0) 24 goto out_vqs; 25 26 /* Bring the device online in case the filesystem is mounted and 27 * requests need to be sent before we return. 28 */ 29 virtio_device_ready(vdev); 30 31 ret = virtio_fs_add_instance(fs); 32 if (ret < 0) 33 goto out_vqs; 34 35 return 0; 36 37 out_vqs: 38 vdev->config->reset(vdev); 39 virtio_fs_cleanup_vqs(vdev, fs); 40 out: 41 vdev->priv = NULL; 42 return ret; 43 }
2. 執行mount -t virtiofs …, 系統調用最終走到vfs_kern_mount,會去調用指定文件系統的mount函數,要做的事有:
創建該文件系統的超級快對象 fill superblock,返回該文件系統的根目錄(root)的dentry實例,最后將創建的超級快對象賦值給新創建的vfsmount結構所指的超級快,同時vfsmount所指的mnt_root點賦值為超級快所指的根dentry.
virtio_fs_mount(struct file_system_type *fs_type , int flags, const char *dev_name, void *raw_data)
-------mount_nodev(fs_type, flags, raw_data, virtio_fs_fill_super)
-------------struct super_block *s = sget(fs_type, NULL, set_anon_super, flags, NULL);// set_anon_super : Filesystems which don't use real block devices can call this function to allocate a virtual block device.
--------------error = fill_super(s, data, flags & SB_SILENT ? 1 : 0) //fill_super即 virtio_fs_fill_super
1 static struct dentry *virtio_fs_mount(struct file_system_type *fs_type, 2 int flags, const char *dev_name, 3 void *raw_data) 4 { 5 return mount_nodev(fs_type, flags, raw_data, virtio_fs_fill_super); 6 }
主要初始化代碼位於函數virtio_fs_fill_super中, 該函數會調用fuse_fill_super_common, 創建和初始化fuse connection. fuse connection在原始的FUSE文件系統中, 本來是用於連接Kernel FUSE與用戶態驅動程序的通道, 在virtio-fs中對這里做了修改, 將該通道替換為virtio通道直接與用戶態的virtiofsd進行通信.
1 int fuse_fill_super_common(struct super_block *sb, 2 struct fuse_mount_data *mount_data) 3 { 4 // ...... 5 fc = kmalloc(sizeof(*fc), GFP_KERNEL); 6 err = -ENOMEM; 7 if (!fc) 8 goto err; 9 10 fuse_conn_init(fc, sb->s_user_ns, mount_data->dax_dev, 11 mount_data->fiq_ops, mount_data->fiq_priv); 12 fc->release = fuse_free_conn; 13 14 if (mount_data->dax_dev) { 15 err = fuse_dax_mem_range_init(fc, mount_data->dax_dev); 16 if (err) { 17 pr_debug("fuse_dax_mem_range_init() returned %d\n", err); 18 goto err_free_ranges; 19 } 20 } 21 22 fud = fuse_dev_alloc_install(fc); 23 if (!fud) 24 goto err_put_conn; 25 26 fc->dev = sb->s_dev; 27 fc->sb = sb; 28 err = fuse_bdi_init(fc, sb); 29 if (err) 30 goto err_dev_free; 31 // ...... 32 }
在fuse_fill_super_common函數中, 還判斷了mount參數中是否有dax參數, 如果有dax參數, 則初始化dax. dax原理暫時還沒有分析.
Qemu
在qemu中, qemu擴展了vhost-user協議, 增加了三條和virtio-fs相關的消息:
1 * VHOST_USER_SLAVE_FS_MAP 2 Id: 4 3 Equivalent ioctl: N/A 4 Slave payload: fd + n * (offset + address + len) 5 Master payload: N/A 6 7 Requests that the QEMU mmap the given fd into the virtio-fs cache; 8 multiple chunks can be mapped in one command. 9 A reply is generated indicating whether mapping succeeded. 10 11 * VHOST_USER_SLAVE_FS_UNMAP 12 Id: 5 13 Equivalent ioctl: N/A 14 Slave payload: n * (address + len) 15 Master payload: N/A 16 17 Requests that the QEMU un-mmap the given range in the virtio-fs cache; 18 multiple chunks can be unmapped in one command. 19 A reply is generated indicating whether unmapping succeeded. 20 21 * VHOST_USER_SLAVE_FS_SYNC 22 Id: 6 23 Equivalent ioctl: N/A 24 Slave payload: n * (address + len) 25 Master payload: N/A 26 27 Requests that the QEMU causes any changes to the virtio-fs cache to 28 be synchronised with the backing files. Multiple chunks can be synced 29 in one command. 30 A reply is generated indicating whether syncing succeeded.
vhost_user_backend_init()
---------........
---------vhost_setup_slave_channel(dev)
------------------.........
------------------qemu_set_fd_handler(u->slave_fd, slave_read, NULL, dev)
------------------.......
//hw/virtio/vhost-user.c中 slave_read() { ...... #ifdef CONFIG_VHOST_USER_FS case VHOST_USER_SLAVE_FS_MAP: ret = vhost_user_fs_slave_map(dev, &payload.fs, fd[0]); break; case VHOST_USER_SLAVE_FS_UNMAP: ret = vhost_user_fs_slave_unmap(dev, &payload.fs); break; case VHOST_USER_SLAVE_FS_SYNC: ret = vhost_user_fs_slave_sync(dev, &payload.fs); break; #endif ……. }
新增的三條消息都是內存映射相關的, 在qemu端, 收到內存映射消息會將用戶空間文件映射到虛擬機virtio-fs緩存中, 加速GuestOS的讀寫速度.
在增加了virtiofsd后, qemu會作為vhost-user的Master端, 啟動時, 會創建一個unix socket, virtiofsd會作為客戶端連接該socket. 在qemu參數中會新增參數:
|
|
virtiofsd和qemu之間的這條通道就是作為vhost-user的控制通道, 用於在GuestOS和Host端virtio數據交互的通知通道.
一個比較簡單的數據交互流程如下所示:
在GuestOS中訪問文件等操作, 會調用到vfs, vfs會根據注冊的驅動程序選擇virtio-fs文件系統進行處理, virtio-fs基於FUSE文件系統, 將訪問請求轉發到virtiofsd中, 在virtiofsd中進行處理, 處理數據會通過virtio的virtqueue進行傳遞, 同時通過vhost user協議, 通知GuestOS接收數據.
2.3 DAX分析
關於DAX的討論list : https://lists.syncevolution.org/hyperkitty/list/linux-nvdimm@lists.01.org/thread/MOCBHA2HTZCPTLFH6Y3YGOQJZQRJ3NZ5/
This patch series adds DAX support to virtiofs filesystem. This allows bypassing guest page cache and allows mapping host page cache directly in guest address space. When a page of file is needed, guest sends a request to map that page (in host page cache) in qemu address space. Inside guest this is a physical memory range controlled by virtiofs device. And guest directly maps this physical address range using DAX and hence gets access to file data on host. This can speed up things considerably in many situations. Also this can result in substantial memory savings as file data does not have to be copied in guest and it is directly accessed from host page cache.
.....
virtio_fs_probe()的時候 virtio_fs_setup_dax(vdev, fs)
2.4 ireg分析
.....
其他
1.
2.
2.linux文件系統
1、抽象vfs layer,成為io體系的controller,其中包含file(文件)、dentry(目錄)、inode(索引)、mount point(安裝點)四個entity。屏蔽了下層文件系統的差異,將下層能力封裝成文件概念,使得應用層能通過操作文件,從而操作底層的塊設備。
- 應用層用戶可以通過樹型目錄來存取文件,符合用戶的思維習慣,方便管理和獲取,如/home/xiaoju/work。
- 通過開放file_operation、dentry_operation、inode_operation、address_space_operation接口,集成file system layer的能力,屏蔽底層的復雜性,使得底層file system的擴展和切換對於操作層無感。
- 抽象block(塊)作為操作的基本單位。硬盤操作的做小單位扇區,但扇區太小,只有512byte,塊的大小=512byte * 2的n次方,一般為1k或4k,就好像RMB最小單位是分,但我們現在都用元。磁盤管理器負責邏輯塊號到具體扇區的映射,所以kernal只存儲邏輯塊號。
2、抽象file system layer,是io體系的service層,實現了文件系統的核心邏輯,包括數據的索引、查詢、存儲、緩存。其可以支持接入多種file system以滿足用戶不同的存儲訴求,比如nfs。每種file system提供多種io mode,方便用戶在存儲空間利用率、性能、可靠性方面做選擇。
- buffered io:普通文件操作,對性能、吞吐量沒有特殊要求,由kernel通過page cache統一管理緩存,page cache的創建和回收由kernel控制。其默認是異步寫,如果使用sync,則是同步寫,保證該文件所有的臟頁落盤后才返回(對於db transaction很重要,通過sync保證redo log落盤,從而保證一致性)。
- mmap:對文件操作的吞吐量、性能有一定要求,且對內存使用不敏感,不要求實時落盤的情況下使用。mmap對比buffered io,可以減少一次從page cache -> user space的內存拷貝,大文件場景下能大幅度提升性能。同時mmap將把文件操作轉換為內存操作,避免lseek。通過msync回寫硬盤(此處只說IO相關的應用,拋開進程內存共享等應用)。
- direct io:性能要求較高、內存使用率也要求較高的情況下使用。適合DB場景,DB會將數據緩存在自己的cache中,換入、換出算法由DB控制,因為DB比kernel更了解哪些數據應該換入換出,比如innodb的索引,要求常駐內存,對於redo log不需要重讀讀寫,更不要page cache,直接寫入磁盤就好。
3、抽象generic block layer,類似於dao層,適配內存和硬盤之間存儲數據的gap,包括存儲單位和存儲地址(ps:不包括存儲速度的匹配)。
4、抽象io scheduler layer,類似於dao層采用的寫入方式是異步的,主要是解決io吞吐量的問題,提升整體數據處理(包括read和write)/ 尋道次數的占比,對於單次io延時略有提升,但從整體提升了磁盤的數據處理效率。也是在這一層調用塊設備驅動(塊設備在設備注冊環節注冊自己request_fn)。
轉自 https://zhuanlan.zhihu.com/p/61123802