1.28k likes | 1.46k Views
第七章 文件系统. Linux 操作系统支持许多不同类型的文件系统。本章介绍虚拟文件系统和高速缓存的概念,并讨论在 Linux 中最常见的 /proc 、 Ext2 和 Ext3 文件系统。. 7.1 虚拟文件系统. 文件系统是 Unix 系统最基本的资源。文件系统的作用就是在应用概念的文件和存储设备之间提供一个中间层,以使多个文件驻留在一个存储设备上,由文件系统来管理所有文件的存储。文件系统将每个存储设备化为一系列目录,每个目录含有若干文件。. 7.1.1 虚拟文件系统概述.
E N D
第七章 文件系统 • Linux 操作系统支持许多不同类型的文件系统。本章介绍虚拟文件系统和高速缓存的概念,并讨论在 Linux 中最常见的/proc、Ext2和Ext3文件系统。
7.1 虚拟文件系统 • 文件系统是Unix系统最基本的资源。文件系统的作用就是在应用概念的文件和存储设备之间提供一个中间层,以使多个文件驻留在一个存储设备上,由文件系统来管理所有文件的存储。文件系统将每个存储设备化为一系列目录,每个目录含有若干文件。
7.1.1虚拟文件系统概述 • 为了在不同文件系统之间可以方便地交换数据和管理,并支持可任意地挂装不同文件系统,现代的操作系统大多都在系统内核和文件系统之间提供一个叫做虚拟文件系统(Virtual File System,VFS)的标准接口。Linux 虚拟文件系统维护描述整个虚拟文件系统以及实际已挂装的文件系统的数据结构。对于内核其他子系统来说,所有的文件系统都是一样的。因此,一个逻辑文件系统要想被 Linux 支持,就必须按照这个接口来编写自己的操作函数,从而将自己的细节对其他子系统隐藏起来。对于逻辑文件系统来说,VFS 是一个管理者,而对于内核的其他部分,则是一个统一的接口。图7.1 是虚拟文件系统和实际文件之间的关系示意图。
7.1.1虚拟文件系统概述 图7.1虚拟文件系统和实际文件系统之间的关系示意图
7.1.1虚拟文件系统概述 • 每个文件系统在初始化时,首先在 VFS 中进行注册。如果文件系统内建于内核中,则在系统引导时初始化;如果文件系统作为内核可装载的模块,则在挂装某个文件系统时进行初始化。在挂装某种基于块设备的文件系统(包括 root 文件系统)时,VFS 必须读取其超块。不同类型的文件系统所对应的超块读取例程必须能够理解实际文件系统的拓扑结构,并且能够将实际的超块结构映射为 VFS 超块结构。每个 VFS 超块包含了文件系统信息,并且还包含一些完成特定功能的函数指针。例如,某个超块代表一个已挂装的 Ext2 文件系统,则超块中包含有专门读取 Ext2 文件系统索引节点的函数地址。每个 VFS 超块中包含指向实际文件系统第一个 VFS 索引节点的指针。对于 root 文件系统来说,第一个索引节点就是代表“/”目录的节点。对 Ext2 文件系统来说,这种映射关系非常高效。
7.1.2 基本对象与方法 • 虚拟文件系统的接口由一组对象及其由这些对象调用的一组方法所构成的。这些基本的对象是 files(文件),file-systems(文件系统),inodes (索引节点)以及 names for inodes(索引节点名字),下面对这些对象进行简单的介绍:
7.1.2 基本对象与方法 • 1. 文件 • 文件(files)是一个可读可写的对象,它也可以映射到内存中,这和 Unix 中文件描述符的概念很接近。文件在 Linux 中使用一个"struct file"结构来实现,并且该结构有一组操作函数,保存在结构"struct file_operations"中。
7.1.2 基本对象与方法 • 2. 索引节点 • 索引节点(inodes)是文件系统中的基本对象。它可以是一个正常文件,一个目录,一个符号链接,或者是其他什么东西。VFS 并不明显地区分这些对象,而把它们留给真正的文件系统,让它们自己实现适宜的行为。从而使内核的高层子系统对于不同的对象区别对待。 • 每一个索引节点都由一个"struct inode"结构表现,它的一组方法保存在结构"struct inode_operations"中。 • 一个符号链接有索引节点,但却没有文件。但有些文件(如管道文件和套接字文件)却没有索引节点。
7.1.2 基本对象与方法 • 3. 文件系统 • 文件系统(file_systems)就是索引节点的集合,其中有一个节点被称为根节点(root)。其他的索引节点 以 root 为起始点进行访问,并且通过文件名来查找其他的索引节点。 • 每一种文件系统有一组唯一的特征,应用于本文件系统内的所有索引节点之上。其中有一些是标志,如只读标志READ-ONLY。另一个重要的特征是块的尺寸(blocksize)。 • 每一个文件系统都通过一个结构"struct super_block"来表现,而针对超级块的一组方法则存储在结构"struct super_operations"之中。在 Linux 中,超级块(super-blocks)和设备号(device number)之间有紧密的联系。每一个文件系统必须有一个唯一的设备号,该文件系统即建立在此设备之上。有一些文件系统(如 nfs 和/proc 文件系统)不需要真实的设备。因此,对于这些文件系统,将会自动地分配以主设备号(major number)为0的匿名设备。 • 在Linux VFS 中,每一个文件系统类型都使用一个"struct file_system_type"结构来表示,在该结构中,只包含一个"read_super"方法,使用这个方法来实例化某指定文件系统的超级块。
7.1.2 基本对象与方法 • 4. 名字 • 在一个文件系统中,所有的索引节点都是通过名字(names)来访问的。对于某些文件系统来说,名字到索引节点的转换非常耗时。因此,Linux 的 VFS 层为当前活动的和最近使用的名字维护了一个高速缓存(cache),这个高速缓存 被称为目录高速缓存(dcache)。 • 进程在访问目录和文件的过程中,调用系统例程对 VFS 节点进行遍历。因为每个文件和目录均由一个索引节点表示,因此有相当多的索引节点会被重复访问。为了提高索引节点的访问速度,VFS 将这些节点保存在索引节点高速缓存中。如果某个节点当前不在高速缓存中,则调用专用于某种文件系统的索引节点的读取例程,以便读取适当的索引节点。读取的索引节点会保存在高速缓存中,而较少使用的 VFS 索引节点会从高速缓存中剔除。参见 fs/inode.c 。
7.1.2 基本对象与方法 • VFS 同时维护一个目录高速缓存dcache,以便能够快速找到频繁使用的目录索引节点。目录高速缓存并不保存目录本身的索引节点,这些索引节点应当保存在索引节点高速缓存中;目录高速缓存保存的是完整目录名到对应索引节点编号的映射关系。 • 目录高速缓存在内存中的组织为树状结构。树中的每一个节点都对应于一个指定目录,指定名称的索引节点。一个索引节点可以与多个树中的节点相联系。如果目录高速缓存不是一棵完整的文件树,它一定是文件树的前缀部分。换句话说,如果一个文件树的节点在cache中,该节点的所有祖先也一定在cache中。每一个树的节点都使用一个结构"struct dentry"来表现,它的一组方法存储在"struct dentry_operations"之中。 • 入口(dentry)是文件和索引节点之间的中介。每一个打开的文件都指向一个dentry,而每一个dentry 则指向它所涉及的索引节点。对于每一个打开的文件,该文件的dentry 和该文件所有的父节点都在内存中被cache,这样就可以更容易地检测被打开文件的全路径。
7.1.2 基本对象与方法 • 所有的 Linux 文件系统使用共同的缓冲区高速缓存,所有的文件系统以及 Linux 内核使用同一个缓冲区高速缓存。缓冲区高速缓存不依赖于任何文件系统,它和 Linux 内核用来分配、读取和写入数据缓冲区的机制集成在一起。Linux 文件系统独立于底层介质以及设备驱动程序而存在,这种机制具有非常明显的优点。所有的块设备在 Linux 内核中注册,并且提供一致的、基于块的、异步的接口。当文件系统从物理介质中读取数据时,实际是向控制该设备的驱动程序发送读取物理数据块的请求。缓冲区高速缓存负责将块设备驱动程序的接口集成在一起。文件系统在读取数据块时,这些块被保存在全局的缓冲区高速缓存中。缓冲区高速缓存中的缓冲区由唯一的设备标识符以及块编号标识,当进程频繁访问相同的数据时,这些数据就可以直接从高速缓存中读出,而不必进行实际的磁盘读取操作。参见 fs/buffer.c及 fs/dcache.c 。
7.1.3 文件系统的注册、安装与卸载 • 1. 文件系统的注册 • 在使用一个文件系统之前,必须要对该文件系统进行注册。在Linux编译的时候,可以选定支持哪些文件系统,这些编译进内核的文件系统,在系统引导的时候,就会在VFS中注册。而如果一个文件系统被编译为内核可装载模块,将在模块安装的时候进行注册,在模块卸载的时候注销。
7.1.3 文件系统的注册、安装与卸载 图7.2注册的文件系统 :
7.1.3 文件系统的注册、安装与卸载 • 每个文件系统的初始化例程在 VFS 中注册,并由一个 file_system_type 数据结构代表,参见 fs/filesystems.c 中的sys_setup()和include/linux/fs.h 中的file_system_type 。其中包含了文件系统的名称以及一个指向对应 VFS 超块读取例程的地址。图7.2 所示是内核中的 file_system_type 链表,链表头由 file_systems 指定。每个 file_system_type 结构成员由表7.1 给出。
7.1.3 文件系统的注册、安装与卸载 • 表7.1file_system_type 结构成员
7.1.3 文件系统的注册、安装与卸载 • 2. 文件系统的挂装 • 要使用一个文件系统,除了注册之外,还必须挂装这个文件系统。和所有的Unix 版本一样,Linux 利用特殊文件代表系统的硬件设备。设备文件实际并不占用任何文件系统的空间,它只是作为访问设备驱动程序的入口。Ext2 文件系统和 VFS 文件系统将设备文件作为特殊索引节点实现。设备分为块设备和字符设备两种。设备文件的索引节点中包含对应设备的主设备号和次设备号,分别标识设备类型以及该设备在同类设备中的编号。 • 文件系统的挂装和卸装一般只能由超级用户完成。参见 fs/super.c add_vfsmnt() 。
7.1.3 文件系统的注册、安装与卸载 • 用户(一般是 root)在挂装文件系统时,要指定三种信息:文件系统的名称、包含文件系统的物理块设备和文件系统在已有文件系统中的挂装点。例如: • $ mount -t iso9660 /dev/hdc /mnt/cdrom • 上述命令指定了要挂装的文件系统类型,文件系统所在设备以及挂装点,并将文件系统挂装为只读。如果要在系统启动时自动挂装文件系统,则可以在 /etc/fstab 文件中为每种文件系统的挂装提供有关文件系统的名称、包含该文件系统的物理设备以及该文件系统的挂装点的信息。有关内容可参见 fstab 手册。
7.1.3 文件系统的注册、安装与卸载 • 图7.3已挂装的文件系统数据结构
7.1.3 文件系统的注册、安装与卸载 • (1)寻找对应的文件系统信息。VFS 通过 file_systems 在 file_system_type 组成的链表中根据指定的文件系统名称搜索文件系统类型信息。参见 fs/super.c中的do_mount() 和fs/super.c中的get_fs_type() 函数。 • (2) 如果在上述链表中找到匹配的文件系统,则说明内核具有对该文件系统的内建支持。否则,说明该文件系统可能由可装载模块支持,VFS 会请求内核装入相应的文件系统模块,此时,该文件系统在 VFS 中注册并初始化。 • (3) 不管是哪种情况,如果 VFS 无法找到指定的文件系统,则返回错误。 • (4) VFS 检验给定的物理块设备是否已经挂装。如果指定的块设备已被挂装,则返回错误。
7.1.3 文件系统的注册、安装与卸载 • (5) VFS 查找作为新文件系统挂装点的目录的 VFS 索引节点。该 VFS 索引节点可能在索引节点高速缓存中,也有可能需要从挂装点所在的块设备中读取。 • (6) 如果该挂装点已经挂装有其他文件系统,则返回错误。因为同一目录只能同时挂装一个文件系统。 • (7) VFS 挂装代码为新的文件系统分配超块,并将挂装信息传递给该文件系统的超块读取例程。系统中所有的 VFS 超块保存在由 super_blocks 指向的 super_block 数据结构指针数组中。 • (8) 文件系统的超块读取例程将对应文件系统的信息映射到 VFS 超块中。如果在此过程中发生错误,例如所读取的超块幻数和指定的文件系统不一致,则返回错误。 • (9) 如果成功挂装,则所有已挂装的文件系统形成如图7.3 所示的数据结构。
7.1.3 文件系统的注册、安装与卸载 • 从图7.3中可看到,每个已挂装的文件系统由 vfsmount 结构描述,所有的 vfsmount 结构形成了一个链表,该链表头由 vfsmntlist 指针代表。系统中还有两个指针,vfsmnttail 和 mru_vfsmnt,分别指向链表尾和最近使用过的 vfsmount 结构。每个 vfsmount 结构包含保存该文件系统的块设备号,文件系统挂装点的目录名称,以及指向为该文件系统分配的 VFS 超块的指针。而 VFS 超块中则包含描述文件系统的 file_system_type 结构指针和该文件系统根节点指针。参见 fs/super.c中的add_vfsmnt() 函数。
7.1.3 文件系统的注册、安装与卸载 • 3. 文件系统的卸装 • 利用 umount命令卸装文件系统,参见 fs/super.c中的do_umount()函数和fs/super.c中的remove_vfsmnt() 函数。umount命令只需一个参数,即希望卸挂的设备或挂装点: • # umount /mnt/dosd • # • 或 • # umount /dev/hda5 • # • 如果文件系统中的文件当前正在使用,该文件系统是不能卸装的。如果文件系统中的文件或目录正在使用,则 VFS 索引节点高速缓存中可能包含相应的 VFS 索引节点。检查代码在索引节点高速缓存中,根据文件系统所在设备的标识符,查找是否有来自该文件系统的 VFS 索引节点,如果有且标志为“脏”,则说明该文件系统正在被使用,因此,文件系统不能被卸装。否则,查看对应的 VFS 超块。如果该文件系统的 VFS 超块标志为“脏”,则必须将超块信息写入实际的文件系统。上述过程结束之后,对应的 VFS 超块被释放,vfsmount 数据结构从 vfsmntlist 链表中断开并释放。
7.1.4虚拟文件系统的实现函数和数据结构 • 有了虚拟文件系统,文件系统的代码就分成了两部分:上层用于处理系统内核的各种表格和数据结构;下层用来实现文件系统本身的函数,并通过VFS来调用。这些函数主要包括: • (1) 管理缓冲区(buffer. c)。 • (2) 响应系统调用fcntl() 和ioctl() (fcntl.c and ioctl.c)。 • (3) 将管道和文件输入/输出映射到索引节点和缓冲区(fifo.c, pipe.c)。 • (4) 锁定和不锁定文件和记录(locks.c)。 • (5) 映射名字到索引节点(namei.c, open.c)。 • (6) 实现select( )函数(select . c)。 • (7) 提供各种信息(stat.c)。 • (8) 挂接和卸载文件系统(super.c)。 • (9) 调用可执行代码和转存核心(exec.c)。 • (10) 装入各种二进制格式(bin_fmt*.c)。
7.1.4虚拟文件系统的实现函数和数据结构 • VFS接口则由一系列相对高级的操作组成,这些操作由和文件系统无关的代码调用,并且由不同的文件系统执行。其中最主要的结构有inode_operations 和file_operations。 • file_system_type是系统内核中指向真正文件系统的结构。每挂接一次文件系统,都将使用file_system_type组成的数组。file_system_type组成的数组嵌入到了fs/filesystems.c中。相关文件系统的read_super函数负责填充super_block结构。 • 下面介绍在VFS中使用的几个重要的数据结构,它们是VFS实现的核心,更是与逻辑文件系统交互的接口,因此必须进行详细的分析。
7.1.4虚拟文件系统的实现函数和数据结构 • 1 VFS超块及其操作 • 许多逻辑文件系统都有超块结构,超块是这些文件系统中最重要的数据结构,用来描述整个文件系统的信息,是一个全局的数据结构。MINIX、EXT2等都有自己的超级块,VFS也有超级块,但和逻辑文件系统的超级块不同,VFS超级块是存在于内存中的结构,每个已挂装的文件系统由一个 VFS 超块代表。它在逻辑文件系统安装时建立,并且在文件系统卸载时自动删除。因此,VFS对于每一个逻辑文件系统,都有一个对应的VFS超级块。参见 include/linux/fs.h 。VFS 超块中包含的信息由表7.2列出。
7.1.4虚拟文件系统的实现函数和数据结构 • 表7.2VFS 超块中的信息
7.1.4虚拟文件系统的实现函数和数据结构 • 因此在实现自己的逻辑文件系统时,必须提供一套自己的超级块操作函数。对这些函数的调用都来自进程正文(process context),而不是来自在中断例程或者bottom half,在所有的方法调用时,都会使用内核锁,因此,操作可以安全地阻塞,但也要避免并发地进行访问。
7.1.4虚拟文件系统的实现函数和数据结构 • 2 VFS的文件及其操作 • 在需要读写时使用文件对象,包括通过文件系统,或者管道以及网络等进行通讯的对象。文件对象和进程关系紧密,进程通过文件描述符(file descriptors)来访问文件。文件描述符是一个整数,Linux通过fs.h中定义的NR_OPEN来规定每个进程最多同时使用的文件描述符个数: • #define NR_OPEN (1024*1024) • 一共有三个与进程相关的结构,第一个是files_struct,在include/linux/sched.h中定义,主要是一个fd数组,数组的下标是文件描述符,其内容就是对应的下面将要介绍的file结构。 • 另外一个结构是fs_struct,主要有两个指针,pwd指向当前工作目录的索引节点;root指向当前工作目录所在文件系统的根目录的索引节点。 • 最后一个结构是file结构,它的作用是为了保证进程对文件的私有记录,以及父子进程对文件的共享。 • 在file结构中,有一个指针指向了一个文件操作集file_operations,它在linux/fs.h中被定义。这些操作用来将VFS对file结构的操作转化为逻辑文件系统处理相应操作的函数。因此,要了解一个逻辑文件系统,就要从这些接口函数入手。
7.1.4虚拟文件系统的实现函数和数据结构 • 3 VFS索引节点及其操作 • VFS 中的文件、目录等均由对应的索引节点代表。每个 VFS 索引节点中的内容由文件系统专属的例程提供。Linux维护了一个活动的及最近使用过的索引节点的高速缓存(cache)。VFS 索引节点实际保存于 VFS 的索引节点高速缓存中。参见 include/linux/fs.h 。有两种方法来访问这些索引节点。第一种是通过dcache。在dcache中的每一个dentry都指向一个索引节点,并且因此而将索引节点维护在缓存中。第二种方法是通过索引节点的哈希表。每一个索引节点都被基于该文件系统超级块的地址和索引节点的编号,被哈希为一个8位的数字。所有拥有同样哈希值的索引节点通过双项链表被链接在一起。 • 通过哈希表访问是通过函数iget而实现的。iget只被个别的文件系统实现所调用(当索引节点不再dcache中而进行查找的时候)。 • VFS 索引节点中的信息由表7.3 给出。
7.1.4虚拟文件系统的实现函数和数据结构 • 表7.3VFS 索引节点中的数据域
7.1.4虚拟文件系统的实现函数和数据结构 • 4. VFS 中文件的定位 • 为了在虚拟文件系统中定位给定文件的索引节点,VFS 必须每次分解文件路径名中的一个目录名,并依次搜索每个中间目录名的 VFS 索引节点。因为 VFS 在 VFS 超块中记录了每个文件系统根的 VFS 索引节点,因此搜索过程从根索引节点开始,具体过程和 Ext2 文件系统的文件定位过程类似。在搜索目录索引节点时,VFS 在目录高速缓存中搜索,如果能够在目录高速缓存中找到对应的索引节点编号,则在索引节点高速缓存中搜索索引节点,否则从底层文件系统中获取。
7.2高速缓存 • 文件和索引节点之间是通过目录入口(dentry)结构来紧密联系的。 • VFS层处理了文件路径名的所有管理工作,并且在底层文件系统能够看到它们之前,将其转变为目录高速缓存(dcache)中的入口(entry)。唯一的一个例外是对于符号链接的目标,VFS将不加改动地传递给底层文件系统,由底层文件系统对其进行解释。 • 目录高速缓存由许多目录入口结构组成。每一个目录入口都对应文件系统中的一个文件名,并且与之联系。每一个目录入口的父节点都必须存在于目录高速缓存中。同时,目录入口还记录了文件系统的装载关系。 • 目录高速缓存负责管理索引节点高速缓存。无论何时,只要在目录高速缓存中存在一个入口,相应的索引节点一定在索引节点高速缓存中。换句话说,如果一个索引节点在高速缓存中,它一定引用dcache中的一个dentry。
7.2.1文件系统分配机制 • 1. 基于块分配 • 磁盘在经过分区之后,单个的物理磁盘就被划分为多个逻辑分区,每个分区上可存在一个文件系统。块设备实际是可以包含文件系统的设备,不管块设备的具体构造如何,Linux 文件系统均将它们当作线性块的集合而访问。每个块设备驱动程序的任务,就是将逻辑块的请求转化为对应块设备可以理解的命令。类似地,对文件系统来说,不管基础的块设备是什么,它都能够进行一致的操作。利用 Linux 的文件系统,用户根本不用关心由不同硬件控制器控制的,处于不同物理介质上的不同文件系统之间的区别。如果将网络服务器的磁盘或分区挂装到本地文件系统上,用户可以和操作本地文件系统一样进行操作。 • 传统的Unix文件系统使用块(block)分配机制,并且提供一个灵活有效的块分配策略(policy)。磁盘块在被使用时分配,就是说只为文件分配一个最小的文件系统块数,以保存存储空间。
7.2.1文件系统分配机制 • 当文件扩展时,从一个空闲块位图中分配块,所以有时块是随机分配的。随机分配会导致过多的磁盘搜寻,随后从文件系统读数据会导致磁盘机制搜寻文件扩展期间所分配的所有随机块位置。随机块分配可以通过块分配策略的优化来避免,它试图分配连续序列的块。 • 更小的块分配获得大的连续分配,大量减少了磁盘搜寻。然而,连续的文件系统块分配最终导致了文件系统中的文件块碎片,结果文件系统访问最终又会回到随机的本质。 • 块分配方案也要保存文件扩展时每个被分配的新块的位置信息,以及文件是否是每次一块的扩展。额外的磁盘I/O被要求读写文件系统块结构信息。文件系统块结构信息成为元数据(metadata)。文件系统元数据总是同步写入存储设备,所以对一个文件大小的改变需要等待每个元数据操作完成。因此,元数据操作极大降低了文件系统的整体性能。
7.2.1文件系统分配机制 • 2. 基于范围分配 • 基于范围(extent)的文件系统每次按一大群来分配磁盘块,因此要强制进行顺序分配。一个文件被写入时,大量块在文件创建时被分配给文件;然后,可以进行大群或簇顺序块的写入。文件系统元数据在文件首次创建是写入;在块的第一个分配范围内的写不要求额外的元数据写,直到被分配下一个范围。 • 这种方法优化了磁盘搜寻方式,到簇的群化块写入允许文件系统提交对存储设备的更大的物理磁盘写入,节省了许多小SCSI转换的开销。图1比较了块分配和范围分配。在块分配的文件中每个逻辑块都需要有一个块地址号,导致每个文件有许多的元数据。在范围分配方法中,每个接连的数据块范围只需有起始块号和长度就够了。所以包含有一些大范围块的文件只有少量的元数据。
7.2.1文件系统分配机制 • 范围分配的文件系统为顺序文件访问提供了更好的性能,因为他的顺序分配策略以及块聚集为更大的写。然而,对于正在用来作随机I/O的文件系统,范围文件系统的长处往往得不到发体现。 • 如果想要通过一个基于范围的文件进行顺序读,只需读取起始的块号和长度;然后可以继续读取此范围内的所有数据,这意味着在顺序读时只有很小的元数据读开销。相反,如果要以随机方法来读,就要先查询想要的每个数据块的块地址,这相当于要处理一个基于块的文件系统。
7.2.2 块高速缓存 • 所有的 Linux 文件系统使用共同的缓冲区高速缓存。所有块设备的读取和写入请求通过标准的内核例程调用,以 buffer_head 数据结构的形式提交给设备驱动程序。buffer_head 中包含了唯一标识块所在设备的设备标识符,以及要读取或写入的块编号。为了加速对物理块设备的访问过程,Linux 使用了缓冲区高速缓存。在高速缓存中,任意给定的时间内,存在着来自不同物理设备的数据块,而且处于不同的状态。任何用来从块设备中读取或写入块设备的块缓冲区,都要经过缓冲区高速缓存。随着时间的流逝,缓冲区占用的缓存可能让给其他更加需要缓存的缓冲区,也有可能因为频繁访问而保持在缓存中。
7.2.2 块高速缓存 • 图7.4缓冲区高速缓存的哈希表结构
缓冲区高速缓存分为两个功能部分。第一部分是空闲块缓冲区表,每个系统支持的缓冲区大小对应一个表,系统的空闲块缓冲区在第一次创建和最后被丢弃时在这些表中排队。当前 Linux 所支持的缓冲区大小为 512、1024、2048、4096 和 8192 字节。第二部分是高速缓存本身,和 VFS 索引节点高速缓存类似,缓冲区高速缓存也是一个具有相似结构的哈希表。哈希索引由块所在的设备标识符以及块编号计算得到。图7.4 是缓冲区高速缓存的哈希表结构。块缓冲区可能在某个空闲缓冲区表中,也有可能在高速缓存中。如果块缓冲区在高速缓存中,则这些缓冲区也在最近最少使用(Least Recently Used,LRU)表中排队。每个缓冲区类型具有一个 LRU 列表,当前 Linux 支持的缓冲区类型见表7.4。
7.2.2 块高速缓存 • 当文件系统需要从底层物理设备上读取数据块时,它首先试图从缓冲区高速缓存中读取。如果无法从缓冲区高速缓存中得到数据块,它会从适当大小的空闲缓冲区表中获得一个干净的缓冲区,并将该缓冲区放入缓冲区高速缓存。如果所需的缓冲区当前在高速缓存中,该缓冲区可能是经过更新的,也有可能不是。如果未经更新,或该缓冲区是新的块缓冲区,则文件系统必须请求设备驱动程序读取适当的数据块。
7.2.2 块高速缓存 • 表7.4Linux 的缓冲区类型
7.2.2 块高速缓存 • 和其他的高速缓存一样,Linux 也需要维护缓冲区高速缓存,以便能够高效运行,并为不同块设备公平分配缓存项。为此,Linux 利用 bdfush内核守护进程完成一些常务工作,而其他一些工作则随着缓存的使用而自动完成。
7.2.3 索引节点高速缓存 • VFS 索引节点实际是一个哈希表(或散列表),哈希表的每个入口包含一个 VFS 索引节点链表的头指针,该链表中的每个 VFS 索引节点具有相同的哈希值。节点的哈希值根据文件系统所在块设备的标识符,以及索引节点的编号计算得到。当虚拟文件系统要访问某个节点时,它首先在 VFS 索引节点中搜索。每个要搜索的 VFS 索引节点可计算得到一个对应的哈希值,然后,VFS 将该哈希值作为访问哈希表的索引,如果对应的哈希表入口指向的索引节点链表中包含有要搜索的索引节点(相同的设备标识符和相同的节点编号),则说明该索引节点包含在高速缓存中,这时,找到的索引节点访问计数加 1,否则,必须寻找一个空闲的 VFS 索引节点,并从底层文件系统中读取该索引节点。VFS 可选择多种方法获取空闲索引节点。如果系统能够分配更多的 VFS 节点,则 VFS 从内核中分配内存页并将其划分为新的空闲节点,然后放在节点列表中。系统所有的 VFS 节点保存在由 first_ionde 指向的链表中。如果系统已经具备了所有可利用的索引节点,则必须寻找一个可重复利用的索引节点。当前使用计数为 0 的索引节点可作为重复利用的节点,对文件系统的根节点来说(以及其他重要的节点),其使用计数始终大于 0,因此不会被重复使用。如果找到可重复利用的索引节点,则必须在使用之前清除该节点。如果该节点标志为“脏”,则需要写回到实际的文件系统中;如果该节点被锁定,则必须等待解锁。
7.2.3 索引节点高速缓存 • 无论利用哪种方法获得了空闲的 VFS 节点,VFS 会调用文件系统专有的例程从底层文件系统中读取信息并填充该索引节点。在该节点被填充时,这一新的 VFS 节点的使用计数为 1,同时被锁定,以免其他进程访问该节点。填充之后,节点被解锁。 • 为了获得实际使用的 VFS 节点,文件系统可能要访问许多其他的节点。读取目录内容时,只有最后一个目录的索引节点是实际要使用的,但同时必须读取中间目录的索引节点。
7.2.4 目录高速缓存 • 为了加速对频繁目录的访问过程,VFS 维护一个目录项高速缓存。实际文件系统读取目录之后,该目录项的细节添加到目录高速缓存中。下次搜索相同的目录项时,可从高速缓存中快速得到。但是,只有名称相对较短的目录项(15 个字符长度)才被高速缓存。 • 目录高速缓存页由一个哈希表组成。哈希值由保存文件系统的设备号以及目录名称计算得到,该值作为访问哈希表的索引。参见 fs/dcache.c 源程序。 • 为了保证缓存的有效性并及时更新,VFS 保持一组“最近使用 (LRU)”目录缓存项表。当某个目录项第一次放入高速缓存时,被添加到第一级 LRU 表的后面。当高速缓存全部占用时,该操作会取代第一级 LRU 表前面的已有项。当该目录再次被访问时,该目录项自动提升到第二级目录项缓存列表的后面,同样会取代第二级缓存列表前面的二级缓存目录项。这样,处于各个缓存表前面的项实际是最近没有被访问的目录项,否则,它们应当处于各目录项列表的后面。
7.3 /proc文件系统 /proc 文件系统是Linux 的一个逻辑文件系统。 /proc 文件系统的实现,也完全遵循 VFS 的规范。在系统的初始化过程中,/proc 文件系统在 VFS 中注册。当 VFS 需要打开其中的目录或文件时,它利用内核信息建立这些文件和目录。 /proc文件系统的源代码共有11个相关文件,位于linux/fs/proc中。其中,源文件root.c负责/proc文件系统的根节点的管理,而base.c和array.c则用来处理/proc目录中进程的信息,包括命令行、进程状态和内存状态等;proc_tty.c用来处理/proc/tty信息,proc_misc.c则用来管理与/proc目录中的大多数文件。此外,在/linux/include/linux/目录中还有两个非常重要的头文件proc_fs.h和proc_fs_i.h。另外的几个源码文件procfs_syms.c,generic.c和inode.c负责/proc文件系统的管理,是最重要的一部分代码。 7.3.1 概述
7.3.2 /proc文件系统的注册 • 遵循VFS的规范,/proc文件系统在使用之前必须进行注册。Linux的每一个文件系统在初始化时,都会在初始化例程中填写一个 file_system_type 的数据结构,然后调用注册函数register_filesystem(struct file_system_type *fs) 进行注册。 • 在procfs_syms.c中,声明了/proc文件系统的类型: • static DECLARE_FSTYPE(proc_fs_type, “proc”, proc_read_super, FS_SINGLE); 而在 fs.h 中可以找到宏DECLARE_FSTYPE的定义: • #define DECLARE_FSTYPE(var,type,read,flags) \ struct file_system_type var = { \ name: type, \ read_super: read, \ fs_flags: flags, \ owner: THIS_MODULE, \ }
7.3.2 /proc文件系统的注册 • 上面的程序的声明中,名字为“proc”的文件类型为proc_fs_type,proc_read_super是读取超级块的函数,fs_flags设置为FS_SINGLE,根据源码中的说明,当文件系统的fs_flags声明为FS_SINGLE时,说明文件系统只有一个超级块,并且,必须在注册函数之后调用kern_mount(),使得在内核范围内的vfsmnt被放置在->kern_mnt处。 • 下面是/proc文件系统的注册函数init_proc_fs()的代码: • static int __init init_proc_fs(void) { int err = register_filesystem(&proc_fs_type); if (!err) { proc_mnt = kern_mount(&proc_fs_type); err = PTR_ERR(proc_mnt); if (IS_ERR(proc_mnt)) unregister_filesystem(&proc_fs_type); else err = 0; } return err; }
7.3.2 /proc文件系统的注册 • 可以看出,/proc文件系统的注册非常简单,主要有如下几个步骤: • (1)调用register_filesystem(&proc_fs_type),将proc文件类型加入到文件类型的单向链表中,如果发生错误,则返回。 • (2)调用kern_mount函数,该函数基本完成三个步骤,首先调用read_super()函数,在这个函数里,VFS将为proc文件系统分配一个超级块结构,并设置s_dev,s_flags等域,然后,将调用proc文件系统的自己的read_super例程。在/proc文件系统中,是proc_read_super()例程,该例程还将设置超级块结构的其他值。 • 其次,使用add_vfsmnt()函数建立proc文件系统的vfsmount结构,并将其加入到已装载文件系统的链表中(参考图7.6)。 • 最后,返回该vfsmount结构,并利用返回值,使用指针proc_mnt指向该vfsmount结构。 • (3)判断返回值是否错误,如果错误,就卸载文件系统。