由于计算机系统所处理的信息量很大,所以不可能把所有信息全部保存在内存中,通常把它们按文件的形式保存在外存中。然而,在多用户系统中,既要保证各用户文件的存放位置不冲突,又要防止任一用户对外存空间占而不用;既要保证任一用户的文件不被末经授权的用户窃取、破坏,又要允许在一定条件下多个用户共享某些文件。因此有一个公共的管理机构来负责统一使用外存空间,管理外存空间的文件,这就引入了文件系统。
所谓文件系统,就是操作系统中实现文件统一管理的一组软件、被管理的文件以及为实施文件管理所需要的一些数据结构的总称。
文件系统作为一个统一的信息管理机构,应具有下5功能:
统一管理文件存储空间(即外存),实施存储空间的分配与回收
确定文件信息的存放位置及存放形式
实现文件从名字空间到外存地址空间的映射,即实现文件的按名存取
有效实现对文件的各种控制操作(如建立、撤消、打开、关闭文件等)和存取操作(如读、写、修改、复制、转储等)
实现文件信息的共享,并且提供可靠的文件保密和保护措施。
文件结构是指文件的组织形式。通常分为文件的逻辑结构和文件的物理结构。
文件的逻辑结构是指:文件的组织形式,即从用户角度看到的文件组织形式,用户以这种形式存取、检索和加工有关信息。
文件的物理结构是指文件的内部组织形式,亦即文件在物理存储设备上的存放方法。由于文件的物理结构决定了文件信息在存储设备上的存储位置,因此,文件信息的逻辑块号到物理块号的转换也是由文件的物理结构决定的。
此外,文件的存取方式也与文件的物理结构有关。不同的文件系统一般对应着不同的物理结构。
要实现操作系统对其它各种不同文件系统的支持,就要将对各种不同文件系统的操作和管理纳入到一个统一的框架中。对用户程序隐去各种不同文件系统的实现细节,为用户程序提供一个统一的、抽象的、虚拟的文件系统界面,这就是所谓的虚拟文件系统(Virtual File System Switch,VFS)。例如,在 Linux 操作系统中,可以将 DOS 格式的磁盘或分区,即文件系统,“安装”到系统中,然后用户程序就可以按完全相同的方式访问这些文件,就好像它们也是 Ext2 格式的文件一样。
通常,虚拟文件系统分为三个层次,如下图所示:
第一层为文件系统接口层,如 open、write、close 等系统调用接口。
第二层为 VFS (Virtual File System) 接口层。该层有两个接口:一个是与用户的接口;一个是与特定文件系统的接口。VFS与用户的接口将所有对文件的操作定向到相应的特定文件系统函数上。VFS 与特定文件系统的接口主要是通过 vfs-operations 来实现的。
第三层是具体文件系统层,提供具体文件系统的结构和实现,包括网络文件系统,如 NFS (network file system) 。
超级块 (superblock) 对象。代表一个文件系统,存放以安装的文件系统信息。如果是基于磁盘的文件系统,该对象便对应于存放在磁盘系统上文件系统控制块,每个文件系统都对应一个超级块对象。
索引节点(inode)对象。代表一个文件。存放通用的文件信息。如果是基于磁盘的文件系统,该对象通常对应于存在在磁盘上的文件控制块(FCB),每个文件都有一个 inode 对象。
目录项(dentry)对象。代表路径中的一个组成部分。存放目录项与对应文件进行链接的各种信息。 VFS 把最近常使用的 dentry 对象放在目录项告诉缓存中,加快文件路径名搜索过程,以提高系统性能。
文件(file)对象。代表进程已打开的一个文件。存放已打开的文件与进程的交互信息,这些信息仅当进程访问文件期间才存于主存中。文件对象在执行系统调用 open() 时创建,执行系统调用 close() 时撤销。
每个主要对象都包含一个操作对象,它描述了内核针对主要对象可以使用的方法。
super_operation 对象。其中包括内核针对特定文件系统所能调用的方法。
inode_operation 对象。其中包括内核针对特定文件所能调用的方法。
dentry_operation 对象。其中包括针对特定目录所能调用的方法。
file_operation 对象。其中包括进程针对已打开文件所能调用的方法。
超级块对象
超级块对象描述一个文件系统的信息,对于每个具体的文件系统来说,都有各自的超级块,如 Ext2 超级块,并被存放在磁盘特定扇区上。
当内核对一个具体文件系统进行初始化和注册时,调用文件系统提供的函数为其分配一个 VFS 超级块,并从此读取具体文件系统超级块中的信息填充进来。
VFS 超级块是各个具体文件系统安装时才建立的,并在这些具体文件系统卸载时被自动删除,故 VFS 超级块仅存于主存中。
struct super_block {
struct list_head s_list; //把所有超级块双向链接起来
dev_t s_dev; //文件系统所在设备标识符
unsigned long s_blocksize; //以字节为单位的盘块大小
unsigned char s_blocksize_bits; //以2的幂次表示的盘块大小,如4KB,则为12
unsigned char s_dirt; //修改脏标记
loff_t s_maxbytes; //文件大小上限
struct file_system_type *s_type; //指向注册表file_system_type结构的指针
const struct super_operations *s_op; //指向超级块操作函数集的指针
unsigned long s_flags; //标记位
unsigned long s_magic; //魔幻数
struct dentry *s_root; //安装目录的目录项对象
struct rw_semaphore s_umount; //卸载信号量
struct mutex s_lock; //超级块信号量
int s_count; //超级块引用计数
struct list_head s_inodes; //所有I节点数
struct list_head s_files;
struct list_head s_dentry_lru; /* unused dentry lru */
int s_nr_dentry_unused; /* # of dentry on lru */
…
struct list_head s_instances; //该类型文件系统
char s_id[32]; /* 文本名字 */
void *s_fs_info; //各个具体文件系统私有数据结构
struct super_operations {
struct inode *(*alloc_inode)(struct super_block *sb); //分配一个inode
void (*destroy_inode)(struct inode *); //销毁一个inode
void (*dirty_inode) (struct inode *); //将inode标记为脏
int (*write_inode) (struct inode *, int); //将给定的inode写回磁盘
void (*drop_inode) (struct inode *); //逻辑上释放inode
void (*delete_inode) (struct inode *); //物理上释放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 *); //清除一个inode
void (*umount_begin) (struct super_block *);
int (*show_options)(struct seq_file *, struct vfsmount *);
int (*show_stats)(struct seq_file *, struct vfsmount *);
int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
};
索引节点对象
inode 对象内包含了内核在操作文件或目录时需要的全部信息,文件名可以更改,但 inode 对文件是唯一的,且随文件的存在而存在。
一个 inode 代表文件系统中的一个文件,它可以是设备或管道这类特殊文件,故 inode 中会包含特殊的项。
当一个文件首次被访问时,内核会在内存中组装相应的索引节点对象,以便向内核提供对一个文件进行操 作时所必需的全部信息;这些信息一部分存储在磁盘特定位置,另外一部分是在加载时动态填充的。
struct inode {
struct hlist_node i_hash;
struct list_head i_list; /* backing dev IO list */
struct list_head i_sb_list;
struct list_head i_dentry;
unsigned long i_ino; //索引节点号
atomic_t i_count; //引用计数
umode_t i_mode; //文件类型与访问权限
……
const struct inode_operations *i_op; //索引操作指针
const struct file_operations *i_fop; //文件操作指针
struct super_block *i_sb; //指向超级块指针
struct file_lock *i_flock; //指向文件锁链表的指针
struct address_space *i_mapping; //共享内存中使用的address_space指针
struct address_space i_data; //设备的address_space对象
struct list_head i_devices; //设备链表
union {
struct pipe_inode_info *i_pipe; //管道设备I节点信息
struct block_device *i_bdev; //指向块设备驱动程序
struct cdev *i_cdev; //指向字符设备驱动程序
};
......
}
struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct nameidata *); //创建一个新的inode
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *); //查找一个inode所在的目录
int (*link) (struct dentry *,struct inode *,struct dentry *); //创建一个硬链接
int (*unlink) (struct inode *,struct dentry *); //删除一个硬链接
int (*symlink) (struct inode *,struct dentry *,const char *); //为符号链接创建一个inode
int (*mkdir) (struct inode *,struct dentry *,int); //为目录项创建一个inode
int (*rmdir) (struct inode *,struct dentry *); //为目录项删除一个inode
int (*mknod) (struct inode *,struct dentry *,int,dev_t); //建立一个目录项和一个特殊文件的对应索引节点
......
}
目录项对象
VFS 把每个目录看作一个文件,如在路径/tmp/test
中, tmp 和 test 都是文件, tmp 是目录文件,而 test 是普通文件, tmp 和 test都有一个 inode 对象表示。
每一个文件除了有一个 inode 数据结构外,还有一个 dentry 数据结构与之关联,该结构中的 d_inode 指针指向相应的 inode 结构。
dentry 数据结构可以加快对文件的快速定位,改进文件系统的效率。
dentry 描述文件的逻辑属性,它在磁盘上没有对应的映像; inode 结构记录文件的物理属性,在磁盘上有对应的映像。
struct dentry {
atomic_t d_count; //目录引用计数
unsigned int d_flags; //目录项状态标志
spinlock_t d_lock; //目录自旋锁
int d_mounted; //是否为安装点目录项
struct inode *d_inode; //目录项所在的Indoe节点
struct hlist_node d_hash; //目录项形成的hash表
struct dentry *d_parent; //父目录的目录项对象
struct qstr d_name; //目录项名字,用于快速查
struct list_head d_lru; //未使用的LRU双向链表
union {
struct list_head d_child; //父目录的子目录形成的双向链表
struct rcu_head d_rcu;
} d_u;
struct list_head d_subdirs; //该目录项的子目录的双向链表
struct list_head d_alias; //inode别名的链表
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]; //文件名前15个字符
};
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 *); //删除d_couny为0的目录项对象
void (*d_release)(struct dentry *); //释放一个目录项对象
void (*d_iput)(struct dentry *, struct inode *); //丢弃目录项对应的Indoe
};
文件对象
文件对象在磁盘上没有映像,在文件被打开时创建由一个 file 结构组成。
文件对象中的信息主要是文件指针,即文件中当前的位置,下一个操作将在该位置发生。
file 结构除保存文件当前位置外,还把指向该文件 inode 的指针放在其中,并形成一个双项链表,称系统打开文件表
struct file {
union {
struct list_head fu_list; //文件对象链表
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path; //
const struct file_operations *f_op; //文件操作函数集
spinlock_t f _lock; /* f_ep_links, f_flags, no IRQ */
atomic_long_t f_count; //文件对象引用计数
unsigned int f_flags;
fmode_t f_mode; //操作文件模式
loff_t f_pos; //当前文件位置
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;
u64 f_version;
/* needed for tty driver, and maybe others */
void *private_data;
struct address_space *f_mapping;
}
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 (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);//向硬设备发送命令
int (*flush) (struct file *, fl_owner_t id); //关闭文件时刷新文件
I int (*release) (struct inode *, struct file *); //释放文件对象
int (*fsync) (struct file *, struct dentry *, int datasync); //将文件在缓存的数据写回磁盘
根据文件系统所在的物理介质和数据在物理介质上的组织方式来区分不同的文件系统类型的。 file_system_type 结构用于描述具体的文件系统的类型信息。被 Linux 支持的文件系统,都有且仅有一 个 file_system_type 结构而不管它有零个或多个实例被安装到系统中。
struct file_system_type {
const char *name; /*文件系统的名字*/
struct subsystem subsys; /*sysfs子系统对象*/
int fs_flags; /*文件系统类型标志*/
/*在文件系统被安装时,从磁盘中读取超级块,在内存中组装超级块对象*/
struct super_block *(*get_sb) (struct file_system_type*,
int, const char*, void *);
void (*kill_sb) (struct super_block *); /*终止访问超级块*/
struct module *owner; /*文件系统模块*/
struct file_system_type * next; /*链表中的下一个文件系统类型*/
struct list_head fs_supers; /*具有同一种文件系统类型的超级块对象链表*/
};
每当一个文件系统被实际安装,就有一个 vfsmount 结构体被创建,这个结构体对应一个安装点。
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; /*子文件系统链表*/
atomic_t mnt_count; /*使用计数*/
int mnt_flags; /*安装标志*/
char *mnt_devname; /*设备文件名*/
struct list_head mnt_list; /*描述符链表*/
struct list_head mnt_fslink; /*具体文件系统的到期列表*/
struct namespace *mnt_namespace; /*相关的名字空间*/
};
用户打开文件相关信息 files_struct
进程的当前的工作目录和根目录的相关信息 fs_struct
struct task_struct {
......
struct fs_struct *fs; /*文件系统信息*/
struct files_struct *files; /*当前打开的文件信息*/
......
};
文件描述符 fd 用来描述打开的文件,每个进程用一个 files_struct 结构来记录文件描述符的使用情况,这个结构称为用户打开文件表。指向该结构的指针被用来保存在进程的 task_struct 结构的成员 files 中。
struct files_struct {
atomic_t count; //共享该表的进程数
struct fdtable *fdt; //文件描述符指针,指向fdtab
struct fdtable fdtab; //文件描述符表
spinlock_t file_lock ____cacheline_aligned_in_smp; //保护该结构的锁
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是指向文件对象的指针数组的指针,数组的长度存放在 max_fdset 域中。通常,fd 域指向 files_struct 结构的 fd_array 域,该域包含 NR_OPEN_DEFAULT (默认值为32)个文件对象指针。如果进程打开的文件数目大于 NR_OPEN_DEFAULT, n 内核分配一个新的文件指针数组,并将其地址存放在 fd 域中,同时更新 max_fds 域的值。
struct fdtable {
unsigned int max_fds; //进程当前具有的最大文件数
struct file ** fd; //指向文件对象(系统打开文件表项)的指针数组
fd_set *close_on_exec; //指向执行exec()函数时需关闭的文件描述符
fd_set *open_fds; //指向打开文件的描述符的指针
struct fdtable *next; //指向下一个描述表
};
对于在fd数组中有入口地址的每个文件来说,数组的下标就是文件描述符。通常,数组的第一个元素(索引为0)、第二个元素(索引为1)、第三个元素(索引为2)分别表示标准输入文件、标准输出文件、和标准错误文件,且这三个文件通常从父进程处继承而来。通过适当的系统调用,两个文件描述符可以指向同一个打开的文件,亦即数组的两个元素可以指向同一个文件对象。
为了进行文件查找,每个进程都有一个当前工作目录和当前工作目录所在文件系统根目录。
struct fs_struct { //建立进程与文件系统的关系
atomic_t count; /*结构的使用计数*/
rwlock_t lock; /*保护该结构体的锁*/
int umask; /*默认的文件访问权限*/
struct dentry * root; /*根目录的目录项对象*/
struct dentry * pwd; /*当前工作目录的目录项对象*/
struct dentry * altroot; /*可供选择的根目录的目录项对象*/
struct vfsmount * rootmnt; /*根目录的安装点对象*/
struct vfsmount * pwdmnt; /*当前工作目录的安装点对象*/
struct vfsmount * altrootmnt; /*可替换的根目录的安装点对象*/
};
文件的打开操作在内核中的实现思路很简单:即通过用户态传递的路径逐项查找文件;如果该文件存在,那么内核将为该文件创建 file 结构;同时将该 file 结构与 files 数组关联,最终返回数组的索引作为用户态的文件描述符。
路径查找是对给定的文件路径以目录项为单位进行逐级解析。主要包括以下几项内容:
确定路径查找的起始位置。比如,起始位置可能是current->fs->cwd
或current->fs->root
;
当前进程是否有对目录项关联的 inode 进行访问的权限;
根据当前的目录项,对下一级目录项进行查找;这里的查找可能是向下查找子文件,也可能是向上反查父目录(比如下一级目录项为“..”);
处理挂载点问题;当前目录项如果是挂载点,那么必须处理不同文件系统之间的跨越;
处理符号链接文件:如果当前目录项为一个符号链接文件,那么必须追随(follow)该文件所指向的真实文件;
查找并创建文件路径中所缺失的部分;比如,通过 open() 创建一个新文件时,那么所传递的路径中可能有部分目录项当前是不存在的;
其中,第1项是路径查找的首要工作;2~6项是在路径查找过程中,针对每个目录项进行检查确认的。
负责 open 系统调用基本实现的是 do_sys_open() ,其内部所调用的 do_filp_open 函数承担了大部分 open 的实现过程,其中就包括路径查找。
首先,get_unused_fd_flags() 得到一个可用的文件描述符;通过该函数,可知文件描述符实质是进程打开文件列表中对应某个文件对象的索引值;
接着,do_filp_open() 打开文件,返回一个 file 对象,代表由该进程打开的一个文件;进程通过这样的一个数据结构对物理文件进行读写操作。
最后,fd_install() 建立文件描述符与 file 对象的联系,以后进程对文件的读写都是通过操纵该文件描述符而进行。
用于设置路径搜寻的起始位置,主要体现在设置nd变量
如果 flags 设置了 LOOKUP_ROOT 标志,则表示该函数被 open_by_handle_at 函数调用,该函数将指定一个路径作为根;这属于特殊情况,这里暂不分析;接下来 path_init 主要分三种情况设置 nd 。
如果路径名 name 以/
为起始,则表示当前路径是一个绝对路径,通过 set_root 设置 nd ;否则,表示路径 name 是一个相对路径;
如果 dfd 为 AT_FDCWD ,那么表示这个相对路径是以当前路径 pwd 作为起始的,因此通过pwd设置 nd ;
如果 dfd 不是 AT_FDCWD ,表示这个相对路径是用户设置的,需要通过 dfd 获取具体相对路径信息,进而设置 nd ;
上述步骤2和3都表示要打开的文件路径是以相对路径为起始的,但是两者稍有不同。步骤2为我们通常默认的 open 操作,而步骤3具体指的是 openat 系统调用,这一点体现在不同打开系统调用向 do_sys_open 中 dfd 参数所传递的值。
不管上述哪一种打开情况,均要设置 nd 变量,它是一个 nameidata 类型。在 path_init 中,nd 的 last_type 都被默认设置成了 LAST_ROOT 。
在 path_init 中,如果为上述步骤1,则通过当前进程的fs->root
字段更新 nd 的 root 字段,并且 nd 的 path 字段也指向 root 字段;如果为步骤2,则通当前进程fs->pwd
更新 nd 的 path 字段;如果为步骤3,则先通过文件描述符 dfd 获取用户指定的工作目录 file 结构,然后通过 file 的 f_path 字段更新 nd 的 path 字段。需要注意的是,步骤2和步骤3均未设置 root 字段。 最终,nd 中的 inode 字段均由path.dentry->d_inode
更新。
link_path_walk() 主要用于对各目录项逐级遍历。其函数声明如下:
static __always_inline int link_path_walk(const char *name, struct nameidata *nd)
该函数核心部分是通过一个循环完成的。在进入这个循环之前,如果路径name是一个绝对路径,那么该函数还对路径进行了一些处理,即过滤掉绝对路径/前多余的符号/。
在循环中,所要做的工作包含如下:
next 为 path 类型的变量,指向下一个目录项;name 指向被搜索的路径;this 为qstr 类型变量,表示当前搜索路径所处目录项的哈希值,用type指明当前目录项类型;
如果有必要,为当前目录项更新哈希值,并保存在 this 中;
如果当前目录项为“.”,则 type 为 LAST_DOT ;如果目录项为“..”,则 type 为 LAST_DOTDOT ;否则,type 默认为 LAST_NORM;
如果当前目录项紧邻的分隔符/有多个(比如/home///edsionte
),则将其过滤,即使 name 指向最后一个/
;
通过 walk_component() 处理当前目录项,更新 nd 和 next ;如果当前目录项为符号链接文件,则只更新 next ;
如果当前目录项为符号链接文件,则通过 nested_symlink() 进行处理,更新 nd ;
如果 name 中的目录项遍历完毕,则结束;否则进行下一轮循环;
通过上述循环,将用户所指定的路径 name 从头至尾进行了搜索,至此 nd 保存了最后一个目录项的信息,但是内核并没有确定最后一个目录项是否真的存在,这些工作将在 do_last() 中进行。
VFS 即虚拟文件系统是 Linux 文件系统中的一个抽象软件层;因为它的支持,各种不同的实际文件系统才能在 Linux 中共存,跨文件系统操作才能实现。 VFS 借助它四个主要的数据结构即超级块、索引节点、目录项和文件对象以及一些辅助的数据结构,向 Linux 中不管是普通的文件还是目录、设备、套接字等都提供同样的操作界面,如打开、读写、关闭等。只有当把控制权传给实际的文件系统时,实际的文件系统才会做出区分,对不同的文件类型执行不同的操作。由此 可见,正是有了 VFS 的存在,跨文件系统操作才能执行,Unix/Linux 中的“一切皆是文件”的口号才能够得以实现。``