虚拟文件系统VFS(下)
9 目录项对象
VFS把目录当作文件对待,所以在路径/bin/vi中,bin和vi都属于文件——bin是特殊的目录文件,vi是一个普通文件,路径中的每一个组成部分都由一个索引节点对象表示。虽然可以统一由索引节点表示,但VFS需要经常执行目录相关的操作,比如路径名查找等。路径名查找需要解析路径中的每一个组成部分,不但需要确保有效,还需要进一步寻找路径中的下一个组成部分。
为了方便查找操作, VFS引入了目录项的概念。每个dentry代表路径中的一个特定部分。对前一个例子,/ 、bin与vi都属于目录项对象。前两个是目录,最后一个是普通文件。需要明确的是,在路径中(包括普通文件在内),每一个部分都是目录项对象。解析一个路径并遍历过程繁琐、耗时较高,引入目录项使得这个过程更为简单。
目录项也可包括安装点。在路径/mnt/cdrom/foo中,元素 / 、mnt、cdrom与foo都属于目录项对象。VFS在执行目录操作时可能会现场创建目录项对象。
目录项对象由dentry结构体表示,定义在文件<linux/dcache.h>中,给出结构体:
struct dentry {
atomic_t d_count; /* 使用计数 */
unsigned long d_vfs_flags; /* 目录项标识 */
spinlock_t d_lock; /* 单目录项锁 */
struct inode * d_inode; /* 相关联的索引节点 */
struct list_head d_lru; /* 未使用的链表 */
struct list_head d_child; /* 目录项内部形成的链表 */
struct list_head d_subdirs; /* 子目录链表 */
struct list_head d_alias; /* 索引节点别名链表 */
unsigned long d_time; /* 重置时间 */
struct dentry_operations *d_op; /* 目录项操作指针 */
struct super_block * d_sb; /* 文件的超级块 */
int d_mounted; /* 是否是挂载点的目录项 */
void * d_fsdata; /* 文件系统特有数据 */
struct rcu_head d_rcu; /* RCU加锁 */
struct dentry * d_parent; /* 父目录的目录项对象 */
struct qstr d_name; /* 目录项名称 */
struct hlist_node d_hash; /* 散列表 */
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 短文件名 */
};
与前两个对象不同,目录项对象没有对应的磁盘数据结构,VFS根据字符串的形式的路径名现场创建,并且由于目录项对象并非真正保存在磁盘上,所以目录项标志没有是否被修改的标志。
目录项对象有三种状态:被使用、未被使用、负状态
- 一个正在被使用的目录项对应一个有效的索引节点(即d_inode指向相应的索引节点)并且表明该对象存在一个或多个使用者(即d_count为正值)。一个目录项处于正被使用状态,意味着它正被VFS使用并且指向有效的数据,因此不能被丢弃。
- 一个未被使用的目录项对应一个有效的索引节点(d_inode指向一个索引节点),但是应指明VFS当前未使用它。该目录项对象仍然指向一个有效对象,而且被保留在缓存中以便需要时再使用它。由于该目录项不会过早地被撤销,以后再需要时不必重新创建。与未缓存的目录项相比,路径查找更为迅速。
- 负状态的目录项没有对应的有效索引节点(d_inode为NULL),因为索引节点被删除或者路径不再正确,但是目录项仍然被保留,以便快速解析以后的路径查询。比如进程不断打开一个已经不再存在的文件,如果需要的话可以撤销。
如果VFS遍历路径名中的所有元素并将它们逐个进喜欢那个解析成目录项对象是意见非常费力的工作,所以内核将目录项对象缓存在目录项缓存(简称dcache)中。
目录项缓存主要包括三个部分:
- 被使用的目录项链表
- 最近被使用的双向链表。含有未被使用的和负状态的目录项对象
- 散列表和相应的散列函数用来快速将给定路径解析为相关目录项对象
散列表由数组dentry_hashtable表示,每一个元素都是一个指向具有相同键值的目录项对象链表的指针,数组的大小取决于系统中物理内存的大小。
实际的散列值由d_hash()函数计算,它是内核提供给文件系统的唯一一个散列函数。
查找散列表要通过d_lookup()函数,如果该函数在dcache中发现了与其相匹配的目录项对象,则匹配的对象被返回;否则返回NULL指针。
10 目录项操纵
dentry_operations结构体说明了VFS操作目录项的所有方法:
struct dentry_operations {
/* 判断目录项对象是否有效,VFS准备从dcache中使用一个目录项时会调用该函数。大部分文件系统认为dcache中的目录项对象总有效,因此将该方法置为NULL */
int (*d_revalidate)(struct dentry *dentry, struct nameidata *);
/* 为目录项生成散列值,当目录项需要加入到散列表中调用该函数 */
int (*d_hash) (struct dentry *, struct qstr *);
/* 调用该函数比较name1和name2两个文件名 */
int (*d_compare) (struct dentry *dentry, struct qstr *name1, struct qstr *name2);
/* 当目录项的d_count为0时,VFS调用该函数,使用该函数需要加dcache_lock锁和目录项的d_lock */
int (*d_delete)(struct dentry *);
/* 当目录项对象将要被释放时时,VFS调用该函数 */
void (*d_release)(struct dentry *);
/* 当目录项对象丢失相关索引节点时,VFS调用该函数 */
void (*d_iput)(struct dentry *, struct inode *);
};
11 文件对象
文件对象表示进程已经打开的文件,进程直接处理的是文件而不是超级块、索引节点、目录项。
文件对象是已打开的文件在内存中的表示,该对象(不是物理文件)由相应的open()系统调用创建,由close()系统调用撤销,这些文件相关的调用实际上都是文件操作表中定义的方法。因为多个进程可能打开和操作同一个文件,所以同一个文件可能存在多个文件对象。文件对象在进程上仅仅代表已打开文件,它反过来指向目录项对象,其实只有目录项对象才表示已打开的实际文件(是只有访问文件才会生成对应的目录项的原因吗??),虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象是唯一的。
文件对象由file结构体表示,定义在文件<linux/fs.h>中:
struct file {
struct list_head f_list; /* 文件对象链表 */
struct dentry *f_dentry; /* 相关的目录项对象 */
struct vfsmount *f_vfsmnt; /* 挂载信息 */
struct file_operations *f_op; /* 文件操作表 */
atomic_t f_count; /* 文件对象的使用计数 */
unsigned int f_flags; /* 打开文件时指定的标志 */
mode_t f_mode; /* 文件访问模式 */
loff_t f_pos; /* 文件当前偏移量 */
struct fown_struct f_owner; /* 拥有者通过信号进行异步IO数据的传送 */
struct file_ra_state f_ra; /* 预读状态 */
unsigned long f_version; /* 版本号 */
void *f_security; /* 安全模块 */
/* needed for tty driver, and maybe others */
void *private_data; /* 事件池链表 */
/* 事件池锁 */
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links; /* 事件池链表 */
spinlock_t f_ep_lock; /* 事件池锁 */
};
类似于目录项对象,文件对象实际没有对应的磁盘数据,所以在结构体中没有代表其对象是否为脏、是否需要写回磁盘的标志。文件对象通过f_dentry对象指向相关的目录项对象,目录项会指向相关的索引节点,索引节点会记录文件是否是脏的。
12 文件操作
文件对象的操作由file_operations结构体表示,定义在文件<linux/fs.h>中:
struct file_operations {
struct module *owner;
/* 用于更新偏移量指针,由系统调用llseek()调用 */
loff_t (*llseek) (struct file *, loff_t, int);
/* 从给定文件的offset偏移处读取count字节的数据到buf中,同时更新文件指针,由系统调用read()调用 */
ssize_t (*read) (struct file *file, char __user *buf, size_t count, loff_t *offset);
/* 从iocb描述的文件中,以同步方式取count字节的数据到buf中,由系统调用aio_read()调用 */
ssize_t (*aio_read) (struct kiocb *iocb, char __user *buf, size_t count, loff_t *offset);
/* 从给定的buf中取出count字节的数据写到给定文件的offset偏移处,同时更新文件指针,由系统调用write()调用 */
ssize_t (*write) (struct file *, char __user *buf, size_t count, loff_t *offset);
/* 以同步方式从给定的buf中取出count字节的数据写到iocb描述的文件中,同时更新文件指针,由系统调用aio_write()调用 */
ssize_t (*aio_write) (struct kiocb *iocb, char __user *buf, size_t count, loff_t *offset);
/* 返回目录列表的下一个目录,由系统调用readdir()调用 */
int (*readdir) (struct file *, void *, filldir_t);
/* 睡眠等待给定文件活动和,由系统调用poll()调用 */
unsigned int (*poll) (struct file *, struct poll_table_struct *);
/* 给设备发送命令参数对当文件是一个被打开的设备节点时,可以通过它进行操作,由系统调用ioctl()调用 */
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
/* 是ioctl()的变种。被32位程序用在64位系统上。被设计成在64位的体系结构上对32位也是安全的,可以进行必要的字大小转换 */
int (*compact_ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
/* 将给定的文件映射到指定的地址空间上,由系统调用mmap()调用 */
int (*mmap) (struct file *, struct vm_area_struct *);
/* 创建一个新的文件对象,并将它和相应的索引节点对象关联起来,由系统调用open()调用 */
int (*open) (struct inode *, struct file *);
/* 当打开文件的引用计数减少时,该函数被VFS调用,由系统调用flush()调用 */
int (*flush) (struct file *);
/* 当文件的最后一个引用被注销时,该函数会被VFS调用 */
int (*release) (struct inode *, struct file *);
/* 将给定文件的所有被缓存数据写回磁盘,由系统调用fsync()调用 */
int (*fsync) (struct file *, struct dentry *, int datasync);
/* 将iocb描述的文件的所有被缓存数据写回磁盘,由系统调用aio_fsync()调用 */
int (*aio_fsync) (struct kiocb *, int datasync);
/* 该函数用于打开或关闭异步IO的通告信号 */
int (*fasync) (int, struct file *, int);
/* 用于给执行文件上锁 */
int (*lock) (struct file *, int, struct file_lock *);
/* 从给定文件中读取数据,并将其写入由vector描述的count个缓冲中,同时增加文件的偏移量,由系统调用readv()调用 */
ssize_t (*readv) (struct file *file, const struct iovec *vector, unsigned long count, loff_t *offset);
/* 将由vector描述的count个缓冲中的数据写入file指定的文件,同时减小文件的偏移量,由系统调用readv()调用 */
ssize_t (*writev) (struct file *file, const struct iovec *vector, unsigned long count, loff_t *offset);
/* 用于从一个文件拷贝数据到另一个文件,执行的拷贝操作完全在内核中完成,避免了向用户空间金星和不必要的拷贝,由系统调用secfile)调用 */
ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void __user *);
/* 用来从一个文件向另一个文件发送数据 */
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);
};
具体的文件系统可以为每一种操作做专门的实现,如果存在通用操作,也可以使用通用操作。
13 和文件系统相关的数据结构
除了以上几种类型,内核还使用了其余一些标准数据结构来管理文件系统的其他相关数据。第一个数据是file_system_type,用来描述各种特定文件系统类型,另一个结构体是vfsmount,用来描述一个挂载文件系统的实例。
struct file_system_type {
const char *name; /* 文件系统的名字 */
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; /* 超级块对象链表 */
};
get_sb()函数从磁盘上读取超级块,在文件系统被挂载时,在内存中组装超级块对象,剩余函数描述文件系统的属性。
每种文件系统无论是否挂载到系统中,还是有多少个实例挂载到系统中,都只有一个file_system_type实例。在文件系统被实际安装时,将会有一个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; /* 子文件系统链表 */
int mnt_flags; /* 安装标志 */
/* 4 bytes hole on 64bits arches */
const char *mnt_devname; /* 设备文件名 */
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; /* 如果标记位到期,则为真 */
int mnt_pinned; /* "钉住"进程计数 */
int mnt_ghosts; /* "镜像"引用计数 */
/*
* This value is not stable unless all of the mnt_writers[] spinlocks
* are held, and all mnt_writer[]s on this mount have 0 as their ->count
*/
atomic_t __mnt_writers; /* 写者引用计数 */
};
vfsmount的mnt_flags域中保存了在安装时指定的标志信息,比如MNT_MODEV禁止访问该文件系统上的设备文件,MNT_NOEXEC禁止执行该文件系统上的可执行文件。
14 和进程相关的数据结构
系统的每一个进程都有自己的一组打开的文件,例如根文件系统、当前工作目录、挂载点等。有三个数据结构将VFS层和系统的进程紧密联系在一起,分别是file_struct、fs_struct和namespace结构体。
file_struct结构体由进程描述符中的files目录项指向,所有与单个进程相关的信息都包含在其中:
struct files_struct {
/*
* read mostly part
*/
atomic_t count; /* 结构的使用计数 */
struct fdtable *fdt; /* 指向其他fd表的指针 */
struct fdtable fdtab; /* 基fd表 */
/*
* written part on a separate cache line in SMP
*/
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_array数组指针指向已打开的文件对象。NR_OPEN_DEFAULT等于BITS_PER_LONG。在64位机器体系结构中该值位64,所以数组可以容纳64个文件对象。如果一个进程打开的文件对象超过64个,内核将分配一个新数组,并且将fdt指向指向它。
fs_struct由进程描述符的fs域指向,包含文件系统和进程相关的信息:
struct fs_struct {
int users; /* 用户数目 */
rwlock_t lock; /* 保护该结构体的锁 */
int umask; /* 掩码 */
int in_exec; /* 当前正在执行的文件 */
struct path root, pwd; /* 根目录路径与当前工作目录的路径 */
};
mnt_namespace由进程描述符的mnt_namespace域指向,该结构体使得每一个进程在系统中都看到唯一的安装文件系统:
struct mnt_namespace {
atomic_t count; /* 结构的使用计数 */
struct vfsmount * root; /* 根目录的安装点对象 */
struct list_head list; /* 安装点链表 */
wait_queue_head_t poll; /* 轮询的等待队列 */
int event; /* 事件计数 */
};
list域是已安装文件系统的双向链表,包含的元素组成了全体命名空间。上述这些数据结构都通过进程描述符连接起来。对多数进程而言,描述符指向唯一的file_struct和fs_struct结构体,但是对于使用CLONE_FILES或CLONE_FS创建的进程,会共享着两个结构体,所以多个进程描述符可能会指向同一个file_struct和fs_struct结构体。每个结构体都维护一个count域作为引用计数,放置在进程正使用该结构体时,该结构被撤销。
对于namespace,在默认情况下,所有进程共享同样的命名空间,只有在进程clone()操作时使用CLONE_NEWSBIAOZHI ,会给进程一个唯一的命名空间结构体的拷贝。因为大多数标志不提供这个标志,所有进程都继承其父进程的命名空间,因此在大多数系统上只有一个命名空间。但是CLONE_NEWS标志可以使这一功能失效。