1.Linux 文件系統組成結構
linux文件系統有兩個重要的特點:一個是文件系統抽象出了一個通用文件表示層——虛擬文件系統或稱做VFS。另外一個重要特點就是它的文件系統支持動態安裝(或說掛載等),大多數文件系統都可以作為根文件系統的葉子節點被掛在到根文件目錄樹下的子目錄上。
1.1.虛擬文件系統
虛擬文件系統為用戶空間程序提供了文件系統接口。系統中所有文件系統不但依賴VFS共存,而且也依靠VFS系統協同工作。通過虛擬文件系統我們可以利用標准的UNIX文件系統調用對不同介質上的不同文件系統進行讀寫操作。虛擬文件系統的目的是為了屏蔽各種各樣不同文件系統的相異操作形式,使得異構的文件系統可以在統一的形式下,以標准化的方法訪問、操作。實現虛擬文件系統利用的主要思想是引入一個通用文件模型——該模型抽象出了文件系統的所有基本操作(該通用模型源於Unix風格的文件系統),比如讀、寫操作等。同時實際文件系統如果希望利用虛擬文件系統,即被虛擬文件系統支持,也必須將自身的諸如“打開文件”、“讀寫文件”等操作行為以及“什么是文件”,“什么是目錄”等概念“修飾”成虛擬文件系統所要求的(定義的)形式,這樣才能夠被虛擬文件系統支持和使用。
我們可以借用面向對象的思想來理解虛擬文件系統,可以想象成面向對象中的多態。
1.2.虛擬文件系統的相關對象
虛擬文件系統的核心概念
1、 VFS 通過樹狀結構來管理文件系統,樹狀結構的任何一個節點都是“目錄節點”
2、 樹狀結構具有一個“根節點”
3、 VFS 通過“超級塊”來了解一個具體文件系統的所有需要的信息。具體文件系統必須先向VFS注冊,注冊后,VFS就可以獲得該文件系統的“超級塊”。
4、 具體文件系統可被安裝到某個“目錄節點”上,安裝后,具體文件系統才可以被使用
5、 用戶對文件的操作,就是通過VFS 的接口,找到對應文件的“目錄節點”,然后調用該“目錄節點”對應的操作接口。
例如下圖:
1、 綠色代表“根文件系統”
2、 黃色代表某一個文件系統 XXFS
3、 根文件系統安裝到“根目錄節點”上
4、 XXFS 安裝到目錄節點B上
關於虛擬文件系統的四個對象大家已經很熟悉了,網上也有很多介紹。這里做一些簡單介紹
- 超級塊對象,它代表特定的已安裝文件系統
- 索引節點對象,它代表特定文件。
- 目錄項對象,它代表特定的目錄項。
- 文件對象,它代表被進程打開的文件。
inode
inode用來描述文件物理上的屬性,比如創建時間,uid,gid等。其對應的操作方法為file_operation,文件被打開后,inode 和 file_operation 都已經在內存中建立,file_operations 的指針也已經指向了具體文件系統提供的函數,此后都文件的操作,都由這些函數來完成。
dentry
本來,inode 中應該包括“目錄節點”的名稱,但由於符號鏈接的存在,導致一個物理文件可能有多個文件名,因此把和“目錄節點”名稱相關的部分從 inode 結構中分開,放在一個專門的 dentry 結構中。這樣:
1、 一個dentry 通過成員 d_inode 對應到一個 inode上,尋找 inode 的過程變成了尋找 dentry 的過程。因此,dentry 變得更加關鍵,inode 常常被 dentry 所遮掩。可以說, dentry 是文件系統中最核心的數據結構,它的身影無處不在。
2、 由於符號鏈接的存在,導致多個 dentry 可能對應到同一個 inode 上.
super_block
super_block 保存了文件系統的整體信息,如訪問權限;
我們通過分析“獲取一個 inode ”的過程來理解超級塊的重要性。
在文件系統的操作中,經常需要獲得一個“目錄節點”對應的 inode,這個 inode 有可能已經存在於內存中了,也可能還沒有,需要創建一個新的 inode,並從磁盤上讀取相應的信息來填充。 在內核中對應的代碼是iget5_locked,對應代碼的過程如下:
1、 通過 iget5_locked() 獲取 inode。如果 inode 在內存中已經存在,則直接返回;否則創建一個新的 inode
2、 如果是新創建的 inode,通過 super_block->s_op->read_inode() 來填充它。也就是說,如何填充一個新創建的 inode, 是由具體文件系統提供的函數實現的。
iget5_locked() 首先在全局的 inode hash table 中尋找,如果找不到,則調用 get_new_inode() ,進而調用 alloc_inode() 來創建一個新的 inode
在 alloc_inode() 中可以看到,如果具體文件系統提供了創建 inode 的方法,則由具體文件系統來負責創建,否則采用系統默認的的創建方法。
static struct inode *alloc_inode(struct super_block *sb)
{
struct inode *inode;
if (sb->s_op->alloc_inode)
inode = sb->s_op->alloc_inode(sb);
else
inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);
if (!inode)
return NULL;
if (unlikely(inode_init_always(sb, inode))) {
if (inode->i_sb->s_op->destroy_inode)
inode->i_sb->s_op->destroy_inode(inode);
else
kmem_cache_free(inode_cachep, inode);
return NULL;
}
return inode;
}
super_block 是在安裝文件系統的時候創建的,后面會看到它和其它結構之間的關系。
為什么說super_block如此重要?
我們來看下打開文件的過程:
- 1 分配文件描述符號。
- 2 獲得新文件對象。
- 3.獲得目標文件的目錄項對象和其索引節點對象,主要通過open_namei()函數)——具體而言是通過調用索引節點對象(該索引節點或是安裝點或是當前目錄)的lookup方法找到目錄項對應的索引節點號ino,然后調用iget(sb,ino)從磁盤讀入相應索引節點並在內核中建立起相應的索引節點(inode)對象(其實還是通過調用sb->s_op->read_inode()超級塊提供的方法),最后還要使用d_add(dentry,inode)函數將目錄項對象與inode對象連接起來。
- 4 初始化目標文件對象的域,特別是把f_op域設置成索引節點中i_fop指向文件對象的操作表——以后對文件的所有操作將調用該表中的實際方法。
看到這里大家應該知道文件系統中超級塊的重要了吧,它是一切文件操作的源頭。
當你要實現一個文件系統的時候,你首先要做的就是生產自己的super_block,即要重載內核的get_sb()函數,這個函數是在mount的時候調用的
2.注冊文件系統
一個具體的文件系統必須先向vfs注冊,才能被使用。通過register_filesystem() ,可以將一個“文件系統類型”結構 file_system_type注冊到內核中一個全局的鏈表file_systems 上。
文件系統注冊的主要目的,就是讓 VFS 創建該文件系統的“超級塊”結構。
一個文件系統在內核中用struct file_system_type來表示:
struct file_system_type {
const char *name;
int fs_flags;
int (*get_sb) (struct file_system_type *, int,
const char *, void *, struct vfsmount *);
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;
struct lock_class_key i_lock_key;
struct lock_class_key i_mutex_key;
struct lock_class_key i_mutex_dir_key;
struct lock_class_key i_alloc_sem_key;
};
這個結構中最關鍵的就是 get_sb() 這個函數指針,它就是用於創建並設置 super_block 的目的的。
因為安裝一個文件系統的關鍵一步就是要為“被安裝設備”創建和設置一個 super_block,而不同的具體的文件系統的 super_block 有自己特定的信息,因此要求具體的文件系統首先向內核注冊,並提供 read_super() 的實現。
3.安裝文件系統
一個注冊了的文件系統必須經過安裝才能被VFS所接受。安裝一個文件系統,必須指定一個目錄作為安裝點。一個設備可以同時被安裝到多個目錄上。 一個目錄節點下可以同時安裝多個設備。
3.1.“根安裝點”、“根設備”和“根文件系統”
安裝一個文件系統,除了需要“被安裝設備”外,還要指定一個“安裝點”。“安裝點”是已經存在的一個目錄節點。例如把 /dev/sda1 安裝到 /mnt/win 下,那么 /mnt/win 就是“安裝點”。 可是文件系統要先安裝后使用。因此,要使用 /mnt/win 這個“安裝點”,必然要求它所在文件系統已也經被安裝。 也就是說,安裝一個文件系統,需要另外一個文件系統已經被安裝。
這是一個雞生蛋,蛋生雞的問題:最頂層的文件系統是如何被安裝的?
答案是,最頂層文件系統在內核初始化的時候被安裝在“根安裝點”上的,而根安裝點不屬於任何文件系統,它對應的 dentry 、inode 等結構是由內核在初始化階段構造出來的。
3.2.安裝連接件vfsmount
“安裝”一個文件系統涉及“被安裝設備”和“安裝點”兩個部分,安裝的過程就是把“安裝點”和“被安裝設備”關聯起來,這是通過一個“安裝連接件”結構 vfsmount 來完成的。
vfsmount 將“安裝點”dentry 和“被安裝設備”的根目錄節點 dentry 關聯起來。
所以,在安裝文件系統時,內核的主要工作就是:
1、 創建一個 vfsmount
2、 為“被安裝設備”創建一個 super_block,並由具體的文件系統來設置這個 super_block。
3、 為被安裝設備的根目錄節點創建 dentry
4、 為被安裝設備的根目錄節點創建 inode, 並由 super_operations->read_inode() 來設置此 inode
5、 將 super_block 與“被安裝設備“根目錄節點 dentry 關聯起來
6、 將 vfsmount 與“被安裝設備”的根目錄節點 dentry 關聯起來
來看下內核中的代碼:
int get_sb_single(struct file_system_type *fs_type,
int flags, void *data,
int (*fill_super)(struct super_block *, void *, int),
struct vfsmount *mnt)
{
struct super_block *s;
int error;
s = sget(fs_type, compare_single, set_anon_super, NULL);
if (IS_ERR(s))
return PTR_ERR(s);
if (!s->s_root) {
s->s_flags = flags;
error = fill_super(s, data, flags & MS_SILENT ? 1 : 0);
if (error) {
deactivate_locked_super(s);
return error;
}
s->s_flags |= MS_ACTIVE;
} else {
do_remount_sb(s, flags, data, 0);
}
simple_set_mnt(mnt, s);
return 0;
}
這個函數中的fill_super是個函數指針,是由我們自己實現的,去填充super_block,並且為被安裝設備的根目錄分配inode和dentry。最后通過simple_set_mnt()函數將super和dentry與vfsmount連接起來。(這樣做的目的就是為了后面查找文件)
void simple_set_mnt(struct vfsmount *mnt, struct super_block *sb)
{
mnt->mnt_sb = sb;
mnt->mnt_root = dget(sb->s_root);
}
在內核將根設備安裝到“根安裝點”上后,內存中有如下結構關系:
現在假設我們在 /mnt/win 下安裝了 /dev/sda1, /dev/sda1 下有 dir1,然后又在 dir1 下安裝了 /dev/sda2,那么內存中就有了如下的結構關系
3.3尋找目標節點
VFS 中一個最關鍵以及最頻繁的操作,就是根據路徑名尋找目標節點的 dentry 以及 inode 。
例如要打開 /mnt/win/dir1/abc 這個文件,就是根據這個路徑,找到‘abc’ 對應的 dentry ,進而得到 inode 的過程。
1、 首先找到根文件系統的根目錄節點 dentry 和 inode
2、 由這個 inode 提供的操作接口 i_op->lookup(),找到下一層節點 ‘mnt’ 的 dentry 和 inode
3、 由 ‘mnt’ 的 inode 找到 ‘win’ 的 dentry 和 inode
4、 由於 ‘win’ 是個“安裝點”,因此需要找到“被安裝設備”/dev/sda1 根目錄節點的 dentry 和 inode,只要找到 vfsmount B,就可以完成這個任務。
5、 然后由 /dev/sda1 根目錄節點的 inode 負責找到下一層節點 ‘dir1’ 的 dentry 和 inode
6、 由於 dir1 是個“安裝點”,因此需要借助 vfsmount C 找到 /dev/sda2 的根目錄節點 dentry 和 inode
7、 最后由這個 inode 負責找到 ‘abc’ 的 dentry 和 inode
4.文件的讀寫
一個文件每被打開一次,就對應着一個 file 結構。 我們知道,每個文件對應着一個 dentry 和 inode,每打開一個文件,只要找到對應的 dentry 和 inode 不就可以了么?為什么還要引入這個 file 結構?
這是因為一個文件可以被同時打開多次,每次打開的方式也可以不一樣。 而dentry 和 inode 只能描述一個物理的文件,無法描述“打開”這個概念。
因此有必要引入 file 結構,來描述一個“被打開的文件”。每打開一個文件,就創建一個 file 結構。
**實際上,打開文件的過程正是建立file, dentry, inode 之間的關聯的過程。 **
文件的讀寫
文件一旦被打開,數據結構之間的關系已經建立,后面對文件的讀寫以及其它操作都變得很簡單。就是根據 fd 找到 file 結構,然后找到 dentry 和 inode,最后通過 inode->i_fop 中對應的函數進行具體的讀寫等操作即可。
5.下面是自己寫的一個小型的文件系統
#include <linux/module.h>
#include <linux/string.h>
#include <linux/fs.h>
#include <linux/time.h>
#include <linux/slab.h>
#include <linux/init.h>
#include <linux/blkdev.h>
#include <linux/parser.h>
#include <linux/smp_lock.h>
#include <linux/buffer_head.h>
#include <linux/exportfs.h>
#include <linux/vfs.h>
#include <linux/random.h>
#include <linux/mount.h>
#include <linux/namei.h>
#include <linux/quotaops.h>
#include <linux/seq_file.h>
#include <asm/uaccess.h>
//mount -t wzjfs /root/t1/ /root/t1/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wzj");
#define wzjfs_MAGIC 0x19980122
static DEFINE_RWLOCK(file_systems_lock);
static struct inode *wzjfs_make_inode(struct super_block *sb, int mode)
{
struct inode *ret = new_inode(sb);
if (ret) {
ret->i_mode = mode;
ret->i_uid = ret->i_gid = 0;
ret->i_blocks = 0;
ret->i_atime = ret->i_mtime = ret->i_ctime = CURRENT_TIME;
}
return ret;
}
static int wzjfs_open(struct inode *inode, struct file *filp)
{
filp->private_data = inode->i_private;
return 0;
}
#define TMPSIZE 20
static ssize_t wzjfs_read_file(struct file *filp, char *buf,
size_t count, loff_t *offset)
{
atomic_t *counter = (atomic_t *) filp->private_data;
int v, len;
char tmp[TMPSIZE];
v = atomic_read(counter);
if (*offset > 0)
v -= 1;
else
atomic_inc(counter);
len = snprintf(tmp, TMPSIZE, "%d\n", v);
if (*offset > len)
return 0;
if (count > len - *offset)
count = len - *offset;
if (copy_to_user(buf, tmp + *offset, count))
return -EFAULT;
*offset += count;
return count;
}
static ssize_t wzjfs_write_file(struct file *filp, const char *buf,
size_t count, loff_t *offset)
{
atomic_t *counter = (atomic_t *) filp->private_data;
char tmp[TMPSIZE];
if (*offset != 0)
return -EINVAL;
if (count >= TMPSIZE)
return -EINVAL;
memset(tmp, 0, TMPSIZE);
if (copy_from_user(tmp, buf, count))
return -EFAULT;
atomic_set(counter, simple_strtol(tmp, NULL, 10));
return count;
}
static struct file_operations wzjfs_file_ops = {
.open = wzjfs_open,
.read = wzjfs_read_file,
.write = wzjfs_write_file,
};
static struct dentry *wzjfs_create_file (struct super_block *sb,
struct dentry *dir, const char *name,
atomic_t *counter)
{
struct dentry *dentry;
struct inode *inode;
struct qstr qname;
qname.name = name;
qname.len = strlen (name);
qname.hash = full_name_hash(name, qname.len);
dentry = d_alloc(dir, &qname);
if (! dentry)
goto out;
inode = wzjfs_make_inode(sb, S_IFREG | 0644);
if (! inode)
goto out_dput;
inode->i_fop = &wzjfs_file_ops;
inode->i_private = counter;
d_add(dentry, inode);
return dentry;
out_dput:
dput(dentry);
out:
return 0;
}
static struct dentry *wzjfs_create_dir (struct super_block *sb,
struct dentry *parent, const char *name)
{
struct dentry *dentry;
struct inode *inode;
struct qstr qname;
qname.name = name;
qname.len = strlen (name);
qname.hash = full_name_hash(name, qname.len);
//dentry的主要作用是建立文件名和inode之間的關聯。
/*所以該結構體包括兩個最主要的字段,d_inode和d_name。
其中,d_name為文件名。qstr是內核對字符串的封裝(可以理解為帶有散列值的char*)。
d_inode是與該文件名對應的inode。*/
dentry = d_alloc(parent, &qname);
if (! dentry)
goto out;
inode = wzjfs_make_inode(sb, S_IFDIR | 0644);
if (! inode)
goto out_dput;
inode->i_op = &simple_dir_inode_operations;
inode->i_fop = &simple_dir_operations;
d_add(dentry, inode);
return dentry;
out_dput:
dput(dentry);
out:
return 0;
}
static atomic_t counter, subcounter;
static void wzjfs_create_files (struct super_block *sb, struct dentry *root)
{
struct dentry *subdir;
atomic_set(&counter, 0);
wzjfs_create_file(sb, root, "counter", &counter);
atomic_set(&subcounter, 0);
subdir = wzjfs_create_dir(sb, root, "subdir");
if (subdir)
wzjfs_create_file(sb, subdir, "subcounter", &subcounter);
}
static struct super_operations wzjfs_s_ops = {
.statfs = simple_statfs,
.drop_inode = generic_delete_inode,
};
static int wzjfs_fill_super (struct super_block *sb, void *data, int silent)
{
struct inode *root;
struct dentry *root_dentry;
sb->s_blocksize = PAGE_CACHE_SIZE;
sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
sb->s_magic = wzjfs_MAGIC;
sb->s_op = &wzjfs_s_ops;
printk(KERN_INFO "wzjfs_fill_super is here\n");
root = wzjfs_make_inode (sb, S_IFDIR | 0755);
if (! root)
goto out;
root->i_op = &simple_dir_inode_operations;
root->i_fop = &simple_dir_operations;
root_dentry = d_alloc_root(root);
if (! root_dentry)
goto out_iput;
sb->s_root = root_dentry;
wzjfs_create_files (sb, root_dentry);
return 0;
out_iput:
iput(root);
out:
return -ENOMEM;
}
static int wzjfs_get_super(struct file_system_type *fst,int flags, const char *devname, void *data,struct vfsmount *mount)
{
printk(KERN_INFO "mount from user\n");
return get_sb_single(fst, flags, data, wzjfs_fill_super,mount);
}
static struct file_system_type wzjfs_type = {
.owner = THIS_MODULE,
.name = "wzjfs",
.get_sb = wzjfs_get_super,
.kill_sb = kill_litter_super,
};
static int __init wzjfs_init(void)
{
struct file_system_type * tmp;
printk("wzjfs_init ok\n");
return register_filesystem(&wzjfs_type);
}
static void __exit wzjfs_exit(void)
{
unregister_filesystem(&wzjfs_type);
printk("wzjfs_exit ok\n");
}
module_init(wzjfs_init);
module_exit(wzjfs_exit);
Makefile
ifneq ($(KERNELRELEASE),)
obj-m := wzj.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.order *.symvers *.unsigned
insmod 模塊后,執行mount -t wzjfs /root/t1 /root/t1(目錄自己指定),再 去你指定的目錄下,你會發現屬於自己文件系統的文件。當你在看上面的代碼有想不通的時候,看看上面的知識點,或許就會明白了。(我就是這樣的,O(∩_∩)O)。