我穿过金黄的麦田
去给稻草人唱歌
等着落山风吹过
——如果有来生
完整代码见:SnowLegend-star/6.s081 at fs (github.com)
Large files(moderate)
这个part的难度我的评价是easy,真正写代码的时间也就十来分钟。下面来看看实验说明给出的hint
Hints:
1、确保您理解bmap()。写出ip->addrs[]、间接块、二级间接块和它所指向的一级间接块以及数据块之间的关系图。确保您理解为什么添加二级间接块会将最大文件大小增加256*256个块(实际上要-1,因为您必须将直接块的数量减少一个)。
将NDIRECT表示的直接索引数减一,给一个二级索引块腾出空间。此时可以容纳的最大文件大小为11+256+256*256=65803。需要注意的点事涉及到用NDIRECT声明的addrs数组的大小也应该做适当的改动。
2、考虑如何使用逻辑块号索引二级间接块及其指向的间接块。
模仿一级索引的读取方式再嵌套一层即可。
3、如果更改NDIRECT的定义,则可能必须更改file.h文件中struct inode中addrs[]的声明。确保struct inode和struct dinode在其addrs[]数组中具有相同数量的元素。
这就是第一点提到的注意维护涉及到NDIRECT的数组大小。
4、如果更改NDIRECT的定义,请确保创建一个新的fs.img,因为mkfs使用NDIRECT构建文件系统。
Lab: locks中提到删除fs.img再用make创建新的img的操作。
INNDIRECT=1024/4=256,我怎么下意识地感觉一个间接块可以容纳512个普通块指针了。
5、如果您的文件系统进入坏状态,可能是由于崩溃,请删除fs.img(从Unix而不是xv6执行此操作)。make将为您构建一个新的干净文件系统映像。
6、别忘了把你bread()的每一个块都brelse()。
7、您应该仅根据需要分配间接块和二级间接块,就像原始的bmap()。
8、确保itrunc释放文件的所有块,包括二级间接块。
修改完bmap()记得继续修改itrunc()。删除二级索引的相关内容可以模仿删除一级索引的内容。
分析完hints之后差不多就可以完成这个part了,故不再赘述。
bmap()
bn = bn - NINDIRECT ;
if(bn < NINDIRECT_level2){ //说明得指向二级块了
if((addr = ip->addrs[NDIRECT+1]) == 0) //ip->addr[12]不能为空
ip->addrs[NDIRECT+1]=addr=balloc(ip->dev);
bp=bread(ip->dev, addr); //读取一级索引目录块
a=(uint*)bp->data;
uint addr_NextLevel=bn/NINDIRECT; //读取bn位于哪个一级索引块内
if((addr=a[addr_NextLevel])==0){
a[addr_NextLevel]=addr=balloc(ip->dev);
log_write(bp);
}
brelse(bp);
//开始读取二级索引块
bp=bread(ip->dev, addr);
a=(uint*)bp->data;
int index=bn%NINDIRECT; //计算bn的索引块内偏移
if((addr=a[index])==0){
a[index]=addr=balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
itrunc()
......
if(ip->addrs[NDIRECT+1]){
bp=bread(ip->dev, ip->addrs[NDIRECT+1]);
a=(uint*) bp->data;
for(j=0; j<NINDIRECT; j++){
if(a[j]){
struct buf* bp2=bread(ip->dev, a[j]);
uint *a2=(uint*)bp2->data;
for(k=0;k<NINDIRECT;k++)
if(a2[k])
bfree(ip->dev, a2[k]);
brelse(bp2);
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)
符号链接这块感觉xv6文档和课上讲的内容都涉及的比较少,我也是看得一知半解。
提示:
1、首先,为symlink创建一个新的系统调用号,在user/usys.pl、user/user.h中添加一个条目,并在kernel/sysfile.c中实现一个空的sys_symlink。
2、向kernel/stat.h添加新的文件类型(T_SYMLINK)以表示符号链接。
前面两个hints都是老生常谈,照做就行。
3、在kernel/fcntl.h中添加一个新标志(O_NOFOLLOW),该标志可用于open系统调用。请注意,传递给open的标志使用按位或运算符组合,因此新标志不应与任何现有标志重叠。一旦将user/symlinktest.c添加到Makefile中,您就可以编译它。
新标志和现有标志一定不能有任何bit位上的重叠,我就是昏头设定了个0x03=0011,直接和O_WRONLY还有O_RDWR两个都重叠了,出现的结果直接搞得我汗流浃背了。
从未见过如此古怪的bug,西巴!
主要是碰到这种问题debug也没什么效果,只能靠printf大法进行排查。
4、实现symlink(target, path)系统调用,以在path处创建一个新的指向target的符号链接。请注意,系统调用的成功不需要target已经存在。您需要选择存储符号链接目标路径的位置,例如在inode的数据块中。symlink应返回一个表示成功(0)或失败(-1)的整数,类似于link和unlink。
到这里我就开始有点迷糊了,所幸GPT还是功能强大写了份大致框架出来。可以照着这个框架把symlink写出来。不过完成symlink时设计到的函数实在是太多,我第一天过了一遍的函数到了第三天都没啥映像了。完成这种设计大量函数调用的lab还是得趁热打铁。我参考其他博客才得以完成这个函数。
该函数即用来生成符号链接. 符号链接相当于一个特殊的独立的文件, 其中存储的数据即目标文件的路径.
因此在该函数中, 首先通过 create()
创建符号链接路径对应的 inode 结构(同时使用 T_SYMLINK
与普通的文件进行区分). 然后再通过 writei()
将链接的目标文件的路径写入 inode (的 block)中即可. 在这个过程中, 无需判断连接的目标路径是否有效.
需要注意的是关于文件系统的一些加锁释放的规则. 函数 create()
会返回创建的 inode, 此时也会持有其 inode 的锁. 而后续 writei()
是需要在持锁的情况下才能写入. 在结束操作后(不论成功与否), 都需要调用 iunlockput()
来释放 inode 的锁和其本身, 该函数是 iunlock()
和 iput()
的组合, 前者即释放 inode 的锁; 而后者是减少一个 inode 的引用(对应字段 ref
, 记录着内存中指向该 inode 的指针数, 这里的 inode 实际上是内存中的 inode, 是从内存的 inode 缓存 icache
分配出来的, 当 ref
为 0 时就会回收到 icache
中), 表示当前已经不需要持有该 inode 的指针对其继续操作了.
5、修改open系统调用以处理路径指向符号链接的情况。如果文件不存在,则打开必须失败。当进程向open传递O_NOFOLLOW标志时,open应打开符号链接(而不是跟随符号链接)。
什么是打开符号链接?这里的描述也是不尽人意,还是得参考别的博主给的分析。
该函数使用来打开文件的, 对于符号链接一般情况下需要打开的是其链接的目标文件, 因此需要对符号链接文件进行额外处理.
考虑到跟踪符号链接的操作相对独立, 此处笔者编写了一个独立的函数 follow_symlink()
用来寻找符号链接的目标文件.
在跟踪符号链接时需要额外考虑到符号链接的目标可能还是符号链接, 此时需要递归的去跟踪目标链接直至得到真正的文件. 而这其中需要解决两个问题: 一是符号链接可能成环, 这样会一直递归地跟踪下去, 因此需要进行成环的检测; 另一方面是需要对链接的深度进行限制, 以减轻系统负担(这两点也是实验的要求).
而对于成环的检测, 此处也是选择了最简单的算法: 创建一个大小为 NSYMLINK
的数组 inums[]
用于记录每次跟踪到的文件的 inode number, 每次跟踪到目标文件的 inode 后都会将其 inode number 与 inums
数组中记录的进行比较, 若有重复则证明成环.
因此整体上 follow_symlink()
函数的流程其实比较简单, 就是至多循环 NSYMLINK
次去递归的跟踪符号链接: 使用 readi()
函数读出符号链接文件中记录的目标文件的路径, 然后使用 namei()
获取文件路径对应的 inode, 然后与已经记录的 inode number 比较判断是否成环. 直到目标 inode 的类型不为 T_SYMLINK
即符号链接类型.
而在 sys_open()
中, 需要在创建文件对象 f=filealloc()
之前, 对于符号链接, 在非 NO_FOLLOW
的情况下需要将当前文件的 inode 替换为由 follow_symlink()
得到的目标文件的 inode 再进行后续的操作.
最后考虑这个过程中的加锁释放的规则. 对于原本不考虑符号链接的情况, 在 sys_open()
中, 由 create()
或 namei()
获取了当前文件的 inode 后实际上就持有了该 inode 的锁, 直到函数结束才会通过 iunlock()
释放(当执行成功时未使用 iput()
释放 inode 的 ref
引用, 笔者认为后续到 sys_close()
调用执行前该 inode 一直会处于活跃状态用于对该文件的操作, 因此不能减少引用). 而对于符号链接, 由于最终打开的是链接的目标文件, 因此一定会释放当前 inode 的锁转而获取目标 inode 的锁. 而在处理符号链接时需要对 ip->type
字段进行读取, 自然此时也不能释放 inode 的锁, 因此在进入 follow_symlink()
时一直保持着 inode 的锁的持有, 当使用 readi()
读取了符号链接中记录的目标文件路径后, 此时便不再需要当前符号链接的 inode, 便使用 iunlockput()
释放锁和 inode. 当在对目标文件的类型判断是否不为符号链接时, 此时再对其进行加锁. 这样该函数正确返回时也会持有目标文件 inode 的锁, 达到了函数调用前后的一致.
这里的采用的方法是直接在open()内部加一个for循环用来判断是否成环
6、如果链接文件也是符号链接,则必须递归地跟随它,直到到达非链接文件为止。如果链接形成循环,则必须返回错误代码。你可以通过以下方式估算存在循环:通过在链接深度达到某个阈值(例如10)时返回错误代码。
如上。不过这种强行设定循环阈值的方法和以前在LeetCode上写到的一题有点类似,那题我记得是只要循环次数大于几十万次就可以判定查找失败了,原因也是搜索点会在一个接近封闭的环内部沿着边界重复探查。
7、其他系统调用(如link和unlink)不得跟随符号链接;这些系统调用对符号链接本身进行操作。
8、您不必处理指向此实验的目录的符号链接。
最后两个hints我是真没看懂,不过忽略这两个也可以完成lab。
sys_open()
uint depth=0;
while(ip->type==T_SYMLINK && !(omode & O_NOFOLLOW)){
if(depth>=10){
iunlockput(ip);
end_op();
return -1;
}
if(readi(ip, 0, (uint64)path, 0, MAXPATH) != MAXPATH){
iunlockput(ip);
end_op();
return -1;
}
iunlockput(ip);
if((ip=namei(path))==0){
end_op();
return -1;
}
depth++;
ilock(ip);
}
sys_symlink()
uint64
sys_symlink(void){
char target[MAXPATH], path[MAXPATH];
struct inode *ip;
int n;
if(argstr(0, target, MAXPATH)<0 || argstr(1, path, MAXPATH)<0){
return -1;
}
begin_op();
if((ip=create(path, T_SYMLINK, 0, 0)) == 0){
end_op();
return -1;
}
if((n=writei(ip, 0, (uint64)target, 0, MAXPATH)) < MAXPATH){
iunlockput(ip);
end_op();
return -1;
}
iunlockput(ip);
end_op();
return 0;
}
总而言之part02确实有点抽象,让人有点无从下手的感觉。还是靠着其他博客的分析才磕磕绊绊完成,自从正式上手s081后也是好久没碰到这种情况了。总的来说part01给个easy,part02给个hard难度我是比较认可的。