天天看點

FUSE檔案系統介紹

文章目錄

  • ​​導引:Fuse簡介​​
  • ​​一、 Fuse架構設計​​
  • ​​二、 Fuse實作細節​​
  • 1. fuse使用者空間流程
  • 2. fuse核心隊列
  • 3. /dev/fuse 讀寫調用流程
  • ​​三、 總結​​

導引:Fuse簡介

Fuse(filesystem in userspace),是一個使用者空間的檔案系統。

通過fuse核心子產品的支援,開發者隻需要根據fuse提供的接口實作具體的檔案操作就可以實作一個檔案系統。由于其主要實作代碼位于使用者空間中,而不需要重新編譯核心,這給開發者帶來了衆多便利。Google在Android 11上,為了實作scoped storage,也引入了fuse。

下面我們從Fuse的架構設計以及具體的實作細節來談一談fuse檔案系統。

一、 Fuse架構設計

FUSE檔案系統介紹

圖檔摘自《To FUSE or Not to FUSE: Performance of User-Space File Systems》

Fuse包含一個核心子產品和一個使用者空間守護程序(下文稱fuse daemon)。核心子產品加載時被注冊成 Linux 虛拟檔案系統的一個 fuse 檔案系統驅動。此外,還注冊了一個/dev/fuse的塊裝置。該塊裝置作為fuse daemon與核心通信的橋梁,fuse daemon通過/dev/fuse讀取fuse request,處理後将reply寫入/dev/fuse。

上圖詳細展示了fuse的構架。當application挂在fuse檔案系統上,并且執行一些系統調用時,VFS會将這些操作路由至fuse driver,fuse driver建立了一個fuse request結構體,并把request儲存在請求隊列中。此時,執行操作的程序會被阻塞,同時fuse daemon通過讀取/dev/fuse将request從核心隊列中取出,并且送出操作到底層檔案系統中(例如 EXT4 或 F2FS)。當處理完請求後,fuse daemon會将reply寫回/dev/fuse,fuse driver此時把requset标記為completed,最終喚醒使用者程序。

二、 Fuse實作細節

下面我們基于Android 11 AOSP 以及 kernel4.19的開源代碼,讨論一些fuse的實作細節,包括:fuse 使用者空間流程、核心隊列、/dev/fuse的讀寫流程等。

1. fuse使用者空間流程

(1) fuse mount

FUSE檔案系統介紹

Fuse的挂載通過mount函數,将指定的fuse_path挂載到/dev/fuse裝置上。之後對于fuse_path下的檔案操作,都會通過fuse檔案系統,并通過/dev/fuse被fuse daemon讀取處理。

(2) fuse thread

FUSE檔案系統介紹

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中實作了一個多線程模式來讀取請求,相比單線程,在請求處理上效率更高。

(3) libfuse

由fuse_session_loop_mt在libfuse中的調用流程如下:

FUSE檔案系統介紹

這裡我們關注兩點:

  1. splice實作記憶體零拷貝。在預設情況下,fuse daemon必須通過read()從/dev/fuse讀取請求,通過write()将請求回複寫入/dev/fuse。每次讀寫系統調用都需要進行一次核心-使用者空間的記憶體拷貝。這樣對讀寫的性能損耗十分嚴重,因為一次記憶體拷貝需要處理大量資料。為了緩解這個問題,fuse支援了Linux核心提供的 splice 功能。splice 允許使用者空間在兩個核心記憶體緩沖區之間傳輸資料,而無需将資料複制給使用者空間。如果fuse daemon實作了write_buf()方法,則 FUSE 從/dev/fuse讀取資料,并以包含檔案描述符的緩沖區的形式将資料直接傳遞給此方法處理,進而省去了一次記憶體申請與拷貝。
  2. 多線程模式。在多線程模式下,fuse daemon以一個線程開始,如果核心隊列中有兩個以上的request,則會自動生成其他線程。預設最大支援10個線程同時處理請求。

2. fuse核心隊列

FUSE檔案系統介紹

圖檔摘自《To FUSE or Not to FUSE: Performance of User-Space File Systems》

fuse在核心中維護了五個隊列,分别為:Backgroud、Pending、Processing、Interrupts、Forgets。一個請求在任何時候隻會存在于一個隊列中。

a) Backgroud:background 隊列用于暫存異步請求。在預設情況下,隻有讀請求進入 background 隊列;當writeback cache啟用時,寫請求也會進入 background 隊列。當開啟writeback cache時,來自使用者程序的寫請求會先在頁緩存中累積,然後當bdflush 線程被喚醒時會下刷髒頁。在下刷髒頁時,FUSE會構造異步請求,并将它們放入 background 隊列中。

b) Pending:同步請求(例如,中繼資料)放在 pending 隊列中,并且pending隊列會周期性接收來自background 的請求。但是pending隊列中異步請求的個數最大為max_background(最大為12),當pending隊列的異步請求未達到12時,background隊列的請求将被移動到pending隊列中。這樣做的目的是為了控制pending隊列中異步請求的個數,防止在突發大量異步請求的情況下,阻塞了同步請求。

c) Processing:當pending隊列中的請求被轉發到fuse daemon的同時,也被移動到processing隊列。是以processing隊列中的請求,表示正在被處理fuse daemon處理的請求。當fuse daemon真正處理完請求,通過/dev/fuse下發reply時,該請求将從processing隊列中删除。

d) Interrupts:用于存放中斷請求,比如當發送的請求被使用者取消時,核心會發送一個Interrupts請求,來取消已被發送的請求。中斷請求的優先級最高,Interrupts中的請求會最先得到處理。

e) Forgets:forget請求用于删除dcache中緩存的inode。

3. /dev/fuse 讀寫調用流程

Fuse driver加載過程中注冊了對/dev/fuse的操作接口fuse_dev_operations。fuse_dev_do_read/fuse_dev_do_write分别對應fuse daemon從核心讀取請求,以及處理完請求後寫回reply的函數調用。我們分别看下具體的代碼片段

FUSE檔案系統介紹

當pending 、interrups、forgets隊列都沒有請求時,讀程序進入休眠。一旦有請求到達,這個等待隊列上的程序将被喚醒。Interrups 和 forgets的請求優先級高于pending隊列。當請求的資料内容被拷貝至使用者空間後,該請求會被移至processing隊列,并且req->flags會儲存目前請求的狀态。

FUSE檔案系統介紹

當fuse daemon處理完請求後,會将結果寫回到/dev/fuse。寫資料儲存在struct fuse_copy_state中,并且會根據unique id在fc(fuse_conn)中找到對應的req,并将寫回的參數從fuse_copy_state拷貝至req->out。

最後我們以unlink為例,看下fuse整體是如何工作的:

FUSE檔案系統介紹

圖檔摘自fuse核心官方文檔

首先,fuse daemon會阻塞在讀/dev/fuse,當app程序在fuse挂載點下面有新的檔案操作(unlink),這時系統調用會調用fuse核心接口,并生成request,同時喚醒阻塞的fuse daemon。fuse daemon讀到request後,在libfuse中進行解析,根據request的opcode來執行對應的ops,完成後會把處理結果傳回給/dev/fuse。此時vfs調用阻塞的行為将被喚醒,最後傳回vfs調用。

三、 總結

雖然Fuse簡化了檔案系統的實作,給開發者帶來了便利。但是其額外的核心态/使用者态切換帶來的性能開銷不能被忽視,是以fuse性能問題,一直是業界繞不開的話題。前面說到的splice、多線程、writeback cache都是為了改善其性能問題。後續,我們再具體談談fuse性能改善。