自制文件系統 —— 03 Go 實戰:hello world 的文件系統

堅持思考,就會很酷

前情提要

終於到了動手的環節,今天我們直接搞起一個叫做 hello world 的文件系統,附上全部代碼實現,且可以體驗測試。

環境準備

環境準備:

  1. go 編程環境(準備個 go 1.13 以上版本的環境即可)

  2. 隨便搞一臺 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 做了哪些事情?

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 實現了以下功能:

  1. 掛載點根目錄下面只有一個叫做 hello 的文件(注意:不需要用戶創建哦,直接掛載之後就有了);

  2. cat 這個 hello 將會返回 hello, world  的內容;

  3. 掛載點目錄的屬性:inode 爲 20210601,mode 爲 555;

  4. 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
}

簡單說下上面做了什麼事情:

  1. 定義了根目錄 readdirgetattr 的行爲回調;

  2. 定義了 hello 文件的 readallgetattr 的行爲回調;

 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

參數說明:

測試跑起來之後,如果沒有任何異常,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=Gid=Pid=891] 0x0 fl=0
2021/06/06 13:49:04 FUSE: -> [ID=0x2] Getattr valid=1m0s ino=20210601 size=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 想要你看到的而已!!!

總結

  1. FUSE 框架三大組件:內核 fuse 模塊,用戶態 FUSE 協議解析庫,fusermount 工具;

  2. FUSE 協議解析本身跟具體語言無關,c 可以實現,Go 可以實現,甚至 Python 都可以實現;

  3. libfuse 是純 c 實現的 FUSE 協議解析庫,如果你想用 c 語言實現一個用戶文件系統,那麼選它就對了;

  4. bazil.org/fuse 是純 Go 實現的 FUSE 協議庫,我們用 Go 語言實現用戶文件系統,那麼選它就對了;

  5. 實現一個用戶文件系統有多簡單?只需要定義 FS,Dir,File 這三大結構的處理邏輯,以上我們實現了一個名叫 hello,world 的文件系統;

後記

這次實現了一個完整的 helloworld 用戶態文件系統,最後留個思考題?實現 hellofs 之後,你理解 “文件” 是什麼呢?

有了這一次的基礎,下一次我們實現一個更復雜的文件系統:加密的分佈式文件系統,敬請期待。

堅持思考,方向比努力更重要。關注我:奇伢雲存儲

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Yf6yBoEQe6ijMlPgZ6P2sA