操作系统导论习题解答(39. Files and Directories)
Interlude: Files and Directories
带着问题学习:操作系统应如何管理持久性设备?什么是API?实施的重要方面是什么?
1. Files And Directories
在虚拟化存储方面有两个重要的抽象:
- 文件(file)
- 目录(directory)
文件:一组字节的线性组合,每个文件你都能进行读或写。每个文件都有一个低级名字(low-level name),文件的低级名称通常称为其索引节点号(inode number)。大部分系统中,操作系统都不清楚文件的结构,文件系统的职责仅仅是将此类数据持久地存储在磁盘上。
目录:一个目录像一个文件,也有低级名字,但是内容相当特殊:它包含(用户可读名称,低级名称)对的列表。通过在目录里嵌套目录,使用者能够建立一个目录树(directory tree)。目录层次结构从根目录开始(在基于UNIX的系统中,根目录简称为/),并使用某种分隔符命名后续子目录,直到命名了所需的文件或目录。
上图表明:目录和文件可以有相同的名字只要它们在目录树中处于不同的位置。
2. The File System Interface
详细讨论文件系统界面,将从创建、访问和删除文件的基础开始。一路上我们会发现用于删除文件的神秘调用unlink()
。
3. Creating Files
创建一个文件(creating a file):通过调用open()
函数然后传递给它O_CREAT
标志,一个程序就能够创建一个新文件。
int fd = open("foo", O_CREAT | O_WRONLY | O_WRUNC, S_IRUSR | S_IWUSR);
O_CRAET:如果文件不存在就创建
O_WRONLY :文件只能进行写操作
O_WRUNC:如果文件存在就将其截断为0字节并删除所有存在的内容
S_IRUSR:允许文件所有者读文件
S_IWUSR:允许文件所有者写文件
函数open()
返回的是一个文件描述符(file descriptor)。文件描述符是一个整数,每个进程私有,在UNIX系统中用于获取文件。一旦你使用open()
函数打开了一个文件,你就可以使用read()
和write()
函数对其进行读和写。
文件描述符由操作系统基于每个进程进行管理意味着在UNIX系统中有一种简单的结构保存在proc结构中,下面代码来自xv6内核:
struct proc {
...
struct file *oflie[NOFILE]; // open files
...
};
数组的每个entry实际上是指向struct file的指针,被用于追踪文件被读或者写的信息。
4. Reading And Writing Files
可以使用cat
程序将文件内容转储到屏幕上:
问题:cat程序是如何获取文件foo的?
为了解决这个问题,将使用一个有用的工具,在UNIX系统中叫做strace,当然在其他系统中也有此类工具,只是名称不同而已。下面是使用strace的一个例子:
cat程序第一件要做的事就是打开文件进行阅读。有三点需要注意:
- 文件只能读不能写(
O_RDONLY
标志) - 使用64位偏移量(
O_LARGEFILE
) - 返回的文件描述符的值为
3
(值为3是因为三个文件被打开(standard(input/output/error)))
成功打开文件后,cat程序使用read()
函数从文件中重复读取字节。read()
函数第三个参数为缓冲区大小,返回值为6是因为“hello”有5个字母加上结尾的"\0"。
然后是对write()
函数的调用,第一个参数为1是该描述符被称为标准输出,第二个参数是准备写到屏幕上的内容,第三个参数为read()
函数返回的描述符。
然后read()
函数继续读取文件内容,由于文件中已无未读取内容,故调用close()
函数关闭文件。
5. Reading And Writing, But Not Sequentially
上述我们讨论的关于读或者写操作都是顺序操作,从文件头到文件尾,有时候,能够读取或写入文件中的特定偏移量很有用。
读取文件中任意偏移量的内容,lseek()
函数就很重要:
off_t lseek(int fildes, off_t offset, int whence);
第一个参数为文件描述符,第二个参数为偏移量,第三个参数精确地确定了执行搜索的方式。
看一个例子对上述过程更加清楚。一个进程首先打开一个300字节大小的文件,然后每次重复读取100字节大小的内容:
接下来让我们打开一个文件两次并且进行读取操作:
最后一个例子,一个进程使用lseek()
在读取之前重新定位当前偏移量:
6. Shared File Table Entries: fork()
And dup()
在许多情况下(如上所示),文件描述符到打开文件表中条目(entry)的映射是一对一的,即使某些其他进程同时读取同一文件,每个进程在打开文件表中也会有自己的条目。
但是,在打开的文件表中的条目有时是共享的(shared),有种情况就是父进程调用fork()
函数创建子进程。
运行上述程序,结果如下:
下图显示了将每个进程的私有描述符组,共享的打开文件表条目以及从该进程到底层文件系统索引节点的引用之间建立连接的关系:
上图最需要注意的点就是refcnt(reference count)。每多一个文件共享文件表条目,它的数量就加1。只有当所有进程都关闭文件后,才会被移除。
另一个共享发生在调用dup()
函数。该函数允许进程创建一个新文件描述符,该文件描述符引用与现有描述符相同的基础打开文件。
7. Writing Immediately With fsync()
多数情况下,程序调用write()
函数时,它只是在告诉文件系统:请在将来的某个时刻将此数据写入持久性存储。出于性能原因,文件系统将在内存中缓冲此类写操作一段时间。这就导致了小概率事件发生,在write()函数调用之后但在写入磁盘之前机器如果发生故障崩溃,数据将丢失。
但是某些应用程序需要的不仅仅是数据不能丢失的问题。如数据库管理系统,既要有正确的数据恢复协议也要定时强制向磁盘写入数据。为了支持此类应用,大部分文件系统提供一些额外的控制APIs(在UNIX系统中为fsync(int fd)
)。
当一个进程调用fsync()
函数,文件系统强制所有的脏(dirty)数据(尚未写入到磁盘的)到磁盘进行响应,当所有写操作都完成后函数返回。下面是一个简单的例子:
此顺序并不能保证你可能期望的一切。在某些情况下,还需要fsync()
包含文件foo的目录。添加此步骤不仅可以确保文件本身在磁盘上,而且可以确保文件(如果是新创建的)也持久地属于目录的一部分。
8. Renaming Files
mv
命令可以对文件进行重命名:
在上述例子中,文件foo被重命名为bar。
使用strace,我们能够发现mv命令使用系统调用rename(char *old, char *new)
函数。该函数是原子级(atomic)调用,如果系统在重命名期间崩溃,则该文件将被命名为旧名称或者新名称,不会出现中间状态。
看如下一个例子:在文件中间插入一行,文件名是foo.txt
首先以临时名称(foo.txt.tmp)写出文件的新版本,然后使用fsync()
将其强制插入磁盘,接着在应用程序确定新文件时使用该文件。元数据和内容在磁盘上,将临时文件重命名为原始文件的名称,最后一步自动将新文件交换到位,同时删除文件的旧版本,从而实现了原子级文件更新。
9. Getting Information About Files
在获取文件前,我们希望文件系统保存描述文件的信息。描述数据的数据就叫做元数据(metadata)。为了查看元数据,可以调用stat()
函数或者fstat()
函数。如下所示:
下面是在LINUX系统上的输出:
10. Removing Files
在UNIX系统中,你可能知道删除文件使用rm指令,但是当使用该指令时系统调用了什么?
继续使用我们的老伙计strace:
函数unlink()
仅需要删除的文件的名称,并在成功后返回值0。但是,为什么将此系统调用命名为unlink?为什么不命名为remove或者delete?要理解这一点,我们必须了解的不仅仅是文件,而且还有目录。
·
11. Making Directories
你永远不能直接写入目录,因为目录的格式被认为是文件系统元数据,你只能间接地更新目录。
创建一个目录,系统调用mkdir()
函数:创建一个目录叫foo
创建上述目录时,目录被视为空的。空目录有两个条目(entry),一个指向自己,一个指向其父级,前者称为.
目录,后者称为..
目录。你能够使用程序ls
加上-a
标志查看这些目录:
12. Reading Directories
让我们使用我们自己的方式来打开目录(opendir()
函数)、读目录(readdir()
函数)、关闭目录(closedir()
函数):
以下声明显示了struct dirent
数据结构中每个目录条目中的可用信息:
13. Deleting Directories
你能够调用rmdir()
函数对目录进行删除操作。但是移除目录是非常危险的,因为你可能删除目录下存在的大量数据。因此,rmdir()
函数要求在删除目录前,该目录为空。
14. Hard Links
我们回到之前提到的问题:为什么删除文件叫做unlink?
首先通过了解link()
函数的系统调用,了解在文件系统树种进行输入的方法。函数link()
有两个参数,一个旧路径名和一个新路径名,当你将新文件名链接到旧文件名时,实际上是在创建另一种引用同一文件的方式。命令行ln
用于执行此操作:
函数link()
的工作原理:只是在要创建链接的目录中创建另一个名称,然后将其引用到原始文件的相同索引节点号,不会以任何方式复制文件:
上述情况表明:创建文件时,实际上是在做两件事:
- 建立一个结构(索引节点),该结构实际上将跟踪有关该文件的所有相关信息
- 将一个易于理解的名称链接到该文件,并将该链接放入目录中
故,从文件系统中移除文件叫做unlink()
。在上述例子中,可移除文件名file
,并且仍然可以轻松访问文件:
实际上移除文件名就是减少索引节点号数量。当索引节点号的值为0时,才真正意义上删除了该文件。
下例对一个文件创建三个链接然后删除:
15. Symbolic Links
硬链接有时会受到限制:
- 无法在目录中创建一个目录
- 无法硬链接到其他磁盘分区中的文件
为了解决上述问题,一种新类型链接被称为符号链接(symbolic link)或者软链接(soft link)出现了。能够使用程序ln
加上-s
标志创建这种链接:
如上所示,表面上软链接和硬链接好像没有什么不同。除了表面相似性之外,符号链接实际上与硬链接大不相同:符号链接实际上是文件本身,具有不同的类型
file2
为4字节是因为符号链接的形成方式是通过将链接文件的路径名保存为链接文件的数据。
符号链接的创建方式,为悬空引用(dangling reference)留下了可能性:
如上所示,删除名为file
的原始文件会使链接指向不再存在的路径名。
16. Permission Bits And Access Control Lists
文件系统提供了磁盘的虚拟视图,将其从一对原始块转换为更加用户友好的文件和目录,文件通常在不同的用户和进程之间共享,并且不是私有的。因此,通常在文件系统种存在用于实现各种程序的共享的更全面的机制。
这种机制的第一种形式是经典的UNIX权限位(permission bits)。要查看文件foo.txt
的权限,只需键入:
主要集中于-rw-r--r--
:
- 第一个字符
-
表明文件类型:regular file - 后9位
rw-r--r--
为权限位(前三个字符rw-
表明该文件可由所有者读取和写入;后六个字符r--r--
表明并且只能由组成员以及系统中的其他任何人读取)
文件的拥有者可以轻松修改这些权限。通过使用chmod
命令删除所有者以外的任何人都不能访问该文件的能力,可以键入:
上述命令为所有者启用可读位(4)和可写位(2)(将其进行或运算得到上面的6),但将组和其他权限位分别设置为0和0,从而将权限设置为rw------
。
除了权限位之外,某些文件系统,例如称为AFS的分布式文件系统,还包括更复杂的控件。
17. Making And Mounting A File System
另外一个主题:如何从许多基础文件系统中组装完整的目录树?
首先制作文件系统,然后安装它们以使其内容可访问来解决此问题。
为了制作文件系统,大多数文件系统都提供了一个工具叫做mkfs
。给该工具输入一个设备和一个文件系统类型作为输入,然后它会简单地写一个空的文件系统,开始就带有根目录的磁盘分区。
一旦创建了这样的文件系统,需要使其在统一的文件树中可访问,该任务由程序mount
实现。