zoukankan      html  css  js  c++  java
  • Linux 虚拟文件系统VFS的简单探究

    前言

    在我的上一篇博客,已经简单了解了一下Linux的文件系统(主要是以 EXT2 为说明),在这篇博客,我将简单了解一下Linux中VFS的实现。

    什么是VFS

    Linux 允许众多不同的文件系统共存,并支持跨文件系统的文件操作,这是因为有虚拟文件系统的存在。虚拟文件系统(Virtual Filesystem),是一个内核软件层,用来处理与Unix标准文件系统相关的所有系统调用。其健壮性表现在能为各种文件系统提供一个通用的接口。VFS是用户的应用程序与文件系统实现之间的抽象层,应用程序不需要知道操作的文件是什么文件系统类型,直接与VFS交互,VFS再与不同的文件系统交互。
    下图所示的体系结构显示了用户空间和内核中与文件系统相关的组件之间的关系:

    为了能够支持各种实际文件系统,VFS 定义了所有文件系统都支持的基本的、概念上的接口和数据结构;同时实际文件系统也提供 VFS 所期望的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式上与VFS的定义保持一致。换句话说,一个实际的文件系统想要被 Linux 支持,就必须提供一个符合VFS标准的接口,才能与 VFS 协同工作。实际文件系统在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS 层和内核的其他部分看来,所有文件系统都是相同的。这个就叫做通用的文件模型(common file model)。

    相关数据结构

    VFS主要对象及其数据结构

    虚拟文件系统(VFS)支持的文件系统可以分成主要三种类型

    1. 基于磁盘(disk-based)的文件系统
      管理本地磁盘和模拟磁盘的设备,基于磁盘的文件系统有:
    • Linux的Ext2、Ext3和ReiserFS
    • Unix文件系统的变种,如sysv、UFS、MINIX和VERITAS VxFS。
    • 微软的文件系统,如MS-DOS、VFAT、NFTS
    • ISO9660 CD-ROM文件系统和UDF(Universal Disk Format)DVD文
    • 件系统
    • 其他文件系统如IBM OS/2中的HPFS、Apple’s Macintosh的HFS、AFFS
    • 和ADFS等。
    1. 网络文件系统
      它允许方便访问网络上其他计算机的文件系统。VFS支持的网络文件系统有NFS、Coda、AFS(Andrew filesystem)、CIFS(Common Internet Filesystem)和NCP(Novell’s NetWare Core Protocol)。
    2. 特殊文件系统
      它不管理磁盘空间。/proc就是一个特殊的文件系统。

    Linux为了实现这种VFS系统,采用面向对象的设计思路,主要抽象了四种对象类型:

    • 超级块对象:代表一个已安装的文件系统。
    • 索引节点对象:代表具体的文件。
    • 目录项对象:代表一个目录项,是文件路径的一个组成部分。
    • 文件对象:代表进程打开的文件。

    每个对象都包含一组操作方法,用于操作相应的文件系统。

    下面我们来看看对应的源码!

    super_block 超级块

    超级块(spuerblock)对象由各自的文件系统实现,用来存储文件系统的信息。这个对象对应为文件系统超级块或者文件系统控制块,它存储在磁盘特定的扇区上。不是基于磁盘的文件系统(基于内存的虚拟文件系统,如sysfs)临时生成超级块,并保存在内存中。超级块存储一个已安装的文件系统的控制信息,代表一个已安装的文件系统。

    /include/linux/fs.h */
    /* 
     * 超级块结构中定义的字段非常多,
     * 这里只介绍一些重要的属性
     */
    struct super_block {
        struct list_head    s_list;               /* 指向所有超级块的链表 */
        const struct super_operations    *s_op; /* 超级块方法 */
        struct dentry        *s_root;           /* 目录挂载点 */
        struct mutex        s_lock;            /* 超级块信号量 */
        int            s_count;                   /* 超级块引用计数 */
     
        struct list_head    s_inodes;           /* inode链表 */
        struct mtd_info        *s_mtd;            /* 存储磁盘信息 */
        fmode_t            s_mode;                /* 安装权限 */
    };
     
    /*
     * 其中的 s_op 中定义了超级块的操作方法
     * 这里只介绍一些相对重要的函数
     */
    struct super_operations {
        struct inode *(*alloc_inode)(struct super_block *sb); /* 创建和初始化一个索引节点对象 */
        void (*destroy_inode)(struct inode *);                /* 释放给定的索引节点 */
        void (*dirty_inode) (struct inode *);                 /* VFS在索引节点被修改时会调用这个函数 */
        int (*write_inode) (struct inode *, int);             /* 将索引节点写入磁盘,wait表示写操作是否需要同步 */
        void (*drop_inode) (struct inode *);                  /* 最后一个指向索引节点的引用被删除后,VFS会调用这个函数 */
        void (*delete_inode) (struct inode *);                /* 从磁盘上删除指定的索引节点 */
        void (*put_super) (struct super_block *);             /* 卸载文件系统时由VFS调用,用来释放超级块 */
        void (*write_super) (struct super_block *);           /* 用给定的超级块更新磁盘上的超级块 */
        int (*sync_fs)(struct super_block *sb, int wait);     /* 使文件系统中的数据与磁盘上的数据同步 */
        int (*statfs) (struct dentry *, struct kstatfs *);    /* VFS调用该函数获取文件系统状态 */
        int (*remount_fs) (struct super_block *, int *, char *); /* 指定新的安装选项重新安装文件系统时,VFS会调用该函数 */
        void (*clear_inode) (struct inode *);                 /* VFS调用该函数释放索引节点,并清空包含相关数据的所有页面 */
        void (*umount_begin) (struct super_block *);          /* VFS调用该函数中断安装操作 */
    };
    

    注:所有以上函数都是由VFS在进程上下文中调用。必要时,它们都可以阻塞。这其中的一些函数是可选的:在超级块操作表中,文件系统可以将不需要的函数指针设置成NULL。如果VFS发现操作函数指针是NULL,那它要么就会调用通用函数执行相应操作,要么什么也不做,如何选择取决于具体函数。

    inode 索引节点

    索引节点对象存储了文件的相关信息,代表了存储设备上的一个实际的物理文件。当一个文件首次被访问时,内核会在内存中组装相应的索引节点对象,以便向内核提供对一个文件进行操作时所必需的全部信息;这些信息一部分存储在磁盘特定位置,另外一部分是在加载时动态填充的。inode它具有惟一标识符。各个文件系统提供将文件名映射为惟一 inode 标识符和 inode 引用的方法 ,文件名可以随时更改,但是索引节点对文件是唯一的,并且随文件的存在而存在。

    /include/linux/fs.h
    /* 
     * 索引节点结构中定义的字段非常多,
     * 这里只介绍一些重要的属性
     */
    struct inode {
        struct hlist_node    i_hash;     /* 散列表,用于快速查找inode */
        struct list_head    i_list;        /* 索引节点链表 */
        struct list_head    i_sb_list;  /* 超级块链表超级块  */
        struct list_head    i_dentry;   /* 目录项链表 */
        unsigned long        i_ino;      /* 节点号 */
        atomic_t        i_count;        /* 引用计数 */
        unsigned int        i_nlink;    /* 硬链接数 */
        uid_t            i_uid;          /* 使用者id */
        gid_t            i_gid;          /* 使用组id */
        struct timespec        i_atime;    /* 最后访问时间 */
        struct timespec        i_mtime;    /* 最后修改时间 */
        struct timespec        i_ctime;    /* 最后改变时间 */
        const struct inode_operations    *i_op;  /* 索引节点操作函数 */
        const struct file_operations    *i_fop;    /* 缺省的索引节点操作 */
        struct super_block    *i_sb;              /* 相关的超级块 */
        struct address_space    *i_mapping;     /* 相关的地址映射 */
        struct address_space    i_data;         /* 设备地址映射 */
        unsigned int        i_flags;            /* 文件系统标志 */
        void            *i_private;             /* fs 私有指针 */
    };
     
    /*
     * 其中的 i_op 中定义了索引节点的操作方法
     * 这里只介绍一些相对重要的函数
     */
    struct inode_operations {
        /* 为dentry对象创造一个新的索引节点 */
        int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
        /* 在特定文件夹中寻找索引节点,该索引节点要对应于dentry中给出的文件名 */
        struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
        /* 创建硬链接 */
        int (*link) (struct dentry *,struct inode *,struct dentry *);
        /* 从一个符号链接查找它指向的索引节点 */
        void * (*follow_link) (struct dentry *, struct nameidata *);
        /* 在 follow_link调用之后,该函数由VFS调用进行清除工作 */
        void (*put_link) (struct dentry *, struct nameidata *, void *);
        /* 该函数由VFS调用,用于修改文件的大小 */
        void (*truncate) (struct inode *);
    };
    

    dentry 目录项对象

    VFS把目录当作文件对待,所以在路径/bin/ls,bin和ls都属于文件,bin是特殊的目录文件,而ls是一个普通文件,路径中的每个组成部分都由一个索引节点对象表示。虽然它们可以统一由索引节点表示,但VFS经常需要执行目录相关的操作,比如路径名查找等。路径名查找需要解析路径中的每一个组成部分,不但要确定它有效,而且还需要进一步寻找路径中的下一个部分。

    为了方便查找,VFS引入目录项的概念。每个dentry代表路径中一个特定部分。对于/bin/ls来说,/、bin和ls都是目录项对象。前面是两个目录,最后一个是普通文件。在路径中,包括普通文件在内,每一个部分都是目录项对象。解析一个路径是一个耗时的、常规的字符串比较过程。

    目录项,用来记录文件的名字、索引节点指针以及与其他目录项的关联关系。多个关联的目录项,就构成了文件系统的目录结构。不过,不同于索引节点,目录项是由内核维护的一个内存数据结构,所以通常也被叫做目录项缓存。

    索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。

    /* /include/linux/dcache.h */
    /* 目录项对象结构 */
    struct dentry {
        atomic_t d_count;       /* 使用计数 */
        unsigned int d_flags;   /* 目录项标识 */
        spinlock_t d_lock;        /* 单目录项锁 */
        int d_mounted;          /* 是否登录点的目录项 */
        struct inode *d_inode;    /* 相关联的索引节点 */
        struct hlist_node d_hash;    /* 散列表 */
        struct dentry *d_parent;    /* 父目录的目录项对象 */
        struct qstr d_name;         /* 目录项名称 */
        struct list_head d_lru;        /* 未使用的链表 */
        /*
         * d_child and d_rcu can share memory
         */
        union {
            struct list_head d_child;    /* child of parent list */
             struct rcu_head d_rcu;
        } d_u;
        struct list_head d_subdirs;    /* 子目录链表 */
        struct list_head d_alias;    /* 索引节点别名链表 */
        unsigned long d_time;        /* 重置时间 */
        const struct dentry_operations *d_op; /* 目录项操作相关函数 */
        struct super_block *d_sb;    /* 文件的超级块 */
        void *d_fsdata;            /* 文件系统特有数据 */
     
        unsigned char d_iname[DNAME_INLINE_LEN_MIN];    /* 短文件名 */
    };
     
    /* 目录项相关操作函数 */
    struct dentry_operations {
        /* 该函数判断目录项对象是否有效。VFS准备从dcache中使用一个目录项时会调用这个函数 */
        int (*d_revalidate)(struct dentry *, struct nameidata *);
        /* 为目录项对象生成hash值 */
        int (*d_hash) (struct dentry *, struct qstr *);
        /* 比较 qstr 类型的2个文件名 */
        int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
        /* 当目录项对象的 d_count 为0时,VFS调用这个函数 */
        int (*d_delete)(struct dentry *);
        /* 当目录项对象将要被释放时,VFS调用该函数 */
        void (*d_release)(struct dentry *);
        /* 当目录项对象丢失其索引节点时(也就是磁盘索引节点被删除了),VFS会调用该函数 */
        void (*d_iput)(struct dentry *, struct inode *);
        char *(*d_dname)(struct dentry *, char *, int);
    };
    

    不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,VFS根据字符串形式的路径名临时创建它。而且由于目录项对象并非真正保存在磁盘上,所以目录项结构体没有是否被修改的标志。

    目录项对象存放在名为dentry_cache的slab分配器高速缓存中。因此,目录项对象的创建和删除是通过kmem_cache_alloc()和kmem_cache_free()实现的。

    目录项有三种状态:

    • 被使用:该目录项指向一个有效的索引节点,并有一个或多个使用者,不能被丢弃。
    • 未被使用:也对应一个有效的索引节点,但VFS还未使用,被保留在缓存中。如果要回收内存的话,可以撤销未使用的目录项。
    • 负状态:没有对应有效的索引节点,因为索引节点被删除了,或者路径不正确,但是目录项仍被保留了。

    将整个文件系统的目录结构解析成目录项,是一件费力的工作,为了节省VFS操作目录项的成本,内核会将目录项缓存起来。

    file 文件对象

    VFS最后一个主要对象是文件对象。文件对象表示进程已打开的文件。如果我们站在用户空间的角度考虑VFS,文件对象会首先进入我们的视野。进程直接处理的是文件,而不是超级块、索引节点或目录项。文件对象包含我们非常熟悉的信息(如访问模式、当前偏移等),同样道理,文件操作和我们非常熟悉的系统调用read()和write()等也很类似。

    文件对象是已打开的文件在内存中的表示。该对象(不是物理文件)由相应的open()系统调用创建,由close()系统调用销毁,所有这些文件相关的调用实际上都是文件操作表中定义的方法。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已打开文件,它反过来指向目录项对象(反过来指向索引节点),其实只有目录项对象才表示已打开的实际文件。虽然同一文件对应的文件对象不是唯一的,但对应的索引节点和目录项则是唯一的。类似于目录项,文件对象也没有实际的磁盘数据,只有当进程打开文件时,才会在内存中产生一个文件对象。

    /include/linux/fs.h
    struct file {
    	union {
    		struct llist_node	fu_llist;
    		struct rcu_head 	fu_rcuhead;
    	} f_u;
    	struct path		f_path;
    	struct inode		*f_inode;	/* cached value */
    	const struct file_operations	*f_op;
    
    	/*
    	 * Protects f_ep_links, f_flags.
    	 * Must not be taken from IRQ context.
    	 */
    	spinlock_t		f_lock;
    	enum rw_hint		f_write_hint;
    	atomic_long_t		f_count;
    	unsigned int 		f_flags;
    	fmode_t			f_mode;
    	struct mutex		f_pos_lock;
    	loff_t			f_pos;
    	struct fown_struct	f_owner;
    	const struct cred	*f_cred;
    	struct file_ra_state	f_ra;
    
    	u64			f_version;
    #ifdef CONFIG_SECURITY
    	void			*f_security;
    #endif
    	/* needed for tty driver, and maybe others */
    	void			*private_data;
    
    #ifdef CONFIG_EPOLL
    	/* Used by fs/eventpoll.c to link all the hooks to this file */
    	struct list_head	f_ep_links;
    	struct list_head	f_tfile_llink;
    #endif /* #ifdef CONFIG_EPOLL */
    	struct address_space	*f_mapping;
    	errseq_t		f_wb_err;
    } __randomize_layout
    

    file结构体中的file_operations

    每个文件系统都有其自己的文件操作集合,执行诸如读写文件这样的操作。当进程打开一个文件时,VFS会用对应的inode->i_fop中的file_operations结构体来初始化新文件对象的file->fop,使得对文件操作的后续调用能够使用这些函数。如果需要VFS也可以通过修改这个字段而修改文件操作的集合。

    /* /include/linux/fs.h */
    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 (*iterate) (struct file *, struct dir_context *);
    	int (*iterate_shared) (struct file *, struct dir_context *);
    	__poll_t (*poll) (struct file *, struct poll_table_struct *);
    	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
    	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
    	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);
    	int (*release) (struct inode *, struct file *);
    	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
    	int (*fasync) (int, struct file *, int);
    	int (*lock) (struct file *, int, struct file_lock *);
    	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
    	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
    	int (*check_flags)(int);
    	int (*flock) (struct file *, int, struct file_lock *);
    	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
    	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
    	int (*setlease)(struct file *, long, struct file_lock **, void **);
    	long (*fallocate)(struct file *file, int mode, loff_t offset,
    			  loff_t len);
    	void (*show_fdinfo)(struct seq_file *m, struct file *f);
    #ifndef CONFIG_MMU
    	unsigned (*mmap_capabilities)(struct file *);
    #endif
    	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
    			loff_t, size_t, unsigned int);
    	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
    			u64);
    	int (*dedupe_file_range)(struct file *, loff_t, struct file *, loff_t,
    			u64);
    	int (*fadvise)(struct file *, loff_t, loff_t, int);
    } __randomize_layout;
    

    具体的文件系统可以为每一种操作做专门的实现,如果存在通用操作,也可以使用通用操作。一般在基于Unix的文件系统上,这些通用操作效果都不错。并不要求实际文件系统实现文件操作函数表中的所有方法,对不需要的操作可以将该函数指针设置为NULL。

    与文件系统相关的数据结构

    file_system_type

    当内核被编译时,就已经确定了可以支持哪些文件系统,这些文件系统在系统引导时,在 VFS 中进行注册。如果文件系统是作为内核可装载的模块,则在实际安装时进行注册,并在模块卸载时注销。每个文件系统都有一个初始化例程,它的作用就是在 VFS 中进行注册,即填写一个叫做 file_system_type的数据结构,该结构包含了文件系统的名称以及一个指向对应的 VFS 超级块读取例程的地址,所有已注册的文件系统的file_system_type结构形成一个链表,为区别后面将要说到的已安装的文件系统形成的另一个链表,我们把这个链表称为注册链表。

    注册列表如下:

    每种文件系统,不管由多少个实例安装到系统中,还是根本没有安装到系统中,都只有一个 file_system_type 结构。但是,该数据结构里面的fs_supers: 这个域是Linux2.4.10以后的内核版本中新增加的,这是一个双向链表。链表中的元素是超级块结构。如前说述,每个文件系统都有一个超级块,但有些文件系统可能被安装在不同的设备上,而且每个具体的设备都有一个超级块,这些超级块就形成一个双向链表。所以,总结起来就是说,每一种文件系统有且只有有一个对应 file_system_type结构,但是每一个该种类的文件系统都会通过该 file_system_type数据结构中的fs_supers,形成一个链表。

    具体源码如下:

    /* include/linux/fs.h */
    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;
    };
    

    vfsmount

    在对某一个文件进行mount 的时候,mount操作主要就是产生这个vfsmount结构,所以每个mount的文件系统都会有一个vfsmount结构。vfsmount结构可以是对这个mount的文件系统的一个概述。

    其实这个结构体的源码在不同版本发生了很大的变化,我发现在Linux 2的时候,它的源码如下:

    struct vfsmount {
        struct list_head mnt_hash;      /* 散列表 */
        struct vfsmount *mnt_parent;    /* 父文件系统,也就是要挂载到哪个文件系统 */
        struct dentry *mnt_mountpoint;    /* 安装点的目录项 */
        struct dentry *mnt_root;        /* 该文件系统的根目录项 */
        struct super_block *mnt_sb;        /* 该文件系统的超级块 */
        struct list_head mnt_mounts;    /* 子文件系统链表 */
        struct list_head mnt_child;        /* 子文件系统链表 */
        int mnt_flags;                  /* 安装标志 */
        /* 4 bytes hole on 64bits arches */
        const char *mnt_devname;        /* 设备文件名 e.g. /dev/dsk/hda1 */
        struct list_head mnt_list;      /* 描述符链表 */
        struct list_head mnt_expire;    /* 到期链表的入口 */
        struct list_head mnt_share;        /* 共享安装链表的入口 */
        struct list_head mnt_slave_list;/* 从安装链表 */
        struct list_head mnt_slave;        /* 从安装链表的入口 */
        struct vfsmount *mnt_master;    /* 从安装链表的主人 */
        struct mnt_namespace *mnt_ns;    /* 相关的命名空间 */
        int mnt_id;            /* 安装标识符 */
        int mnt_group_id;        /* 组标识符 */
        /*
         * We put mnt_count & mnt_expiry_mark at the end of struct vfsmount
         * to let these frequently modified fields in a separate cache line
         * (so that reads of mnt_flags wont ping-pong on SMP machines)
         */
        atomic_t mnt_count;         /* 使用计数 */
        int mnt_expiry_mark;        /* 如果标记为到期,则为 True */
        int mnt_pinned;             /* "钉住"进程计数 */
        int mnt_ghosts;             /* "镜像"引用计数 */
    #ifdef CONFIG_SMP
        int *mnt_writers;           /* 写者引用计数 */
    #else
        int mnt_writers;            /* 写者引用计数 */
    #endif
    };
    

    但是在Linux 3的时候,它的结构体缩减了很多:

    /* /include/linux/mount.h */
    struct vfsmount {
    	struct dentry *mnt_root;	/* root of the mounted tree */
    	struct super_block *mnt_sb;	/* pointer to superblock */
    	int mnt_flags;
    } __randomize_layout;
    

    在这里,我没有细究下去,而是以旧版本的代码去理解,希望自己以后了解更多之后可以回来搞明白这个点。

    通过源码可以看到,vfsmount主要维护了一个mount点的所有信息。vfsmount结构描述的是一个独立文件系统的挂载信息,每个不同挂载点对应一个独立的vfsmount结构,属于同一文件系统的所有目录和文件隶属于同一个vfsmount,该vfsmount结构对应于该文件系统顶层目录,即挂载目录。

    • mnt_hash:内核通过哈希表对vfsmount进行管理,当前vfsmount结构通过该字段链入相应哈希值对应的链表当中;
    • mnt_parent:指向父文件系统对应的vfsmount结构;
    • mnt_mountpoint:指向该文件系统安装点对应的dentry;
    • mnt_root:该文件系统对应的设备根目录的dentry;(两者是否指向同一个dentry?mnt_root是指向该文件系统设备根目录的dentry,具体这个dentry是什么?)
    • mnt_sb:指向该文件系统对应的超级块;
    • mnt_child:同一个父文件系统中的所有子文件系统通过该字段链接成双联表;
    • mnt_mounts:该字段是上述子文件系统形成的链表的头结点;
    • mnt_list:所有已安装的文件系统的vfsmount结构通过该字段链接在一起;

    和进程相关的数据结构

    系统中的每一个进程都有自己的一组打开的文件,如根文件系统、当前工作目录、安装点等。有三个数据结构将VFS层和系统的进程紧密联系在一起,它们分别是:files_struct、fs_struct和namespace。

    fs_struct

    文件描述符task_struct中有一个字段struct fs_struct *fs,它记录了进程当前的工作目录和根目录等信息,这是内核用来表示进程与文件系统相互作用所必须维护的数据。

    struct fs_struct {
     atomic_t count;
     rwlock_t lock;
     int umask;
     struct dentry * root, * pwd, * altroot;
     struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
    };
    

    fs_struct结构体中有六个指针。前三个是dentry结构指针,就是root、pwd和altroot。这些指针各自指向代表着一个“目录项“的dentry数据结构,里面记录着文件的各项属性,如文件名、访问权限等等。其中pwd则指向进程当前所在的目录;而root所指向的dentry结构代表着本进程的“根目录”,那就是当用户登录进入系统时所“看到“的根目录;至于altroot则为用户设置的“替换根目录”。实际运行时这三个目录不一定在同一个文件系统中。所以后三个指针就各自指向代表着这些“安装”的vfsmount数据结构。

    注意:fs_struct结构中的信息都是与文件系统和进程相关的,带有全局性的,而与具体的已打开的文件没有什么关系。

    files_struct

    该结构体由进程描述符中的files域指向。所有与每个进程相关的信息,如打开的文件及文件描述符都包含在其中。

    /* /include/linux/fdtable.h */
    /*
     * Open file table structure
     */
    struct files_struct {
      /*
       * read mostly part
       */
    	atomic_t count;
    	bool resize_in_progress;
    	wait_queue_head_t resize_wait;
    
    	struct fdtable __rcu *fdt;
    	struct fdtable fdtab;
      /*
       * written part on a separate cache line in SMP
       */
    	spinlock_t file_lock ____cacheline_aligned_in_smp;
    	unsigned int next_fd;
    	unsigned long close_on_exec_init[1];
    	unsigned long open_fds_init[1];
    	unsigned long full_fds_bits_init[1];
    	struct file __rcu * fd_array[NR_OPEN_DEFAULT];
    };
    
    struct fdtable {
    	unsigned int max_fds;
    	struct file __rcu **fd;      /* current fd array */
    	unsigned long *close_on_exec;
    	unsigned long *open_fds;
    	unsigned long *full_fds_bits;
    	struct rcu_head rcu;
    };
    

    fd数组指针指向已打开的文件对象链表,默认情况下,指向fd_array数组。因为NR_OPEN_DEFAULT等于32,所以该数组可以容纳32个文件对象。如果一个进程所打开的文件对象超过32个,内核将分配一个新数组,并且将fd指针指向它。所以对适当数量的文件对象访问会执行得很快,因为它是对对静态数组的操作;如果一个进程打开的文件数量过多,那么内核就需要建立新数组。如果系统中由大量的进程都要打开超过32个文件,为了优化性能,管理员可以适当增大NR_OPEN_DEFAULT的预定义值。

    相关数据结构之间的关系

    dentry与inode

    "文件"即按一定形式存储在介质上的信息,该信息包含两方面,其一是存储的数据本身,其二是概述的索引。在内存中,每个文件都有一个inode和dentry结构。dentry记录文件名,上级目录,子目录等信息,正是我们看到的树状结构;inode记录着文件在存储介质上的位置和分布,dentry->d_inode指向对应的inode结构。inode代表物理意义上的文件,通过inode可以得到一个数组,这个数组记录文件内容的位置,若数组为(4,5,9),则对应数据位于硬盘的4,5,9块。其索引节点号为inode->ino,根据ino就可以计算出对应硬盘中inode的具体位置。

    inode结构中有一个队列i_dentry,凡是代表同一个文件的所有目录项都通过d_alias域挂入响应的inode结构中的i_dentry队列。

    在介质中,每个文件对应一个inode结点,每个文件可有多个文件名,即可以通过不同的文件名访问同一个文件,多个文件名对应一个文件的关系就是数据结构中dentry和inode多对一的关系。inode中不存储文件名字,只有节点号,通过节点号(ino),可以找到数据在介质中的具体位置,即通过内存inode结构中的ino可以定位到磁盘上的inode结构;而dentry则保存文件名和对应的节点号(inode号),这样就可以实现不同文件名访问一个inode。不同的dentry是通过ln指令实现的。

    其实说到这里,就有关于软连接和硬链接的关系。

    1. 硬链接

    硬链接是通过索引节点进行的链接。在Linux中,多个文件指向同一个索引节点是允许的,像这样的链接就是硬链接。硬链接只能在同一文件系统中的文件之间进行链接,不能对目录进行创建。如果删除硬链接对应的源文件,则硬链接文件仍然存在,而且保存了原有的内容,这样可以起到防止因为误操作而错误删除文件的作用。由于硬链接是有着相同 inode 号仅文件名不同的文件,因此,删除一个硬链接文件并不影响其他有相同 inode 号的文件。

    硬链接可由命令 link 或 ln 创建,如:

    link oldfile newfile 
    ln oldfile newfile
    
    1. 符号链接

    软链接(也叫符号链接)与硬链接不同,文件用户数据块中存放的内容是另一文件的路径名的指向。软链接文件有类似于Windows的快捷方式。它实际上是一个特殊的文件。在符号连接中,文件实际上是一个文本文件,其中包含的有另一文件的位置信息。

    软链接主要应用于以下两个方面:一是方便管理,例如可以把一个复杂路径下的文件链接到一个简单路径下方便用户访问;另一方面就是解决文件系统磁盘空间不足的情况。例如某个文件文件系统空间已经用完了,但是现在必须在该文件系统下创建一个新的目录并存储大量的文件,那么可以把另一个剩余空间较多的文件系统中的目录链接到该文件系统中,这样就可以很好的解决空间不足问题。删除软链接并不影响被指向的文件,但若被指向的原文件被删除,则相关软连接就变成了死链接。

    使用-s选项的ln命令即可创建符号链接,命令如下:

    ln -s old.file soft.link
    ln -s old.dir soft.link.dir
    

    VFS主要数据结构的关系

    文件与IO: 每个进程在PCB(Process Control Block)中都保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。

    在file结构体中维护File Status Flag(file结构体的成员f_flags)和当前读写位置(file结构体的成员f_pos),不同进程可以打开同一文件,但是对应不同的file结构体,因此可以有不同的File Status Flag和读写位置。file结构体中比较重要的成员还有f_count,表示引用计数(Reference Count),后面我们会讲到,dup、fork等系统调用会导致多个文件描述符指向同一个file结构体,例如有fd1和fd2都引用同一个file结构体,那么它的引用计数就是2,当close(fd1)时并不会释放file结构体,而只是把引用计数减到1,如果再close(fd2),引用计数就会减到0同时释放file结构体,这才真的关闭了文件。

    每个file结构体都指向一个file_operations结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中read一个文件描述符,read通过系统调用进入内核,然后找到这个文件描述符所指向的file结构体,找到file结构体所指向的file_operations结构体,调用它的read成员所指向的内核函数以完成用户请求。在用户程序中调用lseek、read、write、ioctl、open等函数,最终都由内核调用file_operations的各成员所指向的内核函数完成用户请求。

    file_operations结构体中的release成员用于完成用户程序的close请求,之所以叫release而不叫close是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到0才关闭文件。对于同一个文件系统上打开的常规文件来说,read、write等文件操作的步骤和方法应该是一样的,调用的函数应该是相同的,所以图中的三个打开文件的file结构体指向同一个file_operations结构体。如果打开一个字符设备文件,那么它的read、write操作肯定和常规文件不一样,不是读写磁盘的数据块而是读写硬件设备,所以file结构体应该指向不同的file_operations结构体,其中的各种文件操作函数由该设备的驱动程序实现。

    每个file结构体都有一个指向dentry结构体的指针,“dentry”是directory entry(目录项)的缩写。我们传给open、stat等函数的参数的是一个路径,例如/home/akaedu/a,需要根据路径找到文件的inode。为了减少读盘次数,内核缓存了目录的树状结构,称为dentry cache,其中每个节点是一个dentry结构体,只要沿着路径各部分的dentry搜索即可,从根目录/找到home目录,然后找到akaedu目录,然后找到文件a。dentry cache只保存最近访问过的目录项,如果要找的目录项在cache中没有,就要从磁盘读到内存中。

    每个dentry结构体都有一个指针指向inode结构体。inode结构体保存着从磁盘inode读上来的信息。在上图的例子中,有两个dentry,分别表示/home/akaedu/a和/home/akaedu/b,它们都指向同一个inode,说明这两个文件互为硬链接。inode结构体中保存着从磁盘分区的inode读上来信息,例如所有者、文件大小、文件类型和权限位等。每个inode结构体都有一个指向inode_operations结构体的指针,后者也是一组函数指针指向一些完成文件目录操作的内核函数。

    和file_operations不同,inode_operations所指向的不是针对某一个文件进行操作的函数,而是影响文件和目录布局的函数,例如添加删除文件和目录、跟踪符号链接等等,属于同一文件系统的各inode结构体可以指向同一个inode_operations结构体。

    inode结构体有一个指向super_block结构体的指针。super_block结构体保存着从磁盘分区的超级块读上来的信息,例如文件系统类型、块大小等。super_block结构体的s_root成员是一个指向dentry的指针,表示这个文件系统的根目录被mount到哪里,在上图的例子中这个分区被mount到/home目录下。

    总结

    虚拟文件系统是Linux内核比较重要的一块,而我也是参考了很多资料花了很多时间才弄明白其中不同对象之间的关系。

    学习完这一板块之后,需要去搞一下我的密码学作业以及软件工程作业了。

    参考

    https://zhuanlan.zhihu.com/p/102251964
    http://don7hao.github.io/2015/01/30/kernel/vfs_tree/
    https://developer.aliyun.com/article/422578
    https://blog.csdn.net/hguisu/article/details/7401963
    https://blog.csdn.net/gqtcgq/article/details/50811991
    https://blog.csdn.net/zqixiao_09/article/details/50859759?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.channel_param
    https://medium.com/@jack.yin/linux-系统结构详解-bff6d8a2431e
    https://gtcsq.readthedocs.io/en/latest/linux_tools/disk_note.html#linux
    https://www.iteye.com/topic/816268
    https://blog.csdn.net/yuexiaxiaoxi27172319/article/details/45241923
    http://www.360doc.com/content/11/0915/17/3200886_148505332.shtml
    https://www.ibm.com/developerworks/cn/linux/l-cn-hardandsymb-links/
    https://lrita.github.io/images/posts/filesystem/Linux.Virtual.Filesystem.pdf
    https://zhuanlan.zhihu.com/p/69289429
    https://bean-li.github.io/vfs-inode-dentry/
    https://www.sunxiaokong.xyz/2019-12-02/lzx-01-babyVFS/#files-struct

  • 相关阅读:
    delphi idhttpserver ajax 跨域解决方法
    【转】安卓apk反编译(三件套) (com.googlecode.d2j.DexException: not support version问题解决)
    C++ volatile的作用
    GetProcAddress函数
    c++ CArray函数
    CString中TrimLeft()与TrimRight()的用法
    使用Windows API进行串口编程
    SetCommMask
    AttachThreadInput
    关于CoInitialize和CoUninitialize调用的有关问题
  • 原文地址:https://www.cnblogs.com/T1e9u/p/13899542.html
Copyright © 2011-2022 走看看