单机数据持久-文件和目录
2023-1-15
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 
 
操作系统应该如何管理持久存储设备?都需要哪些API?实现有哪些重要方面?
 
随着时间的推移,有关存储虚拟化形成了两个关键的抽象。
  • 第一个是文件(file):文件就是一个线性字节数组,每个字节都可以读取或写入。每个文件都有某种低级名称,通常是某种数字,用户通常不知道这个名字。由于历史原因,文件的低级名称通常称为inode号。每个文件都有一个与其关联的inode号。
  • 第二个是目录(directory):一个目录,像一个文件一样,也有一个低级名字(即inode号),但是它的内容非常具体:它包含一个(用户可读名字,低级名字)对的列表。例如,假设存在一个低级别名称为“10”的文件,它的用户可读的名称为“foo”。“foo”所在的目录因此会有条目(“foo”,“10”),将用户可读名称映射到低级名称。目录中的每个条目都指向文件或其他目录。通过将目录放入其他目录中,用户可以构建任意的目录树(directory tree,或目录层次结构,directory hierarchy),在该目录树下存储所有文件和目录。
    • 目录层次结构从根目录(root directory)开始(在基于UNIX 的系统中,根目录记为“/”),并使用某种分隔符(separator)来命名后续子目录(sub-directories),直到命名所需的文件或目录。例如,如果用户在根目录中创建了一个目录foo,然后在目录foo中创建了一个文件bar.txt,就可以通过它的绝对路径名(absolute pathname)来引用该文件
      notion image
      这个例子中,它将是/foo/bar.txt。示例中的有效目录是/,/foo,/bar,/bar/bar,/bar/foo,有效的文件是/foo/bar.txt 和/bar/foo/bar.txt。目录和文件可以具有相同的名称,只要它们位于文件系统树的不同位置(例如图中有两个名为bar.txt 的文件:/foo/bar.txt 和/bar/foo/bar.txt)。
 

创建文件

通过调用open()并传入O_CREAT标志,程序可以创建一个新文件。。
函数open()接受一些不同的标志。在本例中,程序创建文件(O_CREAT),只能写入该文件,因为以(O_WRONLY)这种方式打开,并且如果该文件已经存在,则首先将其截断为零字节大小,删除所有现有内容(O_TRUNC)。
open()的一个重要方面是它的返回值:文件描述符(file descriptor)。文件描述符只是一个整数,是每个进程私有的,在UNIX系统中用于访问文件。因此,一旦文件被打开,如果你有权限的话,就可以使用文件描述符来读取或写入文件。这样来看,一个文件描述符就是一种权限,即一个不透明的句柄,它可以让你执行某些操作。另一种看待文件描述符的方法,是将它作为指向文件类型对象的指针。一旦你有这样的对象,就可以调用其他“方法”来访问文件,如read()write()
 

读写文件

读取一个现有的文件。如果在命令行键入,可以用cat程序,将文件的内容显示到屏幕上。
将程序echo 的输出重定向到文件foo,然后文件中就包含单词“hello”。然后我们用cat 来查看文件的内容
 
文件成功打开后,就可以对文件进行读写。read()是读取文件的系统调用,它的原型如下:
read()的第一个参数是文件描述符,一个进程可以同时打开多个文件,因此描述符使操作系统能够知道某个特定的读取引用了哪个文件。第二个参数指向一个用于放置read()结果的缓冲区。第三个参数是缓冲区的大小。对read()的成功调用返回它读取的字节数。
 
系统调用write()的原型如下:
它的作用是把缓冲区buf的前nbytes个字节写入与文件描述符fildes关联的文件中,它返回实际写入的字节数。
 

改变文件偏移量

有时能够读取或写入文件中的特定偏移量是有用的。例如,如果你在文本文件上构建了索引并利用它来查找特定单词,最终可能会从文件中的某些随机偏移量中读取数据。为此,我们可以使用lseek()系统调用。
第一个参数是一个文件描述符。第二个参数是偏移量,它将文件偏移量定位到文件中的特定位置。第三个参数,由于历史原因而被称为whence,指定了搜索的执行方式。
对于每个进程所有打开的文件,操作系统都会跟踪一个“当前”偏移量,这将决定在文件中下一次读取或写入开始的位置。因此,打开文件的抽象包括它当前的偏移量,偏移量的更新有两种方式。第一种是当发生N个字节的读或写时,N被添加到当前偏移,因此每次读取或写入都会隐式更新偏移量。第二种是明确的lseek,它改变了上面指定的偏移量。
注意,lseek()调用只是在OS内存中更改一个变量,该变量跟踪特定进程的下一个读取或写入开始的偏移量。调用lseek()与移动磁盘臂的磁盘的寻道(seek)操作无关,执行I/O时,根据磁头的位置,磁盘可能会也可能不会执行实际的寻道来完成请求。
 

同步写入

大多数情况下,当程序调用write()时,它只是告诉文件系统:在将来的某个时刻,将此数据写入持久存储。出于性能的原因,文件系统会将这些写入在内存中缓冲(buffer)一段时间。在稍后的时间点,才会将写入实际发送到存储设备。
从应用程序的角度来看,写入似乎很快完成,并且只有在极少数情况下(例如,在write()调用之后但写入磁盘之前,机器崩溃)数据会丢失。但是,有些应用程序需要的不只是这种保证。例如,在数据库管理系统(DBMS)中,经常要求能够强制写入磁盘。
为了支持这些类型的应用程序,大多数文件系统都提供了一些额外的控制API。在UNIX中,提供给应用程序的接口被称为fsync。当进程针对特定文件描述符调用fsync()时,文件系统通过强制将所有脏数据写入磁盘来响应。
有趣的是,这段代码并不能保证你所期望的一切。在某些情况下,还需要fsync()包含foo文件的目录。添加此步骤不仅可以确保文件本身位于磁盘上,而且可以确保文件(如果新创建)也是目录的一部分。
 

文件重命名

常用的Linux命令mv,就使用了系统调用rename(char old, char new),它只需要两个参数:文件的原来名称和新名称。
rename()调用提供了一个保证:它通常是一个原子(atomic)调用。如果系统在重命名期间崩溃,文件将被命名为旧名称或新名称,不会出现奇怪的中间状态。因此,对于支持某些需要对文件状态进行原子更新的应用程序,rename()非常重要。
 
 

获取文件信息

除了文件访问之外,我们还希望文件系统能够保存关于它正在存储的每个文件的信息,通常将这些数据称为文件元数(metadata)。要查看特定文件的元数据,可以使用stat()fstat()系统调用。
每个文件系统通常将这种类型的信息保存在一个名为inode的stat结构体中。stat结构体的详细信息如下所示:
你可以看到有关于每个文件的大量信息,包括其大小、低级名称(即inode号)、一些所有权信息以及有关何时文件被访问或修改的一些信息等等。
可以使用命令行工具stat:
 
 

删除文件

如果用过UNIX,你知道只需运行程序rm就可以删除一个文件。但是,rm使用什么系统调用来删除文件?
答案是unlink,unlink()只需要待删除文件的名称,并在成功时返回零。
 
 

创建目录

除了文件外,还可以使用一组与目录相关的系统调用来创建、读取和删除目录。请注意,你永远不能直接写入目录。因为目录的格式被视为文件系统元数据,所以只能间接更新目录,例如通过在其中创建文件、目录或其他对象类型。通过这种方式,文件系统可以确保目录的内容始终符合预期。
要创建目录,可以用系统调用mkdir()。新创建的目录被认为是“空的”,空目录有两个条目:一个引用自身的条目,一个引用其父目录的条目。前者称为“.”目录,后者称为“..”目录。
 
 

读取目录

既然我们创建了目录,也可能希望读取目录。下面是一个打印目录内容的示例程序。该程序使用了opendir()readdir()closedir()这3个调用来完成工作。我们只需使用一个简单的循环就可以一次读取一个目录条目,并打印目录中每个文件的名称和inode编号。
由于目录只有少量的信息(基本上,只是将名称映射到inode号,以及少量其他细节),程序可能需要在每个文件上调用stat()以获取每个文件的更多信息,例如长度或其他详细信息。
 

删除目录

你可以通过调用rmdir()来删除目录。然而,与删除文件不同,删除目录更加危险,因为你可以使用单个命令删除大量数据。因此,rmdir()要求该目录在被删除之前是空的(只有“.”和“..”条目)。如果你试图删除一个非空目录,那么对rmdir()的调用就会失败。
 

硬链接

我们来谈论一种在文件系统树中创建条目的新方法,即link()系统调用。link()系统调用有两个参数:一个旧路径名和一个新路径名。当你将一个新的文件名“链接”到一个旧的文件名时,实际上创建了另一种引用同一个文件的方法。命令行程序ln用于执行此操作,如下面的例子所示:
link只是在要创建链接的目录中创建了另一个名称,并将其指向原有文件的相同inode号(即低级别名称)。现在就有了两个可读的名称(file和file2),都指向同一个文件。通过打印每个文件的inode号,我们可以在目录中看到这一点:
创建一个文件时,实际上做了两件事。首先,要构建一个结构(inode),它将跟踪几乎所有关于文件的信息,包括其大小、文件块在磁盘上的位置等等。其次,将人类可读的名称链接到该文件,并将该链接放入目录中。
回到删除文件所提到的unlink()调用。当文件系统取消链接文件时,它检查inode号中的引用计数(reference count)。该引用计数(有时称为链接计数,link count)允许文件系统跟踪有多少不同的文件名已链接到这个inode。调用unlink()时,会删除人类可读的名称与给定inode号之间的“链接”,并减少引用计数。只有当引用计数达到零时,文件系统才会释放inode和相关数据块,从而真正“删除”该文件。
 

符号链接

还有一种非常有用的链接类型,称为符号链接(symbolic link),有时称为软链接(soft link)。事实表明,硬链接有点局限:你不能创建目录的硬链接(因为担心会在目录树中创建一个环)。你不能硬链接到其他磁盘分区中的文件(因为inode号在特定文件系统中是唯一的,而不是跨文件系统),等等。因此,人们创建了一种称为符号链接的新型链接。
要创建这样的链接,可以使用相同的程序ln,但需要使用-s标志。
除了表面相似之外,符号链接实际上与硬链接完全不同。第一个区别是符号链接本身实际上是一个不同类型的文件。运行ls,可以看到常规文件最左列中的第一个字符是“-”,目录是“d”,软链接是“l”。还可以看到符号链接的大小,以及链接指向的内容。
file2是4个字节,原因在于形成符号链接的方式,即将链接指向文件的路径名作为链接文件的数据
最后,由于创建符号链接的方式,有可能造成所谓的悬空引用(dangling reference)。删除名为file的原始文件会导致符号链接指向不再存在的路径名。
 
 

创建并挂载文件系统

如何从许多底层文件系统组建完整的目录树。这项任务的实现是先制作文件系统,然后挂载它们,使其内容可以访问。
为了创建一个文件系统,大多数文件系统提供了一个工具,通常名为mkfs。思路如下:作为输入,为该工具提供一个设备(例如磁盘分区,例如/dev/sda1),一种文件系统类型(例如ext3),它就在该磁盘分区上写入一个空文件系统,从根目录开始。
但是,一旦创建了这样的文件系统,就需要在统一的文件系统树中进行访问。这个任务是通过mount()程序实现的。mount的作用很简单:以现有目录作为目标挂载点(mount point),本质上是将新的文件系统粘贴到目录树的这个点上。
 
 
  • 计算机基础
  • 操作系统
  • 单机数据持久-RAID单机数据持久-文件系统实现
    目录