文件系統小冊(Fuse&Posix&K8s csi)【1 Fuse:用戶空間的文件系統】
Fuse(filesystem in userspace),是一個用戶空間的文件系統。通過fuse內核模塊的支持,開發者只需要根據fuse提供的接口實現具體的文件操作就可以實現一個文件系統。由于其主要實現代碼位于用戶空間中,而不需要重新編譯內核,這給開發者帶來了眾多便利。
- 雖然Fuse簡化了文件系統的實現,給開發者帶來了便利。但是其額外的內核態/用戶態切換帶來的性能開銷不能被忽視,所以fuse性能問題,一直是業界繞不開的話題。下面說到的splice、多線程、writeback cache都是為了改善其性能問題。
1 架構設計(執行流程)
- 用戶程序掛載到fuse文件系統,比如此時執行ls命令
- VFS(虛擬文件系統)檢測到掛載到fuse文件系統上的用戶程序發送的請求,會將其轉發給fuse driver
- fuse driver接受到request請求,會將其保存到queue中,同時暫停用戶程序(ls會卡主,等待返回結果),同時喚醒fuse daemon處理請求
- fuse daemon(守護進程)通過/dev/fuse讀取queue中的request,經過處理后將其轉發給內核底層文件系統(EXT4等)。
- 內核文件系統處理完成后將結果返回給fuse daemon,fuse daemon將結果寫回/dev/fuse
- fuse driver將該request標記為completed,并喚醒用戶進程,返回對應執行結果。(ls執行結束,終端展示文件列表)
2 相關組件
①VFS:轉發請求給fuse driver
VFS(虛擬文件系統)檢測到掛載到fuse文件系統上的用戶程序發送的請求,會將其轉發給fuse driver
② FUSE drvier(queue):接受請求保存到queue
fuse driver接受到request請求,會將其保存到queue中,同時暫停用戶程序(ls會卡主,等待返回結果)
③/dev/fuse(橋梁):fuse daemon通過/dev/fuse讀取queue中的請求
FUSE 驅動程序(fuse driver)處理請求并將其加入隊列,然后通過 /dev/fuse 文件(FUSE 守護程序無法讀取該文件)中的特定連接實例將請求提交給負責處理該 FUSE 文件系統的 FUSE 守護程序。
- fuse daemon通過/dev/fuse來讀取request queue中的請求
④fuse daemon(中間人):從queue中讀取請求轉發給底層文件系統
fuse daemon(守護進程)通過/dev/fuse讀取queue中的request,經過處理后將其轉發給內核底層文件系統(EXT4等)。
⑤fuse lib:提供接口和內核fuse模塊通信
fuse的lib庫,封裝好了對應接口。fuse的lib庫,提供接口和內核fus模塊通信
⑥內核文件系統(如:EXT4)
內核層面的文件系統,真正操作文件的系統。
3 實現細節
① fuse用戶空間流程
1. fuse mount:通過mount函數將path掛載到/dev/fuse設備
Fuse的掛載通過mount函數,將指定的fuse_path掛載到/dev/fuse設備上。之后對于fuse_path下的文件操作,都會通過fuse文件系統,并通過/dev/fuse被fuse daemon讀取處理。
2. fuse thread:fuse daemon創建的服務線程
Fuse daemon還會創建一個服務線程,基于libfuse庫來處理文件操作請求。這里主要關注fuse_session_new和fuse_session_loop_mt。通過fuse_session_new在libfuse中注冊了fuse daemon實現的fuse_lowlevel_ops,之后通過fuse的所有的文件操作,都會通過libfuse回調到fuse daemon進行處理。fuse_session_loop_mt在libfuse中實現了一個多線程模式來讀取請求,相比單線程,在請求處理上效率更高。
- fuse daemon創建的服務線程
- 基于libfuse庫處理請求
- 可多線程模式
- 通過fuse_session_new(new一個session,與內核fuse模塊通信)+fuse_session_loop_mt(多線程處理請求)
3. libfuse:fuse的lib庫,提供接口和內核fus模塊通信
fuse_session_loop_mt:fuse thread基于多線程方式處理請求
- splice實現內存零拷貝。在默認情況下,fuse daemon必須通過read()從/dev/fuse讀取請求,通過write()將請求回復寫入/dev/fuse。每次讀寫系統調用都需要進行一次內核-用戶空間的內存拷貝。這樣對讀寫的性能損耗十分嚴重,因為一次內存拷貝需要處理大量數據。為了緩解這個問題,fuse支持了Linux內核提供的 splice 功能。splice 允許用戶空間在兩個內核內存緩沖區之間傳輸數據,而無需將數據復制給用戶空間。如果fuse daemon實現了write_buf()方法,則 FUSE 從/dev/fuse讀取數據,并以包含文件描述符的緩沖區的形式將數據直接傳遞給此方法處理,從而省去了一次內存申請與拷貝。
[提供緩沖區傳數據,避免用戶空間與內核空間來回切換耗時]
- 多線程模式。在多線程模式下,fuse daemon以一個線程開始,如果內核隊列中有兩個以上的request,則會自動生成其他線程。默認最大支持10個線程同時處理請求。
[多線程:隊列request>2,自動生成新線程,最大支持10并發]
②fuse內核隊列(維護了5個隊列)
fuse在內核中維護了五個隊列,分別為:Backgroud、Pending、Processing、Interrupts、Forgets。一個請求在任何時候只會存在于一個隊列中。
- Backgroud:存異步請求
- Pending:存同步請求
- Processing:存處理中的請求
- Interrupts:存中斷請求(如:用戶ctrl+C,取消請求),優先級最高
- Forgets:存forget請求(清理cache中的inode)
1. Backgroud:暫存異步請求
Backgroud:background 隊列用于暫存異步請求。在默認情況下,只有讀請求進入 background 隊列;當writeback cache啟用時,寫請求也會進入 background 隊列。當開啟writeback cache時,來自用戶進程的寫請求會先在頁緩存中累積,然后當bdflush 線程被喚醒時會下刷臟頁。在下刷臟頁時,FUSE會構造異步請求,并將它們放入 background 隊列中。
2. Pending:存儲同步請求
同步請求(例如,元數據)放在 pending 隊列中,并且pending隊列會周期性接收來自background 的請求。但是pending隊列中異步請求的個數最大為max_background(最大為12),當pending隊列的異步請求未達到12時,background隊列的請求將被移動到pending隊列中。這樣做的目的是為了控制pending隊列中異步請求的個數,防止在突發大量異步請求的情況下,阻塞了同步請求。
3. Processing:存儲正在處理的請求
Processing:當pending隊列中的請求被轉發到fuse daemon的同時,也被移動到processing隊列。所以processing隊列中的請求,表示正在被處理fuse daemon處理的請求。當fuse daemon真正處理完請求,通過/dev/fuse下發reply時,該請求將從processing隊列中刪除。
4. Interrupts:存放中斷請求(用戶取消請求:如:ctrl+C)
Interrupts:用于存放中斷請求,比如當發送的請求被用戶取消時,內核會發送一個Interrupts請求,來取消已被發送的請求。中斷請求的優先級最高,Interrupts中的請求會最先得到處理。
5. Forgets:記錄清理cache中inode的請求
Forgets:存儲forgets請求,forget請求用于刪除cache中緩存的inode。
③/dev/fuse 讀寫調用流程
Fuse driver加載過程中注冊了對/dev/fuse的操作接口fuse_dev_operations。fuse_dev_do_read/fuse_dev_do_write分別對應fuse daemon從內核讀取請求,以及處理完請求后寫回reply的函數調用。
- pending 、interrups、forgets隊列為空時,讀進程休眠。
- 一旦有request到達,對應等待隊列上的進程被喚醒(Interrups 和 forgets優先級高于pending隊列請求)
- 當請求數據內容被拷貝到用戶空間后(fuse daemon在進行處理了)
- 該請求被移動到processing隊列,標識該請求已被處理。
- req->flags會保存當前請求的狀態
- fuse daemon處理完請求后(fuse daemon與內核底層FS打交道)
- fuse daemon將結果寫回到/dev/fuse。
- 其中寫數據保存在struct fuse_copy_state中,并且會根據unique id在fc(fuse_conn)中找到對應的req,并將寫回的參數從fuse_copy_state拷貝至req->out。
源碼邏輯:
當pending 、interrups、forgets隊列都沒有請求時,讀進程進入休眠。一旦有請求到達,這個等待隊列上的進程將被喚醒。Interrups 和 forgets的請求優先級高于pending隊列。當請求的數據內容被拷貝至用戶空間后,該請求會被移至processing隊列,并且req->flags會保存當前請求的狀態。
當fuse daemon處理完請求后,會將結果寫回到/dev/fuse。寫數據保存在struct fuse_copy_state中,并且會根據unique id在fc(fuse_conn)中找到對應的req,并將寫回的參數從fuse_copy_state拷貝至req->out。
4案例:以unlink為例
- fuse daemon會阻塞在讀/dev/fuse,當app進程在fuse掛載點下面有新的文件操作(unlink)
- 這時系統調用會調用fuse內核接口,并生成request,同時喚醒阻塞的fuse daemon
- fuse daemon讀到request后,在libfuse中進行解析,根據request的opcode來執行對應的ops
- 完成后會把處理結果返回給/dev/fuse。此時vfs調用阻塞的行為將被喚醒,最后返回vfs調用。
5 實戰(go-fuse)
相關倉庫地址:
- https://github.com/hanwen/go-fuse
- https://github.com/bazil/fuse
- https://github.com/libfuse/libfuse/
Golang操作fuse的庫主要有go-fuse、libfuse。這里主要講解go-fuse
①概述
Go-Fuse 是一個開源的庫,由 Han-Wen Nienhuys 創建并維護。該庫提供了對 Linux FUSE(Filesystem in Userspace)接口的支持,使得開發人員可以使用 Go 語言構建自己的文件系統。
功能:
- 構建自定義文件系統:使用 Go-Fuse,您可以根據需要構建自己的文件系統。這可能包括加密、壓縮、優化性能等功能。
- 支持各種平臺:由于 Go-Fuse 基于 FUSE,因此它可以跨多個操作系統(如 Linux、macOS 和 Windows)運行。
- 高度自定義:通過實現特定的接口方法,您可以控制文件系統的每個細節。這為實現復雜的文件系統行為提供了極大的靈活性。
②環境準備
我準備在我本地macos上構建,因此需要fuse命令。
- macos:https://github.com/osxfuse/osxfuse/releases(下載dmg安裝配置)
- ubuntu: sudo apt-get -y update && sudo apt-get install -y fuse
- centos:sudo yum -y update && sudo yum install -y fuse
安裝好之后,需要確保當前用戶需要有執行fuse命令的權限
# 如果當前用戶沒有權限,可以進行提權或者切換用戶,或者修改fuse配置
vim /etc/fuse.conf打開user_allow_other
③全部代碼&解析
//安裝依賴
go get "github.com/hanwen/go-fuse/v2/fs"
go get "github.com/hanwen/go-fuse/v2/fuse"
package mainimport ("context""flag""log""syscall""github.com/hanwen/go-fuse/v2/fs""github.com/hanwen/go-fuse/v2/fuse"
)type HelloRoot struct {fs.Inode
}func (r *HelloRoot) OnAdd(ctx context.Context) {ch := r.NewPersistentInode(ctx, &fs.MemRegularFile{Data: []byte("file.txt data"),Attr: fuse.Attr{Mode: 0644,},}, fs.StableAttr{Ino: 2})r.AddChild("file.txt", ch, false)
}func (r *HelloRoot) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno {out.Mode = 0755return 0
}var _ = (fs.NodeGetattrer)((*HelloRoot)(nil))
var _ = (fs.NodeOnAdder)((*HelloRoot)(nil))//./yi-fuse test
func main() {debug := flag.Bool("debug", false, "print debug data")flag.Parse()if len(flag.Args()) < 1 {log.Fatal("Usage:\n ./yi-fuse MOUNTPOINT")}opts := &fs.Options{}opts.Debug = *debugserver, err := fs.Mount(flag.Arg(0), &HelloRoot{}, opts)if err != nil {log.Fatalf("Mount fail: %v\n", err)}server.Wait()
}
- 我們通過go-fuse庫創建了一個用戶空間文件系統,該文件系統只包含一個名為file.txt的文件。
- context:用于處理上下文,可以在異步操作中取消請求。
- flag:處理命令行參數。
- log:日志記錄。
- syscall:系統調用接口。
- fs 和 fuse:來自github.com/hanwen/go-fuse/v2的庫,用于實現用戶空間文件系統。
- HelloRoot 結構體:
- 表示文件系統的根節點,實現了NodeGetattrer和NodeOnAdder接口。
- OnAdd 方法:當文件系統被加載時調用,創建一個包含file.txt的持久化節點。
- Getattr 方法:獲取文件屬性,將file.txt的權限設置為0755。
- main 函數:
處理命令行參數,設置調試標志。
檢查至少有一個掛載點參數。
創建fs.Options,啟用調試模式。
調用fs.Mount掛載文件系統。
如果掛載失敗,打印錯誤信息并退出。- server.Wait()阻塞直到文件系統卸載。
④測試
//編譯可執行文件到linux
GOOS=linux GOARCH=amd64 go build -o yi-fuse main.go
//創建掛載目錄
mkdir -p /root/test
//執行掛載(如果不加nohup,默認前臺運行)
nohup ./yi-fuse /root/test &//預期返回我們代碼里寫的file.txt文件
ls -l /root/test//讀取file.txt文件內容
cat /root/test/file.txt//卸載掛載
umount /root/test
參考文章:
https://www.cnblogs.com/Linux-tech/p/14110335.html
https://blog.csdn.net/gitblog_00007/article/details/136569849