Linux VFS機制簡析(一)


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_keys_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_inodedestroy_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_readquota_write:用於讀寫文件系統的quota文件。

nr_cached_objectsfree_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()函數來打開文件即可。

參考

Linux Documentation: VFS

后記

本篇主要介紹了VFS架構機制和作用,以及如何實現一個底層文件系統的注冊和mount、super block和sb operations、inode和inode operations。
下一篇將繼續介紹有關Address space和address operations、file和file operations、dentry和dentry operations和dentry cache API:Linux VFS機制簡析(二)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM