XV6学习(14)Lab fs: File system
代码在github上。
这次实验是要对文件系统修改,使其支持更大的文件以及符号链接,实验本身并不是很复杂。但文件系统可以说是XV6中最复杂的部分,整个文件系统包括了七层:文件描述符,路径名,目录,inode,日志,缓冲区,磁盘。
文件描述符类似于Linux,将文件、管道、设备、套接字等都抽象为文件描述符,从而可以使用read
和write
系统调用对其进行读写。XV6的read
和write
是使用if-else来对描述符类型进行判断,选择对应的底层函数;而在Linux中,则是使用函数指针直接指向对应的底层函数,避免进行多次判断。
路径名则提供了根据路径名从目录系统中查找文件的功能。在路径查找过程中需要避免可能会出现的死锁,例如路径名中包含..
。
目录层类似于文件,目录文件的内部会保存该目录的目录项struct dirent
,其中包含了文件名和对应的inode号。在XV中目录查找是使用遍历目录项数组来依次比较,时间复杂度为O(n);而在NTFS、ZFS等文件系统中,会使用磁盘平衡树来组织目录项,使目录查找的复杂度降低为O(lgn)。
inode层为文件在磁盘上的组织,在磁盘中会有一块区域用于保存inode
信息,包括文件类型、大小、链接数以及文件每个块对应的磁盘块号。通过路径从目录系统中查找到对应的inode
号,之后就可以从磁盘上读取对应的inode
信息,之后就可以根据偏移量查找对应的磁盘块号,最后对其进行读写。
日志层提供了事务以及故障恢复的功能,当有多个磁盘操作必须原子完成时就要用到事务(如删除文件时要从目录中删除文件,删除文件对应的inode,对空闲块bitmap进行修改等)。日志先将操作写到磁盘的日志区上,写入完成后再写入commit
,最后再将所有操作真正写到磁盘上去。当在写入commit
之前发生故障,就不需要进行操作,因为事务没有被提交;当在写入commit
之后发生故障,就将日志区的日志全部重写一遍,保证事务被正确提交。
缓冲区则提供了磁盘块缓存,同时保证一个磁盘块在缓冲区中只有一个,使得同一时间只能有一个线程对同一个块进行操作,避免读到的数据不一致。
Large files (moderate)
这一个实验是要使XV6支持更大的文件。原始XV6中的文件块号dinode.addr
是使用一个大小为12的直接块表以及一个大小为256的一级块表,即文件最大为12+256
块。可以通过将一个直接块表中的项替换为一个二级块表来使系统支持大小为11+256+256*256
个块的文件。
首先修改对应的宏以及inode
定义。
#define NDIRECT 11
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT)
struct dinode {
...
uint addrs[NDIRECT+2]; // Data block addresses
};
struct inode {
...
uint addrs[NDIRECT+2]; // Data block addresses
};
之后修改bmap
函数,使其支持二级块表,其实就是重复一次块表的查询过程。
static uint
bmap(struct inode *ip, uint bn)
{
...
bn -= NINDIRECT;
if(bn < NINDIRECT * NINDIRECT){
// double indirect
int idx = bn / NINDIRECT;
int off = bn % NINDIRECT;
if((addr = ip->addrs[NDIRECT + 1]) == 0)
ip->addrs[NDIRECT + 1] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[idx]) == 0){
a[idx] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[off]) == 0){
a[off] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}
最后修改itrunc
函数使其能够释放二级块表对应的块,主要就是注意一下brelse
的调用就行了,仿照一级块表的处理就行了。
void
itrunc(struct inode *ip)
{
...
if(ip->addrs[NDIRECT + 1]){
bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
a = (uint*)bp->data;
struct buf *bpd;
uint* b;
for(j = 0; j < NINDIRECT; j++){
if(a[j]){
bpd = bread(ip->dev, a[j]);
b = (uint*)bpd->data;
for(int k = 0; k < NINDIRECT; k++){
if(b[k])
bfree(ip->dev, b[k]);
}
brelse(bpd);
bfree(ip->dev, a[j]);
}
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT + 1]);
ip->addrs[NDIRECT + 1] = 0;
}
ip->size = 0;
iupdate(ip);
}
Symbolic links (moderate)
这一个实验是要实现符号链接,符号链接就是在文件中保存指向文件的路径名,在打开文件的时候根据保存的路径名再去查找实际文件。与符号链接相反的就是硬链接,硬链接是将文件的inode
号指向目标文件的inode
,并将引用计数加一。
symlink
的系统调用实现起来也很简单,就是创建一个inode
,设置类型为T_SYMLINK
,然后向这个inode
中写入目标文件的路径就行了。
uint64
sys_symlink(void)
{
char target[MAXPATH];
memset(target, 0, sizeof(target));
char path[MAXPATH];
if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0){
return -1;
}
struct inode *ip;
begin_op();
if((ip = create(path, T_SYMLINK, 0, 0)) == 0){
end_op();
return -1;
}
if(writei(ip, 0, (uint64)target, 0, MAXPATH) != MAXPATH){
// panic("symlink write failed");
return -1;
}
iunlockput(ip);
end_op();
return 0;
}
最后在sys_open
中添加对符号链接的处理就行了,当模式不是O_NOFOLLOW
的时候就对符号链接进行循环处理,直到找到真正的文件,如果循环超过了一定的次数(10),就说明可能发生了循环链接,就返回-1。这里主要就是要注意namei
函数不会对ip
上锁,需要使用ilock
来上锁,而create
则会上锁。
uint64
sys_open(void)
{
...
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
...
}
if(ip->type == T_SYMLINK){
if(!(omode & O_NOFOLLOW)){
int cycle = 0;
char target[MAXPATH];
while(ip->type == T_SYMLINK){
if(cycle == 10){
iunlockput(ip);
end_op();
return -1; // max cycle
}
cycle++;
memset(target, 0, sizeof(target));
readi(ip, 0, (uint64)target, 0, MAXPATH);
iunlockput(ip);
if((ip = namei(target)) == 0){
end_op();
return -1; // target not exist
}
ilock(ip);
}
}
}
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
...
}