本周完成了剩下的实验。上一篇见2024-xv6-labs-1

1. net lab

1.1 NIC

第一个实验是补全e1000_transmit()e1000_recv()

重点在于先要理解e1000初始化的代码,以及e1000_dev.h里给出的各寄存器的定义。

然后理清一下工作流程:
transmit所做的,其实就是将发送区和发送环的尾部更新为带发送的数据对应的元数据。注意此时status应为0而不是E1000_TXD_STAT_DD,因为这只是代表将数据填入发送,并没有真正发送。然后更新E100_TDT的值。但为了防止竞态条件,所以还要注意用e1000_lock加锁。

而receive所做的就是以帧为单位,不断地将缓冲区里的数据交给net_rx(),并更新缓冲区和E1000_RDT,直到遇见E1000_RXD_STAT_DD。这里不用加锁,但值得注意的是E1000_RDT必须在最后更新?想不通。

1.2 UDP Receive

说来惭愧,这个部分我实在没有思路,是让ai写的。。。

2. lock

2.1 Memory allocator

这里需要做的,就是kinit初始化内存管理的数据结构时,是初始化kmem[NCPU]而不是单个kmem,即通过格式化字符串的方式为每个kmem分配一把锁。然后其他跟单个kmem几乎一致。但是在kalloc里,如果某个kmem没有空闲页了,应找一个还有空闲页的kmem,拿一页进行分配。

注意在调用cpuid取当前cpu的id的时候要push_off()关中断,调用完毕要pop_off()重新开启。

但这个实验部分我个人觉得很有意思的就是格式化字符串

2.2 Buffer cache

这个题虽然是红题。但实际上本质并不复杂,它实际上说的是:

原有一个链表和一把大锁,可是这样太低效了,因为需要进行修改的可能是两个不同的节点,没有必要一把大锁把全局锁死。于是使用哈希表,将大链表拆成多个小链表分别存入每个哈希桶里,然后对每个哈希桶给予锁。这样需要进行修改的若是两个不同的节点,则可以实现并发了。

将原bcache的定义改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
  struct bucket{
struct spinlock lock;

// 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;
};

struct {
struct bucket table[NHASH];
struct buf buf[NBUF];
} bcache;

然后修改下面的代码,使其符合这个新数据结构即可。。。

才怪!!!🤣🤣🤣

还得有一步,否则panic: bget: no buffers

因为初始化时候,所有的缓存区都在同一个桶里,其他桶全为空!

此时就应该像上一题一样,从key对应的桶处“偷”一块缓存:

1
2
3
4
5
6
7
8
9
b->next->prev = b->prev;
b->prev->next = b->next;
release(&bcache.table[key].lock);
acquire(&bcache.table[gethash(blockno)].lock);
b->next = bcache.table[gethash(blockno)].head.next;
b->prev = &bcache.table[gethash(blockno)].head;
bcache.table[gethash(blockno)].head.next->prev = b;
bcache.table[gethash(blockno)].head.next = b;
release(&bcache.table[gethash(blockno)].lock);

3. fs

这个实验很奇怪,make grade时会因为qemu-system-riscv64: terminating on signal 15 from pid xxxx(make)导致fail,但是单独地在qemu环境中运行bigfile symlinktest usertests -q却没有问题。难道是我wsl2 ubuntu系统的版本问题吗?

3.1 Large files

这里就是将fs.h里面NDIRECT再减少一块作为双重间接块。然后添加定义

1
2
#define NDOUBLE (NINDIRECT*NINDIRECT)
#define MAXFILE (NDIRECT + NINDIRECT + NDOUBLE)

bmap里仿照分配间接块的写法,分配双重间接块。

itrunc里仿照释放间接块的写法,释放双重间接块。

(不过,这个是真绕啊)

这里一定要理清楚:对于双重间接块,256个块为一组,第0层的data就表示每一组。第一层的data则表示每一组的每一块的地址,而第二层的data才表示真正的数据

所以,对于分配,bn / NINDIRECT计算得组号;对于每个块在该组的序号则由bn % NINDIRECT给出。

对于释放,ip->addrs[NDIRECT+1]!=0意味着存在未释放的。因此要对每一个未释放的,读取它对应的第一层间接块的data,因为一个data[i]!=0就表示一个这一组未释放掉的块的地址。就这样从第2层一直到第0层逐层释放。

首先是sys_symlink,要在目标路径处创建一个T_SYMLINK类型的文件。并在对应的inode
里分配一个缓冲块,写入target,即源文件的路径。

后一步看起来复杂,实际上就是writei函数的调用。

然后是改写sys_open。这里主要是添加一个T_SYMLINK且不是O_NOFOLLOW的处理。

为了方便处理链接的文件也是符号链接的情况,用了一个递归的函数follow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  struct inode*
follow(char *path, int cnt){
if(cnt>=10){
return 0;
}
struct inode *ip = namei(path);
if(ip==0){
return 0;
}
ilock(ip);
if(ip->type != T_SYMLINK){
return ip;
}
char target[MAXPATH];
if(readi(ip, 0, (uint64)target, 0, MAXPATH) != MAXPATH){
iunlockput(ip);
return 0;
}
iunlockput(ip);
struct inode *ans = follow(target,cnt+1);
return ans;
}

ps:
sys_open是在干什么?
它是创建一个文件描述符(fd),将它绑定到一个 struct file*,供后续 read(fd) /write(fd) 使用。
示意如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 从用户拿参数:路径 和 打开方式
argstr(0, path);
argint(1, &omode);

// 2. 找到这个文件的 inode(如果没有而且带 O_CREATE,则创建)
ip = namei(path) / create(path);

// 3. 分配一个 file 结构
f = filealloc();

// 4. 分配一个 fd 并将其与 file* 绑定
fd = fdalloc(f); // 进程的 proc->ofile[fd] = f;

// 5. 初始化 file 结构:指向 inode,设置读写权限
f->ip = ip;
f->readable = ...
f->writable = ...

4. mmap

呃呃,这个也不是我自己写的。。。😭

这个实验我打算后面再“补票”。(虽然这个flag大概率会倒)