数据库之文件管理--SimpleDB
磁盘和文件管理
数据库引擎将其数据保存在诸如磁盘和闪存驱动器等持久性存储设备上。这里研究了这些设备的特性,并考虑了可以提高其速度和可靠性的技术(如RAID)。它还检查了操作系统为与这些设备交互提供的两个接口——块级接口和文件级接口——并提出了最适合数据库系统的两个接口的组合。最后,它详细地考虑了SimpleDB文件管理器,研究了它的API及其实现。
持久化数据存储
数据库的内容必须保持持久性,这样,当数据库系统或计算机崩溃时,数据就不会丢失。本节将介绍两种特别有用的硬件:磁盘驱动器和闪存驱动器。闪存驱动器还没有像磁盘驱动器那样广泛使用,尽管随着技术的成熟,它们的重要性将随之增加。让我们从磁盘驱动器开始。
磁盘驱动器
磁盘驱动器包含一个或多个旋转磁盘。一个磁盘有多条同心轨道,每个轨道由一系列字节组成。通过一个具有读写头的活动臂从拼盘中读取(并写入)字节。读写头位于所需的轨道上,头部可以读取(或写)在字节下旋转的字节。下图描述了一个单盘式磁盘驱动器的顶视图。当然,这个数字不是按比例绘制的,因为一个磁盘有成千上万的轨道。
现代的磁盘驱动器通常有多个磁盘。为了提高空间效率,成对的磁盘通常背靠背连接,看起来像双面的磁盘;但从概念上讲,每一边仍然是一个单独的盘。每个磁盘都有自己的读写头。这些读写头不能独立移动;它们都连接到一个单一的执行器,执行器同时将它们移动到每个盘上的同一轨道上。此外,一次只能激活一个读/写头,因为计算机只有一个数据路径。下图描述了一个多盘片磁盘驱动器的侧视图。
磁盘驱动器的一般性能可以通过四个值来测量:容量、转速、传输速率和查找时间。
驱动器的容量是可以存储的字节数。这个值取决于磁盘的数量、每个磁盘的轨道数和每个轨道的字节数。考虑到盘往往有或多或少的标准尺寸,制造商主要通过增加盘的密度来增加容量,也就是说,通过每个盘压缩更多的轨道和更多的字节。超过40GB的硬盘容量现在很常见。
旋转速度是圆盘旋转的速率,通常以每分钟转数给出。典型的速度范围:5400rpm~15,000rpm。
传输速率是字节通过磁盘读写头进出内存的速度。例如,整个轨道旋转一周时间内传输的字节数(转化为每秒读取字节数)。因此,传输速率由旋转速度和每条轨道的字节数共同决定。100MB/s的比率很常见。
查找时间是执行器将磁盘头从其当前位置移动到所请求的位置所花费的时间。这个值取决于需要遍历多少个轨道。它可以低至0(如果目标轨道与起始轨道相同),并可以高达15-20ms(如果目标轨道和起始轨道位于磁盘的不同末端)。平均寻找时间通常提供了对执行器速度的合理估计。在现代磁盘上的平均搜索时间约为5ms。
假设一个四盘式磁盘驱动器以10,000rpm旋转,平均寻道时间为5ms。每个磁盘包含10,000个轨道,每个轨道包含50万字节。这里是一些计算:
磁盘的容量:
500,000 bytes/轨道 x 10,000 轨道/盘 x 4 盘/驱动器=20,000,000,000字节,或者大约20GB
转速:
500,000 轨道/周 x 10000 周/分 /60 秒/分 =83,333,333字节/秒,大约83Mb/s
1 KB = 1024 bytes, 1 MB = 1,048,576 bytes, 1 GB = 1,073,741,824 bytes.
磁盘访问
磁盘访问是指将从磁盘驱动器读取一些字节到内存或将一些字节从内存写入磁盘的请求。这些字节必须在某个磁盘上的轨道的连续部分上。磁盘驱动器分三个阶段执行磁盘访问:
- 将磁盘头移动到指定的轨道上。这个时间被称为寻找时间。
- 等待磁盘旋转,直到第一个想要的字节在磁盘头下面。这个时间被称为旋转延迟时间。
- 当磁盘继续旋转时,它会读取出现在磁盘头下的每个字节(或写入每个字节),直到最后一个需要的字节出现。这个时间被称为传输时间。
执行磁盘访问所需的时间是查找时间、旋转延迟和传输时间的总和。每一次都受到磁盘的机械运动的限制。机械运动明显慢于电子运动,这就是为什么磁盘驱动器比RAM慢得多。寻找时间和旋转延迟不能避免。这两次操作只不过是每个磁盘操作都被迫等待的开销。由于精确时间难以计算,一般用平均时间代替其所求时间。平均旋转延迟是旋转时间的一半。传输时间也很容易从传输速率中计算出来。特别是,如果传输速率是r字节/秒,而假设正在传输b字节,则传输时间为b/r秒。
假设某磁盘驱动器以10,000rpm旋转,平均寻路时间为5ms,传输速率为83MB/s。以下是一些计算:
平均旋转延迟时间:
60 seconds/minute x 1 minute/10,000 revolutions x ½ revolution= 0.003 seconds or 3 ms
1个字节传输时间:
1 byte x 1 second/83,000,000 bytes= 0.000000012 seconds or 0.000012 ms
1000个字节传输时间:
1,000 bytes x 1 second/83,000,000 bytes= 0.000012 seconds or 0.012 ms
1个字节读写时间:
5 ms (seek) + 3 ms (rotational delay) + 0.000012 ms (transfer)= 8.000012 ms
1000个字节读写时间:
5 ms (seek) + 3 ms (rotational delay) + 0.012 ms (transfer)= 8.012 ms
现代磁盘的构建使得每个轨道被划分为固定长度的扇区;磁盘的读(或写)必须一次在整个扇区上操作。扇区的大小可以由磁盘制造商决定,也可以在磁盘格式化时选择扇区。一个典型的扇区大小是512字节。
改善磁盘访问时间
由于磁盘驱动器非常慢,因此已经开发了几种技术来帮助改善访问时间。本节将考虑三种技术:磁盘缓存、圆柱体和磁盘条带化。
磁盘缓存
磁盘缓存是与磁盘驱动器捆绑的内存,通常足够大,可以存储数千个扇区的内容。每当磁盘驱动器从磁盘中读取一个扇区时,它就会将该扇区的内容保存在其缓存中;如果缓存已满,则新扇区将替换旧扇区。当请求一个扇区时,磁盘驱动器会检查高速缓存。如果该扇区恰好在缓存中,它可以立即返回到计算机,而没有实际的磁盘访问。
假设一个应用程序在相对较短的时间内多次请求同一个扇区。第一个请求将把该扇区带入缓存中,随后的请求将从缓存中检索该扇区,从而节省了磁盘访问。但是,这个特性对于数据库引擎并不是特别有用,因为它已经在执行自己的缓存。如果一个扇区被请求多次,引擎就会在自己的缓存中找到这个扇区,甚至不用去访问磁盘。
磁盘缓存的真正价值是它具有预取扇区的能力。磁盘驱动器不能只读取一个请求的扇区,而是可以将包含该扇区的整个轨道读取到缓存中,希望以后会请求该轨道的其他扇区。关键是,阅读整条轨道并不比阅读单个扇区更耗时。特别是,没有旋转延迟,因为磁盘可以从读/写头下的任何扇区开始读取轨道,并在整个旋转过程中继续读取。比较访问时间:
Time to read a sector(单分区读取时间) = seek time(寻道时间) + ½ rotation time(旋转延迟时间) + sector rotation time(分区旋转时间)
Time to read a track(单轨道读取设计) = seek time + rotation time
也就是说,读取单个扇区和一个满扇区的轨道之间的差异小于磁盘旋转时间的一半。如果数据库引擎碰巧只请求轨道上的另一个扇区,那么将整个轨道读入缓存将节省时间。
圆柱化存储
数据库系统可以通过相邻扇区存储相关信息来提高磁盘访问时间。例如,存储文件的理想方法是将其内容放在磁盘的同一轨道上。如果磁盘进行基于跟踪的缓存,这种策略显然是最好的,因为整个文件将在单个磁盘访问中被读取。但是,即使没有缓存,这种策略也很好,因为它消除了查找时间——每次读取另一个扇区时,磁盘头将已经位于正确的轨道上。
假设一个文件占用了多个轨道。一个好主意是把它的内容存储在磁盘的相邻轨道上,这样轨道之间的寻找时间尽可能小。然而,一个更好的想法是将其内容存储在其他磁盘的相同轨道上。由于每个盘的读写头一起移动,所有的轨道有相同的轨道号可以访问没有任何额外的寻找时间。
具有相同轨道号的一组轨道被称为圆柱体,因为如果你从磁盘的顶部查看这些轨道,它们描述了一个圆柱体的外部。实际上,一个圆柱体可以被视为一个非常大的轨道,因为它的所有扇区都可以通过任何额外的请求来访问。
磁盘条带
另一种提高磁盘访问时间的方法是使用多个磁盘驱动器。两个小驱动器比一个大驱动器快,因为它们包含两个独立的执行器,因此可以同时响应两个不同的扇区请求。例如,两个连续工作的20GB磁盘的速度大约是一个40GB磁盘的两倍。这种加速速度很好:一般来说,N个磁盘的速度大约是单个磁盘的N倍。(当然,几个较小的驱动器也比一个较大的驱动器更贵,所以增加的效率是有代价的。)
但是,如果多个小磁盘不能保持忙碌,那么它们的效率将会降低。例如,假设一个磁盘包含经常使用的文件,而另一个磁盘包含很少使用的归档文件。然后第一个磁盘将完成所有的工作,而其他磁盘大部分时间处于空闲状态。这种设置与单个磁盘的效率差不多。
所以问题是如何平衡多个磁盘之间的工作负载。数据库管理员可以尝试分析文件的使用情况,以便最好地将文件分布在每个磁盘上,但这种方法并不实用:很难做到,很难保证,而且必须随着时间的推移不断地重新评估和修改。幸运的是,有一种更好的方法,被称为磁盘条带化。
磁盘条带策略使用一个控制器来隐藏较小的磁盘从操作系统,给它一个大磁盘的错觉。控制器将虚拟磁盘上的扇区请求映射到实际磁盘上的扇区请求。该映射的工作原理如下。假设有N个小磁盘,每个小磁盘都有k个扇区。虚拟磁盘将有Nk个扇区;这些扇区以交替的模式被分配给真实磁盘的扇区。磁盘0将包含虚拟扇区0、N、2N等。磁盘1将包含虚拟扇区1、N+1、2N+1等,等等。术语磁盘条带来自以下图像:如果你想象每个小磁盘被涂成不同的颜色,那么虚拟磁盘看起来像有条纹,它的扇区被画成交替的颜色。如下图所示:
磁盘条带化是有效的,因为它在小磁盘中平等地分布数据库。如果一个请求到达一个随机扇区,则该请求将以相等的概率发送到其中一个小磁盘。如果多个请求到达连续的扇区,它们将被发送到不同的磁盘。因此,可以保证磁盘尽可能一致地工作。
通过镜像来提高磁盘的可靠性
数据库的用户希望他们的数据将在磁盘上保持安全,并且不会丢失或损坏。不幸的是,磁盘驱动器并不完全可靠。磁盘上的磁性材料会退化,导致扇区变得不可读。或者一块灰尘或一个不和谐的运动可能会导致一个读写头刮到一个盘子上,破坏受影响的区域(一个“头部碰撞”)。
防止磁盘故障的最明显的方法是保留磁盘内容的副本。例如,可以对该磁盘进行夜间备份;当磁盘发生故障时,只需购买一个新磁盘并将备份复制到上面。此策略的问题是,丢失了从备份磁盘到失败之间发生的对磁盘的所有更改。解决这个问题的唯一方法是在发生时将每个更改复制到磁盘。换句话说,需要保留两个相同的磁盘版本;这些版本被称为彼此的镜像。
与条带化一样,需要一个控制器来管理这两个镜像磁盘。当数据库系统请求读取磁盘时,控制器可以访问任一个磁盘的指定扇区。当请求磁盘写入时,控制器将对两个磁盘执行相同的写入操作。理论上,这两个磁盘写入可以并行执行,这将不需要额外的时间。然而,在实践中,按顺序编写镜像以防止系统崩溃是很重要的。问题是,如果系统在磁盘写入的中间崩溃,该扇区的内容就会丢失。因此,如果两个镜像都是并行写入的,那么扇区的两个副本可能会丢失,而如果镜像是按顺序写入的,那么至少有一个镜像将没有损坏。
假设一个镜像对中的一个磁盘失败了。数据库管理员可以通过执行以下步骤来恢复系统:
- 关闭系统
- 用一个新的磁盘替换出现故障的磁盘
- 将数据从好的磁盘复制到新的磁盘上
- 重启系统
不幸的是,这个过程并不是万无一失的。如果好的磁盘在复制到新磁盘时发生故障,数据仍然会丢失。两个磁盘在几小时内发生故障的几率很小(现在的磁盘发生故障的几率约为六万分之一),但如果数据库很重要,那么这个小风险可能是不可接受的。可以通过使用三个镜像磁盘而不是两个磁盘来降低风险。在这种情况下,只有当三个磁盘在同一几个小时内出现故障时,数据才会丢失;这种可能性,虽然非零,但如此遥远,可以轻松地忽略。
镜像可以与磁盘条带化共存。一种常见的策略是镜像条带式磁盘。例如,一个人可以在4个20GB的驱动器上存储40GB的数据:其中两个驱动器将是条带化的,另外两个将是条带化驱动器的镜像。这样的配置既快速又可靠。如下图所示:
通过存储奇偶校验提高硬盘可靠性
镜像的缺点是,它需要两倍数量的磁盘来存储相同数量的数据。当使用磁盘条带化时,这种负担尤其明显——如果想使用15个20GB的驱动器来存储300GB的数据,那么将需要再购买另外15个驱动器来作为它们的镜像。对于大型数据库安装来说,通过剥离许多小磁盘来创建一个巨大的虚拟磁盘并不罕见,而购买相同数量的磁盘只是作为镜像的前景也没有吸引力。如果不使用这么多镜像磁盘就能从出现故障的磁盘中恢复,那就好了。
事实上,有一种聪明的方法可以使用单个磁盘来备份任意数量的其他磁盘。该策略的工作原理是通过在备份磁盘上存储奇偶校验信息。奇偶性定义为S组位,如下:
- 如果S包含奇数个1,那么它的奇偶性是1。
- 如果S包含偶数个1,则S的奇偶性为0。
换句话说,如果将奇偶校验位添加到S,则总是有偶数1。
奇偶校验具有以下有趣而重要的属性:任何位的值都可以从其他位的值中确定,只要知道奇偶校验。例如,假设S={1,0,1}。S的奇偶性是0,因为它的1的个数为偶数。假设失去了第一位的值。因为奇偶校验为0,所以集合{?,0,1}必须有偶数个1;因此,可以推断缺失的位必须是1。可以对其他每个位(包括奇偶校验位)进行类似的推论。
这种奇偶校验的使用可以扩展到磁盘。假设有N+1个大小相同的磁盘。选择其中一个磁盘作为奇偶校验磁盘,并让其他N个磁盘保存条带数据。奇偶校验磁盘的每个位都是通过计算所有其他磁盘对应的奇偶校验位来计算的。如果任何磁盘出现故障(包括奇偶校验磁盘),则可以通过逐位查看其他磁盘的内容来重建该磁盘的内容。如下图所示:
这些磁盘由一个控制器进行管理。读和写请求的处理基本上与条带化相同——控制器决定哪个磁盘持有所请求的扇区并执行该读/写操作。不同之处在于,写请求还必须更新奇偶校验磁盘的相应扇区。控制器可以通过确定修改后的扇区的哪些位改变来计算更新的奇偶校验;规则是,如果一个位改变了,那么相应的奇偶校验位也必须改变。因此,控制器需要四个磁盘访问来实现扇区写入操作:它必须读取扇区和相应的奇偶校验扇区(以便计算新的奇偶校验位),并且它必须写入两个扇区的新内容。
这种对奇偶校验信息的使用有点神奇,因为一个磁盘能够可靠地备份任意数量的其他磁盘。然而,这种策略也伴随着两个缺点。
使用奇偶校验的第一个缺点是扇区写操作更耗时,因为它需要从两个磁盘进行读和写。经验表明,使用奇偶性可降低条带化的效率约20%。
奇偶校验的第二个缺点是,数据库更容易受到不可恢复的多磁盘故障的影响。考虑一下当一个磁盘发生故障时会发生什么——需要所有其他磁盘来重建故障磁盘,其中任何一个磁盘的故障都是灾难性的。如果数据库由许多小磁盘组成(例如,大约100个),那么第二次故障的可能性就会变得非常现实。与镜像的情况相比,在镜像中,从故障磁盘恢复只需要镜像不失败,这种可能性要小得多。
RAID(磁盘冗余阵列)
前面几节考虑了使用多个磁盘的三种方法:条带化以加快磁盘访问时间,以及镜像和奇偶校验以防止磁盘故障。这些策略使用一个控制器来对操作系统隐藏多个磁盘的存在,并提供单个虚拟磁盘的错觉。控制器将每个虚拟读/写操作映射到底层磁盘上的一个或多个操作。该控制器可以在软件或硬件中实现,尽管硬件控制器更为广泛。
这些策略是被称为RAID的一个更大的策略集合的一部分,RAID代表廉价磁盘的冗余阵列。有7个RAID级别:
- RAID-0是条带化的,没有任何防止磁盘故障。如果其中一个条带磁盘发生故障,那么整个数据库可能会被破坏。
- RAID-1镜像条带化。
- RAID-2使用位条,而不是扇区条,以及基于纠错码而不是奇偶校验的冗余机制。这一战略很难实施,而且表现不佳。它已经不再被使用了。
- RAID-3和RAID-4使用条带化和奇偶校验。它们的不同之处在于,RAID-3使用字节条带,而RAID-4使用扇区条带。一般来说,扇区条带化往往更有效,因为它对应于磁盘访问的单位。
- RAID-5与RAID-4类似,只是没有将所有奇偶校验信息存储在单独的磁盘上,而是将奇偶校验信息分布在数据磁盘上。也就是说,如果有N个数据磁盘,那么每个磁盘的每N个扇区都包含奇偶校验信息。这种策略比RAID-4更有效,因为不再有一个奇偶校验磁盘成为瓶颈。
- RAID-6与RAID-5相似,只是它保留了两种奇偶校验信息。因此,该策略能够处理两个并发磁盘故障,但需要另一个磁盘来保存额外的奇偶校验信息。
最流行的两个RAID级别是RAID-1和RAID-5。它们之间的选择实际上是镜像和平价。在数据库安装中,镜像往往是更可靠的选择,首先是因为它的速度和健壮性,其次是因为附加磁盘驱动器的成本已经变得如此之低。
闪存驱动器
磁盘驱动器在当前的数据库系统中很常见,但它们有一个不可克服的缺点——它们的操作完全依赖于旋转磁盘和移动执行器的机械活动。这一缺陷使得磁盘驱动器与电子内存相比天生就较慢,而且也容易受到跌落、振动和其他冲击的损坏。
闪存是一项较新的技术,它有可能取代磁盘驱动器。它使用了半导体技术,类似于RAM,但不需要不间断的电源。因为它的活动完全是电的,它可以比磁盘驱动器更快地访问数据,而且没有移动部件被损坏。
闪存驱动器目前的查找时间约为50微秒,大约比磁盘驱动器快100倍。电流闪存驱动器的传输速率取决于它所连接到的总线接口。通过快速内部总线连接的闪存驱动器与磁盘驱动器相当;但是,外部USB闪存驱动器比磁盘驱动器慢。
闪存磨损。每个字节可以重写一个固定的次数;试图写入一个已经达到其限制的字节将导致闪存驱动器失效。目前,这个最大值在数百万个以内,这对于大多数数据库应用程序来说是相当高的。高端驱动器采用“磨损调平”技术,自动将频繁写入的字节移动到写入较少的位置;这种技术允许驱动器操作,直到驱动器上的所有字节达到重写限制。
闪存驱动器为操作系统提供了一个基于扇区的接口,这使闪存驱动器看起来像一个磁盘驱动器。在闪存驱动器中使用RAID技术是可能的,尽管条带化不那么重要,因为闪存驱动器的查找时间很低。
采用闪存盘的主要障碍是它的价格。目前的价格大约是一个类似的磁盘驱动器价格的100倍。尽管闪存和磁盘技术的价格都将继续下降,但最终闪存驱动器将足够便宜,可以被视为主流。此时,磁盘驱动器可能会降级为归档存储和超大数据库的存储。
闪存也可以通过作为一个持久的前端来增强磁盘驱动器。如果数据库完全适合于闪存中,那么磁盘驱动器将永远不会被使用。但是随着数据库的增大,使用频率较少的扇区将迁移到磁盘。
就数据库引擎而言,闪存驱动器与磁盘驱动器具有相同的属性:它是持久的、缓慢的和在扇区中访问的。(它只是碰巧比一个磁盘驱动器要慢一些。)
到磁盘的块级接口
磁盘可能具有不同的硬件特性——例如,它们不需要具有相同的扇区大小,而且它们的扇区可能以不同的方式进行处理。操作系统负责隐藏这些(和其他)详细信息,为其应用程序提供一个访问磁盘的简单接口。
块的概念是这个接口的核心。块与扇区相似,只是它的大小由操作系统决定的。每个块对所有磁盘都具有相同的固定大小。操作系统维护块和扇区之间的映射。操作系统还为磁盘的每个块分配一个块号;给定一个块号,操作系统确定实际的扇区地址。
不能从磁盘直接访问块的内容。相反,组成块的扇区必须首先读入内存页面并从那里访问。要修改块的内容,客户端必须将块读入内存页面,修改内存页面中的字节,然后将页内存面写回磁盘上的块。一个操作系统通常提供了几种方法来访问磁盘块,例如:
- readblock(n,p)将磁盘块n处的字节读入内存的页p中。
- writeblock(n,p)将字节写入内存的第p页以阻止磁盘的n。
- allocate(k,n)在磁盘上查找k个连续的未使用块,将它们标记为已使用,并返回第一个块的块号。新的块应尽可能靠近块n。
- deallocate(k,n)将以块n开始的k个连续块标记为未使用的块。
操作系统会跟踪磁盘上哪些块可供分配,哪些块不能分配。它可以采用两种基本策略:磁盘映射或自由列表。
磁盘映射是一个位序列,磁盘上的每个块对应一个位。位值为1表示块是空闲的,而0表示块已被分配。磁盘映射存储在磁盘上,通常在其前几个块中。操作系统可以通过简单地将磁盘映射的位n更改为1来释放块n。它可以通过搜索值为1的磁盘映射中的k个位,然后将这些位设置为0来分配k个连续的块。
自由列表是一个块链,其中块是未分配块的连续序列。每个块的第一个块存储两个值:该块的长度和链上的下一个块的块号。磁盘的第一个块包含一个指向链上的第一个块的指针。当操作系统被要求分配k个连续的块时,它会在自由列表中搜索一个足够大的块。然后,它可以选择从自由列表中删除整个块并分配它,或者分割一个长度为k的块,只分配这些块。当被要求释放一块块时,操作系统只需将其插入到空闲列表中。
下图说明了已分配了块0、1、3、4、8和9的磁盘的这两种技术。(a)部分显示存储在磁盘块0中的磁盘映射;位值0表示已分配的块。(b)部分显示了相应的自由列表。块0包含值2,这意味着自由列表的第一个块从块2开始。块2包含两个值1和5,表示块包含1个块,并且下一个块从块5开始。类似地,块5的内容表示其块为3个块长,并且下一个块位于块10处。块10的值表示它是最后一个块,它包含所有剩余的块。
自由列表技术需要最小的额外空间;您所需要的只是在块0中存储一个整数,以指向列表中的第一个块。另一方面,磁盘映射技术需要空间来保存映射。上图(a)假设映射可以适合于单个块中。但是,一般来说,可能需要几个区块。磁盘映射的优点是,它可以让操作系统更好地了解磁盘中的“孔”的位置。例如,如果操作系统需要支持一次分配多个块,那么磁盘映射通常是可选择的策略。
到磁盘的文件级接口
操作系统为磁盘提供了另一个更高级别的接口,称为文件系统。客户端将文件视为已命名的字节序列。在这个级别上没有块的概念。相反,客户端可以从文件中的任何位置开始读取(或写)任意数量的字节。
RandomAccessFile Java类为文件系统提供了一个典型的API。每个RandomAccessFile对象都包含一个文件指针,该文件指针指示下一个读或写操作将发生的字节。这个文件指针可以通过调用来查找来显式地设置。通过调用readInt(或写writeInt)方法也会将文件指针移过它读取(或写取)的整数。
如下代码片段所示,它增加了在文件“junk”的7992-7995字节处存储的整数。调用readInt方法读取在字节7992处的整数,并将文件指针经过它,移到字节7996。随后的查找调用将文件指针设置回字节7992,以便可以覆盖该位置上的整数。
RandomAccessFile f = new RandomAccessFile("junk", "rws");
f.seek(7992);
int n = f.readInt();
f.seek(7992);
f.writeInt(n+1);
f.close();
对readInt和writeInt方法的调用就像直接访问磁盘一样,隐藏了必须通过内存页面访问磁盘块的事实。一个操作系统通常会保留几页内存供自己使用;这些页面被称为I/O缓冲区。当文件被打开时,操作系统为文件分配一个I/O缓冲区,而客户端不知道。
文件级接口允许将文件视为一系列块。例如,如果块长4096字节(即4Kb),则字节7992位于文件的块1(即它的第二块)。像“文件的块1”这样的块引用被称为逻辑块引用,因为它们告诉我们块相对于文件的位置,而不是块在磁盘上的位置。
给定特定的文件位置,查找(seek)方法确定保存该位置的实际磁盘块。特别是,seek执行两种转换:
- 它将指定的字节位置转换为逻辑块引用。
- 它将逻辑块引用转换为物理块引用。
第一个转换很容易——逻辑块号只是字节位置除以块大小。例如,假设有个4kb的块,则字节7992在块1中,因为7992/4096=1(整数除法)。
第二个转换比较困难,这取决于文件系统是如何实现。本节的其余部分将考虑三种文件实现策略:连续分配、基于范围的分配和索引分配。这三种策略都将有关文件位置的信息存储在文件系统目录中。查找方法在将逻辑块引用转换为物理块引用时,将访问此目录的块。您可以将这些磁盘访问看作是文件系统所施加的一个隐藏的“开销”。操作系统试图最小化这个开销,但它们不能消除它。
连续分配
连续分配是最简单的策略,将每个文件存储为连续块序列。为了实现连续分配,文件系统目录保存每个文件的长度及其第一个块的位置。将逻辑映射到物理块引用很容易——如果文件从磁盘块b开始,那么文件的块N位于磁盘块b+N中。下表描述了一个包含两个文件的文件系统的目录:一个从块32开始的名为“junk”的48块长的文件,另一个从块80开始的16块长的名为“temp”文件。
名字 | 开始块 | 长度 |
---|---|---|
junk | 32 | 48 |
temp | 80 | 16 |
连续分配有两个问题。第一个问题是,如果文件后面有另一个文件,则不能扩展。因此,客户端必须使用他们可能需要的最大块数来创建他们的文件,这将导致在文件不满时浪费空间。这个问题被称为内部碎片化。第二个问题是,当磁盘变满时,它可能会有许多未分配块的小块,但没有大块。因此,即使磁盘包含大量的空闲空间,也可能不可能创建一个大文件。这个问题被称为外部碎片化。换句话说:
- 内部碎片是文件内部浪费的空间。
- 外部碎片是浪费的空间在所有文件之外。
基于范围的分配
基于范围的分配策略是连续分配的变化,减少了内部和外部碎片。在这里,操作系统将一个文件存储为一个固定长度的区段序列,其中每个区段都是一个连续的块。一个文件一次只扩展一个范围。对于每个文件,此策略的文件系统目录都包含了每个文件范围的第一个块的列表。
例如,假设操作系统将文件存储在8块块中。下标描述了两个文件“junk”和“temp”的文件系统目录。这些文件的大小与以前相同,但现在被分成多个区段。文件“junk”有6个区段,而文件“temp”有两个区段。
名字 | 范围 |
---|---|
junk | 32, 480, 696, 72, 528, 336 |
temp | 64, 8 |
要查找保存文件块N的磁盘块,查找方法在文件系统目录中搜索该文件的范围列表;然后搜索范围列表以确定包含块N的范围,从中可以计算块的位置。例如,上表中的文件目录。文件"junk”的块21的位置可以计算如下:
- 块21位于文件的范围2中,因为21/8=2(整数除法)。
- 范围2从文件的逻辑块2 x 8 =16开始。
- 所以区块21在区块21-16=5中。
- 文件的扩展列表显示,扩展范围2从物理块696开始。
- 因此,块21的位置是696+5=701。
基于范围的分配减少了内部碎片,因为文件浪费的空间不能超过一个范围的价值。外部碎片则被消除了,因为所有区段的大小都相同。
索引分配
索引分配采用了一种不同的方法——它甚至不尝试以连续的块来分配文件。相反,文件的每个块都是单独分配的(如果愿意的话,以一个块长的范围分配)。操作系统通过为每个文件分配一个特殊的索引块来实现这个策略,该块跟踪分配给该文件的磁盘块。也就是说,一个索引块ib可以被认为是一个整数数组,其中ib[N]的值是包含该文件的逻辑块N的磁盘块。因此,计算任何逻辑块的位置都是很简单的——只需在索引块中查找它。
下图图a给出了两个文件“junk”和“temp”的文件系统目录。“junk”文件的索引块是第34块。图b给出了该块中的前几个整数。从这个图中,很容易看到文件“junk”的块1位于磁盘的块103处。
这种方法的优点是一次分配一个块,因此不存在碎片化。它的主要问题是,文件将有一个最大的大小,因为它们的块只能达到索引块中有值的数量。UNIX文件系统通过支持多个级别的索引块来解决这个问题,从而允许最大的文件大小非常大。
数据库系统和操作系统
操作系统为磁盘访问提供了两个级别的支持:块级支持和文件提升支持。数据库引擎的实现者应该选择哪种级别?
选择使用块级支持的优点是可以让引擎完全控制哪些磁盘块用于什么目的。例如,经常使用的块可以存储在磁盘的中间,在那里查找时间将会更少。类似地,倾向于一起访问的块可以存储在彼此附近。另一个优点是,数据库引擎不受文件上的操作系统限制的限制,允许它支持大于操作系统限制或跨多个磁盘驱动器的表。
另一方面,使用块级接口有几个缺点:这种策略实现起来很复杂;它要求将磁盘格式化并装载为原始磁盘,即块不是文件系统一部分的磁盘;它要求数据库管理员对块访问模式有广泛的知识,以便对系统进行微调。
另一个极端是,数据库引擎要尽可能多地使用操作系统文件系统。例如,每个表都可以存储在一个单独的文件中,引擎将使用文件级操作访问记录。这种策略更容易实现,并且它允许操作系统从数据库系统中隐藏实际的磁盘访问。这种情况是不可接受的,原因有二。首先,数据库系统需要知道块的边界在哪里,以便它能够有效地组织和检索数据。其次,数据库系统需要管理自己的页面,因为管理I/O缓冲区的操作系统方式不适用于数据库查询。
一种折衷的策略是,数据库系统将其所有数据存储在一个或多个操作系统文件中,但要将这些文件当作原始磁盘来处理。也就是说,数据库系统使用逻辑文件块来访问它的“磁盘”。操作系统负责通过查找方法将每个逻辑块引用映射到其相应的物理块。因为搜索程序在检查文件系统目录时可能会导致磁盘访问,所以数据库系统将不能完全控制该磁盘。然而,与数据库系统访问的大量块相比,这些附加块通常微不足道的。因此,数据库系统能够使用到操作系统的高级接口,同时保持对磁盘访问的重要控制。
这种妥协策略在许多数据库系统中都被使用。微软Access将所有内容保存在一个.mdb文件中,而Oracle、Derby和SimpleDB使用多个文件。
SimpleDB文件管理器
与操作系统交互的数据库引擎的那部分称为文件管理器。本节将实现SimpleDB的文件管理器。
使用文件管理器
一个SimpleDB数据库存储在多个文件中。每个表和每个索引都有一个文件,还有一个日志文件和几个目录文件。SimpleDB文件管理器通过simpledb.file提供了对这些文件的块级访问。这个包公开了三个类:BlockId、Page和FileMgr。它们的API如下图所示:
BlockId
public BlockId(String filename, int blknum);
public String filename();
public int number();
Page
public Page(int blocksize);
public Page(byte[] b);
public int getInt(int offset);
public byte[] getBytes(int offset);
public String getString(int offset);
public void setInt(int offset, int val);
public void setBytes(int offset, byte[] val);
public void setString(int offset, String val);
public int maxLength(int strlen);
FileMgr
public FileMgr(String dbDirectory, int blocksize);
public void read(BlockId blk, Page p);
public void write(BlockId blk, Page p);
public BlockId append(String filename);
public boolean isNew();
public int length(String filename);
public int blockSize();
BlockId对象通过其文件名和逻辑块号来标识特定的块。例如,语句"BlockId blk = new BlockId(“student.tbl”, 23)",创建一个对student.tbl文件的块23的引用。方法的filename和number返回其文件名和块号。
页面(Page)对象保存磁盘块的内容。它的第一个构造函数创建一个页面,从操作系统的I/O缓冲区获取其内存;这个构造函数由缓冲区管理器使用。它的第二个构造函数创建一个页面,从Java数组获取内存;这个构造函数主要由日志管理器使用。各种get和set方法允许客户端在页面的指定位置存储或访问值。一个页面可以包含三种值类型:int、String和“blobs”(即,任意的字节数组)。如果需要,可以添加其他类型的相应方法。客户端可以在页面的任何偏移量处存储一个值,但它负责知道其中存储了哪些值。试图从错误的偏移量中获取一个值将会产生不可预测的结果。
FileMgr类处理与操作系统文件系统的实际交互。它的构造函数包含两个参数:一个表示数据库名称的字符串和一个表示每个块的大小的整数。数据库名称用作包含该数据库文件的文件夹的名称;此文件夹位于引擎的当前目录中。如果不存在这样的文件夹,则将为新数据库创建一个文件夹。在这种情况下,isNew方法返回true,否则将返回false。此方法是正确初始化新数据库所必需的。
读(read)方法会将指定块的内容读取到指定的内存页面中。写(write)方法执行逆操作,将内存页面的内容写入指定的块。长度方法返回指定文件中的块数。
SimpleDB引擎有一个FileMgr对象,它是在数据库系统启动时创建的。由SimpleDB类(在包simpeledb.server中)创建该对象,其方法fileMgr返回所创建的对象。
如下代码块所示的类文件测试说明了这些方法的使用。这段代码有三个部分。第一部分初始化SimpleDB对象;这三个参数指定引擎应该使用名为“filetest”的数据库,使用400字节的块和8个缓冲区池。400字节的块大小是SimpleDB的默认块大小。它可以被人为地减小,因此可以轻松地创建具有大量块的演示数据库。在商业数据库系统中,此值将被设置为由操作系统定义的块大小;典型的块大小为4K字节。
import simpledb.file.BlockId;
import simpledb.file.FileMgr;
import simpledb.file.Page;
import simpledb.server.SimpleDB;
public class FileTest {
public static void main(String[] args) {
SimpleDB db = new SimpleDB("filetest", 400, 8);
FileMgr fm = db.fileMgr();
BlockId blk = new BlockId("testfile", 2);
Page p1 = new Page(fm.blockSize());
int pos1 = 88;
p1.setString(pos1, "abcdefghijklm");
int size = Page.maxLength("abcdefghijklm".length());
int pos2 = pos1 + size;
p1.setInt(pos2, 345);
fm.write(blk, p1);
Page p2 = new Page(fm.blockSize());
fm.read(blk, p2);
System.out.println("offset " + pos2 + " contains " + p2.getInt(pos2));
System.out.println("offset " + pos1 + " contains " + p2.getString(pos1));
}
}
上述代码块的第二部分写入字符串“数据库测试文件”的位置为文件“测试文件”的第二块的88位。然后它调用maxLength方法来确定字符串的最大长度,这样它就可以确定字符串后面的位置。然后,它将整数345写入该位置。第三部分将这个块读取到另一个页面中,并从中提取这两个值。
实现文件管理器
BlockId类
BlockId类的代码如下方代码块所示。除了简单地实现方法、文件名和数字外,该类还实现了equals、hashCode和toString方法。
public class BlockId {
private String filename;
private int blknum;
public BlockId(String filename, int blknum) {
this.filename = filename;
this.blknum = blknum;
}
/**
* 获取文件名称方法
* @return 文件名称
*/
public String fileName() {
return filename;
}
/**
* 获取块数量方法
* @return 块数量
*/
public int number() {
return blknum;
}
public boolean equals(Object obj) {
BlockId blk = (BlockId) obj;
return filename.equals(blk.filename) && blknum == blk.blknum;
}
public String toString() {
return "[file " + filename + ", block " + blknum + "]";
}
public int hashCode() {
return toString().hashCode();
}
}
Page类
实现类页面的代码见下方代码块。每个页面都是使用一个Java字节缓冲(ByteBuffer)对象来实现的。ByteBuffer对象将字节数组封装有在数组任意位置读取和写入值的方。。这些值可以是基本值(如整数),也可以是较小的字节数组。例如,Page的setInt方法通过调用ByteBuffer的putInt方法来在页面中保存一个整数。Page的setBytes方法将一个blob(二进制大对象)保存为两个值:首先是指定blob(二进制大对象)中的字节数,然后是字节本身。它调用ByteBuffer的putInt方法来写入整数,并调用put的方法来写入字节。
public class Page {
private ByteBuffer bb;
public static final Charset CHARSET = StandardCharsets.UTF_8;//StandardCharsets.US_ASCII;
// A constructor for creating data buffers(Page类的构造方法--用于创建指定大小的数据缓冲区)
public Page(int blocksize) {
bb = ByteBuffer.allocateDirect(blocksize);
}
// A constructor for creating log pages(用来创建日志对象的构造方法)
public Page(byte[] b) {
bb = ByteBuffer.wrap(b);
}
/**
* 获取指定偏移位置的整数
* @param offset 偏移位置
* @return 对应位置的整数
*/
public int getInt(int offset) {
return bb.getInt(offset);
}
/**
* 在指定偏移位置写入整数(对应编码,而不是字符整数)
* @param offset 偏移位置
* @param n 要写入的整数
*/
public void setInt(int offset, int n) {
bb.putInt(offset, n);
}
/**
* 获取指定偏移位置的字节数组
* @param offset 偏移位置
* @return 字节数组
*/
public byte[] getBytes(int offset) {
bb.position(offset);
int length = bb.getInt();
byte[] b = new byte[length];
bb.get(b);
return b;
}
/**
* 在指定偏移位置写入字节数组
* @param offset 偏移位置
* @param b 字节数组
*/
public void setBytes(int offset, byte[] b) {
bb.position(offset);
bb.putInt(b.length);
bb.put(b);
}
/**
* 获取指定偏移位置的字符串
* @param offset 偏移位置
* @return 字符串
*/
public String getString(int offset) {
byte[] b = getBytes(offset);
return new String(b, CHARSET);
}
/**
* 在指定偏移位置写入字符串
* @param offset 偏移位置
* @param s 字符串
*/
public void setString(int offset, String s) {
byte[] b = s.getBytes(CHARSET);
setBytes(offset, b);
}
/**
* 计算具有指定编码及字符数的二进制大对象(blob)的最大大小
* @param strlen 字符数
* @return
*/
public static int maxLength(int strlen) {
float bytesPerChar = CHARSET.newEncoder().maxBytesPerChar();
return Integer.BYTES + (strlen * (int)bytesPerChar);
}
// a package private method, needed by FileMgr(包的私有方法,FileMgr调用)
ByteBuffer contents() {
bb.position(0);
return bb;
}
}
ByteBuffer类没有读写字符串的方法,因此Page选择以blob形式(字节数组)写入字符串。Java字符串类有一个getBytes方法,它将字符串转换为字节数组;它还有一个构造函数,可以将字节数组转换回字符串。因此,Page的setString方法调用getBytes方法,将字符串转换为字节数组,然后将这些字节写入为一个blob(二进制大对象)。类似地,Page的getString方法从字节缓冲区(ByteBuffer)中读取一个blob(二进制大对象),然后将这些字节转换为一个字符串。
字符串与其字节表示之间的转换由字符编码决定。存在一些标准编码,如ASCII和Unicode-16。Java字符集类包含实现许多这些编码的对象。字符串的构造函数及其getBytes的方法采用一个字符集参数。在上述代码块中,可以看到Page使用了UTF-8编码,但是可以更改字符集常数,以获得对应偏好的编码。
字符集可选择每个字符编码到的字节数。ASCII每个字符使用一个字节,而Unicode-16每个字符使用2个字节到4个字节。因此,数据库引擎可能不知道给定字符串将编码到多少个字节。Page的maxLength方法计算具有指定编码及字符数的二进制大对象(blob)的最大大小。它将字符数乘以每个字符编码的最大字节数,再加上4个字节,表示用这些字节写的整数。
位于字节缓冲区(ByteBuffer)对象里面的字节数组既可以来自Java数组,也可以来自操作系统的I/O缓冲区。页面(Page)类有两个构造函数,每个构造函数对应于不同类型的底层字节数组。由于I/O缓冲区是一种有价值的资源,因此第一个构造函数的使用将由缓冲区管理器仔细控制。数据库引擎的其他组件(如日志管理器)使用另一个构造函数。
FileMgr类
FileMgr类的代码如下代码块所示。它的主要工作是实现将页面读写到磁盘块的方法。它的读取(reed)方法寻求在指定文件中的适当位置,并将该块的内容读取到指定页面的字节缓冲区中。写(write)的方法也很类似。追加(append)方法寻求到该文件的结尾,并向其写入一个空的字节数组,这将导致操作系统自动扩展该文件。注意文件管理器总是从文件中读取或写入特定块大小的字节数,并且总是在块边界处。在此过程中,文件管理器确保每个要读、写或追加方法的调用都将只产生一次磁盘访问。
public class FileMgr {
private File dbDirectory;
private int blocksize;
private boolean isNew;
private Map<String, RandomAccessFile> openFiles = new HashMap<>();
/**
*指定文件路径与内存块大小的构造方法
* @param dbDirectory 文件路径
* @param blocksize 内存块大小
*/
public FileMgr(File dbDirectory, int blocksize) {
this.dbDirectory = dbDirectory;
this.blocksize = blocksize;
isNew = !dbDirectory.exists();
// create the directory if the database is new(如果该文件路径不存在,则创建该路径)
if (isNew)
dbDirectory.mkdirs();
// remove any leftover temporary tables(删除剩余的所有的暂时表)
for (String filename : dbDirectory.list())
if (filename.startsWith("temp"))
new File(dbDirectory, filename).delete();
}
/**
* 读取数据
* @param blk 块对象
* @param p 内存页对象
*/
public synchronized void read(BlockId blk, Page p) {
try {
RandomAccessFile f = getFile(blk.fileName());
f.seek(blk.number() * blocksize);
f.getChannel().read(p.contents());
} catch (IOException e) {
throw new RuntimeException("cannot read block " + blk);
}
}
/**
* 写入
* @param blk 块对象
* @param p 内存页对象
*/
public synchronized void write(BlockId blk, Page p) {
try {
RandomAccessFile f = getFile(blk.fileName());
f.seek(blk.number() * blocksize);
f.getChannel().write(p.contents());
} catch (IOException e) {
throw new RuntimeException("cannot write block" + blk);
}
}
/**
* 根据指定文件名追加块对象
* @param filename 文件名
* @return 块对象
*/
public synchronized BlockId append(String filename) {
int newblknum = length(filename);
BlockId blk = new BlockId(filename, newblknum);
byte[] b = new byte[blocksize];
try {
RandomAccessFile f = getFile(blk.fileName());
f.seek(blk.number() * blocksize);
f.write(b);
} catch (IOException e) {
throw new RuntimeException("cannot append block" + blk);
}
return blk;
}
/**
* 获取指定文件对象的相对位置
* @param filename 文件名
* @return
*/
public int length(String filename) {
try {
RandomAccessFile f = getFile(filename);
return (int) (f.length() / blocksize);
} catch (IOException e) {
throw new RuntimeException("cannot access " + filename);
}
}
/**
* 判断是否第一次创建
* @return 布尔值
*/
public boolean isNew() {
return isNew;
}
/**
* 获取块大小
* @return 块大小
*/
public int blockSize() {
return blocksize;
}
/**
* 根据文件名获取可读取文件对象
* @param filename 文件名
* @return RandomAccessFile
* @throws IOException IO异常
*/
private RandomAccessFile getFile(String filename)
throws IOException {
RandomAccessFile f = openFiles.get(filename);
if (f == null) {
File dbTable = new File(dbDirectory, filename);
f = new RandomAccessFile(dbTable, "rws");
openFiles.put(filename, f);
}
return f;
}
}
在openFiles map集合中的每个RandomAccessFile对象都对应于一个打开的文件。请注意,文件是在“rws”模式下打开的。“rw”:指定文件已打开以进行读写。“s”部分规定:操作系统不应该为了优化磁盘性能而延迟磁盘I/O;相反,每个写入操作都必须立即写入磁盘。该特性还确保了数据库引擎准确地知道磁盘写入发生的时间。
读、写和追加方法是同步的,这意味着一次只有一个线程可以执行它们。当方法共享可更新的对象,例如RandomAccessFile时,需要同步以保持一致性。例如,如果读取没有同步,则可能会发生以下情况:假设两个JDBC客户端各自运行在自己的线程中,它们试图从同一文件中读取不同的块。线程A首先运行。它开始执行读取,但在调用f.seek后立即被中断,也就是说,它已经设置了文件的位置,但还没有从中读取。线程B接下来运行并执行读取到完成。当线程A恢复时,文件的位置将会改变,但线程将不会注意到它;因此,它将错误地读取到错误的块。
在SimpleDB中只有一个FileMgr对象,它是由simple.server包中的SimpleDB构造函数创建的。FileMgr构造函数确定指定的数据库文件夹是否存在,并在必要时创建该文件夹。构造函数还删除了可能由其他实例化操作符创建的任何临时文件。
小结
- 磁盘驱动器包含一个或多个旋转磁盘。一个磁盘有多条同心轨道,每个轨道由扇区组成。扇区的大小由磁盘制造商决定;通常情况下一个扇区大小为512字节。
- 每个磁盘都有自己的读写头。这些读写头不能独立移动;相反,它们都连接到一个统一的执行器上,即执行器同时将它们移动到每个盘上的同一轨道上。
- 磁盘驱动器分三个阶段执行磁盘访问:
- 执行器将磁盘头移动到指定的轨道上。这个时间被称为寻道时间。
- 驱动器等待磁盘旋转,直到第一个想要的字节在磁盘头下面。这个时间被称为旋转延迟时间。
- 在磁盘头下旋转的字节将被读取(或写入)。这个时间被称为传输时间。
- 磁盘驱动器的速度很慢,因为它们的活动是机械的。可以通过使用磁盘缓存、圆柱体化和磁盘条带来改进访问时间。磁盘缓存允许磁盘通过每次读取整个轨道来预取扇区。一个圆柱体由每个磁盘上具有相同轨道编号的轨道组成。可以访问同一圆柱扇区上的块,而不需要额外的查找时间。磁盘条带化将虚拟磁盘的内容分布在几个小磁盘之间。出现加速现象是因为小磁盘可以同时运行。
- RAID技术可用于提高磁盘的可靠性。基本的RAID级别是:
- RAID-0是条带化的,没有额外的可靠性。如果一个磁盘出现故障,整个数据库就会被破坏。
- RAID-1会将镜像添加到条带化磁盘中。每个磁盘都有一个相同的镜像磁盘。如果一个磁盘出现故障,则可以使用它的镜像来重建它。
- RAID-4使用带有校验磁盘的条带化操作来保存冗余的奇偶校验信息。如果一个磁盘出现故障,则可以通过将其他磁盘上的信息与奇偶校验磁盘相结合来重新构建其内容。
- RAID技术需要一个控制器来在操作系统中隐藏多个磁盘的存在,并提供单个虚拟磁盘的错觉。控制器将每个虚拟读/写操作映射到底层磁盘上的一个或多个操作。
- 磁盘正受到闪存技术的挑战。闪存是持久的,但比磁盘快,因为它是完全电子的。但是,由于闪存仍然比RAM慢得多,操作系统将闪存驱动器当作磁盘驱动器。
- 操作系统通过为磁盘提供基于块的接口来隐藏磁盘和闪存驱动器的物理细节。块与扇区相似,只是其大小是由操作系统定义的。客户端按块号访问设备的内容。该操作系统通过使用磁盘映射或免费列表来跟踪磁盘上的哪些块可供分配。
- 内存页面(Page)是一个指定块大小的内存区域。客户端通过将块的内容读入页面、修改页面,然后将页面重新写入块来修改块。
- 该操作系统还提供了一个到磁盘的文件级接口。客户端将文件视为已命名的字节序列。
- 操作系统可以使用连续分配、基于区段的分配或索引分配来实现文件。连续分配将每个文件存储为连续块的序列。基于范围的分配为文件存储一组范围序列,其中每个范围都是一个连续的块块。索引分配分别分配文件的每个块。每个文件都保留一个特殊的索引块,以跟踪分配给该文件的磁盘块。
- 数据库系统可以选择使用到磁盘的块级或文件级接口。一个很好的妥协方案是将数据存储在文件中,但在块级别访问文件。
推荐阅读
在Chenetal(1994)的文章中,详细介绍了各种RAID策略及其性能特征。vonHagen(2002)的一本讨论基于unix的文件系统的书,讲解很详细;Nagar(1997)的一本讨论WindowsNTFS的书,讲解很好。各种文件系统实现的简要概述可以在许多操作系统教科书中找到,如西尔伯沙茨等人(2004)。
闪存的特性是:覆盖现有值比写入全新值要慢得多。因此,有很多研究都是针对不覆盖值的基于flash的文件系统。这样的文件系统将更新存储在一个日志中。 吴和郭(2006)和李和文(2007)的书都对这些问题进行了研究。
- Chen M , L Ee E , Gibson G , et al. RAID: High-performance reliable secondary memory. 1994.
- Lee S W , Moon B . Design of flash-based DBMS: An in-page logging approach[C]// Proceedings of the ACM SIGMOD International Conference on Management of Data, Beijing, China, June 12-14, 2007. ACM, 2007.
- Nagar R . Windows nt file system internals. O’Reilly & Associates, Inc. 1997.
- Silberschatz A , Gagne G , Galvin P B . Operating System Concepts. Addison-Wesley Pub. Co, 2002.
- Hagen W V . Linux Filesystems[J]. 2002.
- Chin-Hsien, Wu, Tei-Wei, et al. The Design of efficient initialization and crash recovery for log-based file systems over flash memory[J]. ACM Transactions on Storage, 2006, 2(4):449-467.