自制文件系統 — 02 FUSE 框架,開發者的福音
堅持思考,就會很酷
前情提要
前文介紹了一些探索文件系統的命令和方法,並且提到內核文件系統開發的艱難,筆者說過要帶讀者朋友一起動手做一個極簡的文件系統。但筆者總不能帶嬌嫩的讀者趟一次內核的渾水吧?不能食言,怎麼辦?
巧了,這個問題內核開發者也苦思冥想過,最終提供了一套名爲 FUSE 的框架,支持用戶在用戶態製作文件系統。
希望看完這篇文章,大家對以下問題有所瞭解:
-
FUSE 是什麼?
-
FUSE 能做什麼?
-
FUSE 的實現有哪些?
本篇文章旨在幫大家在心裏建立一個 FUSE 框架的模型,並且熟悉用戶態文件系統整條 IO 路徑。
FUSE 是什麼?
Linux 內核官方文檔對 FUSE 的解釋如下:
What is FUSE? FUSE is a userspace filesystem framework. It consists of a kernel module (fuse.ko), a userspace library (libfuse.*) and a mount utility (fusermount).
劃重點:FUSE 是一個用來實現用戶態文件系統的框架,這套 FUSE 框架包含 3 個組件:
-
內核模塊
fuse.ko
:用來接收 vfs 傳遞下來的 IO 請求,並且把這個 IO 封裝之後通過管道發送到用戶態; -
用戶態 lib 庫
libfuse
:解析內核態轉發出來的協議包,拆解成常規的 IO 請求; -
mount 工具
fusermount
;
這就是 FUSE 框架的 3 大內容了,下面我們解釋下。這 3 個組件只爲了完成一件事:讓 IO 在內核態和用戶態之間自由穿梭。一般我們認爲 FUSE 是 Filesystem in Userspace 的縮寫,也就是常說的用戶態文件系統。
FUSE 原理
接下來我們看下 IO 的路徑,來理解下 FUSE 的原理。首先看一眼 wiki 上有對 FUSE 的 ls -l /tmp/fuse
命令的演示圖:
該圖表達的意思有以下幾個:
-
背景:一個用戶態文件系統,掛載點爲
/tmp/fuse
,用戶二進制程序文件爲./hello
; -
當執行
ls -l /tmp/fuse
命令的時候,流程如下: -
IO 請求先進內核,經 vfs 傳遞給內核 FUSE 文件系統模塊;
-
內核 FUSE 模塊把請求發給到用戶態,由
./hello
程序接收並且處理。處理完成之後,響應原路返回;
簡化的 IO 動畫示意圖:
通過這兩張圖,對 FUSE IO 的流程應該就清晰了,內核 FUSE 模塊在內核態中間做協議包裝和協議解析的工作,承接 vfs 下來的請求並按照 FUSE 協議轉發到用戶態,然後接收用戶態的響應,回覆給用戶。
FUSE 在這條 IO 路徑是是指做了一個透明中轉站的作用,用戶完全不感知這套框架。我們把中間的 FUSE 當作一個黑盒遮住,就更容易理解了。
思考問題:內核的 fuse.ko 模塊,還有 libfuse 庫。這兩個角色是的作用?
**劃重點:這兩個模塊一個位於內核,一個位於用戶態,是配套使用的,**最核心的功能是協議封裝和解析,當然還有運輸。
舉個例子,內核 fuse.ko 用於承接 vfs 下來的 io 請求,然後封裝成 FUSE 數據包,轉發給用戶態。這個時候,用戶態文件系統收到這個 FUSE 數據包,它如果想要看懂這個數據包,就必須實現一套 FUSE 協議的代碼,這套代碼是公開透明的,屬於 FUSE 框架的公共的代碼,這種代碼不能夠讓所有的用戶文件系統都重複實現一遍,於是 libfuse 用戶庫就誕生了。
回到開篇的問題,FUSE 能做什麼?
看到這裏你應該就清晰了,FUSE 能夠轉運 vfs 下來的 io 請求到用戶態,用戶程序處理之後,經由 FUSE 框架回應給用戶。從而就可以把文件系統的實現全部放到用戶態實現了。
1 FUSE 協議格式
我們分析一眼 FUSE 數據轉運的數據格式( fuse 協議的格式 ),請求包和響應包是什麼樣子的呢?好奇不?
FUSE 請求包
FUSE 請求包分爲兩部分:
-
Header
:這個是所有請求共用的,比如open
請求,read
請求,write
請求,getxattr
請求,頭部都至少有這個結構體,Header
結構體能描述整個 FUSE 請求,其中字段能區分請求類型; -
Payload
:這個東西是每個 IO 類型會是不同的,比如read
請求就沒這個,write
請求就有這個,因爲write
請求是攜帶數據的;
type inHeader struct {
Len uint32
Opcode uint32
Unique uint64
Nodeid uint64
Uid uint32
Gid uint32
Pid uint32
_ uint32
}
-
Len: 是整個請求的字節數長度(
Header
+Payload
) -
Opcode: 請求的類型,比如區分 open、read、write 等等;
-
Unique: 請求唯一標識(和響應中要對應)
-
Nodeid: 請求針對的文件 nodeid,目標文件或者文件夾的 nodeid;
-
Uid: 文件 / 文件夾操作的進程的用戶 ID
-
Gid: 文件 / 文件夾操作的進程的用戶組 ID
-
Pid: 文件 / 文件夾操作的進程的進程 ID
FUSE 響應包
FUSE 響應包也分爲兩部分:
-
Header
:這個結構體也是在數據頭部的,所有 IO 類型的響應都至少有這個結構體。該結構體用於描述整個響應請求; -
Payload
:每個請求的類型可能不同,比如read
請求就會有這個,因爲要攜帶read
出來的用戶數據,write
請求就不會有;
type outHeader struct {
Len uint32
Error int32
Unique uint64
}
-
Len: 整個響應的字節數長度(
Header
+Payload
); -
Error: 響應錯誤碼,成功返回 0,其他對應着系統的錯誤代碼,負數;
-
Unique: 對應者請求的唯一標識,和請求對應;
2 內核態、用戶態的紐帶
現在對數據協議的格式,轉發和轉運的模塊我們也知道了。現在還差一個關鍵的點:數據包的通道,也就是高速公路。
換句話說,內核模塊的 “包裹” 發到哪裏?用戶程序又從哪裏讀取拿到這個“包裹”。
答案是:****/dev/fuse
,這個虛設備文件就是內核模塊和用戶程序的橋樑。
一切都順理成章了,內核在這個過程中相當於一個信使,用戶的 io 通過正常的系統調用進來,走到內核文件系統 fuse ,fuse 文件系統把這個 io 請求封裝起來,打包成特定的格式,通過 /dev/fuse
這個管道傳遞到用戶態。在此之前有守護進程監聽這個管道,看到有消息出來之後,立馬讀出來,然後利用 libfuse
庫解析協議,之後就是用戶文件系統的代碼邏輯了。
示意圖如下(省略了拆解包的步驟):
3 FUSE 的使用
現在我們知道了 FUSE 框架的 3 大組件,FUSE 的數據包協議,現在就嘗試着使用一下 FUSE 文件系統。
提示,以下命令在 ubuntu 16 版本上執行的。
第一步:怎麼判斷 Linux 內核是否支持 fuse?
前面說過內核裏面也有一個 fuse.ko 模塊,這個模塊是公用的,內核的位置也是位於文件系統層。我們想要自制一個文件系統,那麼第一步需要確保內核支持這個模塊。可以直接運行如下命令,如果沒有報錯,說明你的 Linux 機器支持 fuse 模塊,並且已經加載。
root@ubuntu:~# modprobe fuse
如果當前 Linux 不支持這個內核模塊,那麼就會報錯,如下:
root@ubuntu:~# modprobe xyz
modprobe: FATAL: Module xyz not found in directory /lib/modules/4.4.0-142-generic
或者也可以去目錄 /lib/modules/4.4.0-142-generic/kernel/fs/
裏看是否有 fuse 這個目錄。這些前置基礎知識,在上一篇文章 自制文件系統 — 01 文件系統的樣子 有鋪墊哈。
第二步:掛載 fuse 內核文件系統,便於管理
fuse 這個內核文件系統其實是可以掛載,也可以不掛載,掛載了主要是方便管理多個用戶系統而已,fuse 內核文件系統的 Type 名稱爲 fusectl
,掛載命令:
mount -t fusectl none /sys/fs/fuse/connections
可以用 df -aT
命令查看:
root@ubuntu:~# df -aT|grep -i fusectl
fusectl fusectl 0 0 0 - /sys/fs/fuse/connections
通過掛載內核 fuse 文件系統,可以看到所有實現的用戶文件系統,如下:
root@ubuntu:~# ls -l /sys/fs/fuse/connections/
total 0
dr-x------ 2 root root 0 May 29 19:58 39
dr-x------ 2 root root 0 May 29 20:00 42
在 /sys/fs/fuse/connections
對應兩個目錄,目錄名爲 Unique ID
,能夠唯一標識一個用戶文件系統。這裏表示內核 fuse 模塊通過 /dev/fuse
設備文件,建立了兩個通信管道,分別對應了兩個用戶文件系統,可以用 df -aT
對照確認:
root@ubuntu:~# df -aT|grep -i fuse
fusectl fusectl 0 0 0 - /sys/fs/fuse/connections
lxcfs fuse.lxcfs 0 0 0 - /var/lib/lxcfs
helloworld fuse.hellofs 0 0 0 - /mnt/myfs
每個 Uniqe ID 名錄下,有若干個文件,通過這些文件,我們可以獲取到當前用戶文件系統的狀態,或跟 fuse 文件系統交互,比如:
root@ubuntu:~# ls -l /sys/fs/fuse/connections/42/
total 0
--w------- 1 root root 0 May 29 20:00 abort
-rw------- 1 root root 0 May 29 20:00 congestion_threshold
-rw------- 1 root root 0 May 29 20:00 max_background
-r-------- 1 root root 0 May 29 20:00 waiting
-
waiting 文件:cat 一下就能獲取到當前正在處理的 IO 請求數;
-
abort 文件:該文件寫入任何字符串,都會終止這個用戶文件系統和上面所有的請求;
第三步:用戶文件系統怎麼掛載?
現在只剩最後一個問題:用戶文件系統怎麼掛載(比如上面的 hellofs
和 lxcfs
)?
這就用到了 FUSE 框架的第 3 個組件了,fusermount
工具,這個工具就是專門用來方便掛載用戶文件系統才誕生的。
fusermount -o fsname=helloworld,subtype=hellofs -- /mnt/myfs/
FUSE 的作用在於使用戶能夠繞開內核代碼來編寫文件系統,但是請注意,文件系統要實現對具體的設備的操作的話必須要使用設備驅動提供的接口,而設備驅動位於內核空間,這時可以直接讀寫塊設備文件,就相當於只把文件系統摘到用戶態,用戶直接管理塊設備空間。
FUSE 有哪些?
實現了 FUSE 的用戶態文件系統有非常多的例子,比如,GlusterFS,SSHFS,CephFS,Lustre,GmailFS,EncFS,S3FS、、、等等
上面這些都是實現了 fuse 的用戶態程序:
-
GmailFS 可以讓我們管理文件一樣,管理郵件;
-
S3FS 可以讓我們管理文件一樣,管理對象;
總結
通過這篇文章,我們瞭解了 FUSE 的知識點,總結如下:
-
FUSE 框架就是內核開發者爲了日益多樣的用戶需求開發出來的,使得用戶態程序參與到 IO 路徑的處理成爲可能;
-
FUSE 框架的 3 大組件分別是:內核 fuse 模塊,用戶態
libfuse
庫,fusermount
掛載工具; -
內核 fuse 模塊用於承接 vfs 的請求,並且通過
/dev/fuse
建立的管道,把封裝後的請求發往用戶態; -
libfuse
則是用戶態封裝用來解析 FUSE 數據包協議的庫代碼,服務於所有的用戶態文件系統; -
fusermount
則是用戶態文件系統用來掛載的工具而已; -
/dev/fuse
就是連接內核 fuse 和用戶態文件系統的紐帶; -
上面動圖演示了 FUSE 文件系統的完整 IO 路徑,你學 fei 了嗎?
後記
你已經準備好自制文件系統的全部前置條件了,包括怎麼查看,學習文件系統,掛載和卸載,也瞭解了 FUSE 框架,接下來就只需要研究怎麼實現一個極簡的文件系統的內容了,敬請期待。
堅持思考,方向比努力更重要。關注我:奇伢雲存儲
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/HvbMxNiVudjNPRgYC8nXyg