根據 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 的意義見 Mounting 中 fill_super
參數的說明
而檔案系統的操作則主要透過 super_operations
、inode_operations
、file_operations
等結構體作為 VFS 的界面,當需要對檔案系統進行實作時,再根據檔案系統呼叫各檔案系統中各自的實作函式進行處理。
TODO: 加入 superblock、cache、dentry 與流程
在檔案系統中,用來儲存檔案的基本單位是 data block,對應到實體儲存制裝置中的某個區塊,每個 data block 都有各自的編號。而各個檔案對應到的 data block 以及權限、大小等資訊則會寫入到某些特定的 data block 中,而這些資訊則是透過 inode 這個結構體來描述。
當一個 data block 不足以儲存完檔案的全部內容時,根據檔案系統的設計,可能會需要將檔案內容切割到多個 data block 中儲存,因此一個檔案可能會對應到多個 data block。
inode 結構體定義在 include/linux/fs.h
中,除了負責紀錄檔案的各種資訊之外,還包含了 i_op
與 i_fop
兩個重要的成員:
i_op
對應到定義在 include/linux/fs.h
的 inode_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.h
的 file_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 的一個界面,但主要負責將 read
、write
之類需要對 data block 進行操作的系統呼叫對應到目標檔案系統的實作。
inode ctime
如同實際儲存資料的 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
(若其為目錄的話)d_hash
節點dentry
相關操作的結構體 d_op
當檔案系統提供的某個功能需要解析檔案路徑時,就能夠透過以上幾個資訊來找到對應的 inode
物件,而其大致流程如下(假設檔案存在):
/
切割成數個 path components 並根據其決定起始 dentry
/
目錄作為起始 dentry
$PWD
作為起始 dentry
inode
d_child
或 d_subdirs
尋找下個 path component 的 dentry
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 *);
}
有了 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
等檔案系統層級的操作,而這些操作與 inode
與 dentry
相似,也是透過一個結構體(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 以及 Dentry 都是不可隨著斷電消失的資料,因此必須除存在儲存裝置中,但每次存取檔案時都要從儲存裝置中讀取它們的話,很明顯的會造成效能上的問題,但也不能每次掛載檔案系統時就將所有資訊保存在記憶體中,否則會造成記憶體浪費的問題。因此 VFS 提供了 Cache 的機制作為折衷,讓近期存取過的 Inode 以及 Dentry 可以儲存在記憶體中,以便快速存取。
而這個機制會透過分別定義在 fs/inode.c
與 fs/dcache.c
中的 inode_hashtable
與 dentry_hashtable
兩個雜湊表管理。以 dcache 為例,作為雜湊表節點的 dentry->d_hash
能夠透過 d_add
或 d_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 上限?
而 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
struct module *owner
實際指向檔案系統核心模組的指標,多數情況下就是各核心模組的 THIS_MODULE
。
struct dentry *(*mount)
指向掛載檔案系統用的函式的指標,目標函式的參數有三個:
struct file_system_type *fs_type
: 掛載裝置的類型int flags
: 掛載時的 flagsconst 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
串列中下一個檔案系統的指標。
為了讓系統能夠認知到檔案系統的存在,需要使用 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);
...
}
定義在 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)
取得名稱長度應該比較安全。
相對於負責將檔案系統加入到 file_systems
串列中的 register_filesystem
,unregister_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
串列使用的是單向鏈結串列,所以必須透過迴圈從頭開始一一進行比對。
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
指向的函式,而掛載檔案系統時要做的事主要有以下幾個:
尋找 Superblock
通常為 mount_bdev
、mount_nodev
或 mount_single
三種通用的掛載函式,分別代表 Block Device
而在掛載函式中會嘗試透過 sget
尋找儲存裝置中的 Superblock,
透過 fill_super
初始化 Superblock
在掛載函式中會有一個 function pointer 的參數 fill_super
,會根據不同的檔案系統使用不同的初始化函式,在找到 Superblock 之後就會透過這個函式對 Superblock 進行初始化。
不同的檔案系統的 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;
}
回傳掛載點
而 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);
卸載檔案系統則是將整個檔案系統當中的 Superblock 註銷掉,主要會用到的函式為 kill_block_super
,kill_anon_super
和 kill_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 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
為要映射的第幾個 blockstruct block_device *b_bdev
為要儲存或寫入的裝置