文件系统

虚拟文件系统(VFS)的思想和作用

实际上,看到Virtaul,我们就应该想到VFS的作用,是虚拟出一个中间层,来实现某些部分的统一处理。实际上,VFS要解决的是,让应用能够“以一种通用的方式”,去访问不同类型的文件系统:

可以看到,对于不同类型的文件系统、网络文件系统、伪文件系统,应用对它们的操作接口都是一致的,而底层的驱动则会完成实际的区分工作。

通用文件模型

VFS通过一个通用文件模型,来表示一个文件系统,它从顶层到底层,一共维护四个数据结构。这里我们通过磁盘文件系统来举例子说明:

  • file:文件对象。如果一个进程要和一个对象进行交互,那么久要在访问期间存放一个文件对象在内核内存中。
  • dentry:目录项对象。用来存放目录项和对应文件链接信息,每个磁盘文件系统,都有特殊的方式,将该类信息存在磁盘上。
  • inode:索引节点对象。用来存放关于具体文件的一般信息,对基于磁盘的文件系统来说,通常对应于存放在磁盘上的文件控制块,其节点号唯一标识文件系统中的文件。
  • superblock:超级块对象。对应安装的文件系统的信息,他对应磁盘上的文件系统控制块。

这张图反映出了在一次访问中,各个对象是如何参与到过程中去的。这里我为什么要把对象给加粗呢?VFS实际采用的是一种面向对象的方式(虽然它是用C语言写的)。每个object,都有一系列的“操作方式”,VFS为这些对象提供了“统一的”接口,而具体的文件系统则提供了千差万别的实现方式,这是由一个函数指针表来实现的。

文件对象

这里我们从文件对象开始说。如果留意进程的task_struct(定义在sched.h当中),你会发现一个fs_struct结构,以及一个files_struct结构。

其中fs_struct保存了当前点工作目录和根目录:

struct fs_struct {
int users;
spinlock_t lock;
seqcount_t seq;
int umask;
int in_exec;
struct path root, pwd;
};

files_struct结构则保存了当前进程所打开的所有文件,它们保存在fdtable中:

struct fdtable {
unsigned int max_fds;
struct file __rcu **fd;      /* current fd array */
unsigned long *close_on_exec;
unsigned long *open_fds;
struct rcu_head rcu;
};

open()系统调用发生时,文件描述符实际上是fd中的下标。现在我们终于看到file结构了,这里我只列出了重要的部分:

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;  

struct mutex		f_pos_lock;
loff_t			f_pos;

} __attribute__((aligned(4)));	

这里,loff_t是平时我们用来在文件读写上用于定位的指针,显然这个值必须放在file当中,因为可能会有几个进程同时访问一个文件。
f_op正反映出了“通用文件模型的”特点,它保存了文件操作的对应指针;这个值是在进程打开文件时,从文件索引节点中的i_fop中复制的。

当然在file中我们也看到了dentryinode的影子:f_path包含了dentry *,而*f_indoe也是file的一个域,它们分别指向了对应文件点dentry对象和inode对象,我们将会在接下来对它们进行详细的说明。

目录项对象

在上一节中,我们知道了文件对象描述了应用和文件的联系。这里,目录项是用来描述具体的文件系统中的目录项的,他的作用是:用来快速找到一个路径,并且与文件相关联。假设进程需要查找一个路径,那么路径中的每一个分量,都会有一个目录项与之对应。并且,目录项会把每个分量和它对应的索引节点联系起来。dentry的定义如下:

struct dentry {
unsigned int d_flags;		
seqcount_t d_seq;		
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];	/* small names */

struct lockref d_lockref;	/* per-dentry lock and refcount */
const struct dentry_operations *d_op;	/* dentry操作 */
struct super_block *d_sb;	/* 文件的超级块对象 */
unsigned long d_time;		
void *d_fsdata;			

struct list_head d_lru;		/* LRU list */
struct list_head d_child;	/* child of parent list */
struct list_head d_subdirs;	/* our children */

union {
	struct hlist_node d_alias;	/* inode alias list */
 	struct rcu_head d_rcu;
} d_u;
};  

这里可以看到,除开自身的名称、引用计数等,目录项对象中有这些关键的结构:d_op描述了目录项所对应的操作(通用模型的特点);d_sb则是 文件的超级块对象,至于d_inode则是这个目录项关联的索引节点(当然它们并不是一一对应的关系)。注意,这里还有个很重要的变量:d_lockref,它其实就是一个计数器,说明了这个目录项对象的引用次数。

目录项对象保存在dentry cache中。linux操作系统为了提高目录项对象的处理效率,设计了这个高速缓存。这是因为,从磁盘中读取目录项,并且构造相应的目录项对象是需要花费大量的时间的,因此,在完成对目录项对象的操作之后,在内存中(尽量)保存它们具有很重要的意义。这个dentry cache,其本质是一个哈希链表,它定义在list_bl.h当中(它的hash计算在d_hash中完成)。我注意到,对于每一个dentry,都有一个d_flags,它的可能的值,定义在dcache.h当中。因为dentry cache的大小也是有限的,因此我们也不可能无限制地把dentry保存在cache中。因此linux首先把dentry的状态进行了定义:

  • free:该状态目录项对象不包含有效信息,未被VFS使用
  • unused:目前没有被内核使用,d_count的值为0,d_inode仍然指向相关的索引节点
  • in use:正在被使用,d_count的值大于0,d_inode仍然指向相关的索引节点
  • negative:与目录项关联的索引节点不存在,相应的磁盘索引节点已经被删除(d_inode为负数)。

那么对于unused和negative这一类目录项,linux使用了一个LRU(最近最少使用)的双向链表,一旦dentry cache的大小吃紧,就从这个LRU链表中删除dentry。

索引节点对象

索引节点对象,是VFS当中最为重要的一个数据结构。它的作用是表示文件的相关信息。这里的相关信息,不包括文件本身的内容,而是诸如文件大小、拥有者、创建时间等信息。在一个文件被首次访问时,内核会在内存中构造它的索引节点对象。我们在操作系统中,可以任意修改一个文件的名字,但是索引节点和文件是一一对应的(由索引节点号标识),只要文件存在它就会存在(注意,超级块对象和索引节点对象在硬盘上都是有对应的实体数据结构的,在使用时利用硬盘上的内容,在内存中构造索引节点对象)。目录项是用来找到一个对应的索引节点实体,而具体与文件关联的工作则是由索引节点完成的。

让我们来看看索引节点的数据结构(只取了重要的部分),其定义在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 私有指针 */
    unsigned long i_state;
};

可以看到,inode同样采用了多个链表来保存。i_hash用来快速查找inode,i_list则是相同状态索引结点形成的双链表,这包含有未用索引节点链表,正在使用索引节点链表和脏索引节点链表等。

i_dentry是所有使用该节点的dentry链表。值得注意的是,inode不仅仅包含了自身索引节点的操作函数i_op,还有指向(缺省)文件操作的指针i_fop。当然,inode还会和super_block有联系。

i_sb是索引节点所在的超级块,而i_sb_list则是超级块中的所有节点的链表。

当在某个目录下创建、打开一个文件时,内核就会调用create()为这个文件创建一个inode。VFS通过inode的i_op->create()函数来完成这个工作;它将目录的inode、新打开文件的dentry、访问权限作为参数;lookup()函数用来查找指定文件的dentry,link()symlink()分别用来创建硬链接和软链接。

超级块对象

与前面几类对象不同的是,超级块对象表述的内容更加庞大一些:它表示的是一个“已安装的文件系统”。它在文件系统安装时建立,在文件系统卸载时删除。其定义在fs.h当中,这里我只列举出了较为关键的域。

struct super_block {
        struct list_head        s_list;         //超级块链表的指针
        dev_t                   s_dev;          //设备标识符
        unsigned char           s_blocksize_bits;
        unsigned long           s_blocksize;
        loff_t                  s_maxbytes;     //文件的最长长度
        struct file_system_type *s_type;
        const struct super_operations   *s_op;  //超级块的操作
        const struct dquot_operations   *dq_op; 
        const struct quotactl_ops       *s_qcop;
        const struct export_operations *s_export_op;
        unsigned long           s_flags;	//安装标识
        struct dentry           *s_root;	//根目录的目录项
        int                     s_count;
        const struct xattr_handler **s_xattr;
        struct list_head        s_inodes;       //所有的inodes链(打开文件的inodes链)

        struct block_device     *s_bdev;	//块设备
  	    char s_id[32];                          //块设备名称
        u8 s_uuid[16];                          //UUID	        fmode_t                 s_mode;

        char *s_subtype;
        const struct dentry_operations *s_d_op; //default d_op for dentries
        void *s_fs_indo; 	//文件系统的信息指针
    	};

在linux中,每个超级块代表一个已安装的文件系统。所有的超级块链表,是以一种双向环形链表的形式链接在一起的。其prevnext保存在list_head域中。超级块对象中,保存有其根目录的dentry,以及其所有的inodes_fs_info则指向了文件系统的超级块信息。而对于超级块来说,同样定义有s_op,也即超级块的操作表。

超级块一般是储存在磁盘的特定扇区当中,但如果是基于内存对文件系统,比如proc、sysfs,则是保存在内存当中,而超级块对象,则是在使用时创建的,它保存在内存中。

硬链接和软链接与复制

linux里面,可以把文件分成三个部分:文件名(dentry),inode,数据。

复制的定义很明确,就是为这三个部分,都创建一个新的副本。

  • 硬链接的本质是一个“文件名”,一个文件可能有多个“文件名”,inode并不包含文件名,而只是有一个索引节点号。硬链接实际上就是为链接文件创建一个新的dentry,并将dentry写入父目录的数据中,而硬链接所对应的inode依然没有变。所以删除硬链接只是删除了dentry,而inode结点数减少1而已。
  • 软链接就是一个普通的文件,只不过它的数据保存的是另一个文件的路径。软链接的创建,调用了__ext4_new_inode()来创建一个新的inode,并把dentry->name作为了它的内容。也就是说,软链接也同时创建了这三个部分。

二者的区别在哪里呢?首先,硬链接共享了inode,因此它不能跨文件系统;但是软链接不受这个限制。由于相同的原因,硬链接只能对存在的文件进行创建,而软链接不是。而且硬链接有可能会在目录中引入循环,所以不能指向目录;但软链接不会,因为它有一个inode实体可以跟踪。不过不论删除软链接还是硬链接,都不会对原文件、具有相同inode号的文件造成影响。但如果原文件被删除,软链接会变成死链接,硬链接不会,因为inode的计数并没有变成0。

路径名查找

每当进程需要识别一个文件时,就把它的文件路径名,传递给某个VFS系统调用,比如open()。在路径查找中,有个辅助的数据结构:nameidata,它用来向函数传递参数,并且保存查找的结果:

struct nameidata {
	struct path	path;
	struct qstr	last;
	struct path	root;
	struct inode	*inode; /* path.dentry.d_inode */
	unsigned int	flags;
	unsigned	seq, m_seq;
	int		last_type;
	unsigned	depth;
	struct file	*base;
	char *saved_names[MAX_NESTED_LINKS + 1];
};

在查找完成后,path中保存了目录项,depth表示了当前路径的深度,saved_names保存了符号链接处理中的路径名。

路径查找的复杂性,主要体现在VFS系统的一些特点上:(1)必须对目录的访问权限进行检查;(2)文件名可能是符号链接;(3)要考虑符号链接可能带来的循环引用;(4)文件名可能是文件系统的安装点(5)路径名和进程的命名空间有关等。

路径名查找的入口是path_lookup(),它调用了filename_lookup()。这个函数对nameidata进行了简单的填充,随后调用lookupat()

lookupat()函数中通过一个循环,和path_init()函数,逐级向下进行查找,检查目录的访问权限,并且考虑符号链接等情况。



本文链接: http://home.meng.uno/articles/1b9c8662/ 欢迎转载!

© 2018.02.08 - 2020.10.14 Mengmeng Kuang  保留所有权利!

UV : | PV :

:D 获取中...

Creative Commons License