用戶態文件系統(FUSE)框架分析和實戰
用戶態文件系統介紹
用戶態文件系統(filesystem in userspace, 簡稱FUSE),它能使用戶在無需編輯和編譯內核代碼的情況下,創建用戶自定義的文件系統。文件系統是操作系統的重要組成部分,一般在內核層面實現對於文件系統的支持,而通常內核態的代碼難以調試,生產率較低。在用戶態空間實現文件系統能夠極大幅度的提高生產效率,簡化為實現新的文件系統的工作量。FUSE主要包含兩個部分,內核FUSE模塊(Linux從2.6.14版本開始支持)和用戶態Libfuse庫。
目前FUSE支持的平台:
Linux 完全支持
BSD 部分支持
OX-X參考OSXFUSE
比較知名的用戶態文件系統:
ExpanDrive:商業文件系統,實現了SFTP/FTP/FTPS協議;
GlusterFS:用於集群的分散式文件系統,可以擴展到PB級;
SSHFS:通過SSH協議訪問遠程文件系統;
GmailFS:通過文件系統方式訪問GMail;
EncFS:加密的虛擬文件系統
NTFS-3G和Captive NTFS,在非Windows中對NTFS文件系統提供支持;
WikipediaFS:支持通過文件系統介面訪問Wikipedia上的文章;
昇陽公司的Lustre:和GlusterFS類似但更早的一個集群文件系統
ZFS:Lustre的Linux版;
archivemount:
HDFS: Hadoop提供的分散式文件系統。HDFS可以通過一系列命令訪問,並不一定經過Linux FUSE;
在嵌入式開發平台上,我們利用FUSE實現unionfs,quota fs, RIP和temp sensor的文件系統。
FUSE官網:
https://github.com/libfuse/libfuse
FUSE實現機制分析
在這個章節,我們首先對於虛擬文件系統做一個簡單介紹,Linux下的文件系統都依賴於虛擬文件系統,要了解FUSE,首先要對虛擬文件系統有一個了解。然後我們對於FUSE做一個宏觀框架的分析,先大致了解一下整個FUSE是如何工作的,最後兩個小節分別從用戶態和內核態具體分析FUSE的實現。
虛擬文件系統介紹(VFS)
Linux支持ext,ext2,xia,minix,umsdos,msdes,fat32 ,ntfs,proc,stub,ncp,hpfs,affs 以及 ufs 等多種文件系統。為了實現這一目的,Linux 對所有的文件系統採用統一的文件界面,用戶通過文件的操作界面來實現對不同文件系統的操作。對於用戶來說,我們不要去關心不同文件系統的具體操作過程,而只是對一個虛擬的文件操作界面來進行操作,這個操作界面就是 Linux 的虛擬文件系統(VFS ) 。形象地說,Linux 的內核好像一個 PC 機的主板,VFS 就是上面的一個插槽,具體的文件系統就是外設卡。因此,每一個文件系統之間互不干擾,而只是調用相應的程序來實現其功能。在 Linux 的內核文件中,VFS 和具體的文件系統程序都放在 LinuxFS 中,其中每一種文件系統對應一個子目錄,另外還有一些共用的 VFS 程序。在具體的實現上,每個文件系統都有自己的文件操作數據結構file-operations。所以,VFS 作為 Linux內核中的一個軟體層,用於給用戶空間的程序提供文件系統介面,同時也提供了內核中的一個抽象功能,允許不同的文件系統很好地共存。VFS 使 Linux 同時安裝、支持許多不同類型的文件系統成為可能。VFS 擁有關於各種特殊文件系統的公共界面,如超級塊、inode、文件操作函數入口等。實際文件系統的細節,統一由 VFS 的公共界面來索引,它們對系統核心和用戶進程來說是透明的。
圖2-1 VFS示意圖
FUSE內核模塊的實現跟傳統的文件系統實現既有相似點,也有差別的地方,FUSE內核模塊實現了FUSE文件系統,只不過與傳統的文件系統不同,FUSE需要把VFS層的請求傳到用戶態的fuseapp,在用戶態處理,然後再返回到內核態,把結果返回給VFS層。更多細節,且看下文。
FUSE宏觀框架
當用戶自定義一個新的用戶態文件系統被掛載之後,我們在訪問該文件系統的文件的方式與訪問其他文件系統的文件是一樣的,VFS保證了這一點。不同的是,FUSE文件系統下面的訪問行為是可以用戶自定義的。我們從一個簡單的例子出發,先宏觀上理解一下整個FUSE工作的流程。
以open為例,整個調用的過程如下:
1- 用戶態app調用glibc open介面,觸發sys_open系統調用。
2- sys_open 調用fuse中inode節點定義的open方法。
3- inode中open生成一個request消息,並通過/dev/fuse發送request消息到用戶態libfuse。
4- Libfuse調用fuse_application用戶自定義的open的方法,並將返回值通過/dev/fuse通知給內核。
5- 內核收到request消息的處理完成的喚醒,並將結果放回給VFS系統調用結果。
6- 用戶態app收到open的返回結果。
圖2-2 FUSE實現框架圖
Libfuse實現分析
對於Libfuse的分析,我們從一個簡單的例子開始(example/hello.c)。
static struct fuse_operations hello_oper = {
.getattr = hello_getattr,
.readdir = hello_readdir,
.open = hello_open,
.read = hello_read,
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hello_oper, NULL);
}
這個例子實現了一個最簡單的用戶態文件系統fuse.hello。
# hello /mnt/
# cd /mnt/
# ls -l
total 0
-r--r--r-- 1 root root 13 Jan 1 1970 hello
# cat hello
Hello World!
# touch a
touch: a: Function not implemented
# echo 0 > hello
-sh: can"t create hello: Permission denied
# mkdir x
mkdir: can"t create directory "x": Function not implemented
上面是測試的結果,可以看到:
可以讀取目錄
可以讀取文件屬性
可以讀文件,不可以寫文件
不可以創建目錄
不可以創建文件
我們再結合hello.c中定義的方法,不難看出它們之間的關聯。要使用FUSE實現自己的文件系統,我們需要定義一個fuse_operations類型的結構體變數,並將它傳遞給fuse_main,剩下的交給libfuse去處理,實現一個文件系統簡單了很多。
接下來我們看一下fuse_operations的定義:
struct fuse_operations {
int (*getattr) (const char *, struct stat *);
int (*readlink) (const char *, char *, size_t);
int (*getdir) (const char *, fuse_dirh_t, fuse_dirfil_t);
int (*mknod) (const char *, mode_t, dev_t);
int (*mkdir) (const char *, mode_t);
int (*unlink) (const char *);
int (*rmdir) (const char *);
int (*symlink) (const char *, const char *);
int (*rename) (const char *, const char *);
int (*link) (const char *, const char *);
int (*chmod) (const char *, mode_t);
int (*chown) (const char *, uid_t, gid_t);
int (*truncate) (const char *, off_t);
int (*utime) (const char *, struct utimbuf *);
int (*open) (const char *, struct fuse_file_info *);
int (*read) (const char *, char *, size_t, off_t,
struct fuse_file_info *);
int (*write) (const char *, const char *, size_t, off_t,
struct fuse_file_info *);
int (*statfs) (const char *, struct statvfs *);
int (*flush) (const char *, struct fuse_file_info *);
int (*release) (const char *, struct fuse_file_info *);
int (*fsync) (const char *, int, struct fuse_file_info *);
int (*setxattr) (const char *, const char *, const char *, size_t, int);
int (*getxattr) (const char *, const char *, char *, size_t);
int (*listxattr) (const char *, char *, size_t);
int (*removexattr) (const char *, const char *);
int (*opendir) (const char *, struct fuse_file_info *);
int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t,
struct fuse_file_info *);
int (*releasedir) (const char *, struct fuse_file_info *);
int (*fsyncdir) (const char *, int, struct fuse_file_info *);
void *(*init) (struct fuse_conn_info *conn);
void (*destroy) (void *);
int (*access) (const char *, int);
int (*create) (const char *, mode_t, struct fuse_file_info *);
int (*ftruncate) (const char *, off_t, struct fuse_file_info *);
int (*fgetattr) (const char *, struct stat *, struct fuse_file_info *);
int (*lock) (const char *, struct fuse_file_info *, int cmd,
struct flock *);
int (*utimens) (const char *, const struct timespec tv[2]);
int (*bmap) (const char *, size_t blocksize, uint64_t *idx);
unsigned int flag_nullpath_ok:1;
unsigned int flag_nopath:1;
unsigned int flag_utime_omit_ok:1;
unsigned int flag_reserved:29;
int (*ioctl) (const char *, int cmd, void *arg,
struct fuse_file_info *, unsigned int flags, void *data);
int (*poll) (const char *, struct fuse_file_info *,
struct fuse_pollhandle *ph, unsigned *reventsp);
int (*write_buf) (const char *, struct fuse_bufvec *buf, off_t off,
struct fuse_file_info *);
int (*read_buf) (const char *, struct fuse_bufvec **bufp,
size_t size, off_t off, struct fuse_file_info *);
int (*flock) (const char *, struct fuse_file_info *, int op);
int (*fallocate) (const char *, int, off_t, off_t,
struct fuse_file_info *);
};
在fuse_operations中所有的方法都是可選的,但是為了實現一個有價值的文件系統,有些方法是必須實現的(比如getattr)。
getattr() 類似於stat()
readlink() 讀取鏈接文件的真實文件路徑
getdir() 已經過時,使用readdir()替代
mknod() 創建一個文件節點
mkdir() 創建一個目錄
unlink() 刪除一個文件
rmdir() 刪除一個目錄
syslink() 創建一個軟鏈接
rename() 重命名文件
link() 創建一個硬鏈接
chmod() 修改文件許可權
chown() 修改文件的所有者和所屬組
truncate() 改變文件的大小
utime() 修改訪問和修改文件的時間,已經過時,使用utimens()替代
open() 打開文件
read() 讀取文件
write() 寫文件
statfs() 獲取文件系統狀態
flush() 刷緩存數據
release() 釋放打開的文件
fsync() 同步數據
setxattr() 擴展屬性介面, 下同
getxattr()
listxattr()
removexattr()
opendir() 打開一個目錄
readdir() 讀取目錄
releasedir() 釋放打開的目錄
fsyncdir() 同步目錄
init() 初始化文件系統
destroy() 清理文件系統
access() 檢查訪問許可權
create() 創建並打開文件
ftruncate() 修改文件的大小
fgetattr() 獲取文件屬性
lock()
utimens()
bmap()
ioctl()
poll()
write_buf()
read_buf()
flock()
fallocate()
在探究libfuse的實現之前,我們先給出libfuse的核心的數據結構框架圖。這幅圖可以我們在閱讀代碼的時候作為參考。
圖2-3 Libfuse數據結構圖
接下來我們來看一下libfuse是如何實現的。
Libfuse從fuse_main這個入口開始,從這裡我們註冊進去定義的文件操作方法來實現我們自己的文件系統。在fuse_main中,首先會完成參數解析,註冊用戶定義的operations, 實現文件系統的掛載(系統調用mount),填充fuse相關的數據結構,消息的處理。消息的處理部分是libfuse最核心的部分,實現用戶態與內核的互動(/dev/fuse),從內核接收req消息,解析,調用用戶自定義的ops,完成處理後,把結果通過/dev/fuse返回給內核,內核再返回給VFS層的系統調用,獲得結果。
圖2-4 libfuse實現流程圖
FUSE內核實現分析
對於內核部分又為兩個部分,一個部分是文件系統部分,另一個部分是字元設備部分。兩部分建立關聯是在文件系統掛載的時候。
我們首先從掛載部分看起,利用strace工具,截取mount系統調用相關的信息:
# strace hello /mnt/
…
open("/dev/fuse", O_RDWR|O_LARGEFILE) = 3
…
mount("hello", "/mnt", "fuse.hello", MS_NOSUID|MS_NODEV, "fd=3,rootmode=40000,user_id=0,gr"...) = 0
…
mount參數部分需要關注一下,fd=3,這個是關聯字元設備和文件系統關鍵紐帶。
上面是用戶態的系統調用,接下來我們再來看一下內核態中mount系統調用的處理過程。
sys_mount
|-> do_mount
|-> do_new_mount
|-> get_fs_type
|-> vfs_kern_mount
|-> mount_fs
|-> type->mount() [fuse_mount]
|-> mount_nodev
|-> fuse_fill_super
|-> file = fget(d.fd);
|-> fc->sb = sb
|-> sb->s_fs_info = fc
|-> fud = fuse_dev_alloc(fc)
|-> file->private_data = fud
在最後的fuse_fill_super部分,file就是通過mount傳進來的參數」fd=3」得到的,對應於打開的「/dev/fuse」。在掛載時候創建的superblock,fuse_conn, fuse_dev,file在這裡關聯起來了,具體的可以看一下圖2-5更清楚一些。
圖2-5 Linux FUSE模塊數據結構圖
接下來我們以刪除一個文件為例,看一下FUSE是如何工作的,圖2-6摘自libfuse官方文檔內核部分。
首先fuse_app會阻塞在讀/dev/fuse, 當掛載點下面有新的行為(刪除文件)觸發時,會通過系統調用調用fuse文件系統內核介面,並生成request消息,同時喚醒阻塞的fuse_app讀操作,fuse_app讀到request之後,到用戶態利用libfuse進行解析,根據request中的opcode找到對應的ops並執行,執行之後通過/dev/fuse把處理的結果傳回。VFS阻塞的行為會被喚醒,然後完成VFS的訪問。
圖2-6 用戶態和內核態交互過程示例
FUSE實踐過程記錄
在實踐章節,我們準備在QEMU環境中演示一下一個實用的用戶態文件系統,實現用戶態配額文件系統。這裡我們需要用到buildroot和QEMU,本文主要還是為了演示FUSE,對於buildroot和QEMU本身不做詳細介紹,只介紹一些用到的命令。
Buildroot是一個開源組件,廣泛用於嵌入式開發平台,集toolchain,rootfs,bootloader,kernel,open sourcepackage等於一身,方便開發者定製自己的linux系統。
QEMU是一個虛擬機,可以做到指令集的模擬,支持x86,ARM,powerpc等架構,可以用於模擬實體板卡。
HOST平台: Ubuntu14.04
TARGET平台:qemu_vexpress
GITHUB repo:
https://github.com/JinhuaW/buildroot.git
https://github.com/JinhuaW/target-apps.git
1- 安裝實驗必須的組件:
sudo apt-get install qemu git g++
2- 從github上克隆buildroot庫。
git clone https://github.com/JinhuaW/buildroot.git
3- 編譯,切換到buildroot根目錄
jinhuawu@UbuntuPC:~/buildroot$ make qemu_arm_vexpress_defconfig
#
# configuration written to /home/jinhuawu/buildroot/.config
#
jinhuawu@UbuntuPC:~/buildroot$ make
在編譯完成那個以後,我們可以得到下面的image
jinhuawu@UbuntuPC:~/buildroot/output/images$ ls
rootfs.ext2 vexpress-v2p-ca9.dtb zImage
4- 運行QEMU環境
jinhuawu@UbuntuPC:~/buildroot/output/images$ ls
rootfs.ext2 vexpress-v2p-ca9.dtb zImage
jinhuawu@UbuntuPC:~/buildroot/output/images$ qemu-system-arm -M vexpress-a9 -m 512M -nographic -append "root=/dev/mmcblk0 console=ttyAMA0" -kernel zImage -sd rootfs.ext2 -dtb vexpress-v2p-ca9.dtb
5- 測試app。
在用QEMU把target環境啟動起來以後,我們可以測試我們quotafs,quotafs相應的代碼可以從https://github.com/JinhuaW/target-apps.git獲取。
Welcome to Buildroot
buildroot login: root
# quotafs -h
Usage: quota --src source_dirctory --size quota_size mount_point [OPTIONS]
Mount a user space quota filesytem.
-h, show fuse option help
--src, the source dircotry is going to setup the quota
--size, the size of the quota
# quotafs --src /var/ --size 1024000 /mnt/
# mount
/dev/root on / type ext2 (rw,relatime)
devtmpfs on /dev type devtmpfs (rw,relatime,size=256244k,nr_inodes=64061,mode=755)
proc on /proc type proc (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,gid=5,mode=620)
tmpfs on /dev/shm type tmpfs (rw,relatime,mode=777)
tmpfs on /tmp type tmpfs (rw,relatime)
tmpfs on /run type tmpfs (rw,nosuid,nodev,relatime,mode=755)
sysfs on /sys type sysfs (rw,relatime)
quotafs on /mnt type fuse.quotafs (rw,nosuid,nodev,relatime,user_id=0,group_id=0)
# cd /mnt/
# ls
cache lock run tmp used_size
lib log spool total_size www
# cat total_size
1024000
# cat used_size
492866
# dd if=/dev/zero of=test bs=1 count=1000
1000+0 records in
1000+0 records out
# cat used_size
493866
iphone用戶打賞
※JohnsonChung
※dubbo+zipkin調用鏈監控
TAG:程序員小新人學習 |