如何描述一个文件
文件在内存和磁盘上是如何描述的:每个文件至少要有一个数据结构存放该文件的信息,包括uid、gid、flag、文件长度、文件内容存放位置的数据结构等。在Linux中这个数据结构被称为inode,本来inode中也应该包括文件名称等信息,但是由于符号链接的存在,导致一个文件可能存在多个文件名称,因此把和文件名称相关的信息从inode中提出,专门放到dentry 结构中。dentry通过其成员变量d_inode 指向对应的inode数据结构。如下图所示:
另外,inode结构中还包括了成员i_fop,其类型是struct file_operations,其中包括的针对该文件的一些操作接口。
根据路径名寻找目标文件
在Linux中目录也被作为文件看待,只是目录是一种比较特殊的文件。其 特殊之处在于文件的内容是该目录中文件和子目录的dentry的描述符,通过这些dentry的描述符可以找到文件或子目录的dentry,进而找到相应的inode。
下面我们看看如果根据绝对路径寻找一个文件/tmp/temp/abc的。
- 首先找到根文件系统的根目录文件的 dentry 和 inode
- 由这个 inode 提供的操作接口 i_op->lookup(),找到下一层节点 ‘tmp’ 的 dentry 和 inode
- 由 ‘tmp’ 的 inode 找到 ‘temp’ 的 dentry 和 inode
- 最后由 ‘temp’ 的 inode 找到 ‘abc’ 的 dentry 和 inode
我们再看看如何通过相对路径寻找文件/tmp/temp/abc,假如我们目前的工作目录为/tmp/temp/dir_a 中,比如我们通过拷贝命令拷贝该文件:cp ../abc ./
如何通过相对路径寻找文件呢?我们来看看dentry这个数据结构的成员,其中有一个是d_parent,数据结构定义如下
struct dentry { 删除了无关的成员
struct dentry *d_parent; /* parent directory */
struct inode *d_inode; /* Where the name belongs to - NULL is * negative */
unsigned char d_iname[DNAME_INLINE_LEN]; /* small names */
}
d_parent指向了本目录的父目录的dentry,这样就在通过“..”时就是通过该指针找到的父目录dentry,找到父母inode,进而找到父目录下的所有文件的信息。
进程中打开的文件
一个文件可以被多次打开,并且多个进程对一个文件的访问权限可能不同,因此打开方式就会不同(只读、读写、可执行)。而dentry 和 inode 只能描述一个物理的文件,无法描述“打开”这个概念。因此有必要引入 file 结构,来描述一个“被打开的文件”。每打开一个文件,就创建一个 file 结构。
file 结构中包含以下信息:
- 打开这个文件的uid,pid
- 打开文件的方式
- 读写的方式
- 当前在文件中的位置
实际上,打开文件的过程正是建立file,dentry,inode之间的关联的过程。如下图:
在进程中如何和打开的文件相关联呢,下面来看一下进程的数据结构
struct task_struct { 只保留了相关信息
struct files_struct *files; /* open file information */
}
每一个进程的结构体中包含"files"成员,其类型为files_struct。如下图:
进程中所有打开的文件的指针都存在了fd_array[]数组当中。
虚拟文件系统
Linux 通过虚拟文件系统 (VFS) 来支持不同的具体的文件系统,从程序员的角度看, VFS 就是一套代码框架(framework),它将用户与具体的文件系统隔离开来。每个要通过mount 命令挂接到Linux系统的存储设备,如磁盘、光盘等(它们各自对应具体的文件系统),每个设备对应的文件系统都要按照VFS的要求提供一套统一的接口。这样,用户就可以使用这些统一的接口在不同的文件系统中拷贝数据了。参考下图:
安装一个文件系统,除了需要“被安装设备”外,还要指定一个“安装点”。“安装点”是已经存在的一个目录节点。例如把 /dev/sda1 安装到 /mnt/win 下,那么 /mnt/win 就是“安装点”。
可是文件系统要先安装后使用。因此,要使用 /mnt/win 这个“安装点”,必然要求它所在文件系统已也经被安装。
也就是说,安装一个文件系统,需要另外一个文件系统已经被安装。
这是一个鸡生蛋,蛋生鸡的问题:最顶层的文件系统是如何被安装的?
答案是,最顶层文件系统的时候是被安装在“根安装点”上的,而根安装点不属于任何文件系统,它对应的 dentry 、inode 是由内核在初始化阶段凭空构造出来的。
最顶层的文件系统叫做“根文件系统”。Linux 在启动的时候,要求用户必须指定一个“根设备”,内核在初始化阶段,将“根设备”安装到“根安装点”上,从而有了根文件系统。这样,文件系统才算准备就绪。此后,用户就可以通过 mount 命令来安装新的设备。
mount设备(文件系统)
我们通过mount命令向Linux系统mount了一个设备。其实该命令触发了两个过程,一个是文件系统注册过程(当然,如果文件系统已注册过的话,就不需要该步骤了),另一个才是真正意义上的mount设备的过程。
文件系统注册过程
Linux内核是可加载的,许多模块是可选的,只有真正需要使用时才加载他们。文件系统注册过程就是把对应某类型文件系统相关的模块加载到内核,并创建相关的数据结构。每个文件系统模块都有一个初始化例程,它的作用就是在VFS中进行注册,即填写一个叫做file_system_type的数据结构。所有已注册的文件系统的file_system_type结构形成一个链表,我们把这个链表称为注册链表。
每个设备在mount时都要搜索该注册链表,选择适合自己设备文件系统的一项,并从中取出read_super()函数获取设备的超级块(存储在具体设备上,记录存储设备各种信息的一个存储块),并解析其内容。因为每种类型文件系统的超级块的格式不同,并且各自有特定的信息,每种文件系统必须使用对应的解析函数,否则内核就因为不认识该文件系统而无法完成安装。这就是注册文件系统的意义所在。
设备的真正的mount过程,总体的数据结构,参考下图:
- 创建一个设备的 vfsmount
- 为“被安装设备”创建一个 super_block,并由具体的文件系统来设置这个super_block。在super_block中包含了该类型设备操作的各种接口的结构成员s_op,类型为super_operations。
- 为被安装设备的根目录节点创建 dentry
- 为被安装设备的根目录节点创建 inode, 并由 super_block->s_op->read_inode() 来设置此 inode
- 将 super_block 与“被安装设备“根目录节点 dentry 关联起来
- 将 super_block中的s_root与“被安装设备”的根目录节点 dentry 关联起来如图6所示,在linux2.4.30中有三条链表,文件系统类型结构file_system_type的链表头为file_systems,超级块结构super_block的链表头为super_blocks,挂接点结构vfsmount的链表头为vfsmntlist。
在Linux3.3.5中只有两条链表结构,文件系统类型结构file_system_type的链表头为file_systems,超级块结构super_block的链表头为super_blocks。数据结构vfsmount 的结构定义还存在,但已经没有了mnt_list成员了。
挂接设备中查找文件的过程
下面的流程参考了linux3.3.5中的数据结构。
例如要打开 /mnt/win/dir1/abc 这个文件,就是根据这个路径,找到目标节点 ‘abc’ 对应的 dentry ,进而得到 inode 的过程。
寻找过程大致如下:
- 首先找到根文件系统的根目录节点 dentry 和 inode
- 由这个 inode 提供的操作接口 i_op->lookup(),找到下一层节点 ‘mnt’ 的 dentry 和inode
- 由 ‘mnt’ 的 inode 找到 ‘win’ 的 dentry 和 inode
- 由于 ‘win’ 是个“安装点”,因此需要找到“被安装设备”/dev/sda1 根目录节点的 dentry 和 inode。“win”的dentry中有d_sb(超级块成员),d_sb中有“struct dentry *s_root;”,s_root就是指向“/dev/sda1”的dentry。
- 然后由 /dev/sda1 根目录节点的 inode 负责找到下一层节点 ‘dir1’ 的 dentry 和 inode
- 由于 dir1 是个“安装点”,因此需要借助dir1的dentry->d_sb->s_root找到 /dev/sda2 的根目录节点 dentry 和 inode
- 最后由这个 inode 负责找到 ‘abc’ 的 dentry 和 inode
可以看到,整个寻找过程是一个递归的过程。