VFS - Virtual File System

Overview

根據 Linux Kernel 文件中對 Virtual File System 的描述

The Virtual File System (also known as the Virtual Filesystem Switch) is the software layer in the kernel that provides the filesystem interface to userspace programs. It also provides an abstraction within the kernel which allows different filesystem implementations to coexist.

可以知道 VFS 是 System Call 與 File System 間的界面,能夠讓多種不同的檔案系統同時存在於系統中。

而每個檔案系統實際上都是透過 file_system_type 這個結構體來描述,已掛載的檔案系統的物件會透過一個鏈結串列 file_systems 串在一起,這個列表以全域變數的形式宣告在 fs/filesystems.c 中:

static struct file_system_type *file_systems;

而這個列表對應的檔案則是 /proc/filesystems

$ cat /proc/filesystems       
nodev   sysfs
nodev   tmpfs
nodev   bdev
nodev   proc
nodev   cgroup
nodev   cgroup2
nodev   cpuset
nodev   devtmpfs
nodev   configfs
nodev   debugfs
nodev   tracefs
nodev   securityfs
nodev   sockfs
...

nodev 的意義見 Mountingfill_super 參數的說明

而檔案系統的操作則主要透過 super_operationsinode_operationsfile_operations 等結構體作為 VFS 的界面,當需要對檔案系統進行實作時,再根據檔案系統呼叫各檔案系統中各自的實作函式進行處理。

TODO: 加入 superblock、cache、dentry 與流程

Inode and Data Block

在檔案系統中,用來儲存檔案的基本單位是 data block,對應到實體儲存制裝置中的某個區塊,每個 data block 都有各自的編號。而各個檔案對應到的 data block 以及權限、大小等資訊則會寫入到某些特定的 data block 中,而這些資訊則是透過 inode 這個結構體來描述。

當一個 data block 不足以儲存完檔案的全部內容時,根據檔案系統的設計,可能會需要將檔案內容切割到多個 data block 中儲存,因此一個檔案可能會對應到多個 data block。

inode 結構體定義在 include/linux/fs.h 中,除了負責紀錄檔案的各種資訊之外,還包含了 i_opi_fop 兩個重要的成員:

  • i_op 對應到定義在 include/linux/fs.hinode_operations 結構體:

    ​​​​struct inode_operations {
    ​​​​    int (*create) (struct user_namespace *, 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 *,
    ​​​​    ...
    ​​​​};
    

    這個結構體是 VFS 的一個界面,讓 rename 或 link 等對 inode 進行操作的系統呼叫能夠透過這個界面呼叫到實際檔案系統的對應函式。

  • i_fop 則定義在 include/linux/fs.hfile_operations 結構體中:

    ​​​​struct file_operations {
    ​​​​    struct module *owner;
    ​​​​    loff_t (*llseek) (struct file *, loff_t, int);
    ​​​​    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
    ​​​​    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
    ​​​​    ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
    ​​​​    ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
    ​​​​    int (*mmap) (struct file *, struct vm_area_struct *);
    ​​​​    unsigned long mmap_supported_flags;
    ​​​​    int (*open) (struct inode *, struct file *);
    ​​​​    int (*flush) (struct file *, fl_owner_t id);
    ​​​​    ...
    ​​​​};
    

    inode_operations 結構體一樣是個 VFS 的一個界面,但主要負責將 readwrite 之類需要對 data block 進行操作的系統呼叫對應到目標檔案系統的實作。

inode ctime

Directory Entry

如同實際儲存資料的 data blocks 需要由 inode 紀錄,檔名對應到的 inode 也需要有個機制來紀錄。紀錄 inode 的策略能有很多種,像是可以建立一個 table 紀錄檔案與 inode 的對應關係,但 table 大小會隨著檔案數量增加,進而造成記憶體用量也跟著增加。

而不同於單一 table 儲存對應關係,Linux Kernel 採用的是樹狀結構來管理檔名與 inode 間的對應關係,而每個對應關係則是由 Directory Entry 來管理,其對應的到定義在 include/linux/dcache.h 中的 dentry 結構體

struct dentry {
    ...
    struct hlist_bl_node d_hash;
    struct dentry *d_parent;
    struct qstr d_name;
    struct inode *d_inode;
    unsigned char d_iname[DNAME_INLINE_LEN
    ...
    const struct dentry_operations *d_op;
    struct super_block *d_sb;
    ...
    struct list_head d_child;
    struct list_head d_subdirs;
    ...
};

如其名 Directory Entry,這個結構體就是用來紀錄目錄 d_parent 中檔名 d_name 的對應關係,其主要包含了以下幾個資訊:

  • 所屬目錄的 dentry 物件 d_parent
  • 檔案名稱 d_name 及其對應的 inode 物件 d_inode
  • 目錄中的檔案構成的串列 d_child 以及子目錄串列 d_subdirs(若其為目錄的話)
  • 配合雜湊表快速尋找 dentry 的 d_hash 節點
  • dentry 相關操作的結構體 d_op

當檔案系統提供的某個功能需要解析檔案路徑時,就能夠透過以上幾個資訊來找到對應的 inode 物件,而其大致流程如下(假設檔案存在):

  1. 將路徑以 / 切割成數個 path components 並根據其決定起始 dentry
    • 絕對路徑:以 / 目錄作為起始 dentry
    • 相對路徑:以 $PWD 作為起始 dentry
  2. 是否已找到最後一個 path component 的 inode
    • 是:跳至 5.
    • 否:跳至 3.
  3. 透過 d_childd_subdirs 尋找下個 path component 的 dentry
  4. 回到 2.
  5. 回傳該路徑對應的 inode 物件

一個個檢查檔案以及子目錄很沒效率,因此實際上會透過後面會提到的 Dentry Cache 快速查詢,詳細流程在 File System Operations 中說明。

與 Inode 相似,dentry 也有相關的結構體作為界面、負責處理 dentry 相關的操作,定義在 include/linux/dcache.h 中:

struct dentry_operations {
    int (*d_revalidate)(struct dentry *, unsigned int);
    int (*d_weak_revalidate)(struct dentry *, unsigned int);
    int (*d_hash)(const struct dentry *, struct qstr *);
    int (*d_compare)(const struct dentry *,
                     unsigned int, const char *, const struct qstr *);
    int (*d_delete)(const struct dentry *);
    int (*d_init)(struct dentry *);
    void (*d_release)(struct dentry *);
    void (*d_prune)(struct dentry *);
    void (*d_iput)(struct dentry *, struct inode *);
    char *(*d_dname)(struct dentry *, char *, int);
    struct vfsmount *(*d_automount)(struct path *);
    int (*d_manage)(const struct path *, bool);
    struct dentry *(*d_real)(struct dentry *, const struct inode *);
}

Superblock

有了 Data Block、Inode、Directory Entry 就大致足以描述一個檔案了,但要讓檔案系統運作還是缺少一些資訊,例如 Data Block 的大小、還能用的 Inode 與 Data Block 數量等檔案系統層級的資訊,而這些必要的資訊就要透過 Superblock 來紀錄,對應到的資料結構為定義在 include/linux/fs.h 中的 super_block 結構體:

struct super_block {
    ...
    unsigned char		s_blocksize_bits;
    unsigned long		s_blocksize;
    loff_t			s_maxbytes;	/* Max file size */
    struct file_system_type	*s_type;
    const struct super_operations	*s_op;
    unsigned long		s_magic;
    struct dentry		*s_root;
    void			*s_fs_info;	/* Filesystem private info */
    char			s_id[32];	/* Informational name */
    uuid_t			s_uuid;		/* UUID */
    fmode_t			s_mode;
    const struct dentry_operations *s_d_op; /* default d_op for dentries */
    struct list_head	s_inodes;	/* all inodes */
    struct list_head	s_inodes_wb;	/* writeback inodes */
    ...
};

TODO: 解釋重要的成員

而作為檔案系統中最高層級的物件,除了紀錄檔案系統層級的資訊之外,Superblock 還須負責處理掛載檔案系統、分配 inode 等檔案系統層級的操作,而這些操作與 inodedentry 相似,也是透過一個結構體(s_op)作為 VFS 的界面與各個檔案系統進行互動:

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 *);
    ...
};

Inode Cache and Dentry Cache

由於 Inode 以及 Dentry 都是不可隨著斷電消失的資料,因此必須除存在儲存裝置中,但每次存取檔案時都要從儲存裝置中讀取它們的話,很明顯的會造成效能上的問題,但也不能每次掛載檔案系統時就將所有資訊保存在記憶體中,否則會造成記憶體浪費的問題。因此 VFS 提供了 Cache 的機制作為折衷,讓近期存取過的 Inode 以及 Dentry 可以儲存在記憶體中,以便快速存取。

而這個機制會透過分別定義在 fs/inode.cfs/dcache.c 中的 inode_hashtabledentry_hashtable 兩個雜湊表管理。以 dcache 為例,作為雜湊表節點的 dentry->d_hash 能夠透過 d_addd_drop 等函式將其加入或移除雜湊表,而在 fs/namei.c 中定義的函數 d_lookup 則負責根據路徑 name 尋找對應的 dentry,其實作如下:

struct dentry *__d_lookup(const struct dentry *parent, const struct qstr *name) { unsigned int hash = name->hash; struct hlist_bl_head *b = d_hash(hash); struct hlist_bl_node *node; struct dentry *found = NULL; struct dentry *dentry; rcu_read_lock(); hlist_bl_for_each_entry_rcu(dentry, node, b, d_hash) { if (dentry->d_name.hash != hash) continue; spin_lock(&dentry->d_lock); if (dentry->d_parent != parent) goto next; if (d_unhashed(dentry)) goto next; if (!d_same_name(dentry, parent, name)) goto next; dentry->d_lockref.count++; found = dentry; spin_unlock(&dentry->d_lock); break; next: spin_unlock(&dentry->d_lock); } rcu_read_unlock(); return found; }

可以看到在第 4 行就透過 d_hash(hash) 找出雜湊表中對應的 bucket,接著再透過第 11 行的 hlist_bl_for_each_entry_rcu 巨集依序檢查 bucket 中的節點。

TODO: Inode Cache 是否也相似?Size 上限?


Filesystem

file_system_type 結構體則定義在 include/linux/fs.h 中,用來儲存檔案系統的資訊以及操作時需要的各種物件:

struct file_system_type {
	const char *name;
	int fs_flags;
#define FS_REQUIRES_DEV		1 
#define FS_BINARY_MOUNTDATA	2
#define FS_HAS_SUBTYPE		4
#define FS_USERNS_MOUNT		8	/* Can be mounted by userns root */
#define FS_DISALLOW_NOTIFY_PERM	16	/* Disable fanotify permission events */
#define FS_ALLOW_IDMAP         32      /* FS has been updated to handle vfs idmappings. */
#define FS_RENAME_DOES_D_MOVE	32768	/* FS will handle d_move() during rename() internally. */
	int (*init_fs_context)(struct fs_context *);
	const struct fs_parameter_spec *parameters;
	struct dentry *(*mount) (struct file_system_type *, int,
		       const char *, void *);
	void (*kill_sb) (struct super_block *);
	struct module *owner;
	struct file_system_type * next;
	struct hlist_head fs_supers;

	struct lock_class_key s_lock_key;
	struct lock_class_key s_umount_key;
	struct lock_class_key s_vfs_rename_key;
	struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];

	struct lock_class_key i_lock_key;
	struct lock_class_key i_mutex_key;
	struct lock_class_key invalidate_lock_key;
	struct lock_class_key i_mutex_dir_key;
};

大致上可以分成檔案系統資訊以及 lock 三個部份,其中檔案系統資訊的部份包含了:

  • const char *name

    檔案系統的名稱,例如 ext4NTFS

  • struct module *owner

    實際指向檔案系統核心模組的指標,多數情況下就是各核心模組的 THIS_MODULE

  • struct dentry *(*mount)

    指向掛載檔案系統用的函式的指標,目標函式的參數有三個:

    • struct file_system_type *fs_type: 掛載裝置的類型
    • int flags: 掛載時的 flags
    • const char * dev_name: 掛載裝置的名稱
    • void *data: 掛載時的參數字串 (參數列表見 Mount Options)

    回傳值則會掛載點的 Directory Entry。

  • void (*kill_sb) (struct super_block *)

    TODO: 卸載檔案系統時會呼叫的函式

  • struct hlist_head fs_supers

    TODO: 由 Superblock 組成的串列

  • const struct fs_parameter_spec *parameters

  • struct file_system_type * next

    指向 file_systems 串列中下一個檔案系統的指標。

Registering and Mounting a Filesystem

Initializing Module

為了讓系統能夠認知到檔案系統的存在,需要使用 include/linux/fs.h 中宣告的函式:

#include <linux/fs.h>

extern int register_filesystem(struct file_system_type *);
extern int unregister_filesystem(struct file_system_type *);

而這兩個函式並不是在 mount 時呼叫的,而是透過 module_init 以及 module_exit 兩個巨集在掛載以及卸載核心模組時呼叫,例如在 ext4 檔案系統中是由定義在 fs/ext4/super.c 中的 ext4_init_fs 以及 ext4_exit_fs 兩個函式分別處理:

module_init(ext4_init_fs) module_exit(ext4_exit_fs)

而這兩個函式則會分別呼叫到 register_filesystem 以及 unregister_filesystem 這兩個函式:

static int __init ext4_init_fs(void)
{
    ...
    err = register_filesystem(&ext4_fs_type);
    if (err)
    	goto out;
    return 0;
    ...
}


static void __exit ext4_exit_fs(void)
{
    ...
    unregister_filesystem(&ext4_fs_type);
    ...
}

Registering

定義在 fs/filesystems.c 中的 register_filesystem 負責將一個尚未掛載的檔案系統的物件加入到 file_systems 串列中,其實作如下:

int register_filesystem(struct file_system_type * fs) { int res = 0; struct file_system_type ** p; if (fs->parameters && !fs_validate_description(fs->name, fs->parameters)) return -EINVAL; BUG_ON(strchr(fs->name, '.')); if (fs->next) return -EBUSY; write_lock(&file_systems_lock); p = find_filesystem(fs->name, strlen(fs->name)); if (*p) res = -EBUSY; else *p = fs; write_unlock(&file_systems_lock); return res; }

可以注意到檔案系統的名稱中不應包含 . 這個字元,否則在第 10 行的 BUG_ON 檢查檔案系統的名稱時會發生錯誤。

為什麼需要這個限制? pathlookup?

首先會透過定義在 fs/fs_parser.c 中的 fs_validate_description 檢查是否有重複的參數:

bool fs_validate_description(const char *name,
	const struct fs_parameter_spec *desc)
{
	const struct fs_parameter_spec *param, *p2;
	bool good = true;

	for (param = desc; param->name; param++) {
		/* Check for duplicate parameter names */
		for (p2 = desc; p2 < param; p2++) {
			if (strcmp(param->name, p2->name) == 0) {
				if (is_flag(param) != is_flag(p2))
					continue;
				pr_err("VALIDATE %s: PARAM[%s]: Duplicate\n",
				       name, param->name);
				good = false;
			}
		}
	}
	return good;
}

接著才會呼叫 find_filesystem() 依序從 file_systems 串列中檢查是否有已掛載相同名稱的檔案系統:

static struct file_system_type **find_filesystem(const char *name, unsigned len)
{
	struct file_system_type **p;
	for (p = &file_systems; *p; p = &(*p)->next)
		if (strncmp((*p)->name, name, len) == 0 &&
		    !(*p)->name[len])
			break;
	return p;
}

由於 file_systems 是 Singly-linked List,所以若是名稱為 name 的檔案系統尚未被掛載的話,*p 就會是 NULL;若有重複的檔案系統的話,*p 則會指向該檔案系統的物件,即不為 NULL,用來代表錯誤。

若只是要單純的走訪 file_systems 並檢查是否重複的話,不需要使用到指標的指標。但在 find_filesystem 裡巧妙的利用了指標的指標,若是沒找到相同的檔案系統的話,p 最終會指向最後一個節點的結構體的 next 成員的地址,然後將其回傳,接著在 register_filesystem 的第 18 行將目標檔案系統的物件加入到 file_systems 串列的尾端。

比較需要注意的是由於 strcmp 不安全,所以要改用 strncmp 來比較,而由於只有前 n 個字元相同並無法保證兩個字串完全相同,所以還需要額外檢查 (*p)->name 是否也到結尾了。

register_filesystem 呼叫 find_filesystem 時,第二個參數是用 strlen 取得檔案系統名稱的長度。

但比起透過參數傳遞長度,在 find_filesystem 中再用 strlen(name) 取得名稱長度應該比較安全。

Unregistering

相對於負責將檔案系統加入到 file_systems 串列中的 register_filesystemunregister_filesystem 則會在卸載檔案系統核心模組時將對應的檔案系統的物件從 file_systems 串列中移除,其實做如下:

int unregister_filesystem(struct file_system_type * fs)
{
	struct file_system_type ** tmp;

	write_lock(&file_systems_lock);
	tmp = &file_systems;
	while (*tmp) {
		if (fs == *tmp) {
			*tmp = fs->next;
			fs->next = NULL;
			write_unlock(&file_systems_lock);
			synchronize_rcu();
			return 0;
		}
		tmp = &(*tmp)->next;
	}
	write_unlock(&file_systems_lock);

	return -EINVAL;
}

由於 file_systems 串列使用的是單向鏈結串列,所以必須透過迴圈從頭開始一一進行比對。

Mounting

struct file_system_type {
    const char *name;
    int fs_flags;
    struct dentry *(*mount) (struct file_system_type *, int,
                             const char *, void *);
    void (*kill_sb) (struct super_block *);
    struct module *owner;
    struct file_system_type * next;
    struct list_head fs_supers;
    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;
    ...
};

而在掛載檔案系統時,則會呼叫 mount 指向的函式,而掛載檔案系統時要做的事主要有以下幾個:

  1. 尋找 Superblock

    通常為 mount_bdevmount_nodevmount_single 三種通用的掛載函式,分別代表 Block Device

    而在掛載函式中會嘗試透過 sget 尋找儲存裝置中的 Superblock,

  2. 透過 fill_super 初始化 Superblock

    在掛載函式中會有一個 function pointer 的參數 fill_super,會根據不同的檔案系統使用不同的初始化函式,在找到 Superblock 之後就會透過這個函式對 Superblock 進行初始化。

    • mount_bdev()
      將檔案系統掛載到實體裝置上,比如說硬碟上。
    • mount_single()
      將多個掛載操作將共享在同一個檔案系統實例 (instance)。
    • mount_nodev()
      將檔案系統掛載到非實體裝置上。

    不同的檔案系統的 fill_super() 會有些許的不同,不過最主要都有對 Superblock 做初始化的功能,比如 ramfs 檔案系統:

static int ramfs_fill_super(struct super_block *sb, void *data, int silent)
{
        struct ramfs_fs_info *fsi;
        struct inode *inode;
        int err;

        save_mount_options(sb, data);

        fsi = kzalloc(sizeof(struct ramfs_fs_info), GFP_KERNEL);
        sb->s_fs_info = fsi;
        if (!fsi)
                return -ENOMEM;

        err = ramfs_parse_options(data, &fsi->mount_opts);
        if (err)
                return err;

        sb->s_maxbytes          = MAX_LFS_FILESIZE;
        sb->s_blocksize         = PAGE_SIZE;
        sb->s_blocksize_bits    = PAGE_SHIFT;
        sb->s_magic             = RAMFS_MAGIC;
        sb->s_op                = &ramfs_ops;
        sb->s_time_gran         = 1;

        inode = ramfs_get_inode(sb, NULL, S_IFDIR | fsi->mount_opts.mode, 0);
        sb->s_root = d_make_root(inode);
        if (!sb->s_root)
                return -ENOMEM;

        return 0;
}
  1. 回傳掛載點

    mount 的回傳值則會是掛載點的 Directory Entry,如同 linux/super.c 中的 mount_bdev()

struct dentry *mount_nodev(struct file_system_type *fs_type,
	int flags, void *data,
	int (*fill_super)(struct super_block *, void *, int))
{
	int error;
	struct super_block *s = sget(fs_type, NULL, set_anon_super, flags, NULL);

	if (IS_ERR(s))
		return ERR_CAST(s);

	error = fill_super(s, data, flags & SB_SILENT ? 1 : 0);
	if (error) {
		deactivate_locked_super(s);
		return ERR_PTR(error);
	}
	s->s_flags |= SB_ACTIVE;
	return dget(s->s_root);
}
EXPORT_SYMBOL(mount_nodev);

Unmount

卸載檔案系統則是將整個檔案系統當中的 Superblock 註銷掉,主要會用到的函式為 kill_block_superkill_anon_superkill_litter_super

  • kill_block_super 為將掛載在 block device 上的檔案系統卸載
  • kill_anon_super 為將虛擬檔案系統 ( virtual file system ) 卸載
  • kill_litter_super 為卸載不在實體裝置上的檔案系統(例如:記憶體)

上述三個函式中主要註銷 Superblock 的函式為 fs/super.c 中的generic_shutdown_super,而作為註銷 Superblock 的函式,其功能就在釋放結構體 Superblock 的成員的記憶體空間。

 *	generic_shutdown_super	-	common helper for ->kill_sb()
 *	@sb: superblock to kill
 *
 *	generic_shutdown_super() does all fs-independent work on superblock
 *	shutdown.  Typical ->kill_sb() should pick all fs-specific objects
 *	that need destruction out of superblock, call generic_shutdown_super()
 *	and release aforementioned objects.  Note: dentries and inodes _are_
 *	taken care of and do not need specific handling.

Buffer Cache

Buffer cache 提供一個緩存空間 (buffer block) 來儲存或寫入儲存設備 (block device),可以視將其視為 page cache 和儲存設備的溝通橋樑,靠著映射兩者之間的位址來做資料轉換,其儲存或寫入的單位為 block,buffer cache 在 Linux 內主要是由 linux/buffer_head.h 來運作

struct buffer_head {
	unsigned long b_state;		/* buffer state bitmap (see above) */
	struct buffer_head *b_this_page;/* circular list of page's buffers */
	union {
		struct page *b_page;	/* the page this bh is mapped to */
		struct folio *b_folio;	/* the folio this bh is mapped to */
	};

	sector_t b_blocknr;		/* start block number */
	size_t b_size;			/* size of mapping */
	char *b_data;			/* pointer to data within the page */

	struct block_device *b_bdev;
	bh_end_io_t *b_end_io;		/* I/O completion */
 	void *b_private;		/* reserved for b_end_io */
	struct list_head b_assoc_buffers; /* associated with another mapping */
	struct address_space *b_assoc_map;	/* mapping this buffer is
						   associated with */
	atomic_t b_count;		/* users using this buffer_head */
	spinlock_t b_uptodate_lock;	/* Used by the first bh in a page, to
					 * serialise IO completion of other
					 * buffers in the page */
};

下面是一些重要成員的解釋

  • char *b_data 為 page 內資料的地址,地址的開頭常作為映射的起點
  • sector_t b_blocknr 為要映射的第幾個 block
  • struct block_device *b_bdev 為要儲存或寫入的裝置
Select a repo