FUSE 讀寫流程梳理
1、kernel 側體系結構
fuse_file 作爲 file 的 private_data 存在,其相當於是 FUSE 視角的文件句柄,執行讀寫操作時需要將其 fh 屬性 (用戶態句柄) 傳遞至用戶態一側。
fuse_inode 則相當於對 VFS 的 inode 做了繼承,其 i_time 屬性定義了 inode 的 TTL 有效時間,使用時長超過該閾值需要向 FUSE Deamon 重新發送 FUSE_GETATTR 請求,來獲取 inode 的最新狀態。除此之外,fuse_dentry 同樣定義了與之相應的 time 屬性 (圖中沒有呈現),來作爲 dentry 的 TTL
1.1、請求隊列
fuse_mount 和 fuse_dev 的容器同爲 fuse_conn(可供多個 FUSE FS 實例公用),其內部主要定義了 5 種類型的請求隊列。
(1) background 隊列
readahead、writeback 以及 AIO 異步請求都會先提交至該隊列,在轉移到 pending 隊列進行處理。pending 隊列的請求數達到 max_background 閾值時,不在向其遷移請求,從而達到對異步請求進行限流的目的。
異步請求的 callback 回調則是由 FUSE Deamon 側的線程去觸發,在對 / dev/fuse 字符設備執行 write 系統調用時進行。
(2) forgets 隊列
super_operations 中的 evict_inode 操作會提交 FUSE_FORGET 請求至該隊列,當目標 inode 沒有任何引用時會觸發該調用,以便通知用戶態對無用的緩存進行清理。
(3) interrupt 隊列
應用側執行 CTRL + C 操作時,會提交中斷請求至該隊列,以便對請求的處理線程做 kill 終止。
(4) pending 隊列
file_operations 以及 inode_operations 中的大部分操作函數都是提交請求至該隊列
(5) processing 隊列
存放 FUSE Daemon 正在處理的請求,收到 response 時在將請求從該隊列移除
1.2、字符設備
/dev/fuse 字符設備主要起到了通信管道的目的,來便於 FUSE Deamon 與 kernel 之間進行交互。需要注意的是單獨對 / dev/fuse 進行 open,產生的 file 是沒有 private_data 的 (即 fuse_dev),因此並不能通過它來對 fues_req 進行讀取,fuse_dev 只有在 Kernel 處理 mount 請求時纔會設置,除此之外,mount 階段還會完成對 super_block 以及根目錄 inode 的初始化操作。
2、用戶態體系結構
用戶態一側,FUSE Deamon 對外提供了兩種編程接口來供開發人員使用,其中 LowLevel 接口主要面向 inode 進行,需要開發人員自定義 fuse_lowlevel_ops 實現。而 HighLevel 則是面向 path,其內部通過 node_table 完成了 inode 向 path 的映射轉換,然後觸發 fuse_operation 中的相關函數 (需要用戶自定義實現)
用戶態與 kernel 之間的交互主要基於 / dev/fuse 字符設備進行,比如可通過對字符設備觸發 read 來完成對 fues_req 的讀取。fues_req 會序列化成 fuse_in_header 加 fuse_args(對應 payload 數據) 內存格式,用戶態一側可從 fuse_in_header 中解析出具體的請求類型,然後交由 fuse_lowlevel_ops 進行處理。
FUSE Deamon 與 kernel 的第一個交互請求爲 FUSE_INIT,內核處理 mount 請求時會觸發該調用,以便與用戶態進行 Feature Negotiation,來決定最終要開啓的 Feature,幾個比較重要的 Feature 如下:
(1) FUSE_WRITEBACK_CACHE
開啓 write_back 寫入 (默認寫入方式爲 write_through)
(2) FUSE_DO_READDIRPLUS
執行 readdir 操作時,返回每個 dentry 對應的 inode(而不是隻返回 inodeId)
(3) FUSE_POSIX_ACL
kernel 側可根據文件的 mode 信息來做權限判斷,從而不在需要向用戶態發送 FUSE_ACCESS 請求
(4) FUSE_AUTO_INVAL_DATA
讀取 file 或 dir 之前,先判斷 fuse_inode 的 TTL 是否過期,以便決定是否需要向用戶態重新發送請求來獲取 inode 的最新狀態 (針對本地文件系統可考慮將該特性關閉)
(5) FUSE_DIRECT_IO_ALLOW_MMAP
開啓該特性後,執行 directIO 時會先對髒頁進行 flush
(6) FUSE_ASYNC_READ
默認開啓,關閉該特性後,read 請求將直接走 pending 隊列,而不在走 background 隊列,從而無法對其進行限流
(7) FUSE_SPLICE_READ
開啓該特性後,針對 fues_req 的讀取不在基於 read 系統調用,而是基於 splice 的方式進行。
如圖,splice 調用觸發後,內核側通過執行 fuse_dev_splice_read 將 fuse_req 讀取到 pipe 緩衝區,用戶態一側可基於 pipe_fd 來構建 fuse_buf,從而無需在將 payload 數據向用戶態拷貝傳遞。
3、WRITE 操作
針對不同的應用場景 (NAS、本地文件系統),讀寫操作可以走不同的執行鏈路,其中 splice 方式比較適用於 stackable 類型的文件系統,即文件系統的終端在內核態。
3.1、非 splice 方式
應用側針對 FUSE 文件觸發 write 系統調用後,內核側會進行如下處理 (對應 fuse_file_operations 的 write_iter 實現)
(1) 通過 fuse_get_user_pages 獲取 write buffer 對應的物理頁幀
(2) 提交 FUSE_WRITE 請求到交互隊列
(3) FUSE Deamon 側讀取請求
請求的讀取通過對 / dev/fuse 字符設備觸發 read 系統調用進行
內核側收到 read 請求之後,首先通過 iov_iter_get_pages2 獲取 read buffer 對應的物理頁幀,然後通過 fuse_copy_pages 來完成 payload 數據向目標 pages 的拷貝傳遞。
步驟 (3) 執行以後,FUSE Deamon 側的 read buffer 便包含了以下信息
(1) fuse_write_in
可從中獲取對應的用戶態文件句柄 (圖片綠色 FD),以及寫入偏移量和長度
(2) write buffer
要寫入的數據內容 (圖片藍色 mem)
拿到這些信息後觸發 fuse_lowlevel_ops::write 調用,完成後續的寫入邏輯 (圖片步驟 4)
3.2、splice 方式
步驟 (1) 和(2)與非 splice 方式相同,步驟 (3) 變爲對 / dev/fuse 觸發 splice 系統調用,拷貝 fuse_req 到 pipe 管道,此時 pipe_buffer 的內存格式爲:fuse_in_header + fuse_write_in + write_buffer
用戶態一側可基於 pipe_fd 構建 fuse_buf,並從 fuse_write_in 中解析出用戶態文件句柄 (圖片中綠色的 FD),然後觸發 fuse_lowlevel_ops::write_buf 函數調用,以便通過 splice 來完成 pipe_buffer 向目標 File 的寫入 (對應圖片步驟 5)
與非 splice 方式相比,一個是將應用層的 write buffer 拷貝到字符設備的 read buffer,一個是將 write buffer 拷貝到 pipe 緩衝區。而 splice 方式的優勢主要體現在對 target 文件的寫入上 (圖片綠色的 File),避免了用戶態 buffer 向內核態的拷貝傳遞 (copy_from_user 操作)
4、READ 操作
4.1、非 splice 方式
應用側針對 FUSE 文件觸發 read 系統調用後,內核側會進行如下處理 (對應 fuse_file_operations 的 read_iter 實現)
(1) 通過 fuse_get_user_pages 獲取 read buffer 對應的物理頁幀
(2) 提交 FUSE_READ 請求到交互隊列
(3) 用戶態側讀取請求 (通過對 / dev/fuse 觸發 read 系統調用)
(4) 用戶態側對讀取到的請求做消費處理
請求對象的內存格式爲:fuse_in_header + fuse_read_in
可從 fuse_read_in 中獲取對應的用戶態文件句柄 (圖片綠色 FD),以及讀取偏移量和長度
基於以上信息觸發 read 系統調用完成對目標文件區間的讀取
(5) 將讀取到的數據內容返回給內核 (通過對 / dev/fuse 觸發 write 系統調用)
內核側收到 write 請求之後,首先通過 iov_iter_get_pages2 來獲取 write buffer 對應的物理頁幀。
然後藉助 fuse_copy_pages 函數來實現目標 buffer 數據嚮應用層的拷貝傳遞 (圖片步驟 7)
4.2、splice 方式
用戶態一側可根據 fuse_read_in 來確定待讀取的文件句柄 (圖片綠色的 FD) 以及讀取偏移量和長度,並根據文件句柄信息來構建 fuse_buf(圖片步驟 4),以便通過 fuse_buf_splice 來實現目標待讀取區間向 pipe_buf 的拷貝傳遞 (圖片步驟 6)
最後通過對 pipe_fd 觸發 splice 系統調用 (targetFD 對應字符設備),喚醒內核側 fuse_dev_splice_write 的執行 (圖片步驟 7) 將讀取到的文件內容拷貝到應用側的 read buffer(圖片步驟 8)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/dEEjq7P4ZxCoFqQocRabGQ