当前位置:嗨网首页>书籍在线阅读

17-文件和文件系统

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

1.4.1 文件和文件系统

文件是Linux系统中最基础最重要的抽象。Linux遵循一切皆文件的理念(虽然没有某些其他系统如Plan 9那么严格)[2]。因此,很多交互操作是通过读写文件来完成,即使所涉及的对象看起来并非普通文件。

文件必须先打开才能访问。文件打开方式有只读、只写和读写模式。文件打开后是通过唯一描述符来引用,该描述符是从打开文件关联的元数据到文件本身的映射。在Linux内核中,文件用一个整数表示(C语言的int类型),称为文件描述符(file descriptor,简称fd)。文件描述符在用户空间共享,用户程序通过文件描述符可以直接访问文件。Linux系统编程的大部分工作都会涉及打开、操纵、关闭以及其他文件描述符操作。

普通文件

我们经常提及的“文件”即Linux中的普通文件(regular files)。普通文件包含以字节流(即线性数组)组织的数据。在Linux中,文件没有高级组织结构或格式。文件中包含的字节可以是任意值,可以以任意方式进行组织。在系统层,除了字节流,Linux对文件结构没有特定要求。有些操作系统,如VMS,提供高度结构化的文件,支持如records(记录)这样的概念,而Linux没有这么处理。

在Linux中,可以从文件中的任意字节开始读写。对文件的操作是从某个字节开始,即文件“地址”。该地址称为文件位置(file location)或文件偏移(file offset)。文件位置是内核中与每个打开的文件关联的元数据中很重要的一项。第一次打开文件时,其偏移为0。通常,随着按字节对文件的读写,文件偏移也随之增加。文件偏移还可以手工设置成给定值,该值甚至可以超出文件结尾。在文件结尾后面追加一个字节会使得中间字节都被填充为0。虽然支持通过这种在文件末尾追加字节的操作,但是不允许在文件的起始位置之前写入字节。这种操作看起来就很荒谬,实际上也并无用处。文件位置的起始值为0,不能是负数。在文件中间位置写入字节会覆盖该位置原来的数据。因此,在中间写入数据并不会导致原始数据向后偏移。绝大多数文件写操作都是发生在文件结尾。文件位置的最大值只取决于存储该值的C语言类型的大小,在现代Linux操作系统上,该值是64位。

文件的大小是通过字节来计算,称为文件长度。换句话说,文件长度即组成文件的线性数组的字节数。文件长度可以通过truncation(截断)操作进行改变。比起原始文件大小,文件被截断后的大小可以更小,这相当于删除文件末尾字节。容易让人困惑的是,从truncation操作的名称而言,文件被截断后的大小可以大于原始文件大小。在这种情况下,新增的字节(附加到文件末尾)是以“0”来填充。文件可以为空(即长度为0),不含任何可用字节。如同文件位置的最大值,文件长度的最大值只受限于Linux内核用于管理文件的C语言类型的大小。但是,不同的文件系统也可能规定自己的文件长度最大值,即为文件长度限制设置更小值。

同一个文件可以由多个进程或同一个进程多次打开。系统会为每个打开的文件实例提供唯一文件描述符。因此,进程可以共享文件描述符,支持多个进程使用同一个文件描述符。Linux内核没有限制文件的并发访问。不同的进程可以同时读写同一个文件。对文件并发访问的结果取决于这些操作的顺序,通常是不可预测的。用户空间的程序往往需要自己协调,确保对文件的同步访问是合理的。

文件虽然是通过文件名访问,但文件本身其实并没有直接和文件名关联。相反地,与文件关联的是索引节点inode(最初称为信息节点 ,是information node的缩写),inode是文件系统为该文件分配的唯一整数值(但是在整个系统中不一定是唯一的)。该整数值称为inode number,通常简称为i-number或ino。索引节点中会保存和文件相关的元数据,如文件修改时间戳、所有者、类型、长度以及文件数据的位置——但不含文件名!索引节点就是UNIX文件在磁盘上的实际物理对象,也是在Linux内核中通过数据结构表示的概念实体。

目录和链接

通过索引节点编号访问文件很繁琐(而且潜在安全漏洞),因此文件通常是通过文件名(而不是索引节点号)从用户空间打开。目录用于提供访问文件需要的名称。目录是可读名称到索引编号之间的映射。名称和索引节点之间的配对称为链接(link)。映射在物理磁盘上的形式,如简单的表或散列,是通过特定文件系统的内核代码来实现和管理的。从概念上看,可以把目录看作普通文件,其区别在于它包含文件名称到索引节点的映射。内核直接通过该映射把文件名解析为索引节点。

如果用户空间的应用请求打开指定文件,内核会打开包含该文件名的目录,搜索该文件。内核根据文件名获取索引节点编号。通过索引节点编号可以找到该节点。索引节点包含和文件关联的元数据,其中包括文件数据在磁盘上的存储位置。

刚开始,磁盘上只有一个目录,称为根目录,以路径/表示。然而,系统上通常有很多目录,内核怎么知道到哪个目录查找指定文件呢?

如前所述,目录和普通文件相似。实际上,它们有关联的索引节点。因此,目录内的链接可以指向其他目录的索引节点。这表示目录可以嵌套到其他目录中,形成目录层。这样,就可以支持使用UNIX用户都熟悉的路径名来查找文件,如/home/blackbeard/landscaping.txt。

当内核打开类似的路径名时,它会遍历路径中的每个目录项(directory entry,在内核中称为dentry),查找下一个入口项的索引节点。在前面的例子中,内核起始项是/,先获取home的索引节点,然后获取blackbeard的索引节点,最后获取concorde.png的索引节点。该操作称为目录解析或路径解析。Linux内核也采用缓存(称为dentry cache)储存目录的解析结果,基于时间局部性原理,可以为后续访问更快地提供查询结果。

从根目录开始的路径称为完整路径,也叫绝对路径。有些路径不是绝对路径,而是相对路径(如todo/plunder)。当提供相对路径时,内核会在当前工作目录下开始路径解析。内核在当前工作目录中查找todo目录。在这里,内核获取索引节点plunder。相对路径和当前工作目录的组合得到绝对路径。

虽然目录是作为普通文件存储的,但内核不支持像普通文件那样打开和操作目录。相反地,目录必须通过特殊的系统调用来操作。这些系统调用只支持两类操作:添加链接和删除链接。如果支持用户空间绕过内核操作目录,有可能出现一个简单的错误就会造成文件系统崩溃的巨大悲剧。

硬链接

从概念上看,以上介绍的内容都无法避免多个名字解析到同一个索引节点上。而事实上,多个名字确实可以解析到同一个索引节点。当不同名称的多个链接映射到同一个索引节点时,我们称该链接为硬链接(hard links)。

在复杂的文件系统结构中,硬链接支持多个路径指向同一份数据。硬链接可以在同一个目录下,也可以在不同的目录中。不管哪一种情况,内核都可以把路径名解析到正确的索引节点。举个例子,某个指向特定数据块的索引节点,其硬链接可以是/home/bluebeard/treasure.txt 和/home/blackbeard/to_steal.txt。

要从目录中删除文件,需要从目录结构中取消链接(unlink)该文件,这只需要从目录中删除该文件名和索引节点就可以。然而,由于Linux支持硬链接,文件系统不能对每个unlink操作执行删除索引节点及其关联数据的操作。否则,如果该索引节点在文件系统中还有其他的硬链接怎么办?为了确保在删除所有的链接之前不会删除文件,每个索引节点包含链接计数(link count),记录该索引节点在文件系统中的链接数。当unlink某个路径时,其链接计数会减1;只有当链接计数为0时,索引节点及其关联的数据才会从文件系统中真正删除。

符号链接

硬链接不能跨越多个文件系统,因为索引节点编号在自己的文件系统之外没有任何意义。为了跨越文件系统建立链接,UNIX系统实现了符号链接(symbolic links,简称symlinks)。

符号链接类似于普通文件,每个符号链接有自己的索引节点和数据块,包含要链接的文件的绝对路径。这意味着符号链接可以指向任何地方,包括不同的文件系统上的文件和路径,甚至指向不存在的文件和目录。指向不存在的文件的符号链接称为坏链接(broken link)。

比起硬链接,符号链接会带来更多的开销,因为有效解析符号链接需要解析两个文件:一是符号链接本身,二是该链接所指向的文件。硬链接不会带来这些额外开销——因为访问在文件系统中被多次链接的文件和单次链接的文件没有区别。虽然符号链接的开销很小,但还是被认为是个负面因素。

符号链接没有硬链接那么“透明”。使用硬链接是完全透明的——所需要做的仅仅是确定文件是否被多次链接!但是,操作符号链接需要特定的系统调用。由于符号链接的结构很简单,它通常是作为文件访问的快捷方式,而不是作为文件系统内部链接,因此这种缺乏透明性通常被认为是个正面因素。

特殊文件

特殊文件(special file)是指以文件来表示的内核对象。这些年来,UNIX系统支持了不少不同的特殊文件。Linux只支持四种特殊文件:块设备文件、字符设备文件、命名管道以及UNIX域套接字。特殊文件是使得某些抽象可以适用于文件系统,贯彻一切皆文件的理念。Linux提供了系统调用来创建特殊文件。

在UNIX系统中,访问设备是通过设备文件来实现,把设备当作文件系统中的普通文件。设备文件支持打开、读和写操作,允许用户空间程序访问和控制系统上的(物理和虚拟)设备。UNIX设备通常可以划分成两组:字符设备(character devices)和块设备(block device)。每种设备都有自己的特殊文件。

字符设备是作为线性字节队列来访问。设备驱动程序把字节按顺序写入队列,用户空间程序按照写入队列的顺序读取数据。键盘就是典型的字符设备。举个例子,当用户输入“peg”,应用程序将顺序从键盘设备中读取p、e和g。如果没有更多的字符读取时,设备会返回end-of-file(EOF)。漏读数据或以其他顺序读取都是不可能的。字符设备通过字符设备文件(character device file)进行访问。

和字符设备不同,块设备是作为字节数组来访问。设备驱动把字节映射到可寻址的设备上,用户空间可以按任意顺序随意访问数组中的任何字节——可能读取字节12,然后读取字节7,然后又读取字节12。块设备通常是存储设备。硬盘、软盘、CD-ROM驱动和闪存都是典型的块设备。这些块设备通过块设备文件(block device file)来访问。

命名管道(named pipes),通常称为FIFO(是“先进先出first in, first out”的简称),是以文件描述符作为通信信道的进程间通信(IPC)机制,它可以通过特殊文件来访问。普通管道是将一个程序的输出以“管道”形式作为另一个程序的输入,普通管道是通过系统调用在内存中创建的,并不存在于任何文件系统中。命名管道和普通管道一样,但是它是通过FIFO特殊文件来访问的。不相关的进程可以访问该文件并进行交互。

套接字(socket)是最后一种特殊文件。socket是进程间通信的高级形式,支持不同进程间的通信,这两个进程可以在同一台机器,也可以在不同机器。实际上,socket是网络和互联网编程的基础。socket演化出很多不同的变体,包括UNIX域套接字,它是本地机器进行交互的socket格式。虽然socket在互联网上的通信会使用主机名和端口号来标识通信目标,UNIX域套接字使用文件系统上的特殊文件进行交互,该文件称为socket文件。

文件系统和命名空间

如同所有的UNIX系统,Linux提供了全局统一的文件和目录命名空间。有些操作系统会把不同的磁盘和驱动划分成独立的命名空间——比如,通过路径A:\plank.jpg可以访问软盘上的文件,虽然硬盘驱动安装在C:\目录下。在UNIX,该软盘上的文件可以在其他介质上,通过路径/media/floppy/plank.jpg访问,甚至可以通过/home/captain/stuff/plank.jpg访问。也就是说,在UNIX系统中,命名空间是统一的。

文件系统是以合理有效的层次结构组织的文件和目录的集合。在文件和目录的全局命名空间中,可以分别添加和删除文件系统,这些操作称为挂载(mounting)和卸载(unmounting)。每个文件系统都需要挂载到命名空间的特定位置,该位置即挂载点(mount point)。在挂载点可以访问文件系统的根目录。举个例子,把CD挂载到/media/cdrom,CD上文件系统的根目录就可以通过/media/cdrom访问。第一个被挂载的文件系统是在命名空间的根目录/下,称为根文件系统(root filesystem)。Linux系统必定有个根文件系统,而其他文件系统的挂载点则是可选的。

通常而言,文件系统都是存在物理介质上(即保存在磁盘上),不过Linux还支持只保存在内存上的虚拟文件系统,以及存在于网络中的其他机器上的网络文件系统。物理文件系统保存在块存储设备中,如CD、软盘、闪存或硬盘中。在这些设备中,有些是可以分区的,表示可以切分成可独立操作的多个文件系统。Linux支持的文件系统类型很宽泛,囊括所有一般用户有可能遇到的——包括媒体文件系统(如ISO9660)、网络文件系统(NFS)、本地文件系统(ext4)、其他UNIX系统的文件系统(XFS)以及非UNIX系统的文件系统(FAT)。

块设备的最小寻址单元称为扇区(sector)。扇区是设备的物理属性。扇区大小一般是2的指数倍,通常是512字节。块设备无法访问比扇区更小的数据单元,所有的I/O操作都发生在一个或多个扇区上。

文件系统中的最小逻辑寻址单元是块(block)。块是文件系统的抽象,而不是物理介质的抽象。块大小一般是2的指数倍乘以扇区大小。在Linux,块通常比扇区大,但是必须小于页(page),页是内存的最小寻址单元(内存管理单元是个硬件)[3]。常见的块大小是512B、1KB和4KB。

从历史角度看,UNIX系统只有一个共享的命名空间,对系统上所有的用户和进程都可见。Linux独辟蹊径,支持进程间独立的命名空间,允许每个进程都可以持有系统文件和目录层次的唯一视图[4]。默认情况下,每个进程都继承父进程的命名空间,但是进程也可以选择创建自己的命名空间,包含通过自己的挂载点集和独立的根目录。