自制文件系統 —— 03 Go 實戰:hello world 的文件系統
堅持思考,就會很酷
前情提要
終於到了動手的環節,今天我們直接搞起一個叫做 hello world 的文件系統,附上全部代碼實現,且可以體驗測試。
環境準備
環境準備:
-
go 編程環境(準備個 go 1.13 以上版本的環境即可)
-
隨便搞一臺 Linux 虛擬機(支持 fuse)
確認 Linux 內核支持 FUSE
root@ubuntu:~# modprobe fuse
命令沒有報錯的話,就說明內核支持 fuse ,並且已經加載。
開始自制文件系統
1 第一步:解析 FUSE 協議
在 02 FUSE 框架篇我們介紹了 FUSE 協議,說到了 FUSE 框架的 3 組件:內核 fuse 文件系統,用戶態 libfuse 庫,fusermount 工具。
內核的 fuse 文件系統只有一份,用於承接 vfs 請求,封裝成 FUSE 協議包,走 /dev/fuse
建立起來的通道轉發用戶態。用戶態的任務就是把 FUSE 協議包解析出來並且處理,然後把請求響應按照 FUSE 協議封裝起來,走 /dev/fuse
通道傳回內核,由 vfs 傳回用戶。
所以,我們看到用戶態 libfuse 庫這個東西其實就只是 FUSE 協議解析和封裝用的。童鞋們,注意啦,重點來了。劃重點:只要是數據協議
,就有一個特點:和具體語言無關
。數據協議格式只不過是對字節流的分析方式而已
FUSE 的協議也是如此,libfuse 這個是用 c 語言實現的 FUSE 協議庫,官方的 Github 地址是:https://github.com/libfuse/libfuse/ 。Go 語言不能直接用 libfuse 庫,因爲 libfuse 庫全都是封裝成了 c 的結構體。
這是我們要邁過的第一道關,就是用 Go 語言來解析 FUSE 協議。好吧,準備開始啦。首先看一下 FUSE 的數據包格式:
先思考一個問題:libfuse 做了哪些事情?
-
然後,要和建立
/dev/fuse
通道; -
然後,要實現一個 Server 服務端,監聽這個通道,這樣就和內核 fuse 建立了聯繫,接收和發送消息;
-
然後,對不同的請求做不同的解析;
-
比如 read 的 Opcode 是,write 的 Opcode 是 ;
-
write 請求攜帶用戶數據,其他請求不攜帶;
-
然後,把解析好的 FUSE IO 請求轉發給用戶態文件系統;
-
然後,接收用戶態文件系統的 IO 響應,封裝成 FUSE 響應格式;
-
然後,不同的請求有不同的響應,做不同的處理,算了吧,太麻煩了,
-
read 請求的響應攜帶用戶數據,其他請求則不同;
Go 的 FUSE 協議庫也要做以上這些東西。其實,任何一種協議數據格式的解析從來都是索然無味的,因爲代碼實現的邏輯功能是確定的。這裏推薦一個 Go 的 FUSE 庫:bazil/fuse,這是一個純 Go 寫的 FUSE 協議解析庫,作用和 libfuse 這個純 c 語言寫的庫作用完全一樣。
bazil.org/fuse
is a Go library for writing FUSE userspace filesystems.
有了這個 Go FUSE 協議解析庫,就可以開始寫文件系統的程序了。我們自己能參與創造的部分纔是真正感興趣的。
2 第二步:Go 自制文件系統
我們下面寫了一個 helloworld
的文件系統,首先說結論,hellofs
實現了以下功能:
-
掛載點根目錄下面只有一個叫做
hello
的文件(注意:不需要用戶創建哦,直接掛載之後就有了); -
cat
這個hello
將會返回hello, world
的內容; -
掛載點目錄的屬性:inode 爲 20210601,mode 爲 555;
-
hello
文件的屬性:inode 爲 20210606,mode 爲 444;
跟我一起創建出一個叫做 helloword.go 的文件,寫入下面的代碼:
// 實現一個叫做 hellfs 的文件系統
package main
import (
"context"
"flag"
"log"
"os"
"syscall"
"bazil.org/fuse"
"bazil.org/fuse/fs"
_ "bazil.org/fuse/fs/fstestutil"
)
func main() {
var mountpoint string
flag.StringVar(&mountpoint, "mountpoint", "", "mount point(dir)?")
flag.Parse()
if mountpoint == "" {
log.Fatal("please input invalid mount point\n")
}
// 建立一個負責解析和封裝 FUSE 請求監聽通道對象;
c, err := fuse.Mount(mountpoint, fuse.FSName("helloworld"), fuse.Subtype("hellofs"))
if err != nil {
log.Fatal(err)
}
defer c.Close()
// 把 FS 結構體註冊到 server,以便可以回調處理請求
err = fs.Serve(c, FS{})
if err != nil {
log.Fatal(err)
}
}
// hellofs 文件系統的主體
type FS struct{}
func (FS) Root() (fs.Node, error) {
return Dir{}, nil
}
// hellofs 文件系統中,Dir 是目錄操作的主體
type Dir struct{}
func (Dir) Attr(ctx context.Context, a *fuse.Attr) error {
a.Inode = 20210601
a.Mode = os.ModeDir | 0555
return nil
}
// 當 ls 目錄的時候,觸發的是 ReadDirAll 調用,這裏返回指定內容,表明只有一個 hello 的文件;
func (Dir) Lookup(ctx context.Context, name string) (fs.Node, error) {
// 只處理一個叫做 hello 的 entry 文件,其他的統統返回 not exist
if name == "hello" {
return File{}, nil
}
return nil, syscall.ENOENT
}
// 定義 Readdir 的行爲,固定返回了一個 inode:2 name 叫做 hello 的文件。對應用戶的行爲一般是 ls 這個目錄。
func (Dir) ReadDirAll(ctx context.Context) ([]fuse.Dirent, error) {
var dirDirs = []fuse.Dirent{{Inode: 2, Name: "hello", Type: fuse.DT_File}}
return dirDirs, nil
}
// hellofs 文件系統中,File 結構體實現了文件系統中關於文件的調用實現
type File struct{}
const fileContent = "hello, world\n"
// 當 stat 這個文件的時候,返回 inode 爲 2,mode 爲 444
func (File) Attr(ctx context.Context, a *fuse.Attr) error {
a.Inode = 20210606
a.Mode = 0444
a.Size = uint64(len(fileContent))
return nil
}
// 當 cat 這個文件的時候,文件內容返回 hello,world
func (File) ReadAll(ctx context.Context) ([]byte, error) {
return []byte(fileContent), nil
}
簡單說下上面做了什麼事情:
-
定義了根目錄
readdir
和getattr
的行爲回調; -
定義了 hello 文件的
readall
和getattr
的行爲回調;
3 第三步:讓文件系統 Go 起來
好,激動人心的時候到了,我們先編譯出這個程序,然後跑起來就是可用一個極簡的文件系統了。麻雀雖小,五臟俱全。
編譯 helloworld.go
root@ubuntu:~/gopher/src# go build -gcflags "-N -l" ./helloworld.go
成功編譯,獲得二進制文件 helloworld
。
創建一個空目錄
創建一個空目錄當做掛載點,筆者是在 /mnt
目錄下創建了一個叫做 myfs
的目錄。
root@ubuntu:~# mkdir /mnt/myfs/
掛載運行
好,現在我們用戶文件系統程序準備好了,掛載點也準備好了,萬事俱備了,可以運行了。命令如下:
root@ubuntu:~/gopher/src# ./helloworld --mountpoint=/mnt/myfs --fuse.debug=true
參數說明:
-
mountpoint
:指定掛載點目錄,也就是上面創建的空目錄/mnt/myfs/
; -
fuse.debug
:爲了更好的理解用戶文件系統,可以把這個開關設置成true
,這樣用戶發送的請求對應了後端什麼邏輯就一目瞭然了;
測試跑起來之後,如果沒有任何異常,helloworld
就是作爲一個守護進程,卡主執行,沒有任何日誌。直到收到請求。
這個時候,我們這個終端窗口就不要動了(待會可以看日誌),再新開一個終端用來測試。
4 第四步:極簡文件系統 hellofs 的測試
系統角度探測
現在我們從多個角度測試下 hellofs ,感受下自己做的第一個用戶文件系統是什麼樣子的。
首先,文件系統一定要掛載才能用,所以 df
命令可以看到掛載情況:
root@ubuntu:~# df -aTh|grep hello
helloworld fuse.hellofs 0.0K 0.0K 0.0K - /mnt/myfs
看到了不?有一個叫做 helloworld
,類型爲 fuse.hellofs
的文件系統。這兩個名字都是代碼裏指定的。
然後,如果掛載了 fusectl 文件系統(內核 fuse 文件系統),那麼還可以在 /sys/fs/fuse/connections
看到比以前多一個數字命名的目錄。
文件操作探測
我們通過 ls,stat,cat 等命令對 hellofs 文件系統探測一下。
第一個問題:stat
查看一下掛載點 stat /mnt/myfs
?能得到什麼數據?
root@ubuntu:~# stat /mnt/myfs/
File: '/mnt/myfs/'
Size: 0 Blocks: 0 IO Block: 4096 directory
Device: 29h/41d Inode: 20210601 Links: 1
Access: (0555/dr-xr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2021-06-06 13:49:02.463926775 +0800
Modify: 2021-06-06 13:49:02.463926775 +0800
Change: 2021-06-06 13:49:02.463926775 +0800
Birth: -
我們看到特殊的 inode:20210601,權限:555 (回憶下上面的代碼實現,目錄的 inode 爲 20210601 就是我們指定的)。如下:
注意,在 stat /mnt/myfs
的同時,用戶文件系統會打印出日誌:
root@ubuntu:~/code/gopher/src/myfs# ./helloworld --mountpoint=/mnt/myfs --fuse.debug=true
2021/06/06 13:49:04 FUSE: <- Getattr [ID=0x2 Node=0x1 Uid=0 Gid=0 Pid=891] 0x0 fl=0
2021/06/06 13:49:04 FUSE: -> [ID=0x2] Getattr valid=1m0s ino=20210601 size=0 mode=dr-xr-xr-x
這個日誌明確的告訴了我們,先收到了一個 Getattr
的請求,請求參數是什麼,然後 hellofs
處理完成之後,返回了什麼樣的響應。
第二個問題:ls /mnt/myfs
的反應呢?
root@ubuntu:~# ls -l /mnt/myfs/
total 0
-r--r--r-- 1 root root 13 Jun 6 13:49 hello
我們看到了一個 hello 的文件(體會一下,我們沒有創建過這個文件哦)。
那麼,stat /mnt/myfs/hello
會得到什麼?
root@ubuntu:~# stat /mnt/myfs/hello
File: '/mnt/myfs/hello'
Size: 13 Blocks: 0 IO Block: 4096 regular file
Device: 29h/41d Inode: 20210606 Links: 1
Access: (0444/-r--r--r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2021-06-06 13:49:02.463926775 +0800
Modify: 2021-06-06 13:49:02.463926775 +0800
Change: 2021-06-06 13:49:02.463926775 +0800
Birth: -
我們看到了特殊的 inode 20210606。
第三個問題:cat /mnt/myfs/hello
這個文件?
root@ubuntu:~# cat /mnt/myfs/hello
hello, world
我們看到 hello,world 的返回,雖然你從來沒寫過這個文件。
第四個問題:請大家體會一下 hello
這個文件,這個文件和你平時見的文件有什麼區別呢?
這個是一個額外的問題,也是要讀者朋友重點思考的一個問題。思考 hello 這個文件的特殊性:
-
這個文件明明你沒有創建過,卻出現了?
-
這個文件是存在磁盤的嗎?
-
這個文件能寫嗎?
以上的問題你想通了嗎?可以先思考下,或者找我交流。
請記住,文件這個概念從來都是一個邏輯的對象。是文件系統給你的一個抽象的對象。換句話說,文件表現的任何信息都只是文件系統想要展現給你的而已。
你看到的只是 FS 想要你看到的而已!!!
總結
-
FUSE 框架三大組件:內核 fuse 模塊,用戶態 FUSE 協議解析庫,fusermount 工具;
-
FUSE 協議解析本身跟具體語言無關,c 可以實現,Go 可以實現,甚至 Python 都可以實現;
-
libfuse
是純 c 實現的 FUSE 協議解析庫,如果你想用 c 語言實現一個用戶文件系統,那麼選它就對了; -
bazil.org/fuse
是純 Go 實現的 FUSE 協議庫,我們用 Go 語言實現用戶文件系統,那麼選它就對了; -
實現一個用戶文件系統有多簡單?只需要定義 FS,Dir,File 這三大結構的處理邏輯,以上我們實現了一個名叫
hello,world
的文件系統;
後記
這次實現了一個完整的 helloworld 用戶態文件系統,最後留個思考題?實現 hellofs 之後,你理解 “文件” 是什麼呢?
有了這一次的基礎,下一次我們實現一個更復雜的文件系統:加密的分佈式文件系統,敬請期待。
堅持思考,方向比努力更重要。關注我:奇伢雲存儲
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Yf6yBoEQe6ijMlPgZ6P2sA