Large files (moderate)

要求

在此作业中,您将增加 xv6 文件的最大大小。目前 xv6 文件限制为 268 个块,或 268*BSIZE 字节(在 xv6 中 BSIZE 为 1024)。此限制来自这样一个事实,即 xv6 inode 包含 12 个“直接”块号和一个“单间接”块号,它指的是最多可容纳 256 个块号的块,总共 12+256=268 个块。

bigfile 命令创建它所能创建的最长文件,并报告该大小:

$ bigfile
..
wrote 268 blocks
bigfile: file is too small
$

测试失败,因为 bigfile 希望能够创建包含 65803 个块的文件,但未修改的 xv6 将文件限制为 268 个块。您将更改 xv6 文件系统代码以支持每个 inode 中的“双间接”块,其中包含 256 个单间接块地址,每个地址最多可以包含 256 个数据块地址。结果将是文件将能够包含多达 65803 个块,或 256*256+256+11 块(11 而不是 12,因为我们将牺牲一个直接块号来换取双间接块)。

准备工作

mkfs 程序创建 xv6 文件系统磁盘映像,并确定文件系统总共有多少块; 此大小由kernel/param.h中的 FSSIZE 控制。你将看到此实验存储库中的 FSSIZE 设置为 200,000 个块。您应该在 make 输出中看到以下来自 mkfs/mkfs 的输出:
nmeta 70 (boot, super, log blocks 30 inode blocks 13, bitmap blocks 25) blocks 199930 total 200000

这一行描述了 mkfs/mkfs 构建的文件系统:它有 70 个元数据块(用于描述文件系统的块)和 199,930 个数据块,总计 200,000 个块。

如果在实验期间的任何时候,您发现自己必须从头开始重建文件系统,则可以运行 make clean,哪些强制重建 fs.img

磁盘索引节点的格式由 fs.h 中的 struct dinode 定义。你应该对 NDIRECT、NINDIRECT、MAXFILEstruct dinodeaddrs[] 元素特别感兴趣。查看 xv6 文本中的图 8.3,了解标准 xv6 索引节点的示意图。

在磁盘上查找文件数据的代码位于 fs.cbmap() 中。看看它,确保你明白它在做什么。bmap() 在读取和写入文件时都会被调用。写入时,bmap() 根据需要分配新块来保存文件内容,如果需要保存块地址,还会分配间接块。

bmap() 处理两种块号。bn 参数是一个“逻辑块号”——文件中相对于文件开头的块号。ip->addrs[] 中的块号和 bread() 的参数是磁盘块号。您可以将 bmap() 视为将文件的逻辑块号映射到磁盘块号。

你的工作

修改 bmap(),使其实现一个双间接块,以及直接块和一个单间接块。您只有 11 个直接块,而不是 12 个,因为需要为您的新双间接块腾出空间; 不允许更改磁盘上 inode 的大小。ip->addrs[] 的前 11 个元素应该是直接块;第 12 个应该是一个单间接块(就像当前的块一样);第 13 个应该是您的新双间接块。当 bigfile 写入 65803 个块并且 usertests -q 成功运行时,您就完成了本练习:
$ bigfile
..................................
wrote 65803 blocks
done; ok
$ usertests -q
...
ALL TESTS PASSED
$ 

bigfile至少需要运行一分半钟才能。

提示

  • 确保您了解 bmap()。画出 ip->addrs[]、间接块、双间接块和它指向的单间接块与数据块之间的关系图。确保您了解为什么添加双间接块会使最大文件大小增加 256*256 个块(实际上是 -1,因为您必须将直接块的数量减少 1)。
  • 考虑如何使用逻辑块号为双间接块及其指向的间接块编制索引。
  • 如果你更改了NDIRECT的定义,你可能不得不在file.hstruct inode中更改addrs[]的声明。确保struct inodestruct dinode在其 addrs[] 数组中具有相同数量的元素。
  • 如果更改 NDIRECT 的定义,请确保创建一个新的 fs.img,因为 mkfs 使用 NDIRECT 来构建文件系统。
  • 如果您的文件系统进入错误状态,可能是崩溃,请删除 fs.img(从 Unix 而不是 xv6 执行此操作)。make 将为您构建一个新的干净文件系统映像。
  • 不要忘记 brelse()bread() 的每个块。
  • 您应该仅根据需要分配间接块和双重间接块,就像原始的 bmap() 一样。
  • 确保 itrunc 释放文件的所有块,包括双间接块。
  • usertests的运行时间比以前的实验更长,因为对于此实验,FSSIZE 更大,文件更大。

实现

思路

  1. 关系图
  2. bmap()原本的逻辑:

    • 判读索引是否小于NDIRECT。若小于说明是直接索引,因此可直接返回addrs[]中的地址(若该地址为0则需要先分配balloc()
    • 若大于则说明是间接索引。将原本的索引减去直接索引数量后得到在间接索引表中的索引。然后通过addrs[]获取间接索引表地址(若地址为0则需要分配),该地址需要bread()读取后存入struct buf b中。通过读取到的内容和之前得到的间接表索引可以得到真正的地址(若该地址为0则需要分配)。最后不要忘记brelse()释放读取的缓存。
  3. bigfile后应当增加的逻辑:

    • 若索引比一级间接索引的范围(256)还要大,说明到了二级索引的范围中。然后需要仿照一级索引读取方式通过bread()读取二级索引表,进而读取真正地址。
  4. itrunc()释放函数需要增加对二级间接索引的释放逻辑:仿照一级索引释放,但在释放真正地址之前再通过bread()读取释放....

实现

  1. 由于直接索引数量和最大数量变了,更改一下原来定义的宏:

    #define NDIRECT 11  // 原本12
    #define NINDIRECT (BSIZE / sizeof(uint))
    #define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT) // 原本没第三项

    改变索引后需要改变一些声明和定义:

    1. struct dinodeaddrs[]大小由 NDIRECT + 1 变为 NDIRECT + 2
    2. struct inodeaddrs[]大小由 NDIRECT + 1 变为 NDIRECT + 2
  2. bmap()添加二级索引映射:

    static uint bmap(struct inode* ip, uint bn) {
        uint addr, *a;
        struct buf* bp;
        // 直接索引不变
        if (bn < NDIRECT) {
            ...
        }
        bn -= NDIRECT;
    
        // 一级间接索引也不变
        if (bn < NINDIRECT) {
            ...
        }
        // 若超出一级索引范围则需要减去一级索引数量才得到二级索引
        bn -= NINDIRECT; 
    
        // 二级索引逻辑
        if (bn < NINDIRECT * NINDIRECT) {
            // 获取索引表地址,若没有则分配
            if ((addr = ip->addrs[NDIRECT + 1]) == 0) {
                addr = balloc(ip->dev);
                if (addr == 0) return 0;
                ip->addrs[NDIRECT + 1] = addr;
            }
            // 通过地址读取索引表
            bp = bread(ip->dev, addr);
            a = (uint*)bp->data;
            // bn/NINDIRECT得到在第一个索引表中的索引,若没有则分配
            if ((addr = a[bn / NINDIRECT]) == 0) {
                addr = balloc(ip->dev);
                if (addr) {  // 分配成功后将地址写入该缓存中
                    a[bn / NINDIRECT] = addr;
                    log_write(bp);
                } else {  // 失败后释放,然后返回0地址
                    brelse(bp);
                    return 0;
                }
            }
            brelse(bp); // 得到第二个索引表地址后可以释放第一个索引表了
            // 通过第二个索引表获取真正的地址
            bp = bread(ip->dev, addr);
            a = (uint*)bp->data;
            // 可通过bn%NINDIRECT获取在第二个索引表中的索引
            if ((addr = a[bn % NINDIRECT]) == 0) {
                // 若没有则分配。分配成功就写入缓存,不成功之后返回0地址
                addr = balloc(ip->dev);
                if (addr) {
                    a[bn % NINDIRECT] = addr;
                    log_write(bp);
                }
            }
            brelse(bp);
            return addr;
        }
        panic("bmap: out of range");
    }
  3. itrunc()添加释放二级间接索引的逻辑代码:

    void itrunc(struct inode* ip) {
        int i, j;
        // 添加定义用于释放二级间接索引
        struct buf *bp, *bpp; 
        uint *a, *aa;
    
        // 释放直接索引
        for (i = 0; i < NDIRECT; i++) {
            if (ip->addrs[i]) {
                bfree(ip->dev, ip->addrs[i]);
                ip->addrs[i] = 0;
            }
        }
    
        // 释放一级间接索引
        if (ip->addrs[NDIRECT]) {
            bp = bread(ip->dev, ip->addrs[NDIRECT]);
            a = (uint*)bp->data;
            for (j = 0; j < NINDIRECT; j++) {
                if (a[j]) bfree(ip->dev, a[j]);
            }
            brelse(bp);
            bfree(ip->dev, ip->addrs[NDIRECT]);
            ip->addrs[NDIRECT] = 0;
        }
    
        // 释放二间接级索引
        if (ip->addrs[NDIRECT + 1]) {
            bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
            a = (uint*)bp->data;
            for (i = 0; i < NINDIRECT; ++i) {
                if (a[i]) {  
                    // 原本直接释放的一级间接索引,
                    // 此时需要再次读取、判断并释放
                    bpp = bread(ip->dev, a[i]);
                    aa = (uint*)bpp->data;
                    for (j = 0; j < NINDIRECT; ++j) {
                        if (aa[j]) bfree(ip->dev, aa[j]);
                    }
                    brelse(bpp);  
                    bfree(ip->dev, a[i]);
                }
            }
            brelse(bp);
            bfree(ip->dev, ip->addrs[NDIRECT + 1]);
        }
    
        ip->size = 0;
        iupdate(ip);
    }

结果

  • 运行结果
    image.png
  • 测试结果
    image.png

Symbolic links (moderate)

在本练习中,您将向 xv6 添加符号链接。符号链接(或软链接)按路径名引用链接的文件; 打开符号链接时,内核会跟随指向引用文件的链接。符号链接类似于硬链接,但硬链接仅限于指向同一磁盘上的文件,而符号链接可以跨磁盘设备。尽管 xv6 不支持多个设备,但实现此系统调用是了解路径名查找工作原理的好练习。

要求

您将实现符号symlink(char *target, char *path) 系统调用,该调用在path处创建一个新的符号链接,该链接引用target文件。有关更多信息,请参见手册页symlink。要进行测试,请将符号链接测试添加到Makefile并运行它。当测试生成以下输出(包括用户测试成功)时,您的解决方案就完成了。
$ symlinktest
Start: test symlinks
test symlinks: ok
Start: test concurrent symlinks
test concurrent symlinks: ok
$ usertests -q
...
ALL TESTS PASSED
$ 

提示

  • 首先,为symlink创建一个新的系统调用序号,在user/usys.pluser/user.h中添加一个条目,并在kernel/sysfile.c中实现一个空sys_symlink
  • 将新的文件类型 (T_SYMLINK) 添加到 kernel/stat.h 以表示符号链接。
  • kernel/fcntl.h 添加一个新标志 (O_NOFOLLOW),该标志可用于open系统调用。请注意,传递给 open 的标志是使用按位或运算符组合的,因此新标志不应与任何现有标志重叠。这将允许您在将user/symlinktest.c添加到Makefile后对其进行编译。
  • 实现symlink(target, path)系统调用,以在path处创建目标为target的新符号链接。请注意,target不需要存在,系统调用就会成功。您需要选择某个位置来存储符号链接的目标路径,例如,在 inode 的数据块中。symlink应返回一个整数,表示成功(0)或失败(-1),类似于linkunlink
  • 修改open系统调用以处理路径引用符号链接的情况。如果该文件不存在,则open必须失败。当进程在标志中指定要open O_NOFOLLOW时,open应打开符号链接(而不是遵循符号链接)。
  • 如果链接的文件也是符号链接,则必须递归地跟随它,直到到达非链接文件。如果链接形成循环,则必须返回错误代码。如果链接深度达到某个阈值(例如10),您可以通过返回错误代码来近似这一点。
  • 其他系统调用(例如,linkunlink)不得遵循符号链接; 这些系统调用在符号链接本身上运行。
  • 本实验中您不必处理指向目录的符号链接。

实现

  1. 定义symlink系统调用

    • user.h中声明
    • usys.pl中添加条目
    • syscall.h中定义系统调用序号
    • syscall.c中声明sys_symlink()并将其指针放入数组
    • 最后在sysfile.c中完成sys_symlink()的定义
  2. sys_symlink()的定义。该系统调用的功能是在path生成一个类型为T_SYMLINK的文件,并在文件内容中写上target目标路径。由此可分为三步:(1)获取参数;(2)生成文件;(3)写入内容。

    uint64 sys_symlink(void) {
        char target[MAXPATH], path[MAXPATH];
        struct inode *ip;
        int len;
        
        // (1) 获取字符参数
        if ((len = argstr(0, target, MAXPATH)) < 0 || 
            argstr(1, path, MAXPATH) < 0) {
            return -1;
        }
    
        // (2) 创建symlink文件
        begin_op();
        if ((ip = create(path, T_SYMLINK, 0, 0)) == 0) {
            end_op();
            return -1;
        }
    
        // (3) 写入目标路径   
        // ilock(ip); 加上就会死锁,因为create中已经获取🔒 
        len = writei(ip, 0, (uint64)target, 0, len);
        iunlockput(ip);
        end_op();
        return len > 0 ? 0 : -1;
    }
  3. sys_open()中添加对symlink文件的处理逻辑:sys_open()原本的处理是:如果是O_CREATE模式就创建在path处的inode,否则就获取path处inode。一个symlink文件一定是在后者的选择分支中,因此可在获取inode后添加对symlink文件处理的代码。

    uint64 sys_open() {
        ...
        if (omode & O_CREATE) {
            // 创建inode
            ...
        } else {
            // 打开inode
            if((ip = namei(path)) == 0) {
                end_op();
                return -1;
            }
            ilock(ip);
    
            // 添加迭代读取symlink文件的逻辑代码
            if (ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0) {
                // opensymlink函数迭代读取直到获取真实inode或获取失败
                if ((ip = opensymlink(ip)) == 0) {
                    end_op();
                    return -1;
                }
            }
            
            if(ip->type == T_DIR && omode != O_RDONLY) {
                iunlockput(ip);
                end_op();
                return -1;
            }
        }
    }
  4. 通过迭代获取真实inode的函数struct inode* opensymlink(struct inode*)

    struct inode* opensymlink(struct inode* ip) {
     char target[MAXPATH];
     const int max_times = 10;  // 最大链接次数
     int i, times, nums[max_times];  
    
     // 通过循环max_times次来控制最大链接次数
     for (times = 0; times < max_times; ++times) {
         nums[times] = ip->inum; // 保存inode num用于判断是否已经迭代过
         // 读取target的路径,若失败释放🔒后返回0
         if (readi(ip, 0, (uint64)target, 0, MAXPATH) < 0) {
             iunlockput(ip);
             return 0;
         }
         iunlockput(ip); // 若成功读取,就可以释放该inode。且下一步要切换ip
    
         // 打开target路径文件,若失败则返回0
         if ((ip = namei(target)) == 0) {
             return 0;
         }
         // 通过inode num判断是否已经被迭代过,若有则说明构成环要及时返回
         for (i = 0; i < times; ++i) {
             if (nums[i] == nums[times]) {
                 printf("links form a cycle!\n");
                 return 0;
             }
         }
         // 函数进来和循环开始时是上🔒状态,因此这里也需要上锁后再退出函数或下一轮循环
         ilock(ip);
         // 若新打开的inode非T_SYMLINK类型说明获取了真正的inode,返回
         if (ip->type != T_SYMLINK) {
             return ip;
         }
     }
     printf("open: too deep!\n");
     return 0;
    }

结果

  • 运行结果
    image.png
  • 测试结果
    image.png

总结

make grade

image.png

个人收获

  1. xv6的文件系统包括七层,从底层的磁盘层到顶层的描述符层,分别负责不同的功能
    七层文件系统结构‘.png
  2. 文件系统通过struct inode组织每个文件(在磁盘中为struct dinode)。该结构体包括文件种类、大小、链接数量以及文件保存数据的数据块地址。具体结构如图
    image.png
  3. Unix一切皆文件。无论是设备、目录、链接还是真正的文件,都可以表示为inode,只是在访问和读写时操作有所区别。

Longfar
1 声望4 粉丝