如何实现一个简单的日志系统
时间:2021-09-26 13:40:15
手机看文章
扫描二维码
随时随地手机看文章
[导读]日志本文来聊聊文件系统中的日志系统,来看一个简单的日志系统是如何实现的。本文是接着前面的xv6系列,用到的一些前导知识不再说明,没看的可以先看一下。文件系统设计中通常要考虑错误恢复,这是因为文件系统会涉及对磁盘的多次写操作,如果在写的过程中系统崩溃了,就会使得磁盘上的文件系统处于...
日志
本文来聊聊文件系统中的日志系统,来看一个简单的日志系统是如何实现的。本文是接着前面的 xv6 系列,用到的一些前导知识不再说明,没看的可以先看一下。文件系统设计中通常要考虑错误恢复,这是因为文件系统会涉及对磁盘的多次写操作,如果在写的过程中系统崩溃了,就会使得磁盘上的文件系统处于不一致的错误状态。日志就是设计来解决因为系统崩溃导致的错误问题,本文就 来讲解怎么实现一个简单的日志系统。在 的日志系统中,文件操作方面的系统调用并不会直接对磁盘进行写操作,而是把对磁盘写操作描述包装成一个日志写在磁盘中,当该系统调用执行完成之后,再提交一个记录到磁盘上。为什么日志可以解决文件系统操作中出现的崩溃呢?如果崩溃发生在提交之前,那么磁盘上的日志文件就不会被标记为已完成,恢复系统的代码就会忽视它,磁盘的状态就好像写操作从未进行一样。如果是在提交之后崩溃的,恢复程序会重演所有的写操作。在任何一种情况下,日志文件都使得磁盘操作对于系统崩溃来说是原子操作:在恢复之后,要么所有的写操作都完成了,要么一个写操作都没有完成。上面的理论大都来自 文档,我们能了解到,最为重要的是实现写操作的原子性,那么怎样实现呢? 在磁盘上分配了一片日志区,假如现在内存中有一个缓存块准备同步到磁盘区域 A, 并不立即将该缓存块的数据写到磁盘区域 A,而是先写到磁盘的日志区(提交)。如果没有问题则将日志区的数据写到相应的磁盘区域 A。如果有问题,在提交之前发生了崩溃,则恢复代码忽略日志信息,区域 A 根本就没进行过写操作,当然就能够保证数据的一致性。如果在提交之后发生了崩溃,则恢复代码将日志区的数据重新写到磁盘区域 A,也保证了数据的一致性。日志区也需要相应的数据结构来组织管理,相关的结构定义如下:结构定义
超级块
struct superblock {
uint size; // Size of file system image (blocks) 文件系统大小,也就是一共多少块
uint nblocks; // Number of data blocks 数据块数量
uint ninodes; // Number of inodes. //i结点数量
uint nlog; // Number of log blocks //日志块数量
uint logstart; // Block number of first log block //第一个日志块块号
uint inodestart; // Block number of first inode block //第一个i结点所在块号
uint bmapstart; // Block number of first free map block //第一个位图块块号
};
文件系统的超级块,超级块中记录了文件系统的元信息,比如上述 的超级块记录了数据块、i 结点、日志块的数量和第一块的块号。 文件系统的总体布局如下:日志头
#define MAXOPBLOCKS 10 // max # of blocks any FS op writes
#define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
struct logheader { //日志头部
int n;
int block[LOGSIZE];
};
日志头用来记录每次日志的大小和位置关系信息。 来记录每次日志使用的空间大小,日志空间的总大小记录在超级块中(大小的单位是块),同时 也规定每次日志使用的块数也不能超过 。 是一个 型数组,元素个数最多为 ,用来记录位置关系。写入磁盘是先写入日志区,再写到磁盘的其他区域。这个日志区的磁盘块和其他区域的磁盘之间需要有一个映射关系,这个关系就记录在 数组中。举个例子: 表示日志块 记录的数据应放在 号磁盘块中。struct log {
struct spinlock lock;
int start; //日志区第一块块号
int size; //日志区大小
int outstanding; // 有多少文件系统调用正在执行
int committing; // 正在提交
int dev; //设备,即主盘还是从盘,文件系统在从盘
struct logheader lh; //日志头
};
struct log log;
这个结构体只存在于内存,用来记录当前的日志信息。这个日志信息也是一个公共资源要避免竞争条件所以配了一把锁。 三个属性值从超级块中读取。其他的信息见注释,具体含义后面慢慢讲解。下面直接来看日志的函数实现:函数实现
void readsb(int dev, struct superblock *sb) //读超级块
{
struct buf *bp;
bp = bread(dev, 1); //读取超级块数据到缓存块
memmove(sb, bp->data, sizeof(*sb)); //移动数据
brelse(bp); //释放缓存块
}
这个函数用来读取超级块的内容,超级块在第一块,第零块是引导块。调用 将数据从磁盘读取到缓存块中,然后将缓存块中超级块的数据复制一份到内存中定义的超级块数据结构中去,最后再释放缓存块的锁,因为 调用 获取了锁,使用完该缓存块就该释放,详见磁盘那篇文章void initlog(int dev)
{
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: too big logheader");
struct superblock sb; //定义局部变量超级块sb
initlock(