Linux VFS機制簡析(一)
本文主要基於Linux內核文檔,簡單分析Linux VFS機制,以期對編寫新的內核文件系統(通常是給分布式文件系統編寫內核客戶端)的場景有所幫助。
個人淵源
切入正文之前先扯點別的,艦隊我在04年剛接觸Linux時就深入分析了VFS,當時剛畢業入職一家做NAS存儲的公司,需要對VFS、block device、MD等內核模塊深入了解。時隔10幾年之后的今天,因給一個分布式文件系統做內核客戶端,重拾VFS發現一切還是熟悉的味道。這十幾年過去了,內核版本從2.6到4.x,VFS的機制和整體架構變化不大,依然是各種底層文件系統和用戶態接口之間不可或缺的轉換層。
Overview
VFS(Virtual File System)是Linux內核里提供文件系統接口給用戶態應用程序的一個虛擬文件系統層。同時VFS還提供了抽象化的操作接口以方便實現內核的底層文件系統。
Directory Entry Cache (dcache)
VFS實現open、stat、chmod等類似的文件系統調用,他們傳遞一個pathname參數給VFS。VFS根據文件路徑pathname搜索directory entry cache(dentry cache或者dcache)獲取對應的dentry。所以dcache是一個高速目錄項緩存,用於映射文件路徑和dentry。dentry結構用於優化查詢性能,只存在於內存中,不實際存儲到磁盤。
內存限制,並不是所有dentry都能在緩存命中,當根據pathname找不到對應dentry時,VFS調用lookup接口向底層文件系統查找獲取inode信息,以此建立dentry和其對應的inode結構。
Inode
每個dentry通常對應一個inode結構用於描述文件、目錄等的基本元數據信息。如果底層是磁盤存儲,Inode結構會保存到磁盤。當需要時從磁盤讀取到內存中進行緩存。一個inode結構可以被多個dentry指向,如硬鏈接。對於網絡文件系統(分布式文件系統),Inode結構需要通過網絡協議獲取到緩存中。
VFS通過父目錄的lookup方法來獲取某個文件的inode信息,該方法由底層文件系統實現。一旦獲取了inode信息,open,stat等無聊的操作直接從緩存里進行,變得很快。
File
Open一個文件還需要另外一個數據結構:File。File用於表示一個處於Open狀態的文件,同一個文件被Open多次對應不同的File結構。應用程序打開文件后對應一個句柄(FD, file descriptor),每個FD都對應到內核的一個File結構,因此File結構直接存放在進程的FD表里,通過FD可以快速獲取到File數據結構。
VFS實現用戶態文件讀寫關閉操作時,通過用戶態的FD來獲取對應的File結構,然后調用對應的底層文件系統方法。只要有File結構正在使用,就增加dentry的引用計數,保證dentry和inode結構沒有從緩存里刪除。
Registering and Mounting a Filesystem
通過如下函數進行文件系統的注冊和注銷操作:
#include <linux/fs.h>
extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);
其中struct file_system_type用於描述文件系統基本信息和mount()等操作。當掛載文件系統到目錄時,調用對應file_system_type里的mount()函數。原文件系統目錄樹上掛載點會附上新的vfsmount,當路徑解析到掛載點時,會自動跳轉到vfsmount的根目錄。
通過/proc/filesystems可以查看到所有注冊的文件系統類型。
struct file_system_type
結構體file_system_type的定義如下:
struct file_system_type {
115 const char *name;
116 int fs_flags;
117 struct dentry *(*mount) (struct file_system_type *, int,
118 const char *, void *);
119 void (*kill_sb) (struct super_block *);
120 struct module *owner;
121 struct file_system_type * next;
122 struct list_head fs_supers;
123 struct lock_class_key s_lock_key;
124 struct lock_class_key s_umount_key;
125 };
其中,name是文件系統名稱,如ext4, xfs等等。fs_flags為各種標識,如FS_REQUIRES_DEV, FS_NO_DCACHE等。mount()函數指針用於掛載一個新的文件系統實例。kill_sb()函數指針用於關閉文件系統實例。owner是VFS內部使用,通常設置為THIS_MODULE。next也是VFS內部使用,初始化時設置為NULL即可。s_lock_key和s_umount_key是lockdep相關的結構。
mount()函數有幾個參數:fs_type為對應的file_sytem_type結構指針。flags為掛載的標識。dev_name為掛載的設備名,對於網絡文件系統通常是一個網絡路徑。data為掛載的選項,通常為一組ASCII字符串。
mount()必須返回文件系統目錄樹的root dentry。文件系統的super block增加一個引用計數並處於locked狀態。mount失敗時返回ERR_PTR(err)。mount()函數可以選擇返回一個已經存在的文件系統的一個子樹,而不是創建一個新的文件系統實例,這種情況返回的是子樹的root dentry。
底層文件系統實現mount,可以直接調用通用的mount實現:mount_bdev(在塊設備上掛載文件系統)、mount_nodev(掛載沒有設備的文件系統)和mount_single(掛載在不同的mounts間共享實例的文件系統),並提供一個fill_super()的回調函數用於創建root dentry和inode。比如FUSE就通過調用mount_nodev來實現mount操作。
其中file_super()回調函數的參數包括:struct super_block sb(文件系統sb,需要在fill_super()里進行初始化)、void data(文件系統掛載的選項字符串)、int silent(是否忽略error)。
當然也可以參考通用的mount實現自己的mount操作,比如Ceph就直接調用了sget()函數創建sb並通過set()回調函數初始化sb。
Mount Options
mount函數會傳遞一個options的字符串,以逗號隔開。它是mount命令輸入的選項(通過-o設置)。options的格式可以是如下兩種:
- option
- option=value
Linux內核頭文件linux/parser.h里定義了幫助解析options的API。可以從現有的文件系統代碼里找到使用方法。
如果一個文件系統使用了mount options,則必須實現s_op->show_options()函數將選項進行顯示。顯示的規則如下:
- 如果option不是默認值,則必須顯示。
- 如果option等於默認值,則可選擇是否顯示。
Superblock and struct super_operations
Superblock超級塊(簡稱sb,莫名哈哈一笑)代表一個掛載的文件系統,其數據結構保存了文件系統基本的元數據信息。其中s_op指向了struct super_operations,為sb這一級的函數操作合集。
super_operations的定義如下:
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb);
void (*destroy_inode)(struct inode *);
void (*dirty_inode) (struct inode *, int flags);
int (*write_inode) (struct inode *, int);
void (*drop_inode) (struct inode *);
void (*delete_inode) (struct inode *);
void (*put_super) (struct super_block *);
int (*sync_fs)(struct super_block *sb, int wait);
int (*freeze_fs) (struct super_block *);
int (*unfreeze_fs) (struct super_block *);
int (*statfs) (struct dentry *, struct kstatfs *);
int (*remount_fs) (struct super_block *, int *, char *);
void (*clear_inode) (struct inode *);
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct dentry *);
ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
int (*nr_cached_objects)(struct super_block *);
void (*free_cached_objects)(struct super_block *, int);
};
所有的函數,如果沒有特別說明,都在沒有持有鎖的情況下被調用,因此大部分這些函數都可以安全地進行阻塞操作。所有的函數都只在進程上下文中被調用(區別於中斷處理或者中斷處理下半部分)。
alloc_inode:被inode_alloc()函數調用用於分配inode內存並進行inode結構初始化。如果函數未定義,則簡單的分配一個'struct inode'。通常alloc_inode用於底層文件系統分配一個包含inode結構體的更大的結構體(特定的inode結構,如:fuse_inode)。
destroy_inode:被destroy_inode()函數調用用於釋放inode相關申請的資源。只有alloc_inode定義了才需要定義destroy_inode,並且釋放的也是alloc_inode里申請的相關資源。
dirty_inode:由VFS調用標記inode dirty(元數據信息被修改過並且沒有同步到磁盤或服務器)。
write_inode:由VFS調用用於將inode同步到磁盤。第二個參數用於標識是否同步寫盤。
drop_inode:VFS在當inode的引用計數減為0時,調用該函數。調用者已經持有了inode->i_lock。該函數返回0,則inode將可能被丟到LRU鏈表里,返回1則會由調用者繼續調用evict_inode和destroy_inode。如果文件系統不需要緩存inode,則該函數可以設置為NULL或者generic_delete_inode(函數里直接return 1)。
delete_inode:VFS刪除inode時直接調用該函數。由於查看的Linux文檔版本是2.6.39,所以有該函數指針,在3.10版本已經沒有了detele_inode。
put_super:VFS想要釋放sb時調用(如umount操作)。調用者已經持有sb的lock。
sync_fs:VFS想要把該文件系統所有的臟數據刷盤時調用。
freeze_fs:目前只有LVM使用。用於凍結文件系統,不能進行寫入操作。
unfreeze_fs:解凍文件系統,使其可以寫入。
statfs:用於獲取文件系統的統計信息。
remount_fs:用於重新掛載文件系統,調用者持有kernel lock。
clear_inode:同樣在3.10版本沒有了。
umount_begin:用於umount文件系統。
show_options:用於/proc/mounts里顯示文件系統的mount選項。
quota_read和quota_write:用於讀寫文件系統的quota文件。
nr_cached_objects和free_cache_objects:用於返回可以釋放的cache對象個數,以及進行實際的釋放對象操作。
可以看到super_operations包含了inode的分配、初始化和釋放。inode里的i_op字段指向了底層文件系統inode相關操作合集:struct inode_operations。
struct inode_operations
struct inode_operations定義如下,它描述了VFS如何管理inode對象。
struct inode_operations {
int (*create) (struct inode *,struct dentry *, umode_t, bool);
struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,umode_t);
int (*rmdir) (struct inode *,struct dentry *);
int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *);
int (*readlink) (struct dentry *, char __user *,int);
void * (*follow_link) (struct dentry *, struct nameidata *);
void (*put_link) (struct dentry *, struct nameidata *, void *);
int (*permission) (struct inode *, int);
int (*get_acl)(struct inode *, int);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
int (*setxattr) (struct dentry *, const char *,const void *,size_t,int);
ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
ssize_t (*listxattr) (struct dentry *, char *, size_t);
int (*removexattr) (struct dentry *, const char *);
void (*update_time)(struct inode *, struct timespec *, int);
int (*atomic_open)(struct inode *, struct dentry *,
struct file *, unsigned open_flag,
umode_t create_mode, int *opened);
};
同樣,如果沒有特別注明,所有函數都在沒有鎖持有的情況下調用。
create:由open和create系統調用使用。入參inode為父目錄的inode,入參dentry為新創建的,沒有對應的inode(negative dentry)。底層文件系統需要調用d_instantiate()將dentry和新創建的inode進行關聯。只有目錄類型的inode才會調用該函數指針。
lookup:VFS需要查找目錄下面某個inode信息是調用該函數。入參dentry里攜帶了要查找的文件name。該函數里需要調用d_add()將找到的inode插入到dentry。並且inode的i_count字段需要遞增。如果inode沒有找到,則dentry插入一個NULL inode(這種dentry稱為一個negative dentry)。只有在底層真實錯誤時才能返回error,此時open、create、mknode等涉及創建inode的操作都會失敗。同樣也只有目錄類型的inode才會調用該函數指針。
在lookup函數里,可以將dentry的d_op字段初始化為自己的dentry_operations,來定制對dentry和dcache的一些管理函數操作合集。
link:link系統調用使用,用於創建硬鏈接。同樣需要調用d_instantiate()來關聯dentry和inode。
unlink:unlink系統調用使用,用於刪除一個inode關聯的文件或目錄。
symlink:symlink系統調用使用,用於創建一個軟鏈接。
mkdir:mkdir系統調用使用,用於創建一個子目錄。
rmdir:rmdir系統調用使用,用於刪除一個子目錄。
mknod:mknod系統調用使用,用於創建一個設備inode(char,block)或者一個named pipe (FIFO)或者一個socket。
rename:rename系統調用使用,用於改名。
readlink:readlink系統調用使用,用於讀取軟鏈接文件指向的實際路徑。
follow_link:VFS調用,用於跟蹤獲取一個軟鏈接指向的inode。該函數返回一個指針cookie,該cookie會傳遞給put_link。
put_link:用於釋放follow_link里申請的資源,cookie作為最后一個參數傳入。它在NFS等文件系統上,page cache不是很穩定的情況下使用。
permission:VFS調用,用於檢測訪問權限。有可能在rcu-walk mode下被調用,那么該函數必須不能阻塞或者存儲數據到inode。如果在rcu-walk mode下遇到問題,則返回-ECHILD,它將在ref-walk mode重新被調用。
setattr:VFS調用,用於設置文件的attr屬性。它將被chmod等相關系統調用使用。
getattr:VFS調用,用於獲取文件的attr屬性。它將被stat等相關系統調用使用。
setxattr:VFS調用,用於設置文件的一個擴展attr屬性。它將被setxattr系統調用使用。
getxattr:VFS調用,用於根據屬性名稱獲取文件的一個擴展attr屬性。它將被getxattr系統調用使用。
listxattr:VFS調用,用於列出給定文件的所有擴展屬性。它將被listxattr系統調用使用。
removexattr:VFS調用,用於刪除一個擴展attr屬性。它將被removexattr系統調用使用。
update_time:VFS調用,用於更新inode的時間(如atime)或者i_version字段。如果該函數沒有指定,則VFS將自己更新inode並調用mark_inode_dirty_sync。
atomic_open:該可選的函數,用於性能優化。它將lookup、可能的create操作以及open操作在一個接口里完成。只有negative dentry才會調用該函數。在dentry cache里的positive dentry直接通過f_op->open()函數來打開文件即可。
參考
后記
本篇主要介紹了VFS架構機制和作用,以及如何實現一個底層文件系統的注冊和mount、super block和sb operations、inode和inode operations。
下一篇將繼續介紹有關Address space和address operations、file和file operations、dentry和dentry operations和dentry cache API:Linux VFS機制簡析(二)
