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)。