《Operating System:Three Easy Pieces》阅读笔记<二十五>—文件和目录

219 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情

文件和目录

我们真正开始讨论文件的抽象,这也是重点部分,文件和目录的抽象是文件系统的基石,正如CPU、内存一样,文件也是系统内可共享的持久化存储资源,我们重点谈论一下UNIX文件系统中可交互的接口。

文件与目录概述

首先是基础概念,每一个文件都是一组二进制值,都有一个低位名称,用户通常不会注意到这个名称,这个低位名称通常会链接到inode数,即每个文件都有一个与之关联的inode号,文件系统设计之初就是就是存放并确保能够读取到数据。

然后是目录,目录抽象的内容有两种,一是文件-低级名称,一种是目录-目录。通过构建目录树,能够做到对所有文件的索引。一个磁盘就是一个目录系统,根目录就是最上层的目录,一般是\。这些概念和我们在windows中使用的文件管理器不同,文件系统是一种更加抽象宏观的设计,例如在UNIX里面,文件、设备、管道甚至进程都是由文件系统命名的。统一的命名系统让系统更简单、更模块化。再比如,统一的文件系统(一个磁盘分区)命名磁盘、u盘、CD-ROM和很多其它东西,这一切都位于一个单一的目录树下。

文件接口

接下来我们通过文件系统的接口处理流程来了解其工作原理

首先是创建文件,通过系统调用open来打开文件,函数open的描述如上图所示,可以看到open()有三个参数,第一个指定显式文件名,第二个创建文件,如果文件不存在则确保文件只能被写入,如果文件已经存在则将其截断为零个字节,第三个参数指定权限。open()返回的是一个文件描述符,文件描述符简单来讲可以看作是物理文件地址的指针,是其他文件系统调用的基础。

具体的讲,一个简单的数组跟踪每一个进程中已打开的文件,数组中每一个条目都指向一个file结构体,用于保存文件信息。这个表就是开放文件表,文件标识符用来打开这个表,表里保存的文件信息文件、当前偏移量、和其他相关细节,比如文件是可读还是可写。如下图所示

在xv6系统里面,所有这样的结构体保存在一个数组里面,并且这个数组自带一个锁。

strace是专门用来查看程序使用系统调用的工具,是一个十分强大的工具。

然后是读写文件,我们知道echo和cat命令可以用来构建简单的文件读写程序。我们查看这个简单简单命令的调用链。

cat做的第一件事是打开文件,O_RDONLY标志指示文件只打开读(不是写),O_LARGEFILE表示使用64位偏移量。文件在打开后返回的标识符是3,那是因为0、1、2是进程在创建时就已经打开的文件了,它们分别是标准输入、标准输出和标准错误。

之后使用read()系统调用从文件中读取一些字节,read()的第一个参数是文件描述符,从而告诉文件系统要读取哪个文件,第二个参数指向一个缓冲区,其中将放置read()的结果,第三个参数是缓冲区的大小,在本例中为4 KB。对read()的调用成功时返回它读取的字符数,这里是”hello\n“六个字符。

接下来是write调用,将字符串写入文件1,也就是标准输出流,于是字符串在用户终端显示,同样返回输出的字符串长度,最后再次调用read试图得到更多数据,但是返回0,因此结束访问,删除文件引用。之后大部分的命令都是这样一个个系统调用组成的。

明显上面提到的读写read和write都是顺序读写,我们需要一种随机读写文件位的方法,这就是lseek调用,如下图所示。

其中fildes就是文件描述符,offset就是字符偏移量,whence表示三种offset发挥作用的方式。对于进程打开的每一个文件,操作系统会跟踪一个偏移量,这个偏移量决定了下一次读写文件会从哪一个地方开始,这个偏移量有两种方式进行更新。一是将N个字节的读写直接加到偏移量上(自动),二是使用lseek调用来指定偏移量。系统调用与偏移量之间的关系如下图所示。

很多时候,不只是多个进程会同时访问文件系统,多个线程也需要并发访问文件,但是如果这些线程都使用同一个文件描述符的话,它们就只能在相同的偏移量上进行读写。因此,系统提供了一个dup调用用来复制文件描述符,具体过程如下图所示。通过操作不同文件描述符,可以读取不同的偏移量,可以实现不同线程读写文件不同的区域。

write调用的其中一个特性是,先返回写入成功,再后台延迟写入。而有时我们需要让写入硬盘立即执行。因此我们可以使用fsync调用,fsync调用强制所有脏数据立刻写入磁盘。这会造成操作系统对脏数据的管理出现问题,但是增强了写入数据的安全性。

有时我们想要对文件进行重命名,我们可以用mv命令,而mv命令实际调用了rename,rename有操作上的原子性,这是为了防止系统崩溃造成的命名错误,从旧名到新名一步完成,不会有中间状态。具体的过程如下图所示

文件如此复杂,我们希望在文件系统中保存文件有关的所有数据,这种数据称为文件元数据。如果想要调用查看文件元数据,可以使用stat或者fstat调用来查看。具体的信息如下图所示。

这些元数据都存储在inode指向的地方,还有一份inode拷贝留在内存里面加速文件访问。

如果我们要删除文件,可以使用rm命令,照例我们查看rm命令的调用,发现它只是简单的调用了一个unlink函数。它的作用原理我们需要结合目录系统来一起讲。

目录接口

和文件一样,有一组与目录相关的系统调用能够创建、读取和删除目录。但是和文件不同的是,永远不能直接写入目录。因为目录的格式被认为是文件系统元数据,文件系统认为应该负责目录数据的完整性,只能通过创建文件、目录或其他对象类型等方式间接地更新目录。也就是说,目录是一种封装性强的结构。

特别的,一个空目录会有两个条目,一个引用自身,一个引用其父目录,使用ls -a可以在空目录中查看到./ ../两个文件,它们同时也是可以访问的。

ls命令也是读取目录的主要方式,照例我们查看ls的调用链,发现三个调用:opendir()、readdir()和closedir(),读取的信息来自于目录为其包含文件构造的结构体,称为目录表,目录表包含的信息比较简单,只有文件名和文件索引一点点,因此所以程序可能会对每个文件调用stat(),以获取每个文件的更多信息,这也就是ls -l的底层原理。

删除目录也是我们常用的一项操作,一种简单的调用是rmdir(),但是由于安全考虑,rmdir只能删除空目录。如果想要删除带有文件的目录,也就是删除所有文件,就需要了解删除文件和目录的关系。我们之前讲过删除文件实际调用的是unlink()函数,而与之相对的是link(),一种在文件系统树中创建条目的方法,该调用有两个参数,一个旧路径名和一个新路径名。另一种说法是,系统调用link,也就是命令ln,用来创建文件的引用关系。

链接

其底层原理就是将多个文件名链接到同一个文件名上,如下图所示,file和file2的inode是相同的,结合起来看,创建文件时,创建一个inode结构,同时将文件名硬链接到文件,因此rm的unlink都是删除文件名,而不是删除文件。

与这个机制对应的是文件系统会统计某个文件的引用个数,即reference count,当调用link时,引用数加一,调用unlink时,引用数减一,只有当引用计数达到0时,系统才会彻底删除文件。如下图所示。

上面的链接方式有一个专门的名称,硬链接。相对的还有一种方式,即软链接,也叫符号链接,提出这一概念的原因是硬链接是有限制的,硬链接不能在同一个目录里面,因为有可能创建循环链接,也不能跨盘,因为inode号只在特定的文件系统中是唯一的。

创建软连接的方式也是使用ln命令,不过要加上-s标识符,即ln -s。如下图所示,软链接和硬链接不同,不是链接到同一个inode号,它实际上是一个路径,相当于原文件的另一个引用,删除文件演示也说明了这一原理。

其它

我们知道文件这一抽象资源是各种进程共有的,因此需要更严格的权限管理,在UNIX文件系统这种权限管理是通过权限位来实现的,如下图所示。

在上面的示例中,ls输出的前三个字符表明该文件的所有者(rw-)可以读和写,只有群组的成员和系统中的其他任何人(r——后跟r——)可以读。文件的所有者可以很简单的通过chmod命令更改文件访问模式,设置方法很简单,以chmod 755为例,7代表前三位设为111,即可读可写可执行,5表示中间三位设为101,表示可读可执行,另一个5同理。

在更复杂的文件系统里,还有一种更复杂的访问控制列表允许更精确地控制谁可以访问和操作信息。分布式文件系统AFS用ACL访问控制列表来管理用户权限

我们已经介绍了文件、目录和特殊类型的链接的接口,我们可以看出,文件系统的构建无非就是使用接口将文件和目录一个个挂载到系统中,如果想创建一个文件系统,可以使用mkfs调用,简单输入设备和文件系统类型。就能够创建一个空文件系统。创建出的文件系统不可能是孤立的,必须要挂载到已有的文件系统上,这要使用mount命令,如下图所示。

概念比较多,我们来做一些总结:

  • 文件是一个字节数组,它可以被创建、读取、写入和删除。它有一个唯一引用它的低级名称(即数字)。低级别的名字通常被称为i-number。
  • 目录是元组的集合,每个元组包含一个人类可读的名称和它映射到的底层名称。每个条目都指向另一个目录或一个文件。每个目录本身也有一个低级名称(i-number)。
  • 访问文件后,操作系统会返回一个文件描述符,根据权限和意图的允许,这个文件描述符可以用于读写访问。
  • 每个文件描述符是一个私有的、每个进程的实体,它引用了打开文件表中的一个条目。其中的条目跟踪该访问指的是哪个文件,文件的当前偏移量(即,下一次读写将访问文件的哪一部分),以及其他相关信息。
  • 调用read()和write()自然地更新当前偏移量;否则,进程可以使用lseek()来更改它的值,从而允许对文件的不同部分进行随机访问。
  • 要在文件系统中使用多个人类可读的名称来引用同一个底层文件,要使用硬链接或软链接。每种方法在不同的情况下都是有用的,所以在使用之前要考虑它们的优缺点。请记住,删除文件只是在目录层次结构中执行最后一次unlink()操作。
  • 大多数文件系统都有启用和禁用共享的机制。这种控制的基本形式是由权限位提供的;更复杂的访问控制列表允许更精确地控制谁可以访问和操作信息。