VFS文件系统

        可以认为Linux内核就是围绕文件系统进行展开设计的。从文件系统引出设备文件的概念,设备文件又可以引申到字符设备和块设备,这样就从文件系统过渡到设备管理。设备管理包含了设备驱动,设备驱动要用到中断,设备里面的块设备又控制了通用块层和I/O调度。而文件系统向外引申和网络的socket联系。从文件系统出发,层次推进基本囊括了内核应用层的重要概念和架。

文件系统的基本概念

        在深入分析文件系统之前,有必要介绍文件系统的几个基本概念,这将从架构层次理解文件系统设计的目的,从而从全局层面理解内核文件系统的代码,大大减低分析代码的难度和工作量。

什么是VFS

        内核通过VFS管理各个文件系统,VFS为所有的文件系统提供了统一接口,Linux对于每个具体的文件系统都是通过VFS定义的接口访问的。同时VFS也是一种标准,所有文件系统的设计都必须要遵守这个标准。

        VFS本身只存在于内存里面,在内存里面来表示一个物理硬盘,就需要有相应的数据结构来表示,或者说抽象。dentry、inode、super_block这三个数据结构对一个物理硬盘进行抽象,在内存里面表示出来。也通过dentry、inode可以对文件系统进行一些读写的操作,但是这些操作出来的数据还是在内存里面的,只有在合适的时候才会从内存写回到硬盘或者块设备上。

super_block

        超级块是一个文件系统的控制块,就和进程控制块PCB、或者RTOS上的TCB很像。其中包括了文件块大小、超级块操作函数,还有很多链表,比如说文件系统内所有的inode节点都要链接到超级块里面的inode链表上。而超级块是需要通过读取硬盘里面的超级块结构才能获得的,超级块就是一个具体文件系统在内存里面的抽象。超级块结构很庞大,没必要每个成员都详细了解,下面是简化的代码。

struct super_block {
    unsigned char        s_blocksize_bits;
    unsigned long        s_blocksize;    
    struct file_system_type    *s_type;
    const struct super_operations    *s_op;//最重要的超级块操作,例如read_inode

    unsigned long        s_magic;
    struct dentry        *s_root;//指向根目录dentry
    struct list_head    s_inodes;    /* all inodes */
    struct list_head    s_files;

    struct list_head    s_mounts;    /* list of mounts; _not_ for fs use */
    struct block_device    *s_bdev;//指向文件系统所在的块设备
    void             *s_fs_info;    /* Filesystem private info */
    const struct dentry_operations *s_d_op; /* default d_op for dentries */
}

dentry

        Linux里面文件是按照树状结构来保存的,通过一层层汇聚最终到达根目录,所以VFS是有对应的数据结构来反映这种树状结构,那就是dentry结构。

        在VFS里面,每个文件(目录也是一种文件)都有一个dentry,dentry链接到上级目录的dentry。根目录有一个dentry,称它为根dentry。根目录下面的所有目录、文件都链接到这个根dentry,如果是二级目录,则二级目录的dentry链接到一级目录的dentry,最终构成树状结构。

        而Linux内核里面为了快速查找dentry,用到了dentry cache来缓存,所以在查找一个目录时,首先是从dentry cache里面开始查找。dentry结构一样很庞大,我们精简一下。

struct dentry {

    struct hlist_bl_node d_hash;    /* lookup hash list */
    struct dentry *d_parent;    /* parent directory */
    struct qstr d_name;//文件或者目录名字,打开一个文件时,通过该成员和用户输入的名字对比
    struct inode *d_inode;        /* Where the name belongs to - NULL is
                     * negative */
    const struct dentry_operations *d_op;
    struct super_block *d_sb;    /* The root of the dentry tree */

    struct list_head d_subdirs;    /* our children */
    struct hlist_node d_alias;    /* inode alias list */
};

inode

        inode代表一个文件,它保存了文件的元信息,如大小、创建时间、修改时间,还有最重要的对文件的读写操作函数、文件的读写缓存信息,一个文件可以有多个dentry但是inode只能有一个,因为可以有不同的路径指向一个文件,所以会有多个dentry。同样,inode结构庞大,无需全部理解,精简即可。

struct inode {
    const struct inode_operations    *i_op;
    struct super_block    *i_sb;
    struct address_space    *i_mapping;//文件的缓存,在内存里面,先从内存读写,后面写回硬盘

    dev_t            i_rdev;//设备号
    loff_t            i_size;//文件长度、字节为单位

    unsigned int        i_blkbits;//文件块位数
    blkcnt_t        i_blocks;//块大小

    struct hlist_node    i_hash;
    struct list_head    i_wb_list;    /* backing dev IO list */
    struct hlist_head    i_dentry;
    struct list_head    i_sb_list;
    const struct file_operations    *i_fop;    /* former ->i_op->default_file_ops */
};

文件

        文件是描述进程和文件交互的关系。磁盘上并不存在这个结构,进程打开一个文件,就在内存里面动态创建一个文件对象。同一个文件在不同进程中是不同的对象。

struct file {
    struct list_head    fu_list;

    struct path        f_path;

    struct inode        *f_inode;    /* cached value */
    const struct file_operations    *f_op;

    fmode_t            f_mode;
    loff_t            f_pos;//对文件操作的位置 偏移量

    /* needed for tty driver, and maybe others */
    void            *private_data;

    struct address_space    *f_mapping; //指向文件的读写缓存页面
};

        VFS是具体文件系统的抽象,而VFS又是依靠上面描述的四个数据结构来发挥作用的。所以实际上对文件系统的操作(读写)其实就是在操作这些个数据结构。这四个数据结构之间呢也会存在一些联系,比如前面提到,文件系统内所有的inode都会链接到超级块里面的链表。

具体代码分析

        文件系统很抽象,如果只看概念性的东西,其实也很难真正对它进行理解,所以还是得分析源码。底层再复杂都是为了用户用的更便捷,所以对于用户而言,无非就是打开文件、创建文件(目录)、文件的读写。如果是涉及到基于磁盘的文件系统,还会涉及到块设备的操作,从文件系统层到通用块层再到IO调度层,还要再到块设备驱动层,实在是太复杂了。所以,为了简化分析,目前还是只分析文件系统层就好了。

具体的代码入口

        想要分析文件系统代码,肯定要找到一个切入点,下面给出一段代码(内核里面的文件系统注册和下面代码如出一辙),该段代码只是为了说明我们分析文件系统大体的一个思路,如下。

static int _init aufs_init(void)
{
    retval = register_filesystem(&au_fs_type);

    aufs_mount = kern_mount(&au_fs_type);

    pslot = aufs_create_dir("woman star",NULL);
    aufs_create_file("1bb",S_IFREGI S_IRUGO,pslot,NULL,NULL);
    aufs_create_file("fbb",S_IFREGI S_IRUGO,pslot,NULL,NULL);
    aufs_create_file("lll",S_IFREGI S_IRUGO,pslot,NULL,NULL);
    
    pslot = aufs_create_dir("man star",NULL);
    aufs_create_file("ldh",S_IFREGIS_IRUGO,pslot,NULL,NULL);
    aufs_create_file("lcw",S_IFREGIS_IRUGO,pslot,NULL,NULL);
    aufs_create_flle("jw",S_IFREGIS_IRUGO,pslot,NULL,NULL);
    
    return retval;
}

        上面的代码很简单,思路就是,一、调用register_filesystem向内核注册一个文件系统;二、调用kern_mount函数挂载文件系统;三、在该文件系统下面创建目录、文件。所以我们的分析思路就是这三个步骤。

register_filesystem

        方法实现如下。该方法只是用于向内核注册一个文件系统,内核自定义了一个file_systems全局链表,所有注册的文件系统都在这条链表上,所以这个方法很简单,就是在链表里面遍历查找要注册的文件系统,能找到就返回EBUSY,找不到就放到链表末尾。显然,没去操作那四个重要的数据结构,肯定是放在下面的代码里面去操作了。

int register_filesystem(struct file_system_type * fs)
{
    int res = 0;
    struct file_system_type ** p;


    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;
}
kern_mount

        kern_mount是一个宏定义,真身为kern_mount_data,其中又套了一层,最终真正的函数如下代码,传入的参数就是要注册的文件系统对象,文件系统名字,data为NULL。返回值是一个vfsmount对象,这个对象很重要,它是用来联系文件系统直接的挂载关系的。

struct vfsmount *vfs_kern_mount(struct file_system_type *type, int flags, const char *name,                             
                                void *data)
{
    struct mount *mnt;
    struct dentry *root;  
      
    mnt = alloc_vfsmnt(name);
 
    root = mount_fs(type, flags, name, data);
   
    mnt->mnt.mnt_root = root;
    mnt->mnt.mnt_sb = root->d_sb;
    mnt->mnt_mountpoint = mnt->mnt.mnt_root;
    mnt->mnt_parent = mnt;
    br_write_lock(&vfsmount_lock);
    list_add_tail(&mnt->mnt_instance, &root->d_sb->s_mounts);
    br_write_unlock(&vfsmount_lock);
    return &mnt->mnt;
}

        上述代码也是做了三件事,分配一个mnt对象,类型就是vfsmount。然后调用mount_fs,最后设置mnt对象的各个成员。其中最核心的是第二步,继续展开看看。

struct dentry *mount_fs(struct file_system_type *type, int flags, const char *name, void 
                          *data)
{
    struct dentry *root;
    struct super_block *sb;
    int error = -ENOMEM;
    root = type->mount(type, flags, name, data);
    sb = root->d_sb;

    sb->s_flags |= MS_BORN;
    
    up_write(&sb->s_umount);

    return root;
}

        上面省略了很多代码,保留了核心代码,其中最核心的代码就是type->mount,这是个回调函数,实际指向是最底层的具体文件系统的mount函数,例如ext4_mount、sysfs_mount。这里就借助sysfs_mount方法来分析,因为其他文件系统都大同小异。

static struct dentry *sysfs_mount(struct file_system_type *fs_type,
                                int flags, const char *dev_name, void *data)
{

    info = kzalloc(sizeof(*info), GFP_KERNEL);
        
    sb = sget(fs_type, sysfs_test_super, sysfs_set_super, flags, info);
  
    if (!sb->s_root) {
        error = sysfs_fill_super(sb, data, flags & MS_SILENT ? 1 : 0);
    }

    return dget(sb->s_root);
}

        同样去掉一些繁多的判断代码,留下要分析的代码,其中最核心的就是sget和sysfs_fill_super两个方法,sget方法就不展开了,它是用于得到一个超级块,先查表看有没有该文件系统的超级块,没有就创建一个超级块并对超级块内部一些成员做初始化,然后再交给sysfs_fill_super方法去填充超级块里面其他一些成员,下面给出代码。

static int sysfs_fill_super(struct super_block *sb, void *data, int silent)
{
    struct inode *inode;
    struct dentry *root;

     sb->s_blocksize = PAGE_CACHE_SIZE;
     sb->s_blocksize_bits = PAGE_CACHE_SHIFT;
     sb->s_magic = SYSFS_MAGIC;
     sb->s_op = &sysfs_ops;
     sb->s_time_gran = 1;

     inode = sysfs_get_inode(sb, &sysfs_root);
     root = d_make_root(inode);
     root->d_fsdata = &sysfs_root;
     sb->s_root = root;
     sb->s_d_op = &sysfs_dentry_ops;
     return 0;
}

         由此可见sysfs_fill_super方法就是用于填充超级块的一些成员,还有超级块操作方法集sys_ops;利用sysfs_get_inode方法创建一个inode节点,同时为inode节点成员赋值,操作方法赋值;再利用d_make_root创建一个根目录,把根目录和刚刚创建好的inode节点绑定起来,表示为该超级块的"/"目录;最后再让超级块的根目录成员指向刚刚创建的目录,设置超级块目录操作方法成员。

        到此,kern_mount就分析完了,总结起来就是,kern_mount会创建超级块、根dentry、根inode;同时还会设置这三个对象里面的各个成员,各个操作方法集。返回到vfs_kern_mount,然后让mnt对象里面的一些成员指向刚刚创建的根目录、超级块,把超级块添加到mnt链表里面,然后返回。

创建目录、文件

        分析了前面两个步骤,剩下的就是创建目录和文件了。回忆前面的内容,文件就是由dentry和inode表示的,所以实际上创建目录、文件的操作,就是在创建dentry、inode对象,然后再把两个对象通过方法联系起来。只是,这个过程会涉及到大量的细节的处理,比如要创建文件有没有父目录,没有就说明是在根目录下面创建;还需要看看有没有重名的、已经存在的文件,不存在就创建,创建好了还要继续判断有没有重名的存在的,在dentry_hashtable里面找,进行判断;dentry弄好了就弄inode,然后通过dentry层层链接,形成一棵树状。

        值得一提的是,这些过程方法是随着内核版本的变化而变化的,所以不同的版本是不同的处理方法的,具体的需要对照自己用的版本去看源代码。这里给个2.6.18版本的例子。

static int create_dir(struct kobject * k, struct dentry * p,const char * n, 
                      struct dentry ** d)
{    
    int error;    umode_t mode = S_IFDIR| S_IRWXU | S_IRUGO | S_IXUGO; 
    *d = lookup_one_len(n, p, strlen(n));    if (!IS_ERR(*d)) 
    {         
        if (sysfs_dirent_exist(p->d_fsdata, n))              
            error = -EEXIST;          
        else            
            error = sysfs_make_dirent(p->d_fsdata, *d, k, mode,    SYSFS_DIR);
        if (!error) 
        {            
            error = sysfs_create(*d, mode, init_dir);            
            if (!error) 
            {                
                p->d_inode->i_nlink++;                
                (*d)->d_op = &sysfs_dentry_ops;    
                 d_rehash(*d);            
             }        
          }        
          if (error && (error != -EEXIST)) 
          {            
              struct sysfs_dirent *sd = (*d)->d_fsdata;            
              if (sd) 
              {                 
                  list_del_init(&sd->s_sibling);
                  sysfs_put(sd);            
              }            
               d_drop(*d);        
           }        
           dput(*d);    
}

mount过程

        通过上面的说明,现在内核里面已经有这样一颗树了,但是这棵树还不能供用户使用,因为还需要mount挂载文件系统。从理论上讲,系统本身存在一个“/”文件系统,如果把上面的这个文件系统挂载到系统本身的文件系统里面,就可以识别到并且使用了。

        mount挂载首先执行系统调用sys_mount,然后执行do_mount,在do_mount里面会获取挂载点的dentry和vfs_mount对象,并保存在nameidata对象里面,然后根据不同的选项执行不同的函数,但是对于第一次挂载的文件系统,会执行do_new_mount,重点也是它。如下精简代码

static int do_new_mount(struct path *path, const char *fstype, int flags,
            int mnt_flags, const char *name, void *data)
{
    struct file_system_type *type;

    struct vfsmount *mnt;
    int err;

    type = get_fs_type(fstype);
    
    mnt = vfs_kern_mount(type, flags, name, data);

    err = do_add_mount(real_mount(mnt), path, mnt_flags);

}

        上面的代码是保留的主要内容。先获取要挂载的文件系统,然后获得该文件系统的vfs_mount对象,最后执行do_add_mount函数进行挂载,接着往下分析do_add_mount函数。

static int do_add_mount(struct mount *newmnt, struct path *path, int mnt_flags)
{
    struct mountpoint *mp;
    struct mount *parent;
    int err;

    mp = lock_mount(path);
    
    parent = real_mount(path->mnt);

    if (path->mnt->mnt_sb == newmnt->mnt.mnt_sb &&
        path->mnt->mnt_root == path->dentry)
        goto unlock;

    if (S_ISLNK(newmnt->mnt.mnt_root->d_inode->i_mode))
        goto unlock;
    err = graft_tree(newmnt, parent, mp);
    unlock_mount(mp);
    return err;
}

        这段代码的作用就是,首先获得挂载点对象mp,里面包括了挂载点的哈希链表、挂载点的dentry对象,然后又使用real_mount(这是一个宏定义,用的container_of)去获得挂载点的mount对象,然后下面就进行一系列的判断,第一个判断是看该文件系统在之前有没有挂载过,后面判断是不是link文件,最后又调用graft_tree方法,将需要挂载的文件系统嫁接到挂载点下面,来看看这个方法。

static int graft_tree(struct mount *mnt, struct mount *p, struct mountpoint *mp)
{
    if (mnt->mnt.mnt_sb->s_flags & MS_NOUSER)
        return -EINVAL;

    if (S_ISDIR(mp->m_dentry->d_inode->i_mode) !=
S_ISDIR(mnt->mnt.mnt_root->d_inode->i_mode))
        return -ENOTDIR;

    return attach_recursive_mnt(mnt, p, mp, NULL);
}


//继续看一下attach_recursive_mnt方法
static int attach_recursive_mnt(struct mount *source_mnt,
            struct mount *dest_mnt,
            struct mountpoint *dest_mp,
            struct path *parent_path)
{
    struct mount *child, *p;

    if (parent_path) {
        detach_mnt(source_mnt, parent_path);
        attach_mnt(source_mnt, dest_mnt, dest_mp);
        touch_mnt_namespace(source_mnt->mnt_ns);
    } else {
        mnt_set_mountpoint(dest_mnt, dest_mp, source_mnt);
        commit_tree(source_mnt);
    }

    list_for_each_entry_safe(child, p, &tree_list, mnt_hash) {
        list_del_init(&child->mnt_hash);
        commit_tree(child);
    }
}

        因为传递进来的参数为NULL,所以只会走else分支,mnt_set_mountpoint方法会设置要挂载的文件系统的mnt指向,让mnt里面的某些成员指向挂载点dest_mp,由此要挂载的文件系统(暂且称为源文件系统吧)和挂载点就联系起来了,但是还需要通过commit_tree方法将源文件系统加入到全局hash表,这样所有的进程都可以看到和操作这个源文件系统了。commit_tree方法如下。

static void commit_tree(struct mount *mnt)
{
    struct mount *parent = mnt->mnt_parent;
    struct mount *m;
    LIST_HEAD(head);
    struct mnt_namespace *n = parent->mnt_ns;

    list_add_tail(&head, &mnt->mnt_list);
    list_for_each_entry(m, &head, mnt_list)
        m->mnt_ns = n;

    list_splice(&head, n->list.prev);

    list_add_tail(&mnt->mnt_hash, mount_hashtable +
                hash(&parent->mnt, mnt->mnt_mountpoint));
    list_add_tail(&mnt->mnt_child, &parent->mnt_mounts);
    touch_mnt_namespace(n);
}

        上面就是设置mount对象里面的各种链表,其中最重要的就是mount_hashtable全局哈希链表,这样子任何进程就可以通过这个哈希链表找到我们的源文件系统。至此mount过程分析完毕。

open过程

        系统调用open过程实在是太复杂了,涉及到了太多太多的调用,调用过程还考虑了太多的情况:一是路径解析;二是. 和..目录的处理,尤其是..目录处理可能还会涉及到文件系统的切换;三是查询能不能找到路径里面的dentry目录项;经过处理一直到最后才会调用到具体文件系统的open接口,由于调用过程代码实在是太多了,这里就不给出代码了,直接给出核心函数调用过程即可,并加以说明。

  1. do_sys_open会调用get_unused_fd_flags得到一个空闲fd,调用do_filp_open得到一个file结构体,并把这两项关联起来。
  2. do_filp_open会先后以RCU模式和普通模式调用path_openat,已返回file,同时蕴含这inode和dentry。
  3. path_openat会先调用path_init根据传入的参数确定查找的起始路径,然后调用link_path_walk对每一项进行遍历,最后调用do_last执行一个钩子函数,也就是文件系统自带的inode.open。
  4. link_path_walk会调用walk_component来驱动循环。
  5. walk_component会根据不同的遍历项类型调用不同的处理函数,比如“.”“. .”就会调用handle_dots,而对于一般项就会调用do_lookup查找dentry和inode。
  6. do_lookup会先查找dcache,如果不存在的话就会调用文件系统的钩子函数inode.lookup查找对应的inode和dentry。
  7. handle_dots在处理“. .”的时候可能会遇到文件系统之间的切换,这里需要做一点处理。

        上面3—6步骤就是最复杂的,不得不感叹欧美那边的大佬的实力!最后呢进程的fdtable会和文件系统里面的inode联系起来,并且返回fdtable数组的下标,这个下标就是进程里面的文件描述符,然后数组是一个file类型的指针,file里面有fops文件操作方法,它是文件在open过程中,把inode里面的fops赋值给file的。所以在应用层读写文件,就会使用到inode里面的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值