MIT 6.828 操作系统课程系列9 File System#
lab0中已学习过一些文件系统内容。之前的lab中也经常会遇到文件系统相关逻辑。这次lab详细看文件系统。
看手册第8章。文件系统的作用就是存储和管理数据。通常文件可供多个用户和程序共享。
xv6支持类unix的文件、目录、路径,把数据存在虚拟磁盘上。
做文件系统需要面临一些挑战
需要一个树型结构来描述文件和目录。记录数据的归属、每个文件的内容、磁盘占用情况。
需要支持崩溃后的恢复。比如执行某些事务时突然断电,重启后必须还能正常工作。
多个进程可能会同时操作同个文件。
访问磁盘比访问内存慢了几个数量级,必须有cache机制。
概览#
xv6文件系统分为七层。
层 |
功能 |
---|---|
file descriptor |
unix的很多资源都抽象成fd(管道、设备、文件等等) |
pathname |
路径支持 |
directory |
特殊的inode。内容是一批目录或文件。 |
inode |
独立的文件,每个文件表述为一个incode,有唯一的编号,有一个或多个block保存文件内容。 |
logging |
可让上层把几个更新block的操作打包成事务,这样这些操作就是原子的,可解决崩溃时数据不一致的问题。 |
buffer cache |
缓存disk block。保证一个block在同一时刻最多只有一个kernel进程在对其操作。 |
disk |
读写虚拟硬盘上的block |
文件系统必须规划好inode和block怎么存在磁盘上。xv6把磁盘分成几个部分。
// xv6文件系统布局
// | boot | superblock | log | inodes | bitmap | data |
// block编号 0 1 2 ... ... ... ... FSSIZE-1
// 数量 1 1 nlog ninodeblocks nbitmap nblocks
block 0(保存boot信息)是不用的。
block 1是superblock。保存文件系统的整体信息:总block数量,inode数量,log里的block数量。
block 2存log。
接着是inodes,每个block存数个inode。
bitmap保存block的使用状态。
实际的数据block。每个block要么是空的(bitmap里标注为空闲),要么存了文件或目录的内容。
superblock存文件系统总体信息。
Buffer cache层#
两个功能
对block做并发管理,保证一个block只有一个副本在内存里,而且同一时刻最多只能有一个kernel线程在使用这个副本。
缓存常用的block。这样不用每次读磁盘。
buffer cache提供bread
和bwrite
接口。分别时读写block。
一个kernel线程使用完buffer后必须用brelse
进行释放。
buffer cache用对每个buffer都做一个sleep锁来保证同一时刻只有一个线程在使用同一个buffer。
bread
会返回一个已锁定的buffer。brelse
释放锁。
buffer cache用固定数量的buffer来保存磁盘block。当文件系统请求一个不存在于cache的block时,可能发生回收。
会回收最久没使用的block来给新的block用(假设越久没用意味着再次使用的概率越小)。
#define BSIZE 1024 // block size
#define MAXOPBLOCKS 10 // max # of blocks any FS op writes
#define NBUF (MAXOPBLOCKS*3) // size of disk block cache
// 一个block的buffer
struct buf {
int valid; // has data been read from disk?
int disk; // does disk "own" buf?
uint dev;
uint blockno; // block号
struct sleeplock lock; // 每个buf都有自己的锁
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
uchar data[BSIZE]; // 实际数据
};
// 全局唯一bcache。包含30个buf。
struct {
struct spinlock lock;
struct buf buf[NBUF]; // 30个buf
// Linked list of all buffers, through prev/next.
// Sorted by how recently the buffer was used.
// head.next is most recent, head.prev is least.
struct buf head;
} bcache;
// 初始化
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
// Create linked list of buffers
bcache.head.prev = &bcache.head;
bcache.head.next = &bcache.head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
// head永远不变
// 每次插到head之后
b->next = bcache.head.next;
b->prev = &bcache.head;
initsleeplock(&b->lock, "buffer");
bcache.head.next->prev = b;
bcache.head.next = b;
}
}
// 返回一个cahce资源(buf)
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
acquire(&bcache.lock);
// 遍历所有buf
for(b = bcache.head.next; b != &bcache.head; b = b->next){
// 如果dev和blockno匹配,已经在cache中,带锁返回。
if(b->dev == dev && b->blockno == blockno){
b->refcnt++;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
// Not cached.
// Recycle the least recently used (LRU) unused buffer.
// 没在cache。从buf末尾往前找refcnt为0的。
for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
if(b->refcnt == 0) {
b->dev = dev;
b->blockno = blockno;
b->valid = 0;
b->refcnt = 1;
release(&bcache.lock);
acquiresleep(&b->lock);
return b;
}
}
panic("bget: no buffers");
}
// 读一个磁盘block。返回其buf
struct buf*
bread(uint dev, uint blockno)
{
struct buf *b;
// 获取一个buf
b = bget(dev, blockno);
if(!b->valid) {
virtio_disk_rw(b, 0); // 进驱动层读实际磁盘数据到buf。细节暂时忽略。
b->valid = 1;
}
return b;
}
// buf数据写到磁盘
bwrite(struct buf *b)
{
// 必须为锁定状态
if(!holdingsleep(&b->lock))
panic("bwrite");
virtio_disk_rw(b, 1); // 进驱动实际写磁盘
}
// 释放一个buf
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
acquire(&bcache.lock);
// 减引用
b->refcnt--;
if (b->refcnt == 0) {
// no one is waiting for it.
// 引用减为0。挪到head之后。
b->next->prev = b->prev;
b->prev->next = b->next;
b->next = bcache.head.next;
b->prev = &bcache.head;
bcache.head.next->prev = b;
bcache.head.next = b;
}
release(&bcache.lock);
}
Inode层#
inode可以指磁盘上的文件数据(包括文件大小,数据block列表),也可以指内存中的inode,包含磁盘inode的副本和一些其他信息。
磁盘inode是磁盘上一串连续的数据(inode block)。所有inode大小一样,所以给定一个n,很容易能取得磁盘上的第n个inode。
这个n被成为inode number或i-number。
inode可以看成一个无名的文件。
磁盘inode的结构在lab0学习过,再看一遍。
mkfs的一些函数与fs重名但实现不同。mkfs是独立生成文件系统,不用考虑系统运行时的各种问题。最终的格式一样。
type分为文件、目录、特殊文件(设备)。type为0代表inode为未使用状态。
nlink为引用此inode的目录计数。
size为此文件的字节数。
addrs为文件包含的block编号
使用中状态的inode放在内存里。
处于内存中意味着有指针指向这个inode。ref就是这个计数,如果变为0,就会从内存中去除。
iget
和iput
获取和释放inode指针,加减引用计数。
#define NDIRECT 12
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT)
// 内存inode
// in-memory copy of an inode
struct inode {
uint dev; // Device number
uint inum; // Inode number
int ref; // Reference count
struct sleeplock lock; // protects everything below here
int valid; // inode has been read from disk?
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
uint addrs[NDIRECT+1];
};
// 磁盘inode
// On-disk inode structure
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // Data block addresses
};
#define NINODE 50
// 全局inode表。内存中最多放50个inode。
struct {
struct spinlock lock;
struct inode inode[NINODE];
} itable;
// inode type
#define T_DIR 1 // Directory
#define T_FILE 2 // File
#define T_DEVICE 3 // Device
// cwd初始化
// 首先在mkfs中已经做好了根目录,并把user程序添加到了根目录。见lab0。
// 系统启动时把cwd初始化为根目录,我们命令行ls就能看到对应的文件了。
#define ROOTDEV 1
#define ROOTINO 1
userinit
// 获取根目录的inode
p->cwd = namei("/");
char name[DIRSIZ];
return namex(path, 0, name); // namex(char *path, int nameiparent, char *name)
// iget以dev和inum获取一个内存inode资源
// 这里是根目录
ip = iget(ROOTDEV, ROOTINO); // iget(uint dev, uint inum)
struct inode *ip, *empty;
acquire(&itable.lock);
// Is the inode already in the table?
empty = 0;
// 遍历inode表。如果找到dev/inum相等的,ref++,直接返回。
for(ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++){
if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
ip->ref++;
release(&itable.lock);
return ip;
}
if(empty == 0 && ip->ref == 0) // Remember empty slot.
empty = ip;
}
// 如果没找到,而且没有空闲的inode资源。panic
if(empty == 0)
panic("iget: no inodes");
// 否则占据一个inode资源
ip = empty;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
ip->valid = 0;
release(&itable.lock);
return ip;
return ip;
// 创建目录的流程
mkdir
sys_mkdir // syscall
char path[MAXPATH];
struct inode *ip;
begin_op(); // log相关。暂忽略
argstr(0, path, MAXPATH); // 取path
// 创建目录
// 比如cwd为/。可创建mkdir a。然后可mkdir a/b。
create(path, T_DIR, 0, 0) // create(char *path, short type, short major, short minor)
struct inode *ip, *dp;
char name[DIRSIZ];
// 获取目标目录的inode。比如/a/b/c就会得到/a/b的inode
// 会检查是否存在,一级一级检查,有问题会返回0。
if((dp = nameiparent(path, name)) == 0)
namex(path, 1, name); // namex(char *path, int nameiparent, char *name)
// 先获取当前目录的inode
if(*path == '/')
ip = iget(ROOTDEV, ROOTINO);
else
ip = idup(myproc()->cwd);
ip->ref++;
// 从当前目录cwd开始
// name是一个临时buffer
// 不断从path中取第一项到name。path更新为剩余的路径项。
// 如果返回0,就是路径已经空了,取不出了,跳出。
while((path = skipelem(path, name)) != 0){
ilock(ip);
// 锁定inode。如果不包含实际磁盘数据,读出数据。
struct buf *bp;
struct dinode *dip;
if(ip == 0 || ip->ref < 1)
panic("ilock");
acquiresleep(&ip->lock);
// 如果没读过数据
if(ip->valid == 0){
// 磁盘实际数据读到buf
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
dip = (struct dinode*)bp->data + ip->inum%IPB;
ip->type = dip->type;
ip->major = dip->major;
ip->minor = dip->minor;
ip->nlink = dip->nlink;
ip->size = dip->size;
// 把dinode的block号复制到inode
memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
brelse(bp);
ip->valid = 1;
if(ip->type == 0)
panic("ilock: no type");
}
// 每次得到的inode必须是目录,否则就是有问题。
if(ip->type != T_DIR){
iunlockput(ip);
return 0;
}
// 结束
if(nameiparent && *path == '\0'){
// Stop one level early.
iunlock(ip);
return ip;
}
// 在当前inode中找名为name的目录
// 遍历目录中的dirent
// 不展开了
// 如果找不到就是有问题
if((next = dirlookup(ip, name, 0)) == 0){
iunlockput(ip);
return 0;
}
iunlockput(ip);
// 往下一层目录走
ip = next;
}
if(nameiparent){
iput(ip);
return 0;
}
return ip;
return 0;
// 锁定。并保证读出磁盘数据
ilock(dp);
// 如果name已存在,返回已存在的。
if((ip = dirlookup(dp, name, 0)) != 0){
iunlockput(dp);
ilock(ip);
if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
return ip;
iunlockput(ip);
return 0;
}
// 分配新的inode
if((ip = ialloc(dp->dev, type)) == 0){
ialloc(uint dev, short type)
int inum;
struct buf *bp;
struct dinode *dip;
for(inum = 1; inum < sb.ninodes; inum++){
bp = bread(dev, IBLOCK(inum, sb));
dip = (struct dinode*)bp->data + inum%IPB;
// type0为空闲状态
if(dip->type == 0){ // a free inode
memset(dip, 0, sizeof(*dip));
dip->type = type; // 更新了bcache
log_write(bp); // log相关。暂忽略
brelse(bp);
// 成功。
// 再iget得到内存inode
return iget(dev, inum);
}
brelse(bp);
}
// 失败
printf("ialloc: no inodes\n");
return 0;
// 失败
iunlockput(dp);
return 0;
}
// 锁定。确保读出磁盘数据
ilock(ip);
ip->major = major;
ip->minor = minor;
ip->nlink = 1;
// ip为内存inode,包含实时内存数据。
// 获取对应的dinode。把数据复制到dinode,即cache。
// 写到cache是预更新。最后commit后才是真正写到磁盘。
iupdate(ip);
struct buf *bp;
struct dinode *dip;
// 磁盘数据读到cache
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
// 更新cache
dip = (struct dinode*)bp->data + ip->inum%IPB;
dip->type = ip->type;
dip->major = ip->major;
dip->minor = ip->minor;
dip->nlink = ip->nlink;
dip->size = ip->size;
memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
log_write(bp); // log相关。暂忽略
brelse(bp);
// dirlink在目录的inode中添加dirent项
if(type == T_DIR){ // Create . and .. entries.
// No ip->nlink++ for ".": avoid cyclic ref count.
if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
goto fail;
}
if(dirlink(dp, name, ip->inum) < 0)
//
dirlink(struct inode *dp, char *name, uint inum)
int off;
struct dirent de;
struct inode *ip;
// Check that name is not present.
if((ip = dirlookup(dp, name, 0)) != 0){
iput(ip);
return -1;
}
// Look for an empty dirent.
for(off = 0; off < dp->size; off += sizeof(de)){
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink read");
if(de.inum == 0)
break;
}
strncpy(de.name, name, DIRSIZ);
de.inum = inum;
if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
return -1;
return 0;
goto fail;
if(type == T_DIR){
// now that success is guaranteed:
dp->nlink++; // for ".."
iupdate(dp);
}
iunlockput(dp);
iunlock(ip);
if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
panic("iunlock");
releasesleep(&ip->lock);
iput(ip);
return ip;
fail:
// something went wrong. de-allocate ip.
ip->nlink = 0;
iupdate(ip);
iunlockput(ip);
iunlockput(dp);
return 0;
// create()结束
iunlockput(ip);
end_op(); // log相关,暂忽略。会进行commit,真正执行文件操作。
return 0;
// 写文件流程
#define MAXOPBLOCKS 10 // max # of blocks any FS op writes
write // user
sys_write // syscall
filewrite(struct file *f, uint64 addr, int n)
// 从addr读n字节写到f
if(f->type == FD_INODE) // inode
int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE; // 一次写最大字节数=3k
int i = 0; // 已写字节数
while(i < n){
int n1 = n - i; // 剩余字节数
if(n1 > max) // 每次最多写max
n1 = max;
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
// 从addr的i偏移处,读n1字节写到inode的off偏移处。
writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off)
return -1;
if(off + n > MAXFILE*BSIZE)
return -1;
for(tot=0; tot<n; tot+=m, off+=m, src+=m){
uint addr = bmap(ip, off/BSIZE); // bmap(struct inode *ip, uint bn)
// bmap获取自身offset所对应的全局block号
// addr是全局block号
uint addr, *a;
struct buf *bp;
// bn=offset/BSIZE。落在addrs的哪个block
if(bn < NDIRECT){
// 如果落在direct里
if((addr = ip->addrs[bn]) == 0){
// 如果未分配block。分配新的全局block(标记bitmap)。返回block号
addr = balloc(ip->dev);
int b, bi, m;
struct buf *bp;
bp = 0;
// #define BPB (BSIZE*8)
// 一个block可包含8kbit
// 循环。每次前进8k。
// b为block号
for(b = 0; b < sb.size; b += BPB){
// 得到b的bitmap所在的block buf
bp = bread(dev, BBLOCK(b, sb));
// 遍历每个bit。不能超出BPB也不能超出总block数。
for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
m = 1 << (bi % 8);
if((bp->data[bi/8] & m) == 0){ // Is block free?
bp->data[bi/8] |= m; // Mark block in use.
log_write(bp);
brelse(bp);
bzero(dev, b + bi);
return b + bi;
}
}
brelse(bp);
}
printf("balloc: out of blocks\n");
return 0;
if(addr == 0)
return 0;
ip->addrs[bn] = addr;
}
return addr;
}
bn -= NDIRECT;
// 落在indirect里
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
// 分配承载indirect的block
if((addr = ip->addrs[NDIRECT]) == 0){
addr = balloc(ip->dev);
if(addr == 0)
return 0;
ip->addrs[NDIRECT] = addr;
}
// 读indirect block本身
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
// 读目标block。如未分配,分配新block。
if((addr = a[bn]) == 0){
addr = balloc(ip->dev);
if(addr){
a[bn] = addr;
log_write(bp);
}
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
if(addr == 0)
break;
// 到此得到目标block号addr
bp = bread(ip->dev, addr);
m = min(n - tot, BSIZE - off%BSIZE); // 计算边界
// 用户地址复制数据到kernel
if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
brelse(bp);
break;
}
log_write(bp);
brelse(bp);
}
// writei写完
if(off > ip->size)
ip->size = off;
// write the i-node back to disk even if the size didn't change
// because the loop above might have called bmap() and added a new
// block to ip->addrs[].
iupdate(ip);
return tot;
f->off += r;
iunlock(f->ip);
end_op();
if(r != n1){
// error from writei
break;
}
i += r;
}
ret = (i == n ? n : -1);
可以看到代码非常难看。
Logging层#
文件系统一个有趣的点是可防灾。当一个操作包含多个写时,如果一部分写之后,系统崩溃,那么此时文件系统的一致性就会出现问题。
比如宕机发生在删除文件期间。可能会造成:
indode包含了一个标记为free的block。
遗留一个分配了但无主的block。
后者情况稍好,前者在机器重启后可能造成严重问题。
重启后,kernel可能把这个free的block分配给其他文件,造成两个文件包含了同一个block,可相互篡改。
xv6的syscall不会直接写磁盘。它把所有写操作做成一个log
,需要执行时写一个commit
命令,这时syscall才实际执行这些写操作。完成后把log删除。
如果系统崩溃、重启,在执行任何进程之前,检查log。
如果log标记了commit,执行log里的写操作。
如果没标记commit,直接忽略。
最后把log删除。
Log 设计#
log存在superblock后面的log区域。包含一个header(头block)和后续的数据block(logged blocks)。
header包含一串sector编号,即logged blocks的编号。和logged blocks的计数。
开机时从磁盘读这个计数,如果是0,说明log里没有事务(transaction)。否则说明存在事务。
在可能崩溃的环境下,一系列的写操作必须时原子的。为了让不同的进程并发执行,logging系统能把多个system call的写操作放进一个事务里。
这样一个commit可能涵盖多个system call。为了防止一个system call被分割到多个事务,logging系统只会在没有文件相关的system call处于运行状态时才会commit。
多个事务同时commit称作group commit。这样能减少磁盘操作,因为分摊了固定的commit开销。
还有可能更快速的执行写操作,磁盘只需旋转一次就可完成。
xv6的virtio驱动不支持这种批量操作,但文件系统是支持的。
xv6用磁盘上固定的空间存log。事务里的写block总数b必须在这个空间范围之内。
这样有两个后果:
任何一个system call的写操作不能超出空间范围。
这对于多数system call没影响。除了write和unlink,会写大量block。一个大文件的写可能会写很多block,和bitmap block,和一个inode。
unlink一个大文件可能会写多个block和一个inode。
xv6的system call会把一个大的写操作分成小的写操作,放进log里。unlink不会造成这个问题。
其他后果是logging系统不会允许一个system call开始运行除非能确认它的写操作能放进log的剩余空间。
#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; // log数量
int block[LOGSIZE]; // 最多30个block号
};
// 全局唯一log
struct log {
struct spinlock lock;
int start;
int size;
int outstanding; // how many FS sys calls are executing.
int committing; // in commit(), please wait.
int dev;
struct logheader lh;
};
struct log log;
// fs相关的syscall之前必须调用
// 增加一个outstanding计数,占用一块log资源。
begin_op(void)
{
acquire(&log.lock);
while(1){
if(log.committing){ // commit中。死等。
sleep(&log, &log.lock);
} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){ // log空间不够。等待
// this op might exhaust log space; wait for commit.
sleep(&log, &log.lock);
} else {
log.outstanding += 1; // 正在处理计数+1
release(&log.lock);
break;
}
}
}
// 在begin_op后进行log
// 主要逻辑就是在log数据中添加buf的block号(不重复添加)。并refcnt++。
log_write(struct buf *b)
{
int i;
acquire(&log.lock);
if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
panic("too big a transaction"); // log满
if (log.outstanding < 1)
panic("log_write outside of trans"); // 逻辑严重错误
// 遍历。如果已存在可复用。
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno) // log absorption
break;
}
log.lh.block[i] = b->blockno;
// 如果不存在。refcnt++
if (i == log.lh.n) { // Add new block to log?
bpin(b);
acquire(&bcache.lock);
b->refcnt++;
release(&bcache.lock);
log.lh.n++;
}
release(&log.lock);
}
// fs操作完毕。进行commit。
end_op(void)
{
int do_commit = 0;
acquire(&log.lock);
log.outstanding -= 1; // 正在处理计数-1
if(log.committing)
panic("log.committing"); // 逻辑严重错误
if(log.outstanding == 0){
do_commit = 1;
log.committing = 1;
} else {
// begin_op() may be waiting for log space,
// and decrementing log.outstanding has decreased
// the amount of reserved space.
wakeup(&log);
}
release(&log.lock);
if(do_commit){
// call commit w/o holding locks, since not allowed
// to sleep with locks.
commit();
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
}
commit()
{
if (log.lh.n > 0) {
write_log(); // Write modified blocks from cache to log
int tail;
// 遍历log中的所有block。把这些block的实际数据写到磁盘(log区域)。
// 也就是把log数据本身固化到磁盘
// 会空出第一个位置给header
// 期间崩溃不要紧。因为header还没写到磁盘。直接算作把这次commit丢弃。
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // log block
struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
memmove(to->data, from->data, BSIZE);
bwrite(to); // 驱动磁盘实际写数据
brelse(from);
brelse(to);
}
// 磁盘log区域第一个block存header
write_head(); // Write header to disk -- the real commit
struct buf *buf = bread(log.dev, log.start); // 拿磁盘log区域的第一个block
struct logheader *hb = (struct logheader *) (buf->data); // 拿到header
int i;
hb->n = log.lh.n;
// 内存中log的block号写入buf
for (i = 0; i < log.lh.n; i++) {
hb->block[i] = log.lh.block[i];
}
// 写磁盘
bwrite(buf);
brelse(buf);
// 此时磁盘中已写入完整的log数据
install_trans(0); // Now install writes to home locations
int tail;
// 把磁盘的log数据中所有block写到对应的磁盘block。
// 期间崩溃不要紧。因为header还没写到磁盘。重启后会再次执行这个log的恢复流程。
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
if(recovering == 0)
bunpin(dbuf);
brelse(lbuf);
brelse(dbuf);
}
// 清空log并更新到磁盘。完成整个操作。
log.lh.n = 0;
write_head(); // Erase the transaction from the log
}
}
// 文件系统和log的初始化
#define ROOTDEV 1
struct superblock {
uint magic; // Must be FSMAGIC
uint size; // Size of file system image (blocks)
uint nblocks; // Number of data blocks
uint ninodes; // Number of inodes.
uint nlog; // Number of log blocks
uint logstart; // Block number of first log block
uint inodestart; // Block number of first inode block
uint bmapstart; // Block number of first free map block
};
struct superblock sb;
forkret(void)
static int first = 1;
if (first) {
// File system initialization must be run in the context of a
// regular process (e.g., because it calls sleep), and thus cannot
// be run from main().
first = 0;
// 第一次走到user态时。文件系统初始化。
fsinit(ROOTDEV);
// 读superblock
readsb(dev, &sb);
struct buf *bp;
// 1号为superblock
// 拿到buf,读取数据到sb后释放。
bp = bread(dev, 1);
memmove(sb, bp->data, sizeof(*sb));
brelse(bp);
if(sb.magic != FSMAGIC)
panic("invalid file system");
initlog(dev, &sb);
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: too big logheader");
initlock(&log.lock, "log");
// 获取log的信息
log.start = sb->logstart;
log.size = sb->nlog;
log.dev = dev;
// 尝试从log恢复
recover_from_log();
// 读header
read_head();
struct buf *buf = bread(log.dev, log.start);
struct logheader *lh = (struct logheader *) (buf->data);
int i;
log.lh.n = lh->n;
for (i = 0; i < log.lh.n; i++) {
log.lh.block[i] = lh->block[i];
}
brelse(buf);
// 把磁盘的log数据中所有block写到对应的磁盘block。
install_trans(1); // if committed, copy from log to disk
// 清空log并更新到磁盘。
log.lh.n = 0;
write_head();
}
看懂代码就更清楚了
核心思想就是把一系列文件操作做成一个原子操作。要么全部完成,要么什么都没发生。不存在中间状态,就达到了目的。
比如用户编辑一个新文件并保存,保存时断电。要么就成功保存完整的内容。要么就文件根本别存在,啥也没发生。
千万别出现保存了一部分内容这种中间状态。先
begin_op
,需要争log锁,开启一个transaction。更改某个buf后用
log_write
把该buf插入到内存log。
期间如果系统崩溃,不要紧,此时并没有写磁盘,把整个transaction视为作废即可。操作完后
end_op
,commit时把整个内存log存磁盘。
其中先write_log
写具体block数据,可包含多个bwrite
。期间如果系统崩溃也不要紧,只是写了磁盘log,还没有真正更新实际的block。把transaction视为作废即可。再
write_head
把log的header写到磁盘。
这个起到重要的开关作用,它只包含一个bwrite
,是原子性的,如果失败,即transaction作废。因为下次从log恢复时会依赖header的值,如果这次写失败,下次再读就会发现header里的log数为0,即逻辑上不存在log。
如果成功,磁盘里就包含了完整有效的log信息,磁盘中log.lh.n
变为buf数,而不是0。重启后就要根据log数据来恢复。最后把数据实际写到磁盘(从log恢复的流程跟此步骤基本一样,非常巧妙)
之前已经把本次transaction存到磁盘了,包括block数据和header。这时把磁盘log数据全读出来,写到对应的实际磁盘block。
再把磁盘log清空。完成整个transaction。
期间复制block数据可包含多个bwrite
,即使写到一半崩溃也是安全的。因为重启后还是能看到完整的磁盘log,可以重新执行这次transaction。
写完block数据后,最后同样是一个开关(清空磁盘log),原子操作,标志整个transaction是否成功。
先写实际的大量数据,最后原子操作写一个开关,可保证数据的完整性。不会出现开关打开了但实际数据不对。
File descriptor层#
fd层按”一切皆是文件”的思想实现了把多数资源都表示成一个文件,用一个fd表示。
前面的lab已经看过相关代码,比如lab5的创建pipe流程。
做题#
1. Large files#
默认inode的addrs有12个direct block,另有1个indirect block,里面包含256个block号。
所以默认代码一共12+256=268
个block。
需要把一个direct block变成两层indirect block,即一共11+256+256*256=65803
个block。
看bigfile.c,它每次循环写1k。默认最多只能写268,再写就报错。
修改NDIRECT
修改MAXFILE
修改dinode和inode的addrs长度
修改bmap。实现两层indirect block。
修改itrunc。free两层indirect block。
2. Symbolic links#
需要实现symlink,按hints操作即可。
新建T_SYMLINK
类型的文件,存入目标path。在open中按O_NOFOLLOW
确定是否需要追踪到实际文件。