

FUSE(Filesystem in Userspace)是一個允許使用者在使用者態建立自定義檔案系統的介面,誕生於 2001 年。FUSE 的出現大大降低了檔案系統開發的門檻,使得開發者能夠在不修改核心程式碼的情況下實現創新的檔案系統功能。JuiceFS 就是基於 FUSE 構建的高效能分散式檔案系統,充分發揮了 FUSE 的靈活性和擴充套件性。
在這篇文章中,我們將為大家解析 FUSE 的架構及優勢。首先,我們將回顧核心檔案系統以及網路檔案系統(如 NFS)的發展歷程,這些技術的演進為 FUSE 實現使用者空間檔案系統功能奠定了重要基礎。
在文章的最後,將介紹 JuiceFS 在使用 FUSE 過程中的實踐。由於 FUSE 需要進行使用者態與核心態之間的切換,這會帶來一定的開銷,並可能導致 I/O 延遲,因此許多人對其效能存在顧慮。從我們的實踐經驗來看,FUSE 在大多數 AI 場景下能能夠滿足效能需求,相關細節將在文章中闡述。
檔案系統作為作業系統中的核心底層元件,負責頻繁操作儲存裝置,因此最初的設計完全在核心空間中進行。“核心”這一概念的提出,是隨著計算機硬體和軟體日益複雜化,作業系統將底層資源管理的程式碼與使用者程式進行分離的產物。
核心是擁有超級許可權的程式碼,負責管理計算機的核心資源(如 CPU、記憶體、硬碟、網路等)。當核心程式碼執行時,程式進入核心態,可以完全訪問和操作這些底層裝置。由於核心態許可權極高,其程式碼必須經過嚴格測試和驗證,普通使用者無法隨意修改。
而與核心空間形成對應的是使用者空間(User space),這部分程式碼就是我們平時常見的各種應用程式,像瀏覽器、遊戲等。在使用者空間裡,程式的許可權是受到嚴格限制的,不能直接訪問底層的重要資源。

核心空間與使用者空間
如果應用程式需要使用檔案系統,必須透過作業系統設計好的介面來進行訪問,比如我們常用的 OPEN、READ、WRITE 等,這些就是所謂的系統呼叫。系統呼叫的作用是搭建起使用者空間和核心空間的橋樑。現代作業系統常常定義了數百個系統呼叫,每個呼叫都有自己明確的名稱、編號和引數。
當應用程式呼叫系統呼叫時,它就會進入一段核心空間程式碼,等執行完畢後,再把結果返回給使用者空間。值得注意的是,這個從使用者態到核心態,然後再返回使用者態的整個過程,它都屬於同一個程序範疇。

系統呼叫架構圖
在瞭解上述背景知識後,接下來我們將簡要說明當使用者呼叫檔案系統介面時,使用者態與核心態是如何實現互動的。
核心透過 VFS(virtual file system)封裝了一套通用的虛擬檔案系統介面,向上以系統呼叫的形式暴露給使用者態,向下給底層的檔案系統規定程式設計介面,底層檔案系統需要按照 VFS 的格式來實現自己的檔案系統介面。因此,使用者態訪問底層檔案系統的標準流程一般是 系統呼叫 -> VFS -> 底層檔案系統 -> 物理裝置。
例如,我們在應用程式中呼叫 open,它會攜帶一個路徑作為它的引數。這個呼叫到達 VFS 層以後,VFS 會根據這個路徑,在它的樹狀結構中逐級查詢,最終找到一個對應的目標和它所屬的底層檔案系統,並且這個底層檔案系統也有一個自己實現的 open 方法,然後把這個 open 呼叫傳遞給底層檔案系統。
Linux 核心支援數十種不同的檔案系統。對於記憶體或網路等不同的儲存介質,會採用不同的檔案系統來進行管理。最為關鍵的是,VFS 的可擴充套件性使得 Linux 系統能夠輕鬆地支援多種多樣的檔案系統,以應對各種複雜的儲存需求;同時,這種可擴充套件性也為後續 FUSE 在使用者態實現呼叫核心態的功能提供了基礎。
隨著計算需求的增長,單臺計算機的效能逐漸無法滿足日益增長的計算和儲存要求。人們開始引入多臺計算機,以分擔負載並提高整體效率。
在這一場景下,一個應用程式往往需要訪問分佈在多臺計算機上的資料。為了解決這一問題,人們提出了在網路中引入虛擬儲存層的概念,將遠端計算機的檔案系統(如某個目錄)透過網路介面掛載到本地計算機的節點上。這樣做的目的是使本地計算機能夠無縫地訪問遠端計算機的資料,就好像這些資料儲存在本地一樣。

NFS 架構圖
具體來說,如果有一個需求是希望本地計算機能夠訪問遠端的資料,那麼可以將遠端計算機的一個子目錄透過網路介面虛擬出來,並掛載到本地計算機的一個節點上。在這個過程中,應用程式無需做任何修改,仍然透過標準的檔案系統介面就可以訪問這些路徑,就像訪問這些節點上的本地資料一樣。
當應用程式對這些網路路徑進行操作(如逐級目錄查詢)時,這些操作會被轉化為網路請求,以 RPC(遠端過程呼叫)的形式傳送到遠端計算機上執行。遠端計算機在接收到這些請求後,會進行相應的操作(如查詢檔案、讀取資料等),並將結果返回給本地計算機。
上述過程,就是一個簡易的 NFS 協議實現,作為一種網路檔案系統協議,NFS 為多臺計算機之間的資源共享提供了高效的解決方案,使用者可以像操作本地檔案系統一樣方便地掛載和訪問遠端檔案系統。
傳統的檔案系統通常是整個體系都執行在單機的核心態,而 NFS 首次打破了這一限制,服務端的實現結合了核心態與使用者態。後續 FUSE 的設計,正是受到這一思路的啟發。
隨著計算機技術的不斷發展,許多新興業務場景需要使用自定義檔案系統。傳統的核心態檔案系統存在實現難度高和版本相容性問題。NFS 的架構首次使檔案系統突破了核心的限制。
基於此,有人提出了一個構想:是否可以將 NFS 網路協議移植到單機端,將服務端功能轉移到使用者態程序,同時保留客戶端在核心中執行,用系統呼叫代替網路通訊,從而在使用者態實現檔案系統功能?這一想法最終催生了 FUSE(filesystem in userspace)的誕生。
在 2001 年,匈牙利的計算機學家 Miklos Szeredi 推出 FUSE,這是一種為開發者提供的框架,允許在使用者空間實現檔案系統。FUSE 的核心分為兩個部分:核心模組和使用者態庫(libfuse)。
其核心模組作為作業系統核心的一部分,負責與 VFS 互動,將來自 VFS 的檔案系統請求轉發到使用者態,並將使用者態的處理結果返回給 VFS。這種設計使得 FUSE 能夠在不修改核心程式碼的情況下,實現自定義檔案系統的功能。
FUSE 的使用者態庫(libfuse)提供了與 FUSE 核心模組互動的 API 庫,可以幫助使用者實現一個執行在使用者空間的守護程序(daemon)。守護程序負責處理來自核心的檔案系統請求,並實現具體的檔案系統邏輯。

FUSE 操作示意圖
在具體的實現中,使用者態的守護程序(Daemon)與核心模組透過以下步驟協作完成檔案操作:
-
核心模組會註冊一個 * 字元裝置(/dev/fuse)作為通訊通道。守護程序透過呼叫 read() 函式主動從該裝置讀取請求:
-
若核心的 FUSE 請求佇列為空,read() 會進入阻塞狀態,此時守護程序暫停執行並釋放 CPU,直到佇列中出現新請求(透過核心的等待佇列機制實現)。
-
當應用程式發起檔案操作(如 open、read)時,核心模組將請求封裝為特定格式的資料包,並插入請求佇列,喚醒阻塞中的守護程序。
守護程序從字元裝置中讀取到請求資料包後,根據操作型別(如讀、寫、建立檔案)呼叫對應的使用者態處理函式。
處理完成後,守護程序將結果(如讀取到的檔案內容或錯誤碼)按照 FUSE 協議格式序列化,並透過 write() 將資料包寫回字元裝置。
核心模組接收到響應後:
-
解析資料包內容,將結果傳遞給等待中的應用程式;
-
喚醒應用程式中被阻塞的系統呼叫,使其繼續執行後續邏輯。
FUSE 的出現為檔案系統的開發帶來了革命性的變化,透過將檔案系統的實現從核心態遷移到使用者態,FUSE 大幅降低了開發的難度,提升了系統的靈活性和可擴充套件性,使其廣泛地被應用於多種場景,如網路檔案系統、加密檔案系統以及虛擬檔案系統等。
2017 年,IT 基礎設施全面進入雲時代,架構面臨前所未有的挑戰,在此背景下,JuiceFS 誕生。作為一款基於物件儲存的分散式檔案系統,選擇採用 FUSE 技術來構建其檔案系統架構,以 FUSE 靈活的擴充套件性來應對雲計算環境中的多樣化需求。
透過 FUSE,JuiceFS 檔案系統能夠以 POSIX 相容的方式掛載到伺服器,將海量雲端儲存直接當做本地儲存來使用,常見的檔案系統指令(如 ls、cp、mkdir 等)都可以用來管理 JuiceFS 中的檔案和目錄。

JuiceFS 社群版架構圖
以使用者掛載 JuiceFS 後,open 其中一個檔案的流程為例。請求首先透過核心 VFS,然後傳遞給核心的 FUSE 模組,經過 /dev/fuse 裝置與 JuiceFS 的客戶端程序通訊。VFS 和 FUSE 的關係可以簡單的看做客戶端 – 伺服器協議,VFS 作為客戶端請求服務,使用者態的 JuiceFS 扮演著伺服器端的角色,處理這些請求。
具體步驟如下:
-
當 JuiceFS mount 後,JuiceFS 內部的 go-fuse 模組會 open /dev/fuse 獲取 mount fd,並啟動幾個執行緒讀取核心的 FUSE 請求 ;
-
使用者呼叫 open 函式,透過 C 庫和系統呼叫進入 VFS 層,VFS 層再將請求轉給核心的 FUSE 模組;
-
核心 FUSE 模組根據協議將 open 請求放入 JuiceFS mount 的 fd 對應的佇列中,並喚醒 go-fuse 的讀請求執行緒,等待處理結果;
-
使用者態的 go-fuse 模組讀取 FUSE 請求並在解析請求後呼叫 JuiceFS 的對應實現;
-
go-fuse 將本次請求的處理結果寫入 mount fd,即放入 FUSE 結果佇列,然後喚醒業務等待執行緒;
-
業務執行緒被喚醒,得到本次請求的處理結果,然後再返回到上層。
由於 FUSE 依賴於使用者空間與核心之間的頻繁切換,許多人對其效能表現存有疑慮,實際上並非完全如此,我們使用 JuiceFS 進行了一組測試。
在一臺 1.5T 記憶體、Intel Xeon 架構 176 核的機器上的 JuiceFS 檔案系統中創造一個 512G 的空洞檔案,然後使用 fio 對其進行順序讀測試(掛載引數詳情 [1], fio 命令詳情 [2]),這樣可以排除硬體磁碟的制約,測試出 FUSE 檔案系統相對極限的頻寬。
可以看到單掛載點下的單執行緒頻寬可以達到 2.4 GiB/s;
隨著執行緒的增加,頻寬基本可以實現線性增長,在 20 執行緒時達到了 25.1 GiB/s,這個吞吐量已經可以滿足絕大部分實際業務場景。

在 FUSE 相關的使用上,JuiceFS 實現了平滑升級功能。透過保證 mount fd 的一致性,讓使用者可以在不需要重新掛載檔案系統、不中斷業務的情況下,升級 JuiceFS 版本或修改掛載引數,詳情請參考平滑升級功能詳解,不停服即可更新。
FUSE 也存在一些侷限性,比如程序訪問 FUSE device 需要很高的許可權,尤其在容器環境中,通常需要開啟特權模式。另外,容器通常是短暫且無狀態的,如果容器意外退出,並且資料沒有及時落盤,會存在資料丟失的風險。因此針對 Kubernetes 場景,JuiceFS 提供了 CSI 驅動使得業務可以以非特權容器訪問 JuiceFS 檔案系統,並且 CSI 驅動管理 FUSE 程序的生命週期,確保資料能夠及時落盤不會出現丟失的問題。Kubernetes 資料持久化:從零開始使用 JuiceFS CSI Driver。
FUSE 透過將使用者空間與核心空間解耦,為開發者提供了在使用者空間實現檔案系統的巨大靈活性和便利性。特別是在雲計算和分散式儲存等現代計算環境中,FUSE 使得構建和維護複雜的儲存系統變得更加高效、可定製和易於擴充套件。
JuiceFS 正是基於 FUSE,在使用者空間實現了高效能的分散式檔案系統。未來,我們將繼續深入探索 FUSE 的最佳化方法,不斷提升檔案系統的效能與可靠性,以應對日益複雜的儲存需求,為使用者提供更強大的資料管理能力。
引用連結
[1] 掛載引數詳情: ./cmd/mount/mount mount –no-update –conf-dir=/root/jfs/deploy/docker –cache-dir /tmp/jfsCache0 –enable-xattr –enable-acl -o allow_other test-volume /tmp/jfs
[2] fio 命令詳情: fio –name=seq_read –filename=/tmp/jfs/holefile –rw=read –bs=4M –numjobs=1 –runtime=60 –time_based –group_reporting
