Linux内核设计与实现——虚拟文件系统

虚拟文件系统VFS

虚拟文件系统(虚拟文件交换,简称VFS)作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。

1. Unix文件系统

  1. Unix使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和安装点。

  2. 文件系统是特殊的数据分层存储结构,包含文件、目录和相关的控制信息。文件系统的通用操作包含创建、删除和安装等。在Unix中,文件系统被安装在一个特定的安装点上,该安装点在全局层次结构中被称作命名空间,所有的已安装文件系统都作为根文件系统树的枝叶出现在系统中。DOS和Windows则将文件的命名空间分类为驱动字母,例如C:。

  3. 文件通过目录组织起来,目录可以层层嵌套,形成文件路径。路径中的每一部分都被称作目录条目,统称为目录项。在Unix中,目录属于普通文件,它列出包含在其中的所有文件。VFS把目录当作文件对待,所以可以对目录执行和文件相同的操作。

  4. a. 文件的相关信息,如访问控制权限、大小、拥有者、创建时间等信息,有时被称作文件的元数据,被存储在一个数据结构中,该结 构被称为索引节点(inode,它是index node的缩写)。

    b. 文件系统的控制信息存储在超级块中,超级块是一种包含文件系统信息的数据结构。这些收集起来的信息称为文件系统数据元,包 括单独文件信息和文件系统的信息。

  5. 即使一个文件系统不支持索引节点,也必须在内存中装配索引节点结构体。如果一个文件系统将目录看做一种特殊对象,要想使用VFS,就必须将目录重新表示为文件形式,通常这种转换需要在使用现场引入一些特殊处理,但开销很大。

2. VFS对象及其数据结构

  1. VFS采用面向对象的设计思路,使用一组结构体来代表通用文件对象,这些结构体包含数据和操作这些数据的函数指针,其中的操作函数由具体文件系统实现。

  2. VFS中有四个主要的对象类型,分别是:

    a. 超级块对象,代表一个具体的已安装文件系统。

    b. 索引节点对象,代表一个具体文件。

    c. 目录项对象,代表一个目录项,是路径的一个组成部分。目录项不同于目录,它可能包括一个普通文件。

    d. 文件对象,代表由进程打开的文件。

  3. 每个主要对象中都包含一个操作对象,描述了内核针对主要对象可以使用的方法:

    a. super_operations对象,其中包括内核针对特定文件系统所能调用的方法,如write_inode()sync_fs()等方法。

    b. inode_operations对象,其中包括内核针对特定文件所能调用的方法,如create()link()等方法。

    c. dentry_operations对象,其中包括内核针对特定目录所能调用的方法,如d_compare()d_delete()等方法。

    d. file_operations对象,其中包括进程针对已打开文件所能调用的方法,如read()write()等方法。

  4. 除了上面的主要对象,VFS还使用了大量结构体对象,比如:每个注册的文件系统都由file_system_type结构体表示,它描述了文件系统及其性能;每一个安装点都用vfsmount结构体表示,它包含安装点的相关信息,如位置和安装标志等;fs_structfile结构体分别描述了文件系统以及和进程相关的文件。

3. 超级块对象

各种文件系统都必须实现超级块对象,用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块。对于并非基于磁盘的文件系统(如基于内存的文件系统,比如sysfs),会在使用现场创建超级块并将其保存到内存中。超级块对象由super_block结构体表示,定义在<linux/fs.h>中:

struct super_block {
    struct list_head 				s_list;							/* 指向所有超级块的链表 */
    dev_t 							s_dev;							/* 设备标识符 */
    unsigned long 					s_blocksize;					/* 以字节为单位的块大小 */
    
    unsigned char 					s_blocksize_bits;				/* 以位为单位的块大小 */
    unsigned char 					s_dirt;							/* 修改(脏)标志 */
    unsigned long long 				s_maxbytes;						/* 文件大小上限 */
    struct file_system_type 		s_type;							/* 文件系统类型 */
    struct super_operations 		*op;							/* 超级块方法 */
    struct dquot_operations 		*dq_op;							/* 磁盘限额方法 */
    struct quotact1_ops 			*s_qcop;						/* 限额控制方法 */
    struct export_operations 		*s_export_op;					/* 导出方法 */
    unsigned long 					s_flag;							/* 桂载标志 */
    unsigned long 					s_magic;						/* 文件系统的幻数 */
    struct dentry 					*s_root;						/* 目录挂载点 */
    struct rw_semaphore 			s_umount;						/* 卸载信号量 */
    struct semaphore 				s_lock;							/* 超级块信号量 */
    int 							s_count;						/* 超级块引用计数 */
    int 							s_need_sync;					/* 尚未同步标志 */
    atomic_t 						s_active;						/* 活动引用计数 */
    void 							*s_security;					/* 安全模块 */
    struct xattr_handler 			**s_xattr;						/* 扩展的属性操作 */
    struct list_head 				s_inodes; 						/* inodes链表 */
    struct list_head 				s_dirty; 						/* 脏数据链表 */
    struct list_head 				s_io;							/* 回写链表 */
    struct list_head 				s_more_io;						/* 更多回写的链表 */
    struct hlist_head 				s_anon;							/* 匿名目录项 */
    struct list_head 				s_files;						/* 被分配文件链表 */
    struct list_head 				s_dentry_lru;					/* 未被使用目录项链表 */
    int 							s_nr_dentry_unused; 			/* 链表中目录项的数目 */
    struct block_device 			*s_bdev; 						/* 相关的块设备 */
    struct mtd_info 				*s_mtd;							/* 存储磁盘信息 */
    struct list_head 				s_instances;					/* 该类型文件系统 */
    struct quota_info 				s_dquot; 						/* 限额相关选项 */
    int 							s_frozen; 						/* frozen标志位 */
    wait_queue_head_t 				s_wait_unfrozen; 				/* 冻结的等待队列 */
    char 							s_id[32]; 						/* 文本名字 */
    void 							*s_fs_info; 					/* 文件系统特殊信息 */
    fmode_t 						s_mode;							/* 安装权限 */
    struct 							seraphore s_vfs_rename_sem; 	/* 重命名信号量 */
    u32 							s_time_gran;					/* 时间戳粒度 */
    char 							*s_subtype; 					/* 子类型名称 */
    char    						*s_options; 					/* 已存安装选项 */
};

创建、管理和撤销超级块对象的代码位于文件fs/super.c中。超级块对象通过alloc_super()函数创建并初始化。在文件系统安装时,文件系统会调用该函数从磁盘读取文件系统超级块,并将其信息填充到内存中的超级块对象中。

4. 超级块操作

  1. 超级块对象中的s_op域指向超级块的操作函数表,由super_operations结构体表示,定义在<linux/fs.h>中,其形式如下:
struct super_operations {
    struct inode *(*alloc_inode)(struct super_block *sb);
    void (*destroy_inode)(struct inode *);
    void (*dirty_inode) (struct inode *);
    int (*write_inode) (struct inode *, int);
    void (*drop_inode) (struct inode *);
    void (*delete_inode) (struct inode *);
    void (*put_super) (struct super_block *);
    void (*write_super) (struct super_block *);
    int (*sync_fs)(struct super_block *sb, int wait);
    int (*freeze_fs) (struct super_block *):
    int (*unfreeze_fs) (struct super_block *):
    int (*statfs) (struct dentry *, struct kstatfs *);
    int (*remount_fs) (struct super_block *, int *, char *);
    void (*clear_inode) (struct inode *)
    void (*umount_begin) (struct super_block *);
    int (*show_options)(struct seq_file*, struct vfsmount *);
    int (*show_stats)(struct seq_file*, struct vfsmount *);
    ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
    ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
    int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
}

​ 该结构体中的每一项都是一个指向超级块操作函数的指针,超级块操作函数执行文件系统和索引节点的低层操作。当文件系统需要对 其超级块执行操作时,要先在超级块对象中寻找需要的操作方法。比如,如果一个文件系统要写自己的超级块,需要调用:

sb->s_op->write_super(sb);

​ 在C语言中无法直接得到操作函数的父对象,所以必须将父对象以参数形式传给操作函数。

  1. 下面是super_operation中超级块操作函数的用法:
struct inode *alloc_inode(struct super_block *sb)

​ 在给定的超级块下创建和初始化一个新的索引节点对象。

void destroy_inode(struct inode *inode)

​ 释放给定的索引节点。

void dirty_inode(struct inode *inode)

​ VFS在索引节点脏(被修改)时调用此函数。日志文件系统(如ext3和ext4)执行该函数进行日志更新。

void write_inode(struct inode *inode, int wait)

​ 将给定的索引节点写入磁盘。wait参数指明写操作是否需要同步。

void drop_inode(struct inode *inode)

​ 最后一个指向索引节点的引用被释放后,VFS会调用该函数。VFS删除这个索引节点后,普通Unix文件系统就不会定义这个函数了。

void delete_inode(struct inode *inode)

​ 从磁盘上删除给定的索引节点。

void put_super(struct super_block *sb)

​ 在卸载文件系统时由VFS调用,用来释放超级块。调用者必须持有s_lock锁。

void write_super(struct super_block *sb)

​ 用给定的超级块更新磁盘上的超级块。VFS通过该函数对内存中的超级块和磁盘中的超级块进行同步。调用者必须持有s_lock锁。

int sync_fs(struct super_block *sb, int wait)

​ 使文件系统的数据元与磁盘上的文件系统同步。wait参数指定操作是否同步。

void write_super_lockfs(struct super_block *sb)

​ 先禁止对文件系统做改变,再使用给定的超级块更新磁盘上的超级块。LVM(逻辑卷标管理)会调用该函数。

void unlockfs(struct super_block *sb)

​ 对文件系统解除锁定,是write_super_lockfs()的逆操作。

int statfs(struct super_block *sb, struct statfs *statfs)

​ VFS通过调用该函数获取文件系统状态。指定文件系统相关的统计信息将放置在statfs中。

int remount_fs(struct super_block *sb, int *flags, char *data)

​ 指定新的安装选项重新安装文件系统时,VFS会调用该函数。调用者必须持有s_lock锁。

void clear_inode(struct inode *inode)

​ VFS调用该函数释放索引节点,并清空包含相关数据的所有页面。

void umount_begin(struct super_block *sb)

​ VFS调用该函数中断安装操作。该函数被网络文件系统使用,如NFS。

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

5. 索引节点对象

索引节点对象包含内核在操作文件或目录时需要的全部信息。对于Unix风格的文件系统,这些信息可以从磁盘索引节点直接读入。索引节点对象必须在内存中创建,以便于文件系统使用。当文件被访问时,索引节点才在内存中创建。一个索引节点代表文件系统中的一个文件,可以是设备或管道这样的特殊文件。索引节点对象由inode结构体表示,定义在<linux/fs.h>中:

struct inode {
        struct hlist_node 			i_hash;					/* 散列表 */
        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 */
        kdev_t 						i_rdev;					/* 实际设备标识符 */
        u64 						i_version;				/* 版本号 */
        loff_t 						i_size;					/* 以字节为单位的文件大小 */
        seqcount_t 					i_size_seqcount;		/* 对i_size进行串行计数 */
        struct timespec 			i_atime;				/* 最后访问时间 */
        struct timespec 			i_mtime;				/* 最后修改时间 */
        struct timespec 			i_ctime;				/* 最后改变时间 */
        unsigned int 				i_blkbits;				/* 以位为单位的块大小 */
        blkcnt_t 					i_blocks;				/* 文件的块数 */
        unsigned short 				i_bytes;				/* 使用的字节数 */
        umode_t 					i_mode;					/* 访问权限 */
        spinlock_t 					i_lock; 				/* 自旋锁 */
        struct rw_semaphore 		i_alloc_sem;			/* 嵌入i_sem内部 */
        struct semaphore 			i_sem;					/* 索引节点信号量 */
        struct inode_operations 	*i_op;					/* 索引节点操作表 */
        struct file_operations 		*i_fop;					/* 缺省的索引节点操作 */
        struct super_block 			*i_sb;					/* 相关的超级块 */
        struct fi1e_lock 			*i_flock;				/* 文件锁链表 */
        struct address_space 		*i_mapping;				/* 相关的地址映射 */
        struct address_space 		i_data; 				/* 设备地址映射 */
        struct dquot 				*i_dquot[MAXQUOTAS];	/* 索引节点的磁盘限额 */
        struct list_head 			i_devices;				/* 块设备链表 */
        union {
            struct pipe_inode_info 	*i_pipe;				/* 管道信息 */
            struct block_device 	*i_bdev;				/* 块设备驱动 */
            struct cdev 			*i_cdev;				/* 字符设备驱动 */
        };
        unsigned long 				i_dnotify_mask;			/* 目录通知掩码 */
        struct dnotify_struct 		*i_dnotify;				/* 目录通知 */
        struct list_head 			inotify_watches;		/* 索引节点通知监测链表 */
        struct mutex 				inotify_mutex;			/* 保护inotify_watches */
        unsigned long 				i_state;				/* 状态标志 */
        unsigned long 				dirtied_when;			/* 第一次弄脏数据的时间 */
        unsigned int 				i_flags;				/* 文件系统标志 */
        atomic_t 					i_writecount;			/* 写者计数 */
        void 						*i_security;			/* 安全模块 */
        void 						*i_private; 			/* fs私有指针 */
};

6. 索引节点操作

  1. 索引节点对象中的inode_operations项描述了VFS用以操作索引节点对象的所有方法,由文件系统实现,定义在<linux/fs.h>中:
struct inode_operations {
    int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
    struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
    int (*link) (struct dentry *,struct inode *,struct dentry *);
    int (*unlink) (struct inode *,struct dentry *);
    int (*symlink) (struct inode *,struct dentry *,const char *);
    int (*mkdir) (struct inode *,struct dentry *,int);
    int (*rmdir) (struct inode *,struct dentry *);
    int (*mknod) (struct inode *,struct dentry *,int,dev_t);
    int (*rename) (struct inode *, struct dentry *,
                   struct inode *, struct dentry *);
    int (*readlink) (struct deatry *, char __user *,int);
    void * (*follow_link) (struct dentry *, struct nameidata *);
    void (*put_link) (struct dentry *, struct nameidata *, void *);
    void (*truncate) (struct inode *);
    int (*permission) (struct inode *, int);
    int (*setattr) (struct dentry *, struct iattr *);
    int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);
    int (*setxattr) (struct dentry *, const char *,const void *,size_t, int);
    ssize_t (*getxattr) (struct dentry *, const char *, void *, size_t);
    ssize_t (*listxattr) (struct dentry *, char *, size_t);
    int (*removexattr) (struct dentry *, const char *);
    void (*truncate_range)(struct inode *, loff_t, loff_t);
    long (*fallocate)(struct inode *inode, int mode, loff_t offset,
                      loff_t len);
    int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
                  u64 len);
};
  1. 下面这些接口由各种函数组成,在给定的节点上,可能由VFS执行这些函数,也可能由具体的文件系统执行:
int create(struct inode *dir,struct dentry *dentry, int mode)

​ VFS通过系统调用create()open()来调用该函数,为denty对象创建一个新的索引节点,创建时使用mode指定的初始模式。

struct dentry * lookup(struct inode *dir,struct dentry *dentry)

​ 在特定目录中寻找索引节点,该索引节点对应于denrty中给出的文件名。

int link(struct dentry *old_dentry,
         struct inode *dir,
         struct dentry *dentry)

​ 该函数被系统调用link()调用,用于创建硬连接。连接名由denty指定,连接对象是dir目录中old_dentry目录项代表的文件。

int unlink(struct inode *dir,struct dentry *dentry)

​ 该函数被系统调用unlink()调用,从目录dir中删除由目录项dentry指定的索引节点对象。

int symlink(struct inode *dir,
            struct dentry *dentry,
            const char *symname)

​ 该函数被系统调用symlink()调用,创建符号连接。符号连接名称由symname指定,连接对象是dir目录中的dentry目录项。

int mkdir(struct inode *dir,
          struct dentry *dentry,
          int mode)

​ 该函数被系统调用mkdir()调用,创建一个新目录,创建时使用mode指定的初始模式。

int rmdir(struct inode *dir,
          struct dentry *dentry)

​ 该函数被系统调用rmdir()调用,删除dir目录中的dentry目录项代表的文件。

int mknod(struct inode *dir,
          struct dentry *dentry,
          int mode ,dev_t rdev)

​ 该函数被系统调用mknod()调用,创建特殊文件(设备文件、命名管道或套接字)。创建的文件放在dir目录中,目录项为dentry, 关联的设备为rdev,初始权限由mode指定。

int rename(struct inode *old_dir,
           struct dentry *old_dentry,
           struct inode *new_dir,
           struct dentry *new_dentry)

​ VFS调用该函数来移动文件。文件源路径在old_dir目录中,源文件由old_dentry目录项指定,目标路径在new_dir目录中,目标 文件由new_dentry指定。

int readlink(struct dentry *dentry,
             char *buffer,int buflen)

​ 该函数被系统调用readlink()调用,拷贝数据到特定的缓冲buffer中。拷贝的数据来自dentry指定的符号连接,拷贝大小最大可 达buflen字节。

int follow_link(struct dentry *dentry,
                struct nameidata *nd)

​ 该函数由VFS调用,从一个符号连接查找它指向的索引节点。由dentry指向的连接被解析,其结果存放在由nd指向的nameidata结 构体中。

int put_link(struct dentry *dentry,
             struct nameidata *nd)

​ 在follow_link()调用之后,VFS调用该函数进行清除工作。

void truncate(struct inode *inode)

​ 该函数由VFS调用,修改文件的大小。调用前索引节点的i_size项必须设置为预期的大小。

int permission(struct inode *inode, int mask)

​ 检查给定的inode所代表的文件是否允许特定的访问模式。允许返回零,否则返回负值的错误码。多数文件系统都将此区域设置为 NULL,使用VFS提供的通用方法检查。复杂的系统(比如支持访问控制链(ACLS) 的文件系统),需要使用特殊的permission()方法。

int setattr(struct dentry *dentry,
            struct iattr *attr)

​ 函数被notify_change()调用,在修改索引节点后,通知发生了“改变事件”。

int getattr(struct vfsmount * mnt,
            struct dentry *dentry,
            struct katat *stat)

​ 在通知索引节点需要从磁盘中更新时,VFS会调用该函数。

int setxattr(struct dentry *dentry,
             const char *name,
             const void *value,
             size_t size,int flags)

​ 函数由VFS调用,给dentry指定的文件设置扩展属性。属性名为name,值为value

ssize_t getxattr(struct dentry *dentry,
                 const char *name,
                 void *value,size_t size)

​ 函数由VFS调用,向value中拷贝给定文件的扩展属性name对应的数值。

ssize_t listxattr(struct dentry tdentry,
                  char *list,size_t size)

​ 将特定文件的所有属性列表拷贝到一个缓冲列表中。

int removexattr(struct dentry *dentry,
                const char *name)

​ 从给定文件中删除指定的属性。

7. 目录项对象

路径中的每个组成部分都由一个索引节点对象表示。每个dentry代表路径中的一个特定部分。在路径中(包括普通文件在内),每一个部分都是目录项对象。目录项也可包括安装点。VFS在执行目录操作时,如果需要,会现场创建目录项对象。目录项对象由dentry结构体表示,定义在<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; 				/* 未使用的链表 */
        union {
            struct list_head 	 d_child;				/* 目录项内部形成的链表 */
            struct rcu_head 	 d_rcu;					/* RCU加锁 */
        } d_u;
        struct list_head 		 d_subdirs;				/* 子目录链表 */
        struct list_head 		 d_alias;				/* 索引节点别名链表 */
        unsigned long 			 d_time; 				/* 重置时间 */
        struct dentry_operations *d_op;					/* 目录项操作指针 */
        struct super_block 		 *d_sb;					/* 文件的超级块 */
        void 					 *d_fsdata; 			/* 文件系统特有数据 */
        unsigned char 			 d_iname[DNAME_INLINE_LEN_MIN]; /* 短文件名 */
}

与前面的两个对象不同,目录项对象没有对应的磁盘数据结构,VFS根据字符串形式的路径名现场创建它。目录项对象并非真正保存在磁盘上,所以目录项结构体没有是否被修改的标志(是否为脏、是否需要写回磁盘的标志)。

(1) 目录项状态

目录项对象有三种有效状态:被使用、未被使用和负状态。

  1. 一个被使用的目录项对应一个有效的索引节点(d_inode指向相应的索引节点)且该对象存在一个或多个使用者(d_count为正值),意味着它正被VFS使用且指向有效的数据,因此不能被丢弃。

  2. 一个未被使用的目录项对应一个有效的索引节点(d_inode指向一个索引节点),但VFS当前并未使用它(d_count为0)。该目录项对象仍指向一个有效对象,且被保留在缓存中以便需要时再使用它。如果要回收内存的话,可以撤销未使用的目录项。

  3. 一个负状态的目录项没有对应的有效索引节点(d_inodeNULL),索引节点已被删除,或路径不再正确,但目录项仍保留,以便快速解析以后的路径查询。比如,一个守护进程不断试图打开并读取一个不存在的配置文件;open()系统调用不断返回ENOENT,直到内核构建这个路径、遍历磁盘上的目录结构体并检查这个文件的确不存在为止。

目录项对象释放后也可以保存到slab对象缓存中,此时,任何VFS或文件系统代码都没有指向该目录项对象的有效引用。

(2) 目录项缓存

  1. 内核将目录项对象缓存在目录项缓存(deache)中。目录项缓存包括三个主要部分:

    a. "被使用的"目录项链表。该链表通过索引节点对象中的i_dentry项连接相关的索引节点。一个给定的索引节点可能有多个链接, 就可能有多个目录项对象,因此用链表来连接它们。

    b. “最近被使用的”双向链表。该链表含有未被使用和负状态的目录项对象。该链总是在头部插入目录项,所以链头节点的数据比链尾 的数据新。当内核通过刷除节点项回收内存时,会从链尾删除节点项。

    c. 散列表和相应的散列函数。用来快速将给定路径解析为相关目录项对象。散列表由数组dentry_hashtable表示,其中每一个元素 都是一个指向具有相同键值的目录项对象链表的指针。数组大小取决于系统中物理内存的大小。

    ​ 1) d_hash()函数计算实际的散列值,它是内核提供給文件系统的唯一的散列函数。

    ​ 2) d_lookup()函数查找散列表,如果在dcache中发现了与其匹配的目录项对象,则匹配的对象被返回;否则返回NULL指针。

  2. 为了避免每次访向路径都进行耗时的操作,VFS会先在目录项缓存中搜索路径名;如果该目录项在目录项缓存中不存在,VFS就遍历文件系统为每个路径分量解析路径,解析完毕后将目录项对象加入dcache中。dcache也提供对索引节点的缓存(icache)。和目录项对象相关的索引节点对象不会被释放,只要目录项被缓存,其相应的索引节点也就被缓存了。

8. 目录项操作

  1. dentry_operation结构体指明了VFS操作目录项的所有方法。该结构定义在<linux/dcache.h>中。
struct dentry_operations {
        int (*d_revalidate) (struct dentry *, struct nameidata *)
        int (*d_hash) (struct dentry *, struct qstr *);
        int (*d_compare) (struct dentry *, struct qstr *, struct qstr *);
        int (*d_delete) (struct dentry *);
        void (*d_release) (struct dentry *);
        void (*d_iput) (struct dentry *, struct inode *);
        char *(*d_dname) (struct dentry *, char *, int);
};
  1. 下面是函数的具体用法:
int d_revalidate(struct dentry *dentry,
                 struct nameidata *);

​ 判断目录对象是否有效。VFS准备从dcache中使用一个目录项时,会调用该函数。大部分文件系统将该方法置NULL

int d_hash(struct dentry *dentry,
           struct qstr *name)

​ 为目录项生成散列值,当目录项需要加入到散列表中时,VFS调用该函数。

int d_compare(struct dentry *dentry,
              struct qstr *name1,
              struct qstr *name2)

​ VFS调用该函数来比较name1name2两个文件名。使用该函数需要加dcache_lock锁。多数文件系统使用VFS默认的操作,仅作字 符串比较。而FAT文件系统不区分大小写,所以需要实现一种不区分大小写的字符串比较函数。

int d_delete(struct dentry *dentry)

​ 当目录项对象的d_count计数值等于0时,VFS调用该函数。使用该函数需要加dcache_lock锁和目录项的d_lock

void d_release(struct dentry *dentry)

​ 当目录项对象将要被释放时,VFS调用该函数,默认情况下它什么也不做。

void d_iput(struct dentry *dentry,
            struct inode *inode)

​ 当一个目录项对象丢失其相关的索引节点时(磁盘索引节点被删除了),VFS调用该函数。默认情况下VFS会调用iput()函数释放索引 节点。如果文件系统重载了该函数,那么除了执行此文件系统特殊的工作外,还必须调用iput()函数。

9. 文件对象

  1. 文件对象是已打开的文件在内存中的表示,包含访问模式、当前偏移等信息。该对象(不是物理文件)由相应的open()系统调用创建,由close()系统调用撤销,所有这些文件相关的调用都是文件操作表中定义的方法。多个进程可以同时打开和操作同一个文件,同一个文件也可能存在多个对应的文件对象。文件对象仅在进程观点上代表已打开文件,它反过来指向目录项对象(反过来指向索引节点),只有目录项对象才表示已打开的实际文件。一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象是唯一的。

  2. 文件对象由file结构体表示,定义在<linux/fs.h>中:

struct file {
        union {
            struct list_head 		fu_list;		/* 文件对象链表 */
            struct rcu_head 		fu_rcuhead;		/* 释放之后的RCU链表 */
        } f_u;
        struct path 				f_path; 		/* 包含目录项 */
        struct file_operations 		*f_op;			/* 文件操作表 */
        spinlock_t 					f_lock;			/* 单个文件结构锁 */
        atomic_t 					f_count;		/* 文件对象的使用计数 */
        unsigned int 				f_flags;		/* 当打开文件时所指定的标志 */
        mode_t 						f_mode;			/* 文件的访向模式 */
        loff_t 						f_pos;			/* 文件当前的位移量(文件指针) */
        struct fown_struct 			f_owner;		/* 拥有者通过信号进行异步I/O数据的传送 */
        const struct cred 			*f_cred;		/* 文件的信任状 */
        struct file_ra_state 		f_ra;			/* 预读状态 */
        u64 						f_version;		/* 版本号 */
        void 						*f_security;	/* 安全模块 */
        void 						*private_data; 	/* tty设备驱动的钩子 */
        struct list_head 			f_ep_links;		/* 事件池链表 */
        spinlock_t 					f_ep_lock; 		/* 事件池锁 */
        struct address_space 		*f_mapping; 	/* 页缓存映射 */
        unsigned long 				f_mnt_write_state; /* 调试状态 */
};

​ 文件对象没有对应的磁盘数据,所以结构体中没有代表其对象是否为脏、是否需要写回磁盘的标志。文件对象通过f_dentry指针指 向相关的目录项对象,目录项指向相关的索引节点,索引节点会记录文件是否是脏的。

10. 文件操作

  1. 文件操作和read()write()等系统调用类似,是标准Unix系统调用的基础,由file_operations结构体表示,定义在<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 (*aio_read) (struct kiocb *, const struct iovec *,
                             unsigned long, loff_t);
        ssize_t (*aio_write) (struct kiocb*, const struct iovec *,
                              unsigned long, loff_t);
        int (*readdir) (struct file *, void *, filldir_t);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        int (*ioctl) (struct inode *, struct file *, unsigned int,
                      unsigned long);
        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 *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, struct dentry *, int datasync);
        int (*aio_fsync) (struct kiocb *, 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 *,
                                 ssize_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 **);
};

​ 具体的文件系统可以为每一种操作做专门的实现,或者使用通用操作。不要求实际文件系统实现文件操作函数表中的所有方法,对不 需要的操作可以将该函数指针置为NULL

  1. 下面给出操作的用法说明:
loff_t lleek(struct file *file,
             loff_t offset, int origin)

​ 更新偏移量指针。由系统调用lleek()调用它。

ssize_t read(struct file *file,
             char *buf, size_t count,
             loff_t *offset)

​ 从给定文件的offset偏移处读取conut字节的数据到buf中,同时更新文件指针。由系统调用read()调用它。

ssize_t aio_read(struct kiocb *iocb,
                 char *buf, size_t count,
                 loff_t offset)

​ 从iocb描述的文件里,以同步方式读取count字节的数据到buf中。由系统调用aio_read()调用它。

ssize_t write(struct file *file,
              const,char *buf,size_t count,
              loff_t *offset)

​ 从给定的buf中取出conut字节的数据,写入给定文件的offset偏移处,同时更新文件指针。由系统调用write()调用它。

ssize_t aio_write(struct kiocb *iocb,
                 const,char *buf,
                 size_t count, loff_t offset)

​ 以同步方式从给定的buf中取出count字节的数据,写入由iocb描述的文件中。由系统调用aic_write()调用它。

int readdir(struct file *file,void *dirent,filldir_t filldir)

​ 返回目录列表中的下一个目录。由系统调用readdir()调用它。

unsigned int poll(struct file *file,
                  struct poll_table_struct *poll_table)

​ 睡眠等待给定文件活动。由系统调用poll()调用它。

int ioctl(struct inode *inode,
          struct file *file,
          unsigned int cmd,
          unsigned long arg)

​ 给设备发送命令参数对。文件是一个被打开的设备节点时,可以通过它进行设置操作。由系统调用ioctl()调用它,必须持有BKL。

int unlocked_ioctl(struct file *file,
                   unsigned int cmd,
                   unsigned long arg)

​ 与ioctl()有类似,但不需要持有BKL。如果用户空间调用ioct()系统调用,VFS就可以调用unlocked_jioct()(凡是ioctl()出 现的场所)。文件系统只需要实现其中的一个,一般优先实现unlocked_joct()

int compat_loctl(struct file *file,
                 unsigned int cmd,
                 unsigned long arg)

​ 该函数是ioctl()函数的可移植变种,被32位应用程序用在64位系统上,它可以进行必要的字大小转换,不必持有BKL。新的驱动 程序应该设计自己的ioctl命令以便所有的驱动程序都可移植,使得compat_ioctl()unlocked_ioctl()指向同一个函数。

int mmap(struct file *file,struct vm_area_struct *vma)

​ 将给定的文件映射到指定的地址空间上。由系统调用mmap()调用它。

int open(struct inode *inode,
         struct file *file)

​ 创建一个新的文件对象,并和相应的索引节点对象关联起来。由系统调用open()调用它。

int flush(struct file *file)

​ 当已打开文件的引用计数减少时,该函数被VFS调用。它的作用根据具体文件系统而定。

int release(struct inode *inode,
            struct file *file)

​ 当文件的最后一个引用被注销时(比如,当最后一个共享文件描述符的进程调用了close()或退出时),该函数会被VFS调用。它的作 用根据具体文件系统而定。

int fsync(struct file file,
          struct dentry *dentry,
          int datasync)

​ 将给定文件的所有被缓存数据写回磁盘。由系统调用fsync()调用它。

int aio_fsync(struct kiocb *iocb,
              int datasync)

​ 将iocb描述的文件的所有被缓存数据写回到磁盘。由系统调用aio_fsync()调用它。

int fasync(int fd,struct file *file,int on)

​ 打开或关闭异步I/O的通告信号。

int lock(struct file *file,int cmd,struct file_lock *lock)

​ 给指定文件上锁。

ssize_t readv(struct file *file,
              const struct iovec *vector,
              unsigned long count,
              loff_t *offset)

​ 从给定文件中读取数据,并将其写入由vector描述的count个缓冲中,同时增加文件的偏移量。由系统调用readv()调用它。

ssize_t writev(struct file *file,
               const struct iovec *vector,
               unsigned long count,
               loff_t *offset)

​ 将由vector描述的count个缓冲中的数据写入到由file指定的文件中,同时减小文件的偏移量。由系统调用writev()调用它。

ssize_t sendfile(struct file *file,
                loff_t *offset,
                size_t size,
                read_actor_t actor,
                void *target)

​ 从一个文件拷贝数据到另一个文件,拷贝操作在内核中完成,遇免向用户空间进行不必要的拷贝。由系统调用sendfile()调用它。

ssize_t sendpage(struct fle *fle,
                 struct page *page,
                 int offset, size_t size, loff_t *pos, int more)

​ 从一个文件向另一个文件发送数据。

unsigned long get_unmapped_area(struct file *file,
                                unsigned long addr,
                                unsigned long len,
                                unsigned long offset,
                                unsigned long flags)

​ 获取未使用的地址空间来映射给定的文件。

int check_flags(int flags)

​ 当给出SETFL命令时,这个函数用来检查传递给fcntl()系统调用的flags的有效性。文件系统不必实现check_flags(),目前只 有在NFS文件系统上实现了。这个函数能使文件系统限制无效的SETFL标志,不限制的话,普通的fcntl()函数能使标志生效。在 NFS文件系统中,不允许把O_APPENDO_DIRECT相结合。

int flock(struct file *filp,
          int cmd,
          struct file_lock *fl)

​ 用来实现flock()系统调用,该调用提供忠告锁。

11. 和文件系统相关的数据结构

  1. file_system_type结构体描述各种特定文件系统类型,比如ext3、ext4或UDF。每种文件系统只有一个file_system_type结构,定义在<linux/fs.b>中:
struct file_system_type {
        const char 					*name;			/* 文件系统的名字 */
        int 						fs_flags; 		/* 文件系统类型标志 */
    
        /* 下面的函数用来从磁盘中读取超级块 */
        struct super_block 			*(*get_ab) 		(struct file_system_type *, int,
                                       				 char *, void *);
    	/* 下面的函数用来终止访问超级块 */
        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()函数从磁盘上读取超级块,并且在文件系统被安装时,在内存中组装超级块对象。

  1. 当文件系统被实际安装时,将有一个vfsmount结构体在安装点被创建。该结构体用来代表文件系统的实例——代表一个安装点,被定义在<linux/mount.h>中:
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;				/* 安装标志 */
        char 						*mnt_devname;			/* 设备文件名 */
        struct list_head 			mnt_list;				/* 描述符链表 */
        struct list_head 			mnt_expire;				/* 在到期链表中的入口 */
        struct liat_head 			mnt_share;				/* 在共享安装链表中的入口 */
        struct list_head 			mnt_slave_list;			/* 从安装链表 */
        struct list_head 			mnt_slave;				/* 从安装链表中的入口 */
        struct vfsmount 			*mnt_master;			/* 从安装链表的主人 */
        struct mnt_namespace 		*mnt_namespace;			/* 相关的命名室间 */
        int 						mnt_id; 				/* 安装标识符 */
        int 						mnt_group_id;			/* 组标识符 */
        atomic_t 					mnt_count;				/* 使用计数 */
        int 						mnt_expiry_mark;		/* 如果标记为到期,则值为真 */
        int 						mnt_pinned; 			/* “钉住"进程计数 */
        int 						mnt_ghosts; 			/* “镜像"引用计数 */
    	atomic_t 					__mnt_writers;			/* 写者引用计数 */
};

​ 在安装时指定的标志信息存储在mnt_flages域中。表13-1列出了标准的安装标志,定义在<linux/mount.h>中。
在这里插入图片描述

12. 和进程相关的数据结构

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

  1. file_struct结构体定义在<linux/fdtable.h>中。该结构体由进程描述符中的files目录项指向。所有与单个进程相关的信息(如打开的文件及文件描述符)都包含在其中,其结构和描述如下:
struct files_struct {
        atomic_t 			count; 					/* 结构的使用计数 */
        struct fdtable 		*fdt;					/* 指向其他fd表的指针 */
        struct fdtable 		fdtab;					/* 基fd表 */
        spinlock_t 			file_lock;				/* 单个文件的锁 */
        int 				next_fd;				/* 缓存下一个可用的fd */
        struct embedded_fd_set close_on_exec_init;  /* exec()时关闭的文件描述符链表 */
        struct embedded_fd_set open_fds_init; /* 打开的文件描述符链表 */
        struct file 		*fd_array[NR_OPEN_DEFAULT]; /* 缺省的文件对象数组 */
};

fd_array数组指针指向已打开的文件对象。NR_OPEN_DEFAULT等于BITS_PER_LONG,在64位机器体系结构中这个宏的值为64。 如果一个进程所打开的文件对象超过64个,内核将分配一个新数组,并将fdt指针指向它。如果系统中有大量的进程都要打开超过 64个文件,可以适当增大NR_OPEN_DEFAULT的预定义值。

  1. fs_struct结构由进程描述符的fs域指向。它包含文件系统和进程相关的信息,定义在<linux/fs_struct.h> 中:
struct fs_struct {
    int 		users;			/* 用户数目 */
    rwlock_t 	lock;			/* 保护该结构体的锁 */
    int 		umask;			/* 掩码 */
    int 		in_exec;		/* 当前正在执行的文件 */
    struct path root;			/* 根目录路径 */
    struct path pwd;			/* 当前工作目录的路径 */
}
  1. namespace结构体定义在<linux/mmt_namespace.h>中,由进程描述符中的mmt_namespace域指向。单进程命名空间使得每一个进程在系统中都看到唯一的安装文件系统——不仅是唯一的根目录,而且是唯一的文件系统层次结构。下面是其具体结构和描述:
struct mnt_namespace {
    atomic_t 				count;	/* 结构的使用计数 */
    struct vfsmount 		*root; 	/* 根目录的安装点对象 */
    struct list_head 		list; 	/* 安装点链表 */
    wait_queue_head_t 		poll;	/* 轮询的等待队列 */
    int    					event; 	/* 事件计数 */
}

list域是连接已安装文件系统的双向链表,它包含的元素组成了全体命名空间。namespace结构体的使用方法和前两种结构体不 同,默认情况下,所有的进程共享同样的命名空间(它们都从相同的挂载表中看到同一个文件系统层次结构)。只有进行clone()操作 时使用CLONE_NEWS标志,才会给进程一个唯一的命名空间结构体的拷贝。大多数进程不提供这个标志,所有进程都继承其父进程的 命名空间,因此大多数系统上只有一个命名空间。

  1. 上述这些数据结构都是通过进程描述符连接起来的。多数进程的描述符都指向唯一的files_structfs_struct结构体。但是使用克隆标志CLONE_FILESCLONE_FS创建的进程会共享这两个结构体。每个结构体都维护一个count域作为引用计数,防止在进程正使用该结构时,该结构被撤销。
注:本文摘自《Linux内核设计与实现(第三版)》
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值