操作系统复习:持久化部分

I/O Devices

input/output (I/O) device,显然是计算机必不可少的两个部分。

系统架构

image-20200717162258417

CPU直接通过专用的内存总线和内存进行数据传输。另一些设备通过通用IO总线和系统交流,PCI就是其中的一部分(和显卡连接的,当然不止显卡)最后,也是最低级的就是一些周边设备的总线了,比如USB之类的与你的键盘、硬盘、鼠标等慢速设备进行连接。

注意,总线越快,那么就会越短,所以内存总线不能插太多的设备。现在也有部分设备把内存和图形放到了同一个重要的层面上,大概长这样:

image-20200717163349733

可以看到网络是通过PCIe进行连接的,所以速度很快。而图形话和内存被直接与CPU进行连接,CPU还直接和一块io片通过DMI连接。

一个典型的设备

我们模拟一个设备,这个设备并不是真的。

image-20200717165041119

可以看到一个设备主要分成了两个部分,上面代表了这个设备的“系统”,这部分被称为接口,是因为可以通过操控这个接口来控制整个设备。第二部分就是internal structure,这个相当于接口的实现部分,简单的设备就是一块物理芯片,而复杂一点的会包含一个简单的CPU。

一种典型的协议

还是上面的设备图,一个设备有状态寄存器,命令寄存器和数据寄存器,听名字就知道是干什么的了,操作系统只需要操作这三个寄存器,就能得到相应的结果。如果CPU的时间被用来进行数据移动,那么就叫做programmed I/O (PIO).

一个简单的协议可能可以这么设计:

  1. 不停判断设备是否是忙的,即轮询polling
  2. 如果设备不忙,就把数据写到data寄存器中。
  3. 把命令写到command寄存器中,然后开始执行。
  4. 继续等待,直到设备完成(可能成功也可能失败)。

这个协议看上去简单,而且也能工作,但是有很多问题:比如需要不停判断设备的状态,这样会导致CPU浪费

通过中断

不是通过设备的轮询,而是通过让设备自己触发一个硬件中断,然后CPU就会跳转到操作系统指定好的interrupt handler中去,操作系统会执行相关的操作,比如唤醒一个进程去操作。

当然如果你的设备足够快,那么用轮询也是OK的。如果不确定设备的快慢,可以先轮询一会会,然后在使用中断。

在网络中也不建议使用,因为到达一个包就触发一次中断,可能会引起活锁:操作系统永远在处理中断反而没把精力花在处理数据上面了。

有一种优化就是合并,就是不要完成任务就去触发中断,而是稍微等一等,没准能和别的中断合并,减少系统负担。

DMA更有效的数据传输

如果需要将大量数据写入到磁盘中,也就是说需要CPU时间,把数据从内存搬动到设备中,对于大数据来说这部分时间还是很多的。

这个问题的解决办法是Direct Memory Access (DMA),该设备无需CPU过多的干预就可以在内存和设备之间传输消息。

还是上面的例子,CPU这次不是直接把数据从内存搬动到设备了,而是告诉DMA,数据放在内存哪里了,需要搬动多少,然后接下来就交给DMA去处理,DMA完成之后会触发中断告知已经完成。

设备通信

硬件该如何与设备进行通信呢?

有两种主流的方法

  • 一种是显式的IO指令,该指令让OS把数据发送给设备寄存器,接下来就按照协议的内容操作。在x86上,通过inout可以与设备进行交流。如果要发送数据给设备,那就指定好data寄存器已经对应的port,然后执行指令。当然这个指令是特权的,所以只有操作系统可以进行操作。
  • 另外一种是通过memory- mapped I/O,内存映射IO。首先硬件把设备的寄存器映射成内存地址,那么OS只需要向内存写东西,就可以直接写到设备里取了。

这两种方式各有优缺点,而且目前仍然都在使用。

设备驱动

由于设备千千万,所以这里只能从大体上聊。我们如何才能使大多数OS设备保持中立,从而隐藏主要OS子系统中设备交互的详细信息?

答案就是抽象。在操作系统最低级层次中,有一个软件,叫做device driver,这个软件把和设备的交互细节全部封装在了里面。

举一个真正访问设备的例子。一个应用希望访问一个文件的内容,首先会产生一个系统调用,然后通过文件系统,在到Generic Block Interface,然后是Generic Block Layer,再到Specific Block Interface,最后才是设备驱动。当然也可以不走文件系统,而是直接通过raw interface来访问。

一项研究表明,操作系统中70%的代码是设备驱动

总结

在这一章中,对操作系统如何和设备进行交互有了基本的了解,具体讲了两种技术:DMA和中断,同样介绍两种方式去访问设备寄存器:指令和内存映射IO。最后还了解了一下设备驱动。

Hard Disk Drives

这一章中,了解一下硬盘是如何存储数据的,接口是什么。

接口

一个扇区是512字节,而硬盘有许许多多的这样的扇区,每个扇区都可以读和写。

大部分系统都是用页来作为基本单位的,也就是4K,但是硬盘只能保证对一个扇区的读写是原子性的。

如果读取连续两块block(block和sector是等价的),那么速度会明显快于读取两块分开的block。顺序读是最快,而且远远快于随机读写。

基本结构

这个建议找个视频看看。

简单的硬盘

The Rotational Delay

就是虽然已经定位到了对应的一圈,但是还需要等待磁盘转到指定的位置才可以读写。

Seek Time

如果之前的磁道是错误的,还需要移动磁臂,移动到指定的磁道上面才可以。当然移动并没有想象中那么简单,因为需要什么加速减速啥的。

其它的细节

使用 track skew来保证就算跨越磁道,读取也是正确的。

第二点就是明显外部的扇区比内部的扇区多。

最后,一个重要的部分就是cache,也可以叫做track buffer,大概再8-16M之间。

在写入的时候,硬盘有一个选择:到底是等数据真正落到了磁道上之后,才报告完成呢,还是只要到了硬盘的内存中,就报告完成呢?

IO时间

TI/O = Tseek + Trotation + Ttransfer ,这里给一个参考值:seek=4ms,rotation=2ms,transfer=0.03ms

显而易见嘛,需要先把磁头移动到磁道,然后要等待转到指定的扇区,最后还需要把数据写上去。

文中举了一个例子来说明,顺序读写的速度几乎是随机的200倍-300倍。

硬盘调度

之前在CPU虚拟化的时候提到过job scheduling,分成FIFO,SJF,RR等,而在磁盘这里有所不同,磁盘的job是可以预估出大概要花多少时间的,所以在这里,使用SJF是非常合适的。

SSTF: Shortest Seek Time First

该算法在早期的硬盘中很流行,核心思路就是根据磁头目前的位置,选择出最近的磁道,以最小化seek time。

但是也有弊端,首先是操作系统它并不知道硬盘的这个结构,所以操作系统实现了nearest-block-first (NBF),即地址最接近的块优先。其次是第二个问题,饥饿。也就是我们只要解决了饥饿问题,这算法就非常的棒。

电梯算法(也叫SCAN)

我们把磁头从最外面到最里面(或者反向也可)称为一次sweep,如果有一个请求,并不会立即处理,而是在下一次sweep中处理。

这个就跟我们电梯一模一样,我从20楼坐电梯到1楼,当我达到3楼的时候,一个人进来了想要上去4楼,按了4。如果使用SSTF,那么就应该让它先,但是如果用电梯算法,我会先到1楼,然后才处理他的请求。

然而,这些算法其实都没有表现的很好,因为我们只考虑了seek time,而忽略了rotation time。

SPTF: Shortest Positioning Time First

和之前的页面置换算法中的那个完美算法一样,这个算法也是不现实的,只能说尽量逼近。

其它调度问题

在现代操作系统中,操作系统的调度器决定好request,然后交给磁盘,磁盘已经有能力自己来实现SPTF了。

另外一功能就是I/O merging,这一部分在OS中完成。

总结

我们了解了硬盘是如何工作的。

RAIDS

我们希望中的硬盘:快速,容量大,可靠。而RAID就是通过让多块硬盘共同工作,来完成这个任务的。

RAID接口

当文件系统产生一个逻辑IO的请求的时候,RAID内部则必须要计算出那个硬盘来完成这个request。raid其实甚至可以说是一个小型的系统,有CPU,有内存还有磁盘。

错误模型

raid被设计用来从错误中恢复,所以知道错误模型很有帮助。

第一种模型叫做fail-stop,磁盘要么是工作的,要么的坏的。而一旦坏了则是很容易检测到的。

剩下的模型等下介绍。

如何评估raid

我们从三个角度来陪你一股股,第一个是capacity,第二个是reliability,最后一个是performance。在接下来的内容中,我们认为一个block的大小是4K。

RAID0:striping

这其实压根不算是raid,因为根本没有redundancy。简单来说就是把多个硬盘合并成一个。把每个块按照磁盘分,比如0,4,8分给磁盘1,1,5,9分给磁盘2,以此类推。这个的好处是最大限度提高并行性。

RAID1:复制

在读取的时候可以任选其中一部分,但是写入的时候需要两个都写。

显然,从容量的角度来说,只有所有硬盘总和的一半(甚至更低,如果复制的多的话)。性能方面,读取性能和单硬盘的速度是一样的,在写入方面由于是并行写入的,所以速度也差不到哪里去,但是会略有下降(因为是取两者中比较差的那个)。

在多个硬盘中写入会遇到一个问题:内容更新不一致的问题,我们希望写入到两个硬盘中是原子性的,有一种办法能够解决:使用write-ahead log

剩下的跳过

实际中暂时真的用不上。

Interlude: Files and Directories

操作系统如何操作这些可持久化设备,具体的API是什么呢,如何实现呢?

文件和目录

两个抽象:filedirectory

file其实就是一个字节数组,每个文件都有一个low-level name(不是文件名,而是inode number),directory其实和文件一样,也有low-level name(不是目录名字,而是inode number),只不过它里面的内容就是一堆(文件名-inode number)的pair,这样就可以堆砌出一棵树,树的根就是/

创建文件

通过open这个系统调用就可以创建一个文件,该函数返回一个文件标识符,对于每个进程来说,唯一的一个整数。文件标识符是由操作系统来管理的,每个进程都有一个文件标识符的数组,标识了每个打开的文件。

读写文件

系统中读取一个文件最简单的命令应该就是cat了,然后我们还可以用strace来追踪命令执行过程中使用的系统调用并展示在屏幕上,所以可以用strace cat 文件

image-20200718133541325

这里只截取了部分重要的内容,可以看到首先最重要的是打开文件(只读模式),并且返回了文件描述符3。因为每个进程都有三个默认已经打开的文件,分别是标准输入0,标准输出1和标准错误2,所以只能从3开始了(大多数情况下是3)。

然后打开文件之后就可以用read读取了,可以看到第一个参数是3,就是用来指代那个文件,第二个参数指向了一个缓冲区buffer,第三个是缓冲区的长度,上图中是128KB,返回值是6,说明真正的长度是6字节。

然后就是往文件描述符1写入内容,也就是在屏幕上打印。

然后紧接着又是一个读取,但是因为已经读完了,所以返回了0。最后就是关闭文件了。

不顺序读写

之前学习了如何把一个文件从头读到尾,这一部分学习读取文件的一部分。用到了另外一个系统调用——lseek。

1
off_t lseek(int fildes, off_t offset, int whence);

第一个参数是文件描述符,第二个是偏移量,第三个指明了offset的含义:从头开始计算,从当前开始计算还是从尾开始计算。

从上面也不难推论出,对于进程打开的每个文件,操作系统都知道当前的位置,而lseek则是用来改变这个位置的系统调用。下面是操作系统对于文件的抽象:

1
2
3
4
5
6
7
struct file {
int ref;
char readable;
char writable;
struct inode *ip;
uint off;
};

操作系统对于每个打开的文件(不管是不是同一个文件)都维护了一个结构,所以如果同时打开了一个文件两次,那么会有两个文件描述符,虽然都是指向相同的文件,但是offset是独立的。

同时操作系统整体维护了一个叫open file table的东西,其实就是file的数组,这东西可不是进程独享的,而是整个系统共享的。进程持有一个file数组,是该进程打开的文件描述符的集合,而操作系统则有open file table,是整个操作系统打开的文件描述符的集合。这两个数组都允许有相同的文件,即一个文件可以打开多次,在数组中创建多个。

fork和dup

之前说了,每个进程打开的文件都是各自独立的。那么当使用fork来创建子进程的时候呢?

仔细观察上面的file结构体,可以发现还有一个ref,说明有多少个引用。如果真的是进程之间完全独立,那么根本就不需要这个整数了。所以在父子进程之间,这个文件是共享的,而这是非常有用的。

除了fork之外,还有一个调用是dup,这个调用接受一个文件标识符作为参数,并且返回一个文件标识符。

系统调用——fsync

通常我们使用了write系统调用之后,操作系统为了效率考虑,不是马上写到硬盘上去,而是写到内存的一个buffer里去,然后过一会再写回到磁盘中。当使用fsync的时候就可以强行把数据写入到磁盘中。

然而事实上,你可能还需要把该文件对应的目录也强行刷一遍。

文件重命名

使用系统调用rename即可。这个系统调用很有意思,它是原子性的。

获取关于文件的信息

除了访问文件,我们希望文件系统能够存储关于文件的一些信息,即metadata,为了访问这些metadata,我们可以使用stat系统调用。而这些信息其实就是存储在inode中的。

删除文件

本质上是unlink系统调用,为什么不用delete或者是别的代表删除的词汇呢?

创建目录

首先,你不能直接在目录中写。这是因为需要保证目录数据的完整性,所以你不能直接修改目录的数据,只能通过间接的创建文件等方式来修改。

如果要创建目录,可以用mkdir系统调用,当一个目录创建完成之后,默认有两个目录,分别是...

读取目录

读取目录其实用的就是ls,和文件不同,读取目录的过程有三个系统调用,opendir,readdir和closedir。

删除目录

系统调用rmdir,这个调用必须确保目录是空的(只有...)才可以。

硬链接

现在我们来解决上面的问题,为什么删除是unlink呢。link的本质上其实就是创建对文件的一个引用而已。该引用具有和源文件相同的inode。

我们在创建文件的时候,其实做了两件事:首先是创建了一个inode,这个inode会存储文件的信息,包括大小,在磁盘中的block等等。第二部分就是把文件的名字link到文件,并且放到目录中。

当系统执行unlink的时候,会通过reference count来确认有多少个引用。它当然会断开指定的文件名和inode之间的联系,同时吧reference count减一,如果减完之后发现是0了,那么就会去硬盘中释放对应的Block的内容,此时真正把文件删除。

符号链接

硬链接不可以创建目录的复制,不可以跨越文件系统。软链接的本质,其实是一个文件的绝对路径,通过它你可以找到真实的文件,所以路径越长,软连接的大小就越大。

Permission Bits And Access Control Lists

文件系统也抽象了磁盘,就如之前所述的文件和目录。但是和之前的CPU内存抽象不同,文件和目录是共享的,所以需要有一种机制来保护这些共享资源。

第一种就是permission bits,这块Linux都讲烂了,所以跳过先。这里稍微留意的就是:对于目录的执行权限,就是你能不能进入目录的意思。

对于目录来说,还有一个叫access control list (ACL)的东西。简单点说就是谁可以访问,谁不可以访问。

挂载文件系统

假设我们现在手头上有一个硬盘,然后需要写入文件系统,那么就需要使用mkfs命令,就可以写入文件系统了。

但是,一旦文件系统创建了,我们需要做的就是访问它,通过一棵统一的文件树来进行访问。通过mount系统调用可以完成,简单来讲就是把一个目标的挂载点,粘贴到原有的统一的文件树中。

我为什么需要挂载呢?我直接访问/dev/sda1不行吗?

不行,因为你不知道内容的格式,你必须指定相应的文件系统,才可以进行读取和写入。

总结

虽然这部分内容多又杂,但是细读一遍收获颇丰。

File System Implementation

在这一部分,我们实现一个简单的文件系统——vsfs (the Very Simple File System)。

思考方式

我们从两个方面来思考,首先是文件系统的数据结构。应该使用什么样的数据结构来组织数据和metadata。其次就是文件系统的访问方式,怎么把这些系统调用映射到这个数据结构呢?

整体思考

首先我们需要把硬盘分成blocks,那既然是一个简单的系统,我们就只用一种大小:4K。有了block之后,我们需要知道我们在里面写些什么呢?用户的数据呗,这是自然的。我们把这部分称为data region。文件系统自然要能够追踪这些用户数据,这部分信息叫做metadata,为了能够保存这些信息,我们需要一种叫inode的数据结构。我们把所有的inodes放到一起存放,并且放在一起叫做inode table

现在有了用户数据存储区和一个inode table,之后我们需要一个数据结构来追踪,哪些数据存储区是空着的,哪些inode是可以使用的,这部分的数据结构的选择有很多,为了间接性,我们选择了bitmap,一个bitmap对应数据区,另外一个对应inode。

最后还需要一块superblock,来记录整个文件系统的信息。所以当挂载一个文件系统的时候,只需要读取它的superblock,就可以知道一些关键参数,并执行相对应的操作。

Inode

inode其实是index node的缩写,每一个inode都有一个数字,叫做i-number。在文件系统中,只要给定一个i-number,就可以直接计算出inode位于硬盘的哪个位置。

image-20200718165952382

上图是ext2文件系统的inode内容。这其中最重要的就是就是如何通过inode来找到数据块。最简单的实现就是在里面保存指针,指向真正的block,但是如果这么做,你的文件就不能很大了。

多层index

为了支持更大的文件,我们可以通过层次目录结构来解决。使用 indirect pointer,我不直接指向data block,而是指向另外一个block,这个block里面存放了很多直接pointer。

除了这个方法,还可以使用extents,即我只需要指定数据文件是从哪里开始的,长度多少就可以指定了,缺点么就是文件内容它必须在硬盘中是连续的。

甚至你还可以让data block组成一个链表。当然如果直接去遍历链表,效率是非常低的。于是在操作系统内存中有一个hashtable,key是数据块的地址,值则是下一个数据块的地址。这个思想就是著名的file allocation table(FAT)文件系统——Windows早期的文件系统。

目录组织

在目录中,就是一些pairs,每一个pair里面是文件名和Inode号。假设一个目录下有3个文件(不包含默认的两个),那么data block可能就长这样:

image-20200718174110826

为什么需要reclen呢?因为当删除目录中的文件的时候,下一次可能会有新的文件到来,此时就需要这个字段。

空闲空间管理

之前提到过用到了两个bitmap,分别用来管理inode和data block。由于空闲空间是要分配给新的文件的,而顺序读取的速度远快于随机读取,所以最好能保证连续性。

访问路径

首先我们假设superblock已经放到内存里面了,其余的则全部还在硬盘里。

读文件

现在你要读取一个文件,第一步首先是找到它并且打开它。所以操作系统首先要找到对应的inode,然而目前只有superblock和文件的路径信息。所以不得不进行遍历。

所有的遍历肯定是从根路径开始的。我们如果要找到inode,必须先找到inumber,根据inumber可以直接计算出inode。但是这里有个循环的问题了,inumber需要首先找到它的父目录,父目录中才有对应的信息,但是根路径的父目录是谁?根路径没有父目录。所以在大多数系统中都规定了,根目录的inode是2。通过计算即可知道,inode是磁盘中的第一个inode block,所以去读取内容。

通过读取inode的内容,可以知道data block在哪里;通过读取data block,就可以知道里面存储了哪些文件以及文件的inode,然后就可以递归了。

最终能够找到指定的文件,并且通过open系统调用把它的内容读取进内存中。然后文件系统还需要给这个进程分配一个文件描述符。

写文件

首先文件必须被打开,这个过程上面有了,不在赘述。在写文件的时候,需要一个决定应该写到哪个block里面。所以写IO很麻烦:需要更新两个bitmap,需要读取和重写inode,还需要重写数据块。

如果是新建文件,除了上面的修改,还需要到对应的目录中进行修改。所以肯定需要有相应的优化措施。

Caching and Buffering

缓存和缓冲可以有效解决这些问题。

从上面的例子我们不难发现,访问任何一个文件都是从根目录下开始找的。早期的系统使用固定大小的cache来保存常用的Block,这部分大概会占用大约10%的内存。

现代的操作系统用的则是dynamic partitioning,把内存的虚拟页面和文件系统的block集成到统一的page cache中。这样就有了更大灵活性。

如果是写文件,那么使用的是write buffering,通首先,过延时,可以把一些工作聚集在一起做。其次,系统可以决定什么时候执行IO,得到最高的性能。最后,没准可以抵消(创建了文件但是转念一想不对又删除了)。

数据库系统除外,它们对数据正确性要求极高,所以一般就直接写到硬盘里了,甚至都不使用文件系统,而是直接使用raw disk的接口。

总结

在这一章里我们见识了如何实现一个文件系统。

Locality and The Fast File System

结合上一章的vsfs,我们不难得出硬盘上的数据结构是这个样子的:

image-20200718193529869

问题:性能低下

早期的操作系统把硬盘当成随机存取的设备使用,数据散落在磁盘各个角落,这个可以通过磁盘碎片整理工具来解决。另外一个问题是block太小了,只有512字节。

柱面

首先我们来改善磁盘的结构。把磁盘分成柱面,然后把几个连续的柱面合成一个组。这些细节对于操作系统来说是透明的。在操作系统上,是把一个一个block组成 block groups。也就是物理上实际是柱面组,但是逻辑上是一个一个块组。我们只需要把两个文件放到同一个组里,就可以快速读取了。其中每个组的结构是这样的:

image-20200718194820631

每个组都有一个superblock的拷贝。通过分组,可以有效降低整体的负担。

策略:如何安放文件和目录

我们可以把相关的东西放在一起,不相关的东西分开放。那么问题来了,什么是相关的,什么是不相关的呢?

首先放置的是目录,用一个简单的方法:找到一个柱面组,目录分配的少并且inode空闲的数量多。

然后放置的是文件,文件又分成两部分,首先操作系统确保一个文件的所有data block都放到一个柱面组里,其次是把同一个目录下的文件都放在一个柱面组里。

Measuring File Locality

大部分的文件彼此之间的距离都比较近。

大文件除外

大文件并不受上面的策略影响,它会分散放置在各个group里面。

总结

这一部分主要介绍了对原来操作系统缓慢的文件系统的改进。

Crash Consistency: FSCK and Journaling

如何对抗系统崩溃或者突然断电等问题是硬盘需要解决的。

一个具体的例子

我们现在假设要往一个文件里追加一些内容,那么需要修改磁盘中的三个部分:Inode部分,data的bitmap部分和data block部分。那么会有什么情况发生呢?

  • 只更新了data block。因为inode里面没记录,同时data bitmap也没有,所以更新的data block跟没写入是一样的。
  • 只更新了inode。由于data block里没有数据,所以读到的是垃圾数据。而且更为严重的是,inode显示可以读取,但是bitmap里面对应的Block却是空的。
  • 只更新了bitmap。这样会造成space leak,因为这块空间在inode里面没有记录,永远也释放不了了。
  • 同时更新了inode和bitmap。造成垃圾数据的问题。
  • 同时更新了inode和data block。冲突问题。
  • 同时更新了bitmap和data block。没有inode的内容就没有文件使用,更新了也白更新。

我们希望文件系统保持一致性,但是断电随时可能会发生。我们把这个不一致的问题称为crash-consistency problem

解决办法1:文件系统checker

早期系统允许上面的情况发生,并且尝试去修复它——fsck。当然你并不能期望它能够解决垃圾数据的问题,它需要在文件系统挂载之前就运行,具体有这些步骤:

  • 首先确认一下superblocks。
  • 接下来扫描inodes等,对系统里block的分配情况有一个了解。然后根据扫描结果,fsck自动构造出一个bitmap,然后与原来的bitmap进行比较。
  • 详细确认每一个inode的情况
  • 检查link数量。如果发现一个inode没有被任何目录使用,那么就把它放到lost+found目录中。
  • 越界block等检查。
  • 其它检查等。

这个命令非常非常的慢,虽然有用但是…太慢了。

解决办法2:Write-Ahead Logging

write-ahead logging,aka journaling,这个办法是从数据库那里“偷”过来的。现在,Linux中的ext3 ext4和Windows的NTFS都是用的这个。

核心思想是,在动手更新之前,首先先记录好,然后再开始做。

现在我们又想写文件了,还是原来的配方。那我们首先把我们要更新的三个部分写到日志里。日志一共有五个部分,除了要更新的这三个部分外,还有一个事务头(包含了ID等信息)和事务的尾巴(同样包含了ID等信息)。只要这个事务成功写入了日志(日志也在磁盘上),那么就可以把这些数据真正写到磁盘中对应的位置去了。这个对应的进程叫checkpointing

当然,在你写入日志文件的时候,也可能发生断电,所以为了保护,文件系统会保证,只有当数据齐全之后,才会加上事务的尾巴。

恢复

如果断电发生在事务还没提交之前,那么就直接无视。如果发生在事务已经提交,但是还没有写入到硬盘,那么就需要重新再做一次,所以相应的日志就叫做redo logging