文章目录
- 虚拟文件系统的数据结构
- 超级快
- 挂载描述符
- 文件系统类型
- 索引节点
- 目录项
- 文件的打开实例和打开文件表
虚拟文件系统的数据结构
虽然不同文件系统类型的物理结构不同,但是虚拟文件系统定义了一套统一的数据结构。
(1)超级块。文件系统的第一块是超级块,描述文件系统的总体信息,挂载文件系统的时候在内存中创建超级块的副本:结构体 super_block。
(2)虚拟文件系统在内存中把目录组织为一棵树。一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符:mount 结构体,并且读取文件系统的超级块,在内存中创建超级块的一个副本。
(3)每种文件系统的超级块的格式不同,需要向虚拟文件系统注册文件系统类型file_system_type,并且实现 mount 方法用来读取和解析超级块。
(4)索引节点。每个文件对应一个索引节点,每个索引节点有一个唯一的编号。当内核访问存储设备上的一个文件时,会在内存中创建索引节点的一个副本:结构体 inode。
(5)目录项。文件系统把目录看作文件的一种类型,目录的数据是由目录项组成的,每个目录项存储一个子目录或文件的名称以及对应的索引节点号。当内核访问存储设备上的一个目录项时,会在内存中创建该目录项的一个副本:结构体 dentry。
(6)当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例:file结构体,然后在进程的打开文件表中分配一个索引,这个索引称为文件描述符,最后把文件描述符和 file 结构体的映射添加到打开文件表中。
超级快
文件系统的第一块是超级块,用来描述文件系统的总体信息。当我们把文件系统挂载到内存中目录树的一个目录下时,就会读取文件系统的超级块,在内存中创建超级块的副本:结构体 super_block,主要成员如下:
include/linux/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; … unsigned long s_flags; unsigned long s_iflags; /*内部 SB_I_* 标志 */ unsigned long s_magic; struct dentry *s_root; … struct hlist_bl_head s_anon; struct list_head s_mounts; struct block_device *s_bdev; struct backing_dev_info *s_bdi; struct mtd_info *s_mtd; struct hlist_node s_instances; … void *s_fs_info; …
};
(1)成员 s_list 用来把所有超级块实例链接到全局链表 super_blocks。
(2)成员 s_dev 和 s_bdev 保存文件系统所在的块设备,前者保存设备号,后者指向内存中的一个 block_device 实例。
(3)成员 s_blocksize 是块长度,成员 s_blocksize_bits 是块长度以 2 为底的对数。
(4)成员 s_maxbytes 是文件系统支持的最大文件长度。
(5)成员 s_flags 是标志位。
(6)成员 s_type 指向文件系统类型。
(7)成员 s_op 指向超级块操作集合。
(8)成员 s_magic 是文件系统类型的魔幻数,每种文件系统类型被分配一个唯一的魔幻数。
(9)成员 s_root 指向根目录的结构体 dentry。
(10)成员 s_fs_info 指向具体文件系统的私有信息。
(11)成员 s_instances 用来把同一个文件系统类型的所有超级块实例链接在一起,链表的头节点是结构体 file_system_type 的成员 fs_supers。
超级块操作集合的数据结构是结构体 super_operations,主要成员如下:
include/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 flags); int (*write_inode) (struct inode *, struct writeback_control *wbc); int (*drop_inode) (struct inode *); void (*evict_inode) (struct inode *); void (*put_super) (struct super_block *); int (*sync_fs)(struct super_block *sb, int wait); … int (*statfs) (struct dentry *, struct kstatfs *); int (*remount_fs) (struct super_block *, int *, char *); void (*umount_begin) (struct super_block *); …
};
(1)成员 alloc_inode 用来为一个索引节点分配内存并且初始化。
(2)成员 destroy_inode 用来释放内存中的索引接点。
(3)成员 dirty_inode 用来把索引节点标记为脏。
(4)成员 write_inode 用来把一个索引节点写到存储设备。
(5)成员 drop_inode 用来在索引节点的引用计数减到 0 时调用。
(6)成员 evict_inode 用来从存储设备上的文件系统中删除一个索引节点。
(7)成员 put_super 用来释放超级块。
(8)成员 sync_fs 用来把文件系统修改过的数据同步到存储设备。
(9)成员 statfs 用来读取文件系统的统计信息。
(10)成员 remount_fs 用来在重新挂载文件系统的时候调用。
(11)成员 umount_begin 用来在卸载文件系统的时候调用。
挂载描述符
一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符:mount 结构体。挂载描述符用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以多次挂载,每次挂载到不同的目录下。挂载描述符的主要成员如下:
fs/mount.h
struct mount { struct hlist_node mnt_hash; struct mount *mnt_parent; struct dentry *mnt_mountpoint; struct vfsmount mnt; union { struct rcu_head mnt_rcu; struct llist_node mnt_llist; };
#ifdef CONFIG_SMP struct mnt_pcp __percpu *mnt_pcp;
#else int mnt_count; int mnt_writers;
#endif struct list_head mnt_mounts; struct list_head mnt_child; struct list_head mnt_instance; const char *mnt_devname; struct list_head mnt_list; … struct mnt_namespace *mnt_ns; struct mountpoint *mnt_mp; struct hlist_node mnt_mp_list; …
}
假设我们把文件系统2挂载到目录“/a”下,目录a属于文件系统1。目录a称为挂载点,文件系统2的mount实例是文件系统1的 mount 实例的孩子,文件系统1的 mount 实例是文件系统2的 mount 实例的父亲。
(1)成员 mnt_parent 指向父亲,即文件系统1的 mount 实例。
(2)成员 mnt_mountpoint 指向作为挂载点的目录,即文件系统1的目录a,目录a的dentry实例的成员d_flags设置了标志位DCACHE_MOUNTED。
(3)成员 mnt 的类型如下:
struct vfsmount { struct dentry *mnt_root; struct super_block *mnt_sb; int mnt_flags;
};
mnt_root 指向文件系统2的根目录,mnt_sb指向文件系统2的超级块。
(4)成员 mnt_hash 用来把挂载描述符加入全局散列表 mount_hashtable,关键字是{父挂载描述符,挂载点}。
(5)成员 mnt_mounts 是孩子链表的头节点。
(6)成员 mnt_child 用来加入父亲的孩子链表。
(7)成员 mnt_instance 用来把挂载描述符添加到超级块的挂载实例链表中,同一个存储设备上的文件系统,可以多次挂载,每次挂载到不同的目录下。
(8)成员 mnt_devname 指向存储设备的名称。
(9)成员 mnt_ns 指向挂载命名空间。
(10)成员 mnt_mp 指向挂载点,类型如下:
struct mountpoint { struct hlist_node m_hash; struct dentry *m_dentry; struct hlist_head m_list; int m_count;
};
m_dentry 指向作为挂载点的目录,m_list 用来把同一个挂载点下的所有挂载描述符链接起来。为什么同一个挂载点下会有多个挂载描述符?这和挂载命名空间有关。
(11)成员 mnt_mp_list 用来把挂载描述符加入同一个挂载点的挂载描述符链表,链表的头节点是成员 mnt_mp 的成员 m_list。
文件系统类型
因为每种文件系统的超级块的格式不同,所以每种文件系统需要向虚拟文件系统注册文件系统类型 file_system_type,并且实现 mount 方法用来读取和解析超级块。结构体 file_system_type 如下:
include/linux/fs.h
struct file_system_type { const char *name; int fs_flags;
#define FS_REQUIRES_DEV 1
#define FS_BINARY_MOUNTDATA 2
#define FS_HAS_SUBTYPE 4
#define FS_USERNS_MOUNT 8
#define FS_RENAME_DOES_D_MOVE 32768 struct dentry *(*mount) (struct file_system_type *, int, const char *, void *); void (*kill_sb) (struct super_block *); struct module *owner; struct file_system_type * next; struct hlist_head fs_supers; …
};
(1)成员 name 是文件系统类型的名称。
(2)方法 mount 用来在挂载文件系统的时候读取并且解析超级块。
(3)方法 kill_sb 用来在卸载文件系统的时候释放超级块。
(4)多个存储设备上的文件系统的类型可能相同,成员 fs_supers 用来把相同文件系统
类型的超级块链接起来。
索引节点
在文件系统中,每个文件对应一个索引节点,索引节点描述两类信息。
(1)文件的属性,也称为元数据(metadata),例如文件长度、创建文件的用户的标识符、上一次访问的时间和上一次修改的时间,等等。
(2)文件数据的存储位置。
每个索引节点有一个唯一的编号。
当内核访问存储设备上的一个文件时,会在内存中创建索引节点的一个副本:结构体inode,主要成员如下:
include/linux/fs.h
struct inode { umode_t i_mode; unsigned short i_opflags; kuid_t i_uid; kgid_t i_gid; unsigned int i_flags; #ifdef CONFIG_FS_POSIX_ACL struct posix_acl *i_acl; struct posix_acl *i_default_acl;
#endif const struct inode_operations *i_op; struct super_block *i_sb; struct address_space *i_mapping; … unsigned long i_ino; union { const unsigned int i_nlink; unsigned int __i_nlink; }; dev_t i_rdev; loff_t i_size; struct timespec i_atime; struct timespec i_mtime; struct timespec i_ctime; spinlock_t i_lock; unsigned short i_bytes; unsigned int i_blkbits; blkcnt_t i_blocks; … struct hlist_node i_hash; struct list_head i_io_list; … struct list_head i_lru; struct list_head i_sb_list; struct list_head i_wb_list; union { struct hlist_head i_dentry; struct rcu_head i_rcu; }; u64 i_version; atomic_t i_count; atomic_t i_dio_count; atomic_t i_writecount;
#ifdef CONFIG_IMA atomic_t i_readcount;
#endif const struct file_operations *i_fop; struct file_lock_context *i_flctx; struct address_space i_data; struct list_head i_devices; union { struct pipe_inode_info *i_pipe; struct block_device *i_bdev; struct cdev *i_cdev; char *i_link; unsigned i_dir_seq; }; … void *i_private; };
i_mode 是文件类型和访问权限,i_uid 是创建文件的用户的标识符,i_gid 是创建文件的用户所属的组标识符。
i_ino 是索引节点的编号。
i_size 是文件长度;i_blocks 是文件的块数,即文件长度除以块长度的商;i_bytes 是文件长度除以块长度的余数;i_blkbits 是块长度以 2 为底的对数,块长度是 2 的 i_blkbits 次幂。
i_atime(access time)是上一次访问文件的时间,i_mtime(modified time)是上一次修改文件数据的时间,i_ctime(change time)是上一次修改文件索引节点的时间。
i_sb 指向文件所属的文件系统的超级块。
i_mapping 指向文件的地址空间。
i_count 是索引节点的引用计数,i_nlink 是硬链接计数。
如果文件的类型是字符设备文件或块设备文件,那么 i_rdev 是设备号,i_bdev 指向块设备,i_cdev 指向字符设备。
文件分为以下几种类型。
(1)普通文件(regular file):就是我们通常说的文件,是狭义的文件。
(2)目录:目录是一种特殊的文件,这种文件的数据是由目录项组成的,每个目录项
存储一个子目录或文件的名称以及对应的索引节点号。
(3)符号链接(也称为软链接):这种文件的数据是另一个文件的路径。
(4)字符设备文件。
(5)块设备文件。
(6)命名管道(FIFO)。
(7)套接字(socket)。
字符设备文件、块设备文件、命名管道和套接字是特殊的文件,这些文件只有索引节点,没有数据。字符设备文件和块设备文件用来存储设备号,直接把设备号存储在索引节点中。
内核支持两种链接。
(1)软链接,也称为符号链接,这种文件的数据是另一个文件的路径。
(2)硬链接,相当于给一个文件取了多个名称,多个文件名称对应同一个索引节点,索引节点的成员 i_nlink 是硬链接计数。
索引节点的成员 i_op 指向索引节点操作集合 inode_operations,成员 i_fop 指向文件操作集合 file_operations。两者的区别是:inode_operations 用来操作目录(在一个目录下创建或删除文件)和文件属性,file_operations 用来访问文件的数据。索引节点操作集合的数据结构是结构体 inode_operations,主要成员如下:
include/linux/fs.h
struct inode_operations { struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int); const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *); int (*permission) (struct inode *, int); struct posix_acl * (*get_acl)(struct inode *, int); int (*readlink) (struct dentry *, char __user *,int); int (*create) (struct inode *,struct dentry *, umode_t, bool); 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 *,umode_t); int (*rmdir) (struct inode *,struct dentry *); int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t); int (*rename) (struct inode *, struct dentry *, struct inode *, struct dentry *, unsigned int); int (*setattr) (struct dentry *, struct iattr *); int (*getattr) (const struct path *, struct kstat *, u32, unsigned int); ssize_t (*listxattr) (struct dentry *, char *, size_t); int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,u64 len); int (*update_time)(struct inode *, struct timespec *, int); int (*atomic_open)(struct inode *, struct dentry *, struct file *, unsigned open_flag, umode_t create_mode, int *opened); int (*tmpfile) (struct inode *, struct dentry *, umode_t); int (*set_acl)(struct inode *, struct posix_acl *, int);
} ____cacheline_aligned;
lookup 方法用来在一个目录下查找文件。
系统调用 open 和 creat 调用 create 方法来创建普通文件,系统调用 link 调用 link 方法来创建硬链接,系统调用 symlink 调用 symlink 方法来创建符号链接,系统调用 mkdir 调用 mkdir 方法来创建目录,系统调用 mknod 调用 mknod 方法来创建字符设备文件、块设备文件、命名管道和套接字。
系统调用 unlink 调用 unlink 方法来删除硬链接,系统调用 rmdir 调用 rmdir 方法来删除目录。
系统调用 rename 调用 rename 方法来给文件换一个名字。
系统调用 chmod 调用 setattr 方法来设置文件的属性,系统调用 stat 调用 getattr 方法来读取文件的属性。
系统调用 listxattr 调用 listxattr 方法来列出文件的所有扩展属性。
目录项
文件系统把目录当作文件,这种文件的数据是由目录项组成的,每个目录项存储一个子目录或文件的名称以及对应的索引节点号。
当内核访问存储设备上的一个目录项时,会在内存中创建目录项的一个副本:结构体dentry,主要成员如下:
include/linux/dcache.h
struct dentry { /* RCU查找访问的字段 */ 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]; /* 引用查找也访问下面的字段 */ struct lockref d_lockref; const struct dentry_operations *d_op; struct super_block *d_sb; unsigned long d_time; void *d_fsdata; union { struct list_head d_lru; wait_queue_head_t *d_wait; }; struct list_head d_child; struct list_head d_subdirs; /** d_alias和d_rcu可以共享内存*/ union { struct hlist_node d_alias; struct hlist_bl_node d_in_lookup_hash; struct rcu_head d_rcu; } d_u;
};
d_name 存储文件名称,qstr 是字符串的包装器,存储字符串的地址、长度和散列值;如果文件名称比较短,把文件名称存储在 d_iname;d_inode 指向文件的索引节点。
d_parent 指向父目录,d_child 用来把本目录加入父目录的子目录链表。
d_lockref 是引用计数。
d_op 指向目录项操作集合。
d_subdirs 是子目录链表。
d_hash 用来把目录项加入散列表 dentry_hashtable。
d_lru 用来把目录项加入超级块的最近最少使用(Least Recently Used,LRU)链表 s_dentry_lru 中,当
目录项的引用计数减到 0 时,把目录项添加到超级块的 LRU 链表中。
d_alias 用来把同一个文件的所有硬链接对应的目录项链接起来。
以文件“/a/b.txt”为例,目录项和索引节点的关系如图所示。
目录项操作集合的数据结构是结构体 dentry_ operations,其代码如下:
include/linux/dcache.h
struct dentry_operations { int (*d_revalidate)(struct dentry *, unsigned int); int (*d_weak_revalidate)(struct dentry *, unsigned int); int (*d_hash)(const struct dentry *, struct qstr *); int (*d_compare)(const struct dentry *, unsigned int, const char *, const struct qstr *); int (*d_delete)(const struct dentry *); int (*d_init)(struct dentry *); void (*d_release)(struct dentry *); void (*d_prune)(struct dentry *); void (*d_iput)(struct dentry *, struct inode *); char *(*d_dname)(struct dentry *, char *, int); struct vfsmount *(*d_automount)(struct path *); int (*d_manage)(const struct path *, bool); struct dentry *(*d_real)(struct dentry *, const struct inode *, unsigned int);
} ____cacheline_aligned;
d_revalidate 对网络文件系统很重要,用来确认目录项是否有效。
d_hash 用来计算散列值。
d_compare 用来比较两个目录项的文件名称。
d_delete 用来在目录项的引用计数减到 0 时判断是否可以释放目录项的内存。
d_release 用来在释放目录项的内存之前调用。
d_iput 用来释放目录项关联的索引节点。
文件的打开实例和打开文件表
当进程打开一个文件的时候,虚拟文件系统就会创建文件的一个打开实例:file 结构体,主要成员如下。
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; const struct file_operations *f_op; spinlock_t f_lock; 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; … void *private_data; … struct address_space *f_mapping;
} __attribute__((aligned(4)));
(1)f_path 存储文件在目录树中的位置,类型如下:
struct path { struct vfsmount *mnt; struct dentry *dentry;
};
mnt 指向文件所属文件系统的挂载描述符的成员 mnt,dentry 是文件对应的目录项。
(2)f_inode 指向文件的索引节点。
(3)f_op 指向文件操作集合。
(4)f_count 是 file 结构体的引用计数。
(5)f_mode 是访问模式。
(6)f_pos 是文件偏移,即进程当前正在访问的位置。
(7)f_mapping 指向文件的地址空间。
文件的打开实例和索引节点的关系如图所示。
文件系统信息结构体的主要成员如下:
include/linux/fs_struct.h
struct fs_struct { … struct path root, pwd;
};
成员 root 存储进程的根目录,成员 pwd 存储进程的当前工作目录。
假设首先调用系统调用 chroot,把目录“/a”设置为进程的根目录,然后创建子进程,子进程继承父进程的文件系统信息,那么把子进程能看到的目录范围限制为以目录“/a”为根的子树。当子进程打开文件“/b.txt”(文件路径是绝对路径,以“/”开头)时,真实的文件路径是“/a/b.txt”。
假设调用系统调用 chdir,把目录“/c”设置为进程的当前工作目录,当子进程打开文件“d.txt”(文件路径是相对路径,不以“/”开头)时,真实的文件路径是“/c/d.txt”。
打开文件表也称为文件描述符表,数据结构如图所示,结构体 files_struct 是打开文件表的包装器,主要成员如下:
include/linux/fdtable.h
struct files_struct { atomic_t count; … struct fdtable __rcu *fdt; struct fdtable fdtab; 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];
};
成员 count 是结构体 files_struct 的引用计数。
成员 fdt 指向打开文件表。
当进程刚刚创建的时候,成员 fdt 指向成员 fdtab。运行一段时间以后,进程打开的文件数量超过NR_OPEN_DEFAULT,就会扩大打开文件表,重新分配 fdtable 结构体,成员fdt 指向新的 fdtable 结构体。
打开文件表的数据结构如下:
include/linux/fdtable.h
struct fdtable { unsigned int max_fds; struct file __rcu **fd; unsigned long *close_on_exec; unsigned long *open_fds; unsigned long *full_fds_bits; struct rcu_head rcu;
};
(1)成员 max_fds 是打开文件表的当前大小,即成员 fd 指向的 file 指针数组的大小。随着进程打开文件的数量增加,打开文件表逐步扩大。
(2)成员 fd 指向 file 指针数组。当进程调用 open 打开文件的时候,返回的文件描述符是 file 指针数组的索引。
(3)成员 close_on_exec 指向一个位图,指示在执行 execve()以装载新程序的时候需要关闭哪些文件描述符。
(4)成员 open_fds 指向文件描述符位图,指示哪些文件描述符被分配。