├── .vscode
└── settings.json
├── README.md
├── doc
├── Copy-On-Write.md
├── Locking.md
├── cow.assets
│ ├── image-20230509135805346.png
│ └── image-20230509140247324.png
├── cow.md
├── fs.md
├── mmap.md
├── net.assets
│ └── ptPxv.png
├── net.md
├── pagetable.assets
│ ├── image-20221106201148103.png
│ └── image-20221106202219124.png
├── pagetable.md
├── syscall.assets
│ ├── image-20221029213304626.png
│ ├── image-20221029213416970.png
│ ├── image-20221029213443990.png
│ ├── image-20221029213635688.png
│ ├── image-20221029214535825.png
│ ├── image-20221030190809519.png
│ ├── image-20221030191403194.png
│ ├── image-20221030192658267.png
│ ├── image-20221030200810283.png
│ ├── image-20221030200839704.png
│ ├── image-20221030201332014.png
│ ├── image-20221030201953431.png
│ ├── image-20221030203515710.png
│ └── image-20221030204838501.png
├── syscall.md
├── thread.md
├── traps.assets
│ ├── image-20221119182846614.png
│ ├── image-20221119183441521.png
│ ├── image-20221119183727421.png
│ ├── image-20221119183826612.png
│ ├── image-20221119184248121.png
│ ├── image-20221119184424152.png
│ └── image-20221119185052056.png
├── traps.md
├── utils.assets
│ └── 1.png
├── utils.md
└── 如何将一个二进制值的指定位设置为指定的值.md
└── mkfs
└── mkfs.c
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "files.associations": {
3 | "sysinfo.h": "c"
4 | }
5 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # xv6-labs-2022-solutions
2 |
3 | MIT 6.828 (6.S081) (6.1810) xv6-labs-2022 实验的答案和解析。
4 |
5 | 各 Lab 的代码都在相应的 Branch 分支中。
6 |
7 | 课程主页:[https://pdos.csail.mit.edu/6.828/2022/schedule.html](https://pdos.csail.mit.edu/6.828/2022/schedule.html)
8 |
9 | ## Labs 代码和解析
10 |
11 | Lab: Xv6 and Unix utilities : [解析](./doc/utils.md) [代码分支](https://github.com/relaxcn/xv6-labs-2022-solutions/tree/util)
12 |
13 | Lab: system calls :[解析](./doc/syscall.md) [代码分支](https://github.com/relaxcn/xv6-labs-2022-solutions/tree/syscall)
14 |
15 | Lab: Page tables: [解析](./doc/pagetable.md) [代码分支](https://github.com/relaxcn/xv6-labs-2022-solutions/tree/pgtbl)
16 |
17 | Lab: Traps:[解析](./doc/traps.md) [代码分支](https://github.com/relaxcn/xv6-labs-2022-solutions/tree/traps)
18 |
19 | Lab Copy on-write [解析](./doc/cow.md) [代码分支](https://github.com/relaxcn/xv6-labs-2022-solutions/tree/cow)
20 |
21 | Lab Multithreading [解析](./doc/thread.md) [代码分支](https://github.com/relaxcn/xv6-labs-2022-solutions/tree/thread)
22 |
23 | Lab network driver [解析](./doc/net.md)
24 |
25 | Lab Lock [解析](./doc/Locking.md)
26 |
27 | Lab File system [解析](./doc/fs.md)
28 |
29 | Lab mmap [解析](./doc/mmap.md)
30 |
--------------------------------------------------------------------------------
/doc/Copy-On-Write.md:
--------------------------------------------------------------------------------
1 | ## Lab: Copy-On-Write Fork for xv6
2 |
3 | ### 1. 概览
4 |
5 | ---
6 |
7 | xv6 操作系统中原来对于 fork()的实现是将父进程的用户空间全部复制到子进程的用户空间。但如果父进程地址空间太大,那这个复制过程将非常耗时。另外,现实中经常出现 fork() + exec() 的调用组合,这种情况下 fork()中进行的复制操作完全是浪费。基于此,我们可以利用页表实现写时复制机制。
8 |
9 | ### 2. 具体实现
10 |
11 | ---
12 |
13 | #### 2.1 改写 fork()
14 |
15 | 在 xv6 的 fork 函数中,会调用 uvmcopy 函数给子进程分配页面,并将父进程的地址空间里的内容拷贝给子进程。改写 uvmcopy 函数,不再给子进程分配页面,而是将父进程的物理页映射进子进程的页表,并将两个进程的 PTE_W 都清零。
16 |
17 | ```c
18 | int
19 | uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
20 | {
21 | pte_t *pte;
22 | uint64 pa, i;
23 | uint flags;
24 |
25 | for(i = 0; i < sz; i += PGSIZE){
26 | if((pte = walk(old, i, 0)) == 0)
27 | panic("uvmcopy: pte should exist");
28 | if((*pte & PTE_V) == 0)
29 | panic("uvmcopy: page not present");
30 | pa = PTE2PA(*pte);
31 | reference_count[pa >> 12] += 1; // reference count ++;
32 | *pte &= ~PTE_W; // both child and parent can not write into this page
33 | *pte |= PTE_COW; // flag the page as copy on write
34 | flags = PTE_FLAGS(*pte);
35 | if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
36 | goto err;
37 | }
38 | }
39 | return 0;
40 |
41 | err:
42 | uvmunmap(new, 0, i / PGSIZE, 1);
43 | return -1;
44 | }
45 | ```
46 |
47 | 上面函数的实现逻辑也很简单,首先利用 walk 函数在父进程的页表中找到对应虚拟地址的物理地址,然后将该物理地址映射进子进程的页表,同时注意设置 PTE_COW 位以及清除 PTE_W 位。
48 |
49 | #### 2.2 编写 COW handler
50 |
51 | 此时父子进程对所有的 COW 页都没有写权限,如果某个进程试图对某个页进行写,就会触发 page fault(scause = 15),因此需要在 trap.c/usertrap 中处理这个异常。
52 |
53 | ```c
54 | if (r_scause() == 15) // write page fault
55 | {
56 | if (cowhandler(p->pagetable, r_stval()) < 0)
57 | p->killed = 1;
58 | }
59 | ```
60 |
61 | 我们会检查 scause 寄存器的值是否是 15,如果是的话就调用 cowhandler 函数。
62 |
63 | ```c
64 | // allocate a new page for the COW
65 | // return -1 if the va is invalid or illegal.
66 | int cowhandler(pagetable_t pagetable, uint64 va)
67 | {
68 | if (va >= MAXVA)
69 | return -1;
70 | pte_t *pte;
71 | pte = walk(pagetable, va, 0);
72 | if (pte == 0) return -1;
73 | if ((*pte & PTE_U) == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_COW) == 0)
74 | return -1;
75 |
76 | // allocate a new page
77 | uint64 pa = PTE2PA(*pte); // original physical address
78 | uint64 ka = (uint64) kalloc(); // newly allocated physical address
79 |
80 | if (ka == 0){
81 | return -1;
82 | }
83 | memmove((char*)ka, (char*)pa, PGSIZE); // copy the old page to the new page
84 | kfree((void*)pa);
85 | uint flags = PTE_FLAGS(*pte);
86 | *pte = PA2PTE(ka) | flags | PTE_W;
87 | *pte &= ~PTE_COW;
88 | return 0;
89 | }
90 | ```
91 |
92 | cowhandler 做的事情也很简单,它首先会检查一系列权限位,然后分配一个新的物理页,并将它映射到产生缺页异常的进程的页表中,同时设置写权限位。
93 |
94 | #### 2.3 增加物理页计数器
95 |
96 | 由于现在可能有多个进程拥有同一个物理页,如果某个进程退出时 free 掉了这个物理页,那么其他进程就会出错。所以我们得设置一个全局数组,记录每个物理页被几个进程所拥有。同时注意这个数组可能会被多个进程同时访问,因此需要用一个锁来保护。
97 |
98 | ```c
99 | int reference_count[PHYSTOP >> 12];
100 | struct spinlock ref_cnt_lock;
101 | ```
102 |
103 | 每个物理页所对应的计数器将在下面几个函数内被修改:
104 |
105 | 首先在 kalloc 分配物理页函数中将对应计数器置为 1
106 |
107 | ```c
108 | // Allocate one 4096-byte page of physical memory.
109 | // Returns a pointer that the kernel can use.
110 | // Returns 0 if the memory cannot be allocated.
111 | void *
112 | kalloc(void)
113 | {
114 | struct run *r;
115 |
116 | acquire(&kmem.lock);
117 | r = kmem.freelist;
118 | if(r) {
119 | kmem.freelist = r->next;
120 | acquire(&ref_cnt_lock);
121 | reference_count[(uint64)r>>12] = 1; // first allocate, reference = 1
122 | release(&ref_cnt_lock);
123 | }
124 | release(&kmem.lock);
125 |
126 | if(r) memset((char*)r, 5, PGSIZE); // fill with junk
127 | return (void*)r;
128 | }
129 | ```
130 |
131 | 进程在 fork 时会调用 uvmcopy 函数,我们要在其中将 COW 页对应的计数器加 1。
132 |
133 | 另外在某个进程想 free 掉某个物理页时,我们要将其计数器减 1。
134 |
135 | ```c
136 | void
137 | kfree(void *pa)
138 | {
139 | struct run *r;
140 | int tmp, pn;
141 |
142 | if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
143 | panic("kfree");
144 |
145 | acquire(&ref_cnt_lock);
146 | pn = (uint64) pa >> 12;
147 | if (reference_count[pn] < 1)
148 | panic("kfree ref");
149 | reference_count[pn] -= 1;
150 | tmp = reference_count[pn];
151 | release(&ref_cnt_lock);
152 |
153 | if (tmp > 0) return;
154 | // Fill with junk to catch dangling refs.
155 | memset(pa, 1, PGSIZE);
156 |
157 | r = (struct run*)pa;
158 |
159 | acquire(&kmem.lock);
160 | r->next = kmem.freelist;
161 | kmem.freelist = r;
162 | release(&kmem.lock);
163 | }
164 | ```
165 |
166 | #### 2.4 修改 copyout
167 |
168 | 最后,如果内核调用 copyout 函数试图修改一个进程的 COW 页,也需要进行 cowhandler 类似的操作来处理。
169 |
170 | ```c
171 | int
172 | copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
173 | {
174 | uint64 n, va0, pa0;
175 | pte_t *pte;
176 |
177 | while(len > 0){
178 | va0 = PGROUNDDOWN(dstva);
179 | pa0 = walkaddr(pagetable, va0);
180 | if (pa0 == 0) {
181 | return -1;
182 | }
183 | pte = walk(pagetable, va0, 0);
184 | if (*pte & PTE_COW)
185 | {
186 | // allocate a new page
187 | uint64 ka = (uint64) kalloc(); // newly allocated physical address
188 |
189 | if (ka == 0){
190 | struct proc *p = myproc();
191 | p->killed = 1; // there's no free memory
192 | } else {
193 | memmove((char*)ka, (char*)pa0, PGSIZE); // copy the old page to the new page
194 | uint flags = PTE_FLAGS(*pte);
195 | uvmunmap(pagetable, va0, 1, 1);
196 | *pte = PA2PTE(ka) | flags | PTE_W;
197 | *pte &= ~PTE_COW;
198 | pa0 = ka;
199 | }
200 | }
201 | n = PGSIZE - (dstva - va0);
202 | if(n > len)
203 | n = len;
204 | memmove((void *)(pa0 + (dstva - va0)), src, n);
205 |
206 | len -= n;
207 | src += n;
208 | dstva = va0 + PGSIZE;
209 | }
210 | return 0;
211 | }
212 |
213 | ```
214 |
--------------------------------------------------------------------------------
/doc/Locking.md:
--------------------------------------------------------------------------------
1 | ## Lab: Locking
2 |
3 | #### 1. 概述
4 |
5 | ---
6 |
7 | 在并发编程中我们经常用到锁来解决同步互斥问题,但是一个多核机器上对锁的使用不当会带来很多的所谓 “lock contention” 问题。这个 lab 的目标就是对涉及到锁的数据结构进行修改已降低对锁的竞争。
8 |
9 | #### 2. Memory allocator
10 |
11 | ---
12 |
13 | 第一部分涉及到内存分配的代码,xv6 将空闲的物理内存 kmem 组织成一个空闲链表 kmem.freelist,同时用一个锁 kmem.lock 保护 freelist,所有对 kmem.freelist 的访问都需要先取得锁,所以会产生很多竞争。解决方案也很直观,给每个 CPU 单独开一个 freelist 和对应的 lock,这样只有同一个 CPU 上的进程同时获取对应锁才会产生竞争。
14 |
15 | ```c
16 | struct {
17 | struct spinlock lock;
18 | struct run *freelist;
19 | } kmem[NCPU];
20 | ```
21 |
22 | 同时得修改对应的 kinit 和 kfree 的代码以适应数据结构的修改
23 |
24 | ```c
25 | void
26 | kinit()
27 | {
28 | char buf[10];
29 | for (int i = 0; i < NCPU; i++)
30 | {
31 | snprintf(buf, 10, "kmem_CPU%d", i);
32 | initlock(&kmem[i].lock, buf);
33 | }
34 | freerange(end, (void*)PHYSTOP);
35 | }
36 | void
37 | kfree(void *pa)
38 | {
39 | struct run *r;
40 |
41 | if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
42 | panic("kfree");
43 |
44 | // Fill with junk to catch dangling refs.
45 | memset(pa, 1, PGSIZE);
46 |
47 | r = (struct run*)pa;
48 |
49 | push_off();
50 | int cpu = cpuid();
51 | pop_off();
52 | acquire(&kmem[cpu].lock);
53 | r->next = kmem[cpu].freelist;
54 | kmem[cpu].freelist = r;
55 | release(&kmem[cpu].lock);
56 | }
57 | ```
58 |
59 | 另外,一个相对麻烦的问题是当一个 CPU 的 freelist 为空时,需要向其他 CPU 的 freelist“借”空闲块。
60 |
61 | ```c
62 | void *
63 | kalloc(void)
64 | {
65 | struct run *r;
66 |
67 | push_off();
68 | int cpu = cpuid();
69 | pop_off();
70 |
71 | acquire(&kmem[cpu].lock);
72 | r = kmem[cpu].freelist;
73 | if(r)
74 | kmem[cpu].freelist = r->next;
75 | else // steal page from other CPU
76 | {
77 | struct run* tmp;
78 | for (int i = 0; i < NCPU; ++i)
79 | {
80 | if (i == cpu) continue;
81 | acquire(&kmem[i].lock);
82 | tmp = kmem[i].freelist;
83 | if (tmp == 0) {
84 | release(&kmem[i].lock);
85 | continue;
86 | } else {
87 | for (int j = 0; j < 1024; j++) {
88 | // steal 1024 pages
89 | if (tmp->next)
90 | tmp = tmp->next;
91 | else
92 | break;
93 | }
94 | kmem[cpu].freelist = kmem[i].freelist;
95 | kmem[i].freelist = tmp->next;
96 | tmp->next = 0;
97 | release(&kmem[i].lock);
98 | break;
99 | }
100 | }
101 | r = kmem[cpu].freelist;
102 | if (r)
103 | kmem[cpu].freelist = r->next;
104 | }
105 | release(&kmem[cpu].lock);
106 |
107 | if(r)
108 | memset((char*)r, 5, PGSIZE); // fill with junk
109 | return (void*)r;
110 | }
111 | ```
112 |
113 | #### 3. Buffer cache
114 |
115 | ---
116 |
117 | Buffer cache 是 xv6 的文件系统中的数据结构,用来缓存部分磁盘的数据块,以减少耗时的磁盘读写操作。但这也意味着 buffer cache 的数据结构是所有进程共享的(不同 CPU 上的也是如此),如果只用一个锁 bcache.lock 保证对其修改的原子性的话,势必会造成很多的竞争。
118 |
119 | 我的解决策略是根据数据块的 blocknumber 将其保存进一个哈希表,而哈希表的每个 bucket 都有一个相应的锁来保护,这样竞争只会发生在两个进程同时访问同一个 bucket 内的 block。
120 |
121 | bcache 的数据结构如下:
122 |
123 | ```c
124 | struct {
125 | struct spinlock lock;
126 | struct buf head[NBUCKET];
127 | struct buf hash[NBUCKET][NBUF];
128 | struct spinlock hashlock[NBUCKET]; // lock per bucket
129 | } bcache;
130 | ```
131 |
132 | 相应地,需要修改 binit, bget 和 breles 函数:
133 |
134 | ```c
135 | void
136 | binit(void)
137 | {
138 | struct buf *b;
139 |
140 | initlock(&bcache.lock, "bcache");
141 | for (int i = 0; i < NBUCKET; i++) {
142 | initlock(&bcache.hashlock[i], "bcache");
143 |
144 | // Create linked list of buffers
145 | bcache.head[i].prev = &bcache.head[i];
146 | bcache.head[i].next = &bcache.head[i];
147 | for(b = bcache.hash[i]; b < bcache.hash[i]+NBUF; b++){
148 | b->next = bcache.head[i].next;
149 | b->prev = &bcache.head[i];
150 | initsleeplock(&b->lock, "buffer");
151 | bcache.head[i].next->prev = b;
152 | bcache.head[i].next = b;
153 | }
154 | }
155 | }
156 | static struct buf*
157 | bget(uint dev, uint blockno)
158 | {
159 | struct buf *b;
160 |
161 | uint hashcode = blockno % NBUCKET;
162 | acquire(&bcache.hashlock[hashcode]);
163 |
164 | // Is the block already cached?
165 | for(b = bcache.head[hashcode].next; b != &bcache.head[hashcode]; b = b->next){
166 | if(b->dev == dev && b->blockno == blockno){
167 | b->refcnt++;
168 | release(&bcache.hashlock[hashcode]);
169 | acquiresleep(&b->lock);
170 | return b;
171 | }
172 | }
173 |
174 | // Not cached.
175 | // Recycle the least recently used (LRU) unused buffer.
176 | for(b = bcache.head[hashcode].prev; b != &bcache.head[hashcode]; b = b->prev){
177 | if(b->refcnt == 0) {
178 | b->dev = dev;
179 | b->blockno = blockno;
180 | b->valid = 0;
181 | b->refcnt = 1;
182 | release(&bcache.hashlock[hashcode]);
183 | acquiresleep(&b->lock);
184 | return b;
185 | }
186 | }
187 | panic("bget: no buffers");
188 | }
189 |
190 | void
191 | brelse(struct buf *b)
192 | {
193 | if(!holdingsleep(&b->lock))
194 | panic("brelse");
195 |
196 | releasesleep(&b->lock);
197 |
198 | uint hashcode = b->blockno % NBUCKET;
199 | acquire(&bcache.hashlock[hashcode]);
200 | b->refcnt--;
201 | if (b->refcnt == 0) {
202 | // no one is waiting for it.
203 | b->next->prev = b->prev;
204 | b->prev->next = b->next;
205 | b->next = bcache.head[hashcode].next;
206 | b->prev = &bcache.head[hashcode];
207 | bcache.head[hashcode].next->prev = b;
208 | bcache.head[hashcode].next = b;
209 | }
210 |
211 | release(&bcache.hashlock[hashcode]);
212 | }
213 | ```
214 |
--------------------------------------------------------------------------------
/doc/cow.assets/image-20230509135805346.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/cow.assets/image-20230509135805346.png
--------------------------------------------------------------------------------
/doc/cow.assets/image-20230509140247324.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/cow.assets/image-20230509140247324.png
--------------------------------------------------------------------------------
/doc/cow.md:
--------------------------------------------------------------------------------
1 | # Lab: Copy-on-Write Fork for xv6
2 |
3 | 虚拟内存提供了一种间接性:内核可以将 PTE 标记为无效 (invalid) 或者只读 (read-only) 来阻止内存引用,并且导致页面故障 (page faults) 。在计算机系统中有一个说法,任何系统问题都能通过一定的间接性来解决。本次实验探索了一个例子:写时复制 (copy-on-write) fork.
4 |
5 | 开始之前切换到 `cow` 分支:
6 |
7 | ```bash
8 | git fetch
9 | git checkout cow
10 | make clean
11 | ```
12 |
13 | ## 问题描述
14 |
15 | xv6 中的 `fork()` 系统调用将父进程的所有用户空间内存都拷贝到子进程。如果父进程很大,那么这种拷贝将会花费很长时间。更糟糕的是,这经常会出现很大的浪费,因为 `fork()` 在子进程中,通常后面跟着 `exec()`,它通常使用已复制内存的很少一部分,这样就浪费了大量的内存空间。
16 |
17 | ## 解决方法
18 |
19 | 你的任务是实现写时复制 (copy-on-write) fork(),它可以推迟实际的物理内存的分配,直到真正使用要使用的时候,才会复制物理内存页。
20 |
21 | COW fork() 仅仅为子进程创建了一个页表 (pagetable) ,页表中映射用户内存的 PTE 实际指向的是父进程的物理页。COW fork() 将父进程和子进程的用户 PTEs(user PTEs) 标记为只读 (read-only)。当任何一个进程试图向 COW 页写入操作的时候,CPU 将会引发一个页面错误 (page fault)。内核中的页面错误处理函数将会检测到这种情况,为发生错误的进程分配一个物理内存页,并把原始页(也就是父进程和子进程共享的物理页)中的数据复制到新分配的页中,然后修改故障进程中相关的 PTE ,使其指向新分配的物理页的地址,最后将此 PTE 标记为可写 (writeable)。 错误页面处理函数完成任务之后,用户进程对于它的复制页将是可写的。
22 |
23 | COW fork() 使得用户内存的物理页面的释放变得稍微有些复杂。一个给定的物理页有可能同时被多个进程所引用,这是因为由于 COW 的延迟分配机制,父进程和子进程共享同一个物理页所导致的。所以,我们应该在释放一个物理页的时候,确保该物理页没有一个进程在使用。在 xv6 这种简单内核中,我们的实现非常直接简单,仅仅使用一个数组来进行标记。但是在成熟内核中,这可能很难做到。
24 |
25 | ## Implement copy-on-write fork([hard](https://pdos.csail.mit.edu/6.828/2022/labs/guidance.html))
26 |
27 | > 你的任务是在 xv6 内核中实现 copy-on-write fork 。如果运行 `cowtest` 和 `usertests -q` 都成功,那么这个实验就成功了。
28 |
29 | 为了帮助你测试你的代码,我们提供了一个 `cowtest` xv6 程序(源代码在 `user/cowtest.c`。它将会运行多个测试,在没有修改的 xv6 中,它将会在第一个测试中就失败。
30 |
31 | ```bash
32 | $ cowtest
33 | simple: fork() failed
34 | $
35 | ```
36 |
37 | 这个 "simple" 测试会分配超过一半的可用物理内存,然后调用 `fork()` 。由于此时没有实现 COW fork() ,所以这里会因为没有足够的空闲内存空间而发生失败。
38 |
39 | 当你完成的时候,运行这两个测试程序,应该显示如下结果:
40 |
41 | ```bash
42 | $ cowtest
43 | simple: ok
44 | simple: ok
45 | three: zombie!
46 | ok
47 | three: zombie!
48 | ok
49 | three: zombie!
50 | ok
51 | file: ok
52 | ALL COW TESTS PASSED
53 | $ usertests -q
54 | ...
55 | ALL TESTS PASSED
56 | $
57 | ```
58 |
59 | 下面是一些建议:
60 |
61 | 1. 修改 `uvmcopy()` ,将父进程的物理页映射到子进程,以避免分配新的页。同时清楚子进程和父进程 PTE 的 PTE_W 位,使它们标记为不可写。
62 | 2. 修改 `usertrap()` ,以此来处理页面错误 (page fault)。当在一个本来可写的 COW 页发生“写页面错误” (write page-falut) 的时候,使用 `kalloc()` 分配一个新的页,将旧页中的数据拷贝到新页中,并将新页的 PTE 的 PTE_W 位置为1. **值得注意的是,对于哪些本来就使只读的(例如代码段),不论在旧页还是新页中,应该依旧保持它的只读性,那些试图对这样一个只读页进行写入的进程应该被杀死。**
63 | 3. 确保没有一个物理页被 PTE 引用之后,再释放它们,不能提前释放。一个好的做法是,维持一个“引用计数数组” (reference count) ,使用索引代表对应的物理页,值代表被引用的个数。当调用 `kalloc()` 分配一个物理页的时候,将其 `reference count` 设置为 `1` 。当 `fork` 导致子进程共享同一个页的时候,递增其页的引用计数;当任何一个进程从它们的页表中舍弃这个页的时候,递减其页的引用计数。`kfree()` 仅当一个页的引用计数为零时,才将这个页放到空闲页列表中。将这些引用计数放到一个 int 类型的数组中是可以的,你必须思考,如何去索引这个数组,以及如何选择这个数组的大小。例如,你可以用一个 `页的物理地址/4096` 来索引数组。
64 | 4. 修改 `copyout()` ,使用类似的思路去处理 COW 页。
65 |
66 | 一些注意的点:
67 |
68 | 1. 记录一个 PTE 是否是 COW 映射是有帮助的。你可以使用 RISC-V PTE 的 RSW (reserved for software) 位来指示。
69 | 2. `usertests -q` 测试一些 `cowtest` 没有测试的东西,所以不要忘记两个测试都要通过。
70 | 3. 一些对于页表标志 (page table flags) 有用的宏和定义可以在 `kernel/riscv.h` 中找到。
71 | 4. 如果一个 COW 页错误发生时,没有剩余可用内存,那么该进程应该被杀死。
72 |
73 | ## 思路
74 |
75 | 首先需要处理引用计数的问题。在 `kernel/kalloc.c` 中定义一个全局变量和锁。
76 |
77 | ```cpp
78 | // the reference count of physical memory page
79 | int useReference[PHYSTOP/PGSIZE];
80 | struct spinlock ref_count_lock;
81 | ```
82 |
83 | 然后在函数 `kalloc()` 中,初始化新分配的物理页的引用计数为 1.
84 |
85 | ```cpp
86 | void *
87 | kalloc(void)
88 | {
89 | ...
90 | if(r) {
91 | kmem.freelist = r->next;
92 | acquire(&ref_count_lock);
93 | // initialization the ref count to 1
94 | useReference[(uint64)r / PGSIZE] = 1;
95 | release(&ref_count_lock);
96 | }
97 | release(&kmem.lock);
98 | ...
99 | }
100 | ```
101 |
102 | 接着修改 `kfree()` ,仅当引用计数小于等于 0 的时候,才回收对应的页。
103 |
104 | ```cpp
105 | void
106 | kfree(void *pa)
107 | {
108 | ...
109 | if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
110 | panic("kfree");
111 |
112 | acquire(&ref_count_lock);
113 | // decrease the reference count, if use reference is not zero, then return
114 | useReference[(uint64)pa/PGSIZE] -= 1;
115 | temp = useReference[(uint64)pa/PGSIZE];
116 | release(&ref_count_lock);
117 | if (temp > 0)
118 | return;
119 |
120 | // Fill with junk to catch dangling refs.
121 | ...
122 | }
123 | ```
124 |
125 | 之所以不使用 `if (temp != 0)` ,因为在系统运行开始的时候,需要对空闲页列表 (kmem.freelist) 进行初始化,此时的引用计数就为 `-1` ,如果条件为 `temp != 0` 那么这些空闲页就不能够回收,也就不能够 `kmem.freelist` 列表了。
126 |
127 | `fork` 会首先调用 `uvmcopy()` 给子进程分配内存空间。但是如果要实现 COW 机制,就需要在 fork 时不分配内存空间,而是让子进程和父进程同时共享父进程的内存页,并将其设置为只读,使用 PTE_RSW 位标记 COW 页。这样子进程没有使用到某些页的时候,系统就不会真正的分配物理内存。**注意,此时需要将对应的引用计数加一。**
128 |
129 | > 这里在修改 PTE_W 和 PTE_RSW 位的时候,需要考虑原本的页是否使可写的。如果原本的页是只读的,那么就不用将其修改为 COW 页,因为 COW 页会在 `usertrap()` 中重新分配内存,并赋予可写权限,这样就违背了其原来的意愿,导致安全问题。
130 | >
131 | > 只有原本的页是可写的,才将其标记为 COW 和只读。
132 |
133 | 首先在 `kernel/riscv.h` 中增加一些定义。
134 |
135 | ```cpp
136 | #define PTE_RSW (1L << 8) // RSW
137 | ```
138 |
139 | 然后修改 `uvmcopy` 函数。此函数在 `vm.c` 文件中,由于需要使用 `useReference` 引用计数,所以需要提前在文件开头声明。
140 |
141 | ```cpp
142 | #include "spinlock.h"
143 | #include "proc.h"
144 |
145 | // Just declare the variables from kernel/kalloc.c
146 | extern int useReference[PHYSTOP/PGSIZE];
147 | extern struct spinlock ref_count_lock;
148 |
149 | int
150 | uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
151 | {
152 | ...
153 | // char *mem;
154 | for(i = 0; i < sz; i += PGSIZE){
155 | if((pte = walk(old, i, 0)) == 0)
156 | panic("uvmcopy: pte should exist");
157 | if((*pte & PTE_V) == 0)
158 | panic("uvmcopy: page not present");
159 | // PAY ATTENTION!!!
160 | // 只有父进程内存页是可写的,才会将子进程和父进程都设置为COW和只读的;否则,都是只读的,但是不标记为COW,因为本来就是只读的,不会进行写入
161 | // 如果不这样做,父进程内存只读的时候,标记为COW,那么经过缺页中断,程序就可以写入数据,于原本的不符合
162 | if (*pte & PTE_W) {
163 | // set PTE_W to 0
164 | *pte &= ~PTE_W;
165 | // set PTE_RSW to 1
166 | // set COW page
167 | *pte |= PTE_RSW;
168 | }
169 | pa = PTE2PA(*pte);
170 |
171 | // increment the ref count
172 | acquire(&ref_count_lock);
173 | useReference[pa/PGSIZE] += 1;
174 | release(&ref_count_lock);
175 |
176 | flags = PTE_FLAGS(*pte);
177 | // if((mem = kalloc()) == 0)
178 | // goto err;
179 | // memmove(mem, (char*)pa, PGSIZE);
180 | if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
181 | // kfree(mem);
182 | goto err;
183 | }
184 | }
185 | ...
186 | }
187 | ```
188 |
189 | 一旦子进程真正需要某些页,由于页被设置为只读的,此时会触发 Store/AMO page fault,scause 寄存器的值为 15。
190 |
191 | 
192 |
193 | 在 `usertrap()` 函数中捕获到 Store/AMO page fault 错误之后开始处理。首先应该知道哪个虚拟地址的操作导致了页错误。RISC-V 中的 stval (Supervisor Trap Value (stval) Register ) 寄存器中的值是导致发生异常的虚拟地址。vx6 中的函数 `r_stval()` 可以获取该寄存器的值。
194 |
195 | 
196 |
197 | 修改 `usertrap` 函数。
198 |
199 | ```cpp
200 | void
201 | usertrap(void)
202 | {
203 | ...
204 | syscall();
205 | }
206 | else if (r_scause() == 15) {
207 | // Store/AMO page fault(write page fault)
208 | // see Volume II: RISC-V Privileged Architectures V20211203 Page 71
209 |
210 | // the faulting virtual address
211 | // see Volume II: RISC-V Privileged Architectures V20211203 Page 70
212 | // the download url is https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf
213 | uint64 va = r_stval();
214 | if (va >= p->sz)
215 | p->killed = 1;
216 | int ret = cowhandler(p->pagetable, va);
217 | if (ret != 0)
218 | p->killed = 1;
219 | } else if((which_dev = devintr()) != 0){
220 | // ok
221 | } else {
222 | ...
223 | }
224 | ```
225 |
226 | 定义 `cowhandler` 函数。
227 |
228 | ```cpp
229 | int
230 | cowhandler(pagetable_t pagetable, uint64 va)
231 | {
232 | char *mem;
233 | if (va >= MAXVA)
234 | return -1;
235 | pte_t *pte = walk(pagetable, va, 0);
236 | if (pte == 0)
237 | return -1;
238 | // check the PTE
239 | if ((*pte & PTE_RSW) == 0 || (*pte & PTE_U) == 0 || (*pte & PTE_V) == 0) {
240 | return -1;
241 | }
242 | if ((mem = kalloc()) == 0) {
243 | return -1;
244 | }
245 | // old physical address
246 | uint64 pa = PTE2PA(*pte);
247 | // copy old data to new mem
248 | memmove((char*)mem, (char*)pa, PGSIZE);
249 | // PAY ATTENTION
250 | // decrease the reference count of old memory page, because a new page has been allocated
251 | kfree((void*)pa);
252 | uint flags = PTE_FLAGS(*pte);
253 | // set PTE_W to 1, change the address pointed to by PTE to new memory page(mem)
254 | *pte = (PA2PTE(mem) | flags | PTE_W);
255 | // set PTE_RSW to 0
256 | *pte &= ~PTE_RSW;
257 | return 0;
258 | }
259 | ```
260 |
261 | `cowhandler` 要进行严格的检查。只有被标记为 COW,存在,且是属于用户级别的,才可以被分配内存。如果本来的页就是只读的,那么在此时尝试对其进行写入,就会返回 -1,最终被杀死。
262 |
263 | 接着修改 `kernel.vm.c` 中的 `copyout()` 函数。这里的思路和 `uvmcopy()` 中类似。
264 |
265 | ```cpp
266 | int checkcowpage(uint64 va, pte_t *pte, struct proc* p) {
267 | return (va < p->sz) // va should blow the size of process memory (bytes)
268 | && (*pte & PTE_V)
269 | && (*pte & PTE_RSW); // pte is COW page
270 | }
271 |
272 | int
273 | copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
274 | {
275 | ...
276 | pa0 = walkaddr(pagetable, va0);
277 | if(pa0 == 0)
278 | return -1;
279 |
280 | struct proc *p = myproc();
281 | pte_t *pte = walk(pagetable, va0, 0);
282 | if (*pte == 0)
283 | p->killed = 1;
284 | // check
285 | if (checkcowpage(va0, pte, p))
286 | {
287 | char *mem;
288 | if ((mem = kalloc()) == 0) {
289 | // kill the process
290 | p->killed = 1;
291 | }else {
292 | memmove(mem, (char*)pa0, PGSIZE);
293 | // PAY ATTENTION!!!
294 | // This statement must be above the next statement
295 | uint flags = PTE_FLAGS(*pte);
296 | // decrease the reference count of old memory that va0 point
297 | // and set pte to 0
298 | uvmunmap(pagetable, va0, 1, 1);
299 | // change the physical memory address and set PTE_W to 1
300 | *pte = (PA2PTE(mem) | flags | PTE_W);
301 | // set PTE_RSW to 0
302 | *pte &= ~PTE_RSW;
303 | // update pa0 to new physical memory address
304 | pa0 = (uint64)mem;
305 | }
306 | }
307 |
308 | n = PGSIZE - (dstva - va0);
309 | if(n > len)
310 | n = len;
311 | ...
312 | }
313 | ```
314 |
315 | 使用 `checkcowpage` 检测 PTE 的标志。`uvmunmap` 函数将 PTE 置零,并 `kfree` PTE 引用的物理页,其实这里的 `kfree` 就是将 PTE 引用的物理页的引用计数减一。因为 `copyout` 分配的一个新的物理页,所以不需要共享之前的旧页了。
316 |
317 | 由于 `uvmunmap` 将 PTE 置零,所以 `uint flags = PTE_FLAGS(*pte);` 必须在其前面。
318 |
319 | ## 具体代码
320 |
321 | 具体的代码变动,请见 [GitHub Comment](https://github.com/relaxcn/xv6-labs-2022-solutions/commit/d79e00a630a3944c76cf92cffc0d43f31a4ad7ee).
322 |
--------------------------------------------------------------------------------
/doc/fs.md:
--------------------------------------------------------------------------------
1 | ## Lab : file system
2 |
3 | ### 1. 概述
4 |
5 | ---
6 |
7 | 这个 lab 中会给 xv6 的文件系统添加大文件和符号链接。
8 |
9 | ### 2. 代码实现
10 |
11 | ---
12 |
13 | #### 2.1 Large files
14 |
15 | ---
16 |
17 |
18 |
19 | 原先 xv6 的 inode 包含 12 个直接数据块号和 1 个间接数据块号,其中间接数据块包含 256 个块号(BSIZE/4),因此一个 xv6 的文件最多只能含有 268 个数据块的数据。
20 |
21 | 这个 lab 的目的就是将一个直接数据块号替换成一个两层间接数据块号,即指向一个包含间接数据块号的数据块。
22 |
23 | 主要的代码就是修改 bmap 函数,它会将指定 inode 的第 bn 个数据块(相对位置)映射到磁盘的数据块号。
24 |
25 | ```c
26 | // Return the disk block address of the nth block in inode ip.
27 | // If there is no such block, bmap allocates one.
28 | static uint
29 | bmap(struct inode *ip, uint bn)
30 | {
31 | uint addr, *a;
32 | struct buf *bp;
33 |
34 | if(bn < NDIRECT){
35 | if((addr = ip->addrs[bn]) == 0)
36 | ip->addrs[bn] = addr = balloc(ip->dev);
37 | return addr;
38 | }
39 | bn -= NDIRECT;
40 |
41 | if(bn < NINDIRECT){
42 | // Load indirect block, allocating if necessary.
43 | if((addr = ip->addrs[NDIRECT]) == 0)
44 | ip->addrs[NDIRECT] = addr = balloc(ip->dev);
45 | bp = bread(ip->dev, addr);
46 | a = (uint*)bp->data;
47 | if((addr = a[bn]) == 0){
48 | a[bn] = addr = balloc(ip->dev);
49 | log_write(bp);
50 | }
51 | brelse(bp);
52 | return addr;
53 | }
54 | bn -= NINDIRECT;
55 |
56 | uint indirect_idx, final_offset;
57 | struct buf *bp2;
58 | if (bn < NDOUBLEINDIRECT) {
59 | // Load double-indirect block, allocating if necessary
60 | if((addr = ip->addrs[NDIRECT+1]) == 0)
61 | ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
62 | indirect_idx = bn / NINDIRECT;
63 | final_offset = bn % NINDIRECT;
64 | bp = bread(ip->dev, addr);
65 | a = (uint*)bp->data;
66 | // Load indirect block, allocating if necessary
67 | if ((addr = a[indirect_idx]) == 0) {
68 | a[indirect_idx] = addr = balloc(ip->dev);
69 | log_write(bp);
70 | }
71 | brelse(bp);
72 |
73 | bp2 = bread(ip->dev, addr);
74 | a = (uint*)bp2->data;
75 | if ((addr = a[final_offset]) == 0) {
76 | a[final_offset] = addr = balloc(ip->dev);
77 | log_write(bp2);
78 | }
79 | brelse(bp2);
80 | return addr;
81 | }
82 |
83 | panic("bmap: out of range");
84 | }
85 | ```
86 |
87 | #### 2.2 Symbolic links
88 |
89 | ---
90 |
91 | 这个 lab 的目标是给 xv6 新增一个符号链接的系统调用。常见的硬链接(例如 A 链接到 B),会将 A 的 inode 号设置成和 B 文件一样,并且将对应 inode 的 ref 加一。而所谓符号链接(软链接),并不会影响 B 的 inode,而是将 A 标记成特殊的软链接文件,之后对 A 进行的所有文件操作,操作系统都会转换到对 B 的操作,类似于一个快捷方式。
92 |
93 | 首先新增 syslink 这个系统调用:
94 |
95 | ```c
96 | uint64
97 | sys_symlink(void)
98 | {
99 | char target[MAXPATH];
100 | char path[MAXPATH];
101 | struct inode *ip;
102 | int n;
103 |
104 | if((n = argstr(0, target, MAXPATH)) < 0 || argstr(1, path, MAXPATH) < 0)
105 | return -1;
106 |
107 | begin_op();
108 |
109 | // create a new symlink, return with a locked inode
110 | ip = create(path, T_SYMLINK, 0, 0);
111 | if(ip == 0){
112 | end_op();
113 | return -1;
114 | }
115 |
116 | // write the target into the symlink's data block
117 | if(writei(ip, 0, (uint64)target, 0, n) != n) {
118 | end_op();
119 | return -1;
120 | }
121 |
122 | iunlockput(ip);
123 | end_op();
124 | return 0;
125 | }
126 | ```
127 |
128 | 这个系统调用会创建一个类型为 T_SYMLINK 的的文件,并把 target 的文件路径写到它的 datablock 中。
129 |
130 | 另外需要修改 open 这个系统调用,使得操作系统能够正确处理 symlink 类型的文件:
131 |
132 | ```c
133 | if (ip->type == T_SYMLINK && !(omode & O_NOFOLLOW)) {
134 | int tolerate = 10;
135 | while (ip->type == T_SYMLINK && tolerate > 0) {
136 | if(readi(ip, 0, (uint64)path, 0, ip->size) != ip->size) {
137 | iunlockput(ip);
138 | end_op();
139 | return -1;
140 | }
141 | iunlockput(ip);
142 | if((ip = namei(path)) == 0) {
143 | end_op();
144 | return -1;
145 | }
146 | ilock(ip);
147 | tolerate--;
148 | }
149 | // cycle symlink is not allowed
150 | if (tolerate == 0) {
151 | iunlockput(ip);
152 | end_op();
153 | return -1;
154 | }
155 | }
156 | ```
157 |
158 | 注意如果软链接文件链接到了另一个软链接文件,系统需要递归地查找直到找到第一个非软链接的文件(对于成环的情况,需要返回-1)。
159 |
160 | 另外如果用户在打开文件时添加了 O_NOFOLLOW 模式,那么系统需要像处理正常文件一样将软链接文件打开。
161 |
--------------------------------------------------------------------------------
/doc/mmap.md:
--------------------------------------------------------------------------------
1 | # mmap
2 |
3 | 在本次实验中我们要求实现 `mmap` 和 `munmap` 系统调用,在实现之前,我们首先需要了解一下 `mmap` 系统调用是做什么的。根据 `mmap` 的描述,`mmap` 是用来将文件或设备内容映射到内存的。`mmap` 使用懒加载方法,因为需要读取的文件内容大小很可能要比可使用的物理内存要大,当用户访问页面会造成页错误,此时会产生异常,此时程序跳转到内核态由内核态为错误的页面读入文件并返回用户态继续执行。当文件不再需要的时候需要调用 `munmap` 解除映射,如果存在对应的标志位的话,还需要进行文件写回操作。`mmap` 可以由用户态直接访问文件或者设备的内容而不需要内核态与用户态进行拷贝数据,极大提高了 IO 的性能。
4 |
5 | 接下来,我们来研究一下实现,首先我们需要一个结构体用来保存 mmap 的映射关系,也就是文档中的映射关系,用于在产生异常的时候映射与解除映射,我们添加了 `virtual_memory_area` 这个结构体:
6 |
7 | ```c
8 | // 记录 mmap 信息的结构体
9 | struct virtual_memory_area {
10 | // 映射虚拟内存起始地址
11 | uint64 start_addr;
12 | // 映射虚拟内存结束地址
13 | uint64 end_addr;
14 | // 映射长度
15 | uint64 length;
16 | // 特权
17 | int prot;
18 | // 标志位
19 | int flags;
20 | // 文件描述符
21 | // int fd;
22 | struct file* file;
23 | // 文件偏移量
24 | uint64 offset;
25 | };
26 | ```
27 |
28 | 每个进程都需要有这样一个结构体用于记录信息,因此我们也需要在 `proc` 中添加这个结构体,由于其属于进程的私有域,所以不需要加锁访问。之后我们就将要去实现 `mmap` 系统调用:
29 |
30 | ```c
31 | uint64 sys_mmap(void) {
32 | uint64 addr, length, offset;
33 | int prot, flags, fd;
34 | argaddr(0, &addr);
35 | argaddr(1, &length);
36 | argint(2, &prot);
37 | argint(3, &flags);
38 | argint(4, &fd);
39 | argaddr(5, &offset);
40 | uint64 ret;
41 | if((ret = (uint64)mmap((void*)addr, length, prot, flags, fd, offset)) < 0){
42 | printf("[Kernel] fail to mmap.\n");
43 | return -1;
44 | }
45 | return ret;
46 | }
47 |
48 | void* mmap(void* addr, uint64 length, int prot, int flags, int fd, uint64 offset) {
49 | // 此时应当从该进程中发现一块未被使用的虚拟内存空间
50 | uint64 start_addr = find_unallocated_area(length);
51 | if(start_addr == 0){
52 | printf("[Kernel] mmap: can't find unallocated area");
53 | return (void*)-1;
54 | }
55 | // 构造 mm_area
56 | struct virtual_memory_area m;
57 | m.start_addr = start_addr;
58 | m.end_addr = start_addr + length;
59 | m.length = length;
60 | m.file = myproc()->ofile[fd];
61 | m.flags = flags;
62 | m.prot = prot;
63 | m.offset = offset;
64 | // 检查权限位是否满足映射要求
65 | if((m.prot & PROT_WRITE) && (m.flags == MAP_SHARED) && (!m.file->writable)){
66 | printf("[Kernel] mmap: prot is invalid.\n");
67 | return (void*)-1;
68 | }
69 | // 增加文件的引用
70 | struct file* f = myproc()->ofile[fd];
71 | filedup(f);
72 | // 将 mm_area 放入结构体中
73 | if(push_mm_area(m) == -1){
74 | printf("[Kernel] mmap: fail to push memory area.\n");
75 | return (void*)-1;
76 | }
77 | return (void*)start_addr;
78 | }
79 | ```
80 |
81 | 在实现中我们首先从虚拟内存域中找到一块可用的内存,然后不实际分配内存而只是构造 `virtual_memory_area` 结构体并将其存到进程的 `mm_area` 中去,然后增加文件引用并返回。如果用户程序访问这段内存,我们的操作系统将会触发异常并调用 `map_file` 来进行处理:
82 |
83 | ```c
84 | int map_file(uint64 addr) {
85 | struct proc* p = myproc();
86 | struct virtual_memory_area* mm_area = find_area(addr);
87 | if(mm_area == 0){
88 | printf("[Kernel] map_file: fail to find mm_area.\n");
89 | return -1;
90 | }
91 | // 从文件中读一页的地址并映射到页中
92 | char* page = kalloc();
93 | // 将页初始化成0
94 | memset(page, 0, PGSIZE);
95 | if(page == 0){
96 | printf("[Kernel] map_file: fail to alloc kernel page.\n");
97 | return -1;
98 | }
99 | int flags = PTE_U;
100 | if(mm_area->prot & PROT_READ){
101 | flags |= PTE_R;
102 | }
103 | if(mm_area->prot & PROT_WRITE){
104 | flags |= PTE_W;
105 | }
106 | if(mappages(p->pagetable, addr, PGSIZE, (uint64)page, flags) != 0) {
107 | printf("[Kenrel] map_file: map page fail");
108 | return -1;
109 | }
110 |
111 | struct file* f = mm_area->file;
112 | if(f->type == FD_INODE){
113 | ilock(f->ip);
114 | // printf("[Kernel] map_file: start_addr: %p, addr: %p\n", mm_area->start_addr, addr);
115 | uint64 offset = addr - mm_area->start_addr;
116 | if(readi(f->ip, 1, addr, offset, PGSIZE) == -1){
117 | printf("[Kernel] map_file: fail to read file.\n");
118 | iunlock(f->ip);
119 | return -1;
120 | }
121 | // mm_area->offset += PGSIZE;
122 | iunlock(f->ip);
123 | return 0;
124 | }
125 | return -1;
126 | }
127 | ```
128 |
129 | 在此函数中我们检查权限位并实际分配物理内存并进行映射,随后将文件内容读入到内存中来。当用户态不再需要文件内容的时候就会调用 `munmap` 进行取消映射:
130 |
131 | ```c
132 | uint64 sys_munmap(void){
133 | uint64 addr, length;
134 | argaddr(0, &addr);
135 | argaddr(1, &length);
136 | uint64 ret;
137 | if((ret = munmap((void*)addr, length)) < 0){
138 | return -1;
139 | }
140 | return ret;
141 | }
142 |
143 | int munmap(void* addr, uint64 length){
144 | // 找到地址对应的区域
145 | struct virtual_memory_area* mm_area = find_area((uint64)addr);
146 | // 根据地址进行切割,暂时进行简单地考虑
147 | uint64 start_addr = PGROUNDDOWN((uint64)addr);
148 | uint64 end_addr = PGROUNDUP((uint64)addr + length);
149 | if(end_addr == mm_area->end_addr && start_addr == mm_area->start_addr){
150 | struct file* f = mm_area->file;
151 | if(mm_area->flags == MAP_SHARED && mm_area->prot & PROT_WRITE){
152 | // 将内存区域写回文件
153 | printf("[Kernel] start_addr: %p, length: 0x%x\n", mm_area->start_addr, mm_area->length);
154 | if(filewrite(f, mm_area->start_addr, mm_area->length) < 0){
155 | printf("[Kernel] munmap: fail to write back file.\n");
156 | return -1;
157 | }
158 | }
159 | // 对虚拟内存解除映射并释放
160 | for(int i = 0; i < mm_area->length / PGSIZE; i++){
161 | uint64 addr = mm_area->start_addr + i * PGSIZE;
162 | uint64 pa = walkaddr(myproc()->pagetable, addr);
163 | if(pa != 0){
164 | uvmunmap(myproc()->pagetable, addr, PGSIZE, 1);
165 | }
166 | }
167 | // 减去文件引用
168 | fileclose(f);
169 |
170 | // 将内存区域从表中删除
171 | if(rm_area(mm_area) < 0){
172 | printf("[Kernel] munmap: fail to remove memory area from table.\n");
173 | return -1;
174 | }
175 | return 0;
176 | }else if(end_addr <= mm_area->end_addr && start_addr >= mm_area->start_addr){
177 | // 此时表示该区域只是一个子区域
178 | struct file* f = mm_area->file;
179 | if(mm_area->flags == MAP_SHARED && mm_area->prot & PROT_WRITE){
180 | // 写回文件
181 | // 获取偏移量
182 | uint offset = start_addr - mm_area->start_addr;
183 | uint len = end_addr - start_addr;
184 | if(f->type == FD_INODE){
185 | begin_op(f->ip->dev);
186 | ilock(f->ip);
187 | if(writei(f->ip, 1, start_addr, offset, len) < 0){
188 | printf("[Kernel] munmap: fail to write back file.\n");
189 | iunlock(f->ip);
190 | end_op(f->ip->dev);
191 | return -1;
192 | }
193 | iunlock(f->ip);
194 | end_op(f->ip->dev);
195 | }
196 | }
197 | // 对虚拟内存解除映射并释放
198 | uint64 len = end_addr - start_addr;
199 | for(int i = 0; i < len / PGSIZE; i++){
200 | uint64 addr = start_addr + i * PGSIZE;
201 | uint64 pa = walkaddr(myproc()->pagetable, addr);
202 | if(pa != 0){
203 | uvmunmap(myproc()->pagetable, addr, PGSIZE, 1);
204 | }
205 | }
206 | // 修改 mm_area 结构体
207 | if(start_addr == mm_area->start_addr) {
208 | mm_area->offset = end_addr - mm_area->start_addr;
209 | mm_area->start_addr = end_addr;
210 | mm_area->length = mm_area->end_addr - mm_area->start_addr;
211 | }else if(end_addr == mm_area->end_addr){
212 | mm_area->end_addr = start_addr;
213 | mm_area->length = mm_area->end_addr - mm_area->start_addr;
214 | }else{
215 | // 此时需要进行分块
216 | panic("[Kernel] munmap: no implement!\n");
217 | }
218 | return 0;
219 | }else if(end_addr > mm_area->end_addr){
220 | panic("[Kernel] munmap: out of current range.\n");
221 | }else{
222 | panic("[Kernel] munmap: unresolved solution.\n");
223 | }
224 | }
225 | ```
226 |
227 | 在 `munmap` 中我们判断是否写回页面并取消映射,减少文件引用。注意这里需要根据系统调用的地址和长度来判断不同的取消映射方式,一共有三种情况:
228 |
229 | - 要取消映射区间的一个端点或者两个端点与内存区域重合
230 | - 要取消映射区间的两个端点都在内存区域范围内
231 | - 要取消映射区间的一个端点或者两个端点在内存区域外
232 |
233 | 这里由于测试用例没给太多我就没有全部实现,不过实现起来应该也不难。
234 |
235 | 在最后我们需要在 `exit` 的时候对所有内存取消映射和对文件减少引用;在 `fork` 的时候子进程需要从父进程这里拿到 `mm_area` 并对文件增加引用:
236 |
237 | ```c
238 | void
239 | exit(int status)
240 | {
241 | struct proc *p = myproc();
242 |
243 | if(p == initproc)
244 | panic("init exiting");
245 |
246 | // Close all open files.
247 | for(int fd = 0; fd < NOFILE; fd++){
248 | if(p->ofile[fd]){
249 | struct file *f = p->ofile[fd];
250 | fileclose(f);
251 | p->ofile[fd] = 0;
252 | }
253 | }
254 |
255 | // 释放所有 mmap 区域的内存
256 | for(int i = 0; i < MM_SIZE; i++){
257 | if(p->mm_area[i].start_addr != 0){
258 | munmap((void*)p->mm_area[i].start_addr, p->mm_area[i].length);
259 | }
260 | }
261 |
262 | begin_op(ROOTDEV);
263 | iput(p->cwd);
264 | end_op(ROOTDEV);
265 | p->cwd = 0;
266 |
267 | // we might re-parent a child to init. we can't be precise about
268 | // waking up init, since we can't acquire its lock once we've
269 | // acquired any other proc lock. so wake up init whether that's
270 | // necessary or not. init may miss this wakeup, but that seems
271 | // harmless.
272 | acquire(&initproc->lock);
273 | wakeup1(initproc);
274 | release(&initproc->lock);
275 |
276 | // grab a copy of p->parent, to ensure that we unlock the same
277 | // parent we locked. in case our parent gives us away to init while
278 | // we're waiting for the parent lock. we may then race with an
279 | // exiting parent, but the result will be a harmless spurious wakeup
280 | // to a dead or wrong process; proc structs are never re-allocated
281 | // as anything else.
282 | acquire(&p->lock);
283 | struct proc *original_parent = p->parent;
284 | release(&p->lock);
285 |
286 | // we need the parent's lock in order to wake it up from wait().
287 | // the parent-then-child rule says we have to lock it first.
288 | acquire(&original_parent->lock);
289 |
290 | acquire(&p->lock);
291 |
292 | // Give any children to init.
293 | reparent(p);
294 |
295 | // Parent might be sleeping in wait().
296 | wakeup1(original_parent);
297 |
298 | p->xstate = status;
299 | p->state = ZOMBIE;
300 |
301 | release(&original_parent->lock);
302 |
303 |
304 | // Jump into the scheduler, never to return.
305 | sched();
306 | panic("zombie exit");
307 | }
308 |
309 | int
310 | fork(void)
311 | {
312 | int i, pid;
313 | struct proc *np;
314 | struct proc *p = myproc();
315 |
316 | // Allocate process.
317 | if((np = allocproc()) == 0){
318 | return -1;
319 | }
320 |
321 | // Copy user memory from parent to child.
322 | if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
323 | freeproc(np);
324 | release(&np->lock);
325 | return -1;
326 | }
327 | np->sz = p->sz;
328 |
329 | np->parent = p;
330 |
331 | // copy saved user registers.
332 | *(np->tf) = *(p->tf);
333 |
334 | // Cause fork to return 0 in the child.
335 | np->tf->a0 = 0;
336 |
337 | // increment reference counts on open file descriptors.
338 | for(i = 0; i < NOFILE; i++)
339 | if(p->ofile[i])
340 | np->ofile[i] = filedup(p->ofile[i]);
341 | np->cwd = idup(p->cwd);
342 |
343 | // 复制 mmap 结构体
344 | for(int i = 0; i < MM_SIZE; i++){
345 | if(p->mm_area[i].start_addr != 0){
346 | np->mm_area[i] = p->mm_area[i];
347 | // 增加文件引用
348 | filedup(p->mm_area[i].file);
349 | }
350 | }
351 |
352 |
353 | safestrcpy(np->name, p->name, sizeof(p->name));
354 |
355 | pid = np->pid;
356 |
357 | np->state = RUNNABLE;
358 |
359 | release(&np->lock);
360 |
361 | return pid;
362 | }
363 | ```
364 |
--------------------------------------------------------------------------------
/doc/net.assets/ptPxv.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/net.assets/ptPxv.png
--------------------------------------------------------------------------------
/doc/net.md:
--------------------------------------------------------------------------------
1 | # Networking
2 |
3 | 在本次实验中我们需要自己去实现网卡驱动和网络套接字,在写网卡驱动前我们需要知道网卡收发包的工作原理,再看了文档和查阅了一些资料之后总结了一下。
4 |
5 | ## 网卡接收与传输
6 |
7 | 
8 |
9 | 由这张图我们可以梳理下关于网卡收发包的细节,首先内核需要分配 `rx_ring` 和 `tx_ring` 两块环形缓冲区的内存用来接收和发送报文。其中以太网控制器的寄存器记录了关于 `rx_ring` 和 `tx_ring` 的详细信息。接收 packet 的细节如下:
10 |
11 | 1. 内核首先在主存中分配内存缓冲区和环形缓冲区,并由 CPU 将 `rx_ring` 的详细信息写入以太网控制器
12 | 2. 随后 NIC (Network Interface Card) 通过 DMA 获取到下一个可以写入的缓冲区的地址,当 packet 从硬件收到的时候外设通过 DMA 的方式写入对应的内存地址中
13 | 3. 当写入内存地址后,硬件将会向 CPU 发生中断,操作系统检测到中断后会调用网卡的异常处理函数
14 | 4. 异常处理函数可以通过由以太网控制寄存器映射到操作系统上的内存地址访问寄存器获取到下一个收到但未处理的 packet 的描述符,根据该描述符可以找到对应的缓冲区地址进行读取并传输给上层协议。
15 |
16 | 
17 |
18 | 由这张图可以看出软硬件在接收到 packet 的时候是如何工作的,首先硬件通过 DMA 拿到了 `Head` 所在 `rx_ring` 描述符的内存地址,并获取到其缓冲区地址,然后将收到的 packet 通过 DMA 拷贝到内存中,随后更新 `rx_ring` 描述符的内容并向寄存器中写入 `HEAD` 的新位置,随后向操作系统发出中断,操作系统收到中断通过获取 `Tail` 所在位置的文件描述符的信息来获取下一个将要处理的 packet,处理后由软件而非硬件更新 `Tail` 的位置。
19 |
20 | 所以我们可以按照以下逻辑写一下网卡收包与发包的过程:
21 |
22 | ```c
23 | int
24 | e1000_transmit(struct mbuf *m)
25 | {
26 | //
27 | // Your code here.
28 | //
29 | // the mbuf contains an ethernet frame; program it into
30 | // the TX descriptor ring so that the e1000 sends it. Stash
31 | // a pointer so that it can be freed after sending.
32 | //
33 | // 获取 ring position
34 | acquire(&e1000_lock);
35 | uint64 tdt = regs[E1000_TDT];
36 | uint64 index = tdt % TX_RING_SIZE;
37 | struct tx_desc send_desc = tx_ring[index];
38 | if(!(send_desc.status & E1000_TXD_STAT_DD)) {
39 | release(&e1000_lock);
40 | return -1;
41 | }
42 | if(tx_mbufs[index] != 0){
43 | // 如果该位置的缓冲区不为空则释放
44 | mbuffree(tx_mbufs[index]);
45 | }
46 | tx_mbufs[index] = m;
47 | tx_ring[index].addr = (uint64)tx_mbufs[index]->head;
48 | tx_ring[index].length = (uint16)tx_mbufs[index]->len;
49 | tx_ring[index].cmd = E1000_TXD_CMD_RS | E1000_TXD_CMD_EOP;
50 | tx_ring[index].status = 0;
51 |
52 | tdt = (tdt + 1) % TX_RING_SIZE;
53 | regs[E1000_TDT] = tdt;
54 | __sync_synchronize();
55 | release(&e1000_lock);
56 | return 0;
57 | }
58 |
59 | static void
60 | e1000_recv(void)
61 | {
62 | //
63 | // Your code here.
64 | //
65 | // Check for packets that have arrived from the e1000
66 | // Create and deliver an mbuf for each packet (using net_rx()).
67 | //
68 |
69 | // 获取接收 packet 的位置
70 | uint64 rdt = regs[E1000_RDT];
71 | uint64 index = (rdt + 1) % RX_RING_SIZE;
72 | // struct rx_desc recv_desc = rx_ring[index];
73 | if(!(rx_ring[index].status & E1000_RXD_STAT_DD)){
74 | // 查看新的 packet 是否有 E1000_RXD_STAT_DD 标志,如果
75 | // 没有,则直接返回
76 | return;
77 | }
78 | while(rx_ring[index].status & E1000_RXD_STAT_DD){
79 | // acquire(&e1000_lock);
80 | // 使用 mbufput 更新长度并将其交给 net_rx() 处理
81 | struct mbuf* buf = rx_mbufs[index];
82 | mbufput(buf, rx_ring[index].length);
83 | // 分配新的 mbuf 并将其写入到描述符中并将状态吗设置成 0
84 | rx_mbufs[index] = mbufalloc(0);
85 | rx_ring[index].addr = (uint64)rx_mbufs[index]->head;
86 | rx_ring[index].status = 0;
87 | rdt = index;
88 | regs[E1000_RDT] = rdt;
89 | __sync_synchronize();
90 | release(&e1000_lock);
91 | net_rx(buf);
92 | index = (regs[E1000_RDT] + 1) % RX_RING_SIZE;
93 | }
94 |
95 | }
96 | ```
97 |
98 | 这里要注意在从网卡接收 pakcet 的时候必须从 `E1000_RDT` 的位置开始遍历并向上层进行分发直到遇到未被使用的位置,因为网卡并非是收到 pakcet 立刻向操作系统发起中断,而是使用 `NAPI` 机制,对 IRQ 做合并以减少中断次数,`NAPI` 机制让 NIC 的 driver 能够注册一个 `poll` 函数,之后 `NAPI` 的子系统可以通过 `poll` 函数从 ring buffer 批量获取数据,最终发起中断。`NAPI` 的处理流程如下所示:
99 |
100 | 1. NIC driver 初始化时向 Kernel 注册 `poll` 函数,用于后续从 Ring Buffer 拉取收到的数据
101 | 2. driver 注册开启 NAPI,这个机制默认是关闭的,只有支持 NAPI 的 driver 才会去开启
102 | 3. 收到数据后 NIC 通过 DMA 将数据存到内存
103 | 4. NIC 触发一个 IRQ,并触发 CPU 开始执行 driver 注册的 Interrupt Handler
104 | 5. driver 的 Interrupt Handler 通过 [napi_schedule](http://elixir.free-electrons.com/linux/v4.4/source/include/linux/netdevice.h#L421) 函数触发 softirq ([NET_RX_SOFTIRQ](http://elixir.free-electrons.com/linux/v4.4/source/net/core/dev.c#L3204)) 来唤醒 NAPI subsystem,NET_RX_SOFTIRQ 的 handler 是 [net_rx_action 会在另一个线程中被执行,在其中会调用 driver 注册的 `poll` 函数获取收到的 Packet](http://elixir.free-electrons.com/linux/v4.4/source/net/core/dev.c#L4861)
105 | 6. driver 会禁用当前 NIC 的 IRQ,从而能在 `poll` 完所有数据之前不会再有新的 IRQ
106 | 7. 当所有事情做完之后,NAPI subsystem 会被禁用,并且会重新启用 NIC 的 IRQ
107 | 8. 回到第三步
108 |
109 | 而本次实验使用的是 qemu 模拟的 e1000 网卡也使用了 `NAPI` 机制。
110 |
111 | ## socket 实现
112 |
113 | 在类 Unix 操作系统上面,设备、`pipe` 和 `socket` 都要当做文件来处理,但在操作系统处理的时候需要根据文件描述符来判断是什么类型的文件并对其进行分发给不同的部分进行出来,我们需要实现的就是操作系统对于 `socket` 的处理过程。
114 |
115 | `socket` 的读取过程需要根绝给定的 `socket` 从所有 `sockets` 中找到并读取 `mbuf`,当对应的 `socket` 的缓冲区为空的时候则需要进行 `sleep` 从而将 CPU 时间让给调度器,当对应的 `socket` 收到了 packet 的时候再唤醒对应的进程:
116 |
117 | ```c
118 | sockrecvudp(struct mbuf *m, uint32 raddr, uint16 lport, uint16 rport)
119 | {
120 | //
121 | // Your code here.
122 | //
123 | // Find the socket that handles this mbuf and deliver it, waking
124 | // any sleeping reader. Free the mbuf if there are no sockets
125 | // registered to handle it.
126 | //
127 | acquire(&lock);
128 | struct sock* sock = sockets;
129 | // 首先找到对应的 socket
130 | while(sock != 0){
131 | if(sock->lport == lport && sock->raddr == raddr && sock->rport == rport){
132 | break;
133 | }
134 | sock = sock->next;
135 | if(sock == 0){
136 | printf("[Kernel] sockrecvudp: can't find socket.\n");
137 | return;
138 | }
139 | }
140 | release(&lock);
141 | acquire(&sock->lock);
142 | // 将 mbuf 分发到 socket 中
143 | mbufq_pushtail(&sock->rxq, m);
144 | // 唤醒可能休眠的 socket
145 | release(&sock->lock);
146 | wakeup((void*)sock);
147 | }
148 |
149 | int sock_read(struct sock* sock, uint64 addr, int n){
150 | acquire(&sock->lock);
151 | while(mbufq_empty(&sock->rxq)) {
152 | // 当队列为空的时候,进入 sleep, 将 CPU
153 | // 交给调度器
154 | if(myproc()->killed) {
155 | release(&sock->lock);
156 | return -1;
157 | }
158 | sleep((void*)sock, &sock->lock);
159 | }
160 | int size = 0;
161 | if(!mbufq_empty(&sock->rxq)){
162 | struct mbuf* recv_buf = mbufq_pophead(&sock->rxq);
163 | if(recv_buf->len < n){
164 | size = recv_buf->len;
165 | }else{
166 | size = n;
167 | }
168 | if(copyout(myproc()->pagetable, addr, recv_buf->head, size) != 0){
169 | release(&sock->lock);
170 | return -1;
171 | }
172 | // 或许要考虑一下读取的大小再考虑是否释放,因为有可能
173 | // 读取的字节数要比 buf 中的字节数少
174 | mbuffree(recv_buf);
175 | }
176 | release(&sock->lock);
177 | return size;
178 | }
179 | ```
180 |
181 | 而写 `socket` 的过程则更为简单,直接将用户态的数据写入对应的缓冲区并传入下层协议即可:
182 |
183 | ```c
184 | int sock_write(struct sock* sock, uint64 addr, int n){
185 | acquire(&sock->lock);
186 | struct mbuf* send_buf = mbufalloc(sizeof(struct udp) + sizeof(struct ip) + sizeof(struct eth));
187 | if (copyin(myproc()->pagetable, (char*)send_buf->head, addr, n) != 0){
188 | release(&sock->lock);
189 | return -1;
190 | }
191 | mbufput(send_buf, n);
192 | net_tx_udp(send_buf, sock->raddr, sock->lport, sock->rport);
193 | release(&sock->lock);
194 | return n;
195 | }
196 | ```
197 |
198 | 最终要实现关闭 `socket` 的操作,即将对应的 `socket` 从操作系统维护的所有的 `socket` 的链表中删除,并释放其所有缓冲区的空间,最终释放 `socket` 的空间:
199 |
200 | ```c
201 | void sock_close(struct sock* sock){
202 | struct sock* prev = 0;
203 | struct sock* cur = 0;
204 | acquire(&lock);
205 | // 遍历 sockets 链表找到对应的 socket 并将其
206 | // 从链表中移除
207 | cur = sockets;
208 | while(cur != 0){
209 | if(cur->lport == sock->lport && cur->raddr == sock->raddr && cur->rport == sock->rport){
210 | if(cur == sockets){
211 | sockets = sockets->next;
212 | break;
213 | }else{
214 | sock = cur;
215 | prev->next = cur->next;
216 | break;
217 | }
218 | }
219 | prev = cur;
220 | cur = cur->next;
221 | }
222 | // 释放 sock 所有的 mbuf
223 | acquire(&sock->lock);
224 | while(!mbufq_empty(&sock->rxq)){
225 | struct mbuf* free_mbuf = mbufq_pophead(&sock->rxq);
226 | mbuffree(free_mbuf);
227 | }
228 | // 释放 socket
229 | release(&sock->lock);
230 | release(&lock);
231 | kfree((void*)sock);
232 | }
233 |
234 | ```
235 |
236 | ## References
237 |
238 | - [网卡收包流程简析](https://cclinuxer.github.io/2020/07/%E7%BD%91%E5%8D%A1%E6%94%B6%E5%8C%85%E6%B5%81%E7%A8%8B%E6%B5%85%E6%9E%90/)
239 | - [What is the relationship of DMA ring buffer and TX/RX ring for a network card?](https://stackoverflow.com/questions/47450231/what-is-the-relationship-of-dma-ring-buffer-and-tx-rx-ring-for-a-network-card)
240 |
--------------------------------------------------------------------------------
/doc/pagetable.assets/image-20221106201148103.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/pagetable.assets/image-20221106201148103.png
--------------------------------------------------------------------------------
/doc/pagetable.assets/image-20221106202219124.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/pagetable.assets/image-20221106202219124.png
--------------------------------------------------------------------------------
/doc/pagetable.md:
--------------------------------------------------------------------------------
1 | # Lab: page tables
2 |
3 | 这个实验中,你将会探索页表,修改它们去加速一些系统调用,查看那些页被访问过。
4 |
5 | 开始之前,需要将代码仓库切换到 `pgtbl` 分支。
6 |
7 | ```bash
8 | git fetch
9 | git checkout pgtbl
10 | make clean
11 | ```
12 |
13 | ## Speed up system calls
14 |
15 | 这个实验的原理就是,将一些数据存放到一个只读的共享空间中,这个空间位于内核和用户之间。这样用户程序就不用陷入内核中,而是直接从这个只读的空间中获取数据,省去了一些系统开销,加速了一些系统调用。这次的任务是改进 `getpid()` 。
16 |
17 | 当每一个进程被创建,映射一个只读的页在 **USYSCALL** (在`memlayout.h`定义的一个虚拟地址)处。存储一个 `struct usyscall` (定义在 `memlayout.h`)结构体在该页的开始处,并且初始化这个结构体来保存当前进程的 PID。这个 lab 中,`ugetpid()` 已经在用户空间给出,它将会使用 **USYSCALL** 这个映射。运行 `pgtbltest` ,如果正确,`ugetpid` 这一项将会通过。(注意,这个程序包含两个测试,所以不用慌)。
18 |
19 | 首先在 `kernel/proc.h` proc 结构体中添加一项指针来保存这个共享页面的地址。
20 |
21 | ```c
22 | struct proc {
23 | ...
24 | struct usyscall *usyscallpage; // share page whithin kernel and user
25 | ...
26 | }
27 | ```
28 |
29 | 之后需要在 `kernel/proc.c` 的 `allocproc()` 中为其分配空间(`kalloc`)。并初始化其保存当前进程的PID。
30 |
31 | ```c
32 | static struct proc*
33 | allocproc(void) {
34 | ...
35 | if ((p->usyscallpage = (struct usyscall *)kalloc()) == 0) {
36 | freeproc(p);
37 | release(&p->lock);
38 | return 0;
39 | }
40 |
41 | p->usyscallpage->pid = p->pid;
42 |
43 | // Set up new context to start executing at forkret,
44 | // which returns to user space.
45 | memset(&p->context, 0, sizeof(p->context));
46 | p->context.ra = (uint64)forkret;
47 | p->context.sp = p->kstack + PGSIZE;
48 | ...
49 | }
50 | ```
51 |
52 | 然后在 `kernel/proc.c` 的 `proc_pagetable(struct proc *p)` 中将这个映射(PTE)写入 pagetable 中。权限是用户态可读。
53 |
54 | ```c
55 | pagetable_t
56 | proc_pagetable(struct proc *p) {
57 | ...
58 | if(mappages(pagetable, USYSCALL, PGSIZE, (uint64)(p->usyscallpage), PTE_R | PTE_U) < 0) {
59 | uvmfree(pagetable, 0);
60 | return 0;
61 | }
62 | // map the trampoline code (for system call return)
63 | // at the highest user virtual address.
64 | // only the supervisor uses it, on the way
65 | ...
66 | }
67 | ```
68 |
69 | 之后要确保释放进程的时候,能够释放该共享页。同样在 `kernel/proc.c` 中的 `freeproc(struct proc *p)` 。
70 |
71 | ```c
72 | static void
73 | freeproc(struct proc *p) {
74 | if(p->trapframe)
75 | kfree((void*)p->trapframe);
76 | p->trapframe = 0;
77 | // add start
78 | if(p->usyscallpage)
79 | kfree((void *)p->usyscallpage);
80 | p->usyscallpage = 0;
81 | // add end
82 | if(p->pagetable)
83 | proc_freepagetable(p->pagetable, p->sz);
84 | }
85 | ```
86 |
87 | 此刻完成之后,运行 qemu 会 `panic` 错误。
88 |
89 | ```bash
90 | xv6 kernel is booting
91 |
92 | hart 2 starting
93 | hart 1 starting
94 | panic: freewalk: leaf
95 | ```
96 |
97 |
98 |
99 | 这是因为在 `pagetable` 中任然存在我们之前的 PTE 映射。我们需要在 `kernel/proc.c` 的 `proc_freepagetable(pagetable_t pagetable, uint64 sz)` 中对其取消映射。
100 |
101 | ```c
102 | void
103 | proc_freepagetable(pagetable_t pagetable, uint64 sz) {
104 | uvmunmap(pagetable, TRAMPOLINE, 1, 0);
105 | uvmunmap(pagetable, TRAPFRAME, 1, 0);
106 | uvmunmap(pagetable, USYSCALL, 1, 0); // add
107 | uvmfree(pagetable, sz);
108 | }
109 | ```
110 |
111 | 具体的代码改动见 [github commit](https://github.com/flyto2035/xv6-labs-2022-solutions/commit/a4609f4237e864ce3c5085c8d4be4b3d00d637d8)。
112 |
113 | > 注:`proc_pagetable(struct proc *p)` 中映射 PTE 时的权限应该为 `PTE_R | PTE_U` 而不是 `PTE_R | PTE_U | PTE_W`。
114 |
115 | 运行 `./grade-lab-pgtbl ugetpid` 即可得到成功信息。或者在 `qemu` 中运行 `pgtbltest` 。此时 `pgaccess_test` 会失败,这个是下面的任务。
116 |
117 | ## Print a page table
118 |
119 | 第二个任务是写一个函数来打印页表的内容。这个函数定义为 `vmprint()` 。它应该接收一个 `pagetable_t` 类型的参数,并且按照下面的格式打印。在 `exec.c` 中的 `return argc` 之前插入 `if(p->pid==1) vmprint(p->pagetable)` ,用来打印第一个进程的页表。
120 |
121 | 当你做完这些之后,运行 `qemu` 之后应该看到一下输出,它在第一个进程 `init` 完成之前打印出来。
122 |
123 | ```bash
124 | page table 0x0000000087f6b000
125 | ..0: pte 0x0000000021fd9c01 pa 0x0000000087f67000
126 | .. ..0: pte 0x0000000021fd9801 pa 0x0000000087f66000
127 | .. .. ..0: pte 0x0000000021fda01b pa 0x0000000087f68000
128 | .. .. ..1: pte 0x0000000021fd9417 pa 0x0000000087f65000
129 | .. .. ..2: pte 0x0000000021fd9007 pa 0x0000000087f64000
130 | .. .. ..3: pte 0x0000000021fd8c17 pa 0x0000000087f63000
131 | ..255: pte 0x0000000021fda801 pa 0x0000000087f6a000
132 | .. ..511: pte 0x0000000021fda401 pa 0x0000000087f69000
133 | .. .. ..509: pte 0x0000000021fdcc13 pa 0x0000000087f73000
134 | .. .. ..510: pte 0x0000000021fdd007 pa 0x0000000087f74000
135 | .. .. ..511: pte 0x0000000020001c0b pa 0x0000000080007000
136 | init: starting sh
137 | ```
138 |
139 | 注意点:
140 |
141 | 1. 可以将 `vmprint()` 实现到 `kernel/vm.c` 中。
142 | 2. 使用在 `kernel/riscv.h` 文件末尾的宏定义。
143 | 3. 函数 `freewalk` 的实现方法对本实验很有帮助。
144 | 4. 将函数 `vmprint` 的声明放到 `kernel/defs.h` 中,以便可以在 `exec.c` 中调用它。
145 | 5. 使用 `%p` 格式化打印64位十六进制的 PTEs 和 地址。
146 |
147 | 值得注意的是 `freewalk` 函数的具体实现。该函数会释放所有的页表(page-table pages),使用递归的形式访问到每一个子页面。
148 |
149 | 
150 |
151 | 我们此次的 `vmprint` 函数也可以效仿此递归方法,但是需要展示此页表的深度,这时我们可以另外设置一个静态变量来指示当前打印页的深度信息,如果需要进入下一级页表就将其加一,函数返回就减一。具体实现如下:
152 |
153 | ```c
154 | void
155 | vmprint(pagetable_t pagetable)
156 | {
157 | if (printdeep == 0)
158 | printf("page table %p\n", (uint64)pagetable);
159 | for (int i = 0; i < 512; i++) {
160 | pte_t pte = pagetable[i];
161 | if (pte & PTE_V) {
162 | for (int j = 0; j <= printdeep; j++) {
163 | printf("..");
164 | }
165 | printf("%d: pte %p pa %p\n", i, (uint64)pte, (uint64)PTE2PA(pte));
166 | }
167 | // pintes to lower-level page table
168 | if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
169 | printdeep++;
170 | uint64 child_pa = PTE2PA(pte);
171 | vmprint((pagetable_t)child_pa);
172 | printdeep--;
173 | }
174 | }
175 | }
176 | ```
177 |
178 | 详细文件改动见:[github commit](https://github.com/flyto2035/xv6-labs-2022-solutions/commit/3343757349ffeff0c1d49a605bb4b6561a8d8ac5)
179 |
180 | ## Detect which pages have been accessed
181 |
182 | 首先需要了解的是,在一个 `Risc V Sv32` page table 包含了 2^10 个 PTEs,每一个 4 bytes。`Sv32` PTE 的图示如下:
183 |
184 | 
185 |
186 | 参考 [RISC-V privileged architecture manual](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMFDQC-and-Priv-v1.11/riscv-privileged-20190608.pdf) 的 P68 及以下几页。
187 |
188 | > Each leaf PTE contains an accessed (A) and dirty (D) bit. The A bit indicates the virtual page has been read, written, or fetched from since the last time the A bit was cleared. The D bit indicates the virtual page has been written since the last time the D bit was cleared.
189 |
190 | (A)位代表此虚拟页被访问(读,写,获取)自上次(A)位被清理(置零)。
191 |
192 | 这个实验中,我们将实现一个系统调用 `sys_pgaccess()` 在文件 `kernel/sysproc.c` 中。这个系统调用会告诉我们哪一个页被访问过。此系统调用接收三个参数。第一:被检查的第一个用户页的起始虚拟地址;第二:被检查页面的数量;第三:接收来自用户地址空间的一个 buffer 地址,将结果以掩码(bitmask)的形式写入。(掩码 bitmask 就是一个数据结构,其一个位代表一个页面,第一个页代表最低有效位)。
193 |
194 | 注意:
195 |
196 | 1. 在 `user/pgtlbtest.c` 的 `pgacess_test()` 展示了如何使用 `pgacess` 。
197 | 2. 使用 `argaddr()` 和 `argint()` 获取参数。
198 | 3. 对于要返回的 `bitmask` 值,在 kernel 中建立临时的buffer,然后使用 `copyout` 拷贝到用户空间(user space)。
199 | 4. 可以限制可以被扫描的页的最大数量(我的实现中没有设置)。
200 | 5. 在 `kernel/vm.c` 中的 `walk()` 很有用。它可以找到一个虚拟地址对应的 PTE,返回其 physical address。
201 | 6. 需要在 `kernel/riscv.h` 中定义一个 `PTE_A`,其为 `Risc V` 定义的 access bit。详细信息查看 [RISC-V privileged architecture manual](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMFDQC-and-Priv-v1.11/riscv-privileged-20190608.pdf) 。
202 | 7. 检查完 `PTE_A` 位时候设置之后,**确保将其清零**。因为如果不清零,那么这些位都将会被设置为 1 。因为检查其是否设置这个过程就访问了此页面,在之后的过程中不能确定该页面(被检查之前)是否被访问过。
203 | 8. `vmprint()` 可能会在调试页表时很有用。
204 |
205 | `walk()` 函数的使用非常重要,它可以找到一个虚拟地址对应的 `PTE` 的地址。而我们就是需要检查 PTE 来判断其是否被访问过(`PTE_A` 是否被设置)。
206 |
207 | 另一个重要的是要在检查之后清零此 `PTE_A` 位,其在 `PTE` 的第 6 位(从零开始)。如何将一个二进制值的指定位设置为指定的值呢?
208 |
209 | 
210 |
211 | 公式:`x = ((x&(1 << n)) ^ x) ^ (a << n)`,详细解释见文章:[如何将一个二进制值的指定位设置为指定的值](./如何将一个二进制值的指定位设置为指定的值.md)。
212 |
213 | 实现如下:
214 |
215 | ```c
216 | int
217 | sys_pgaccess(void)
218 | {
219 | uint64 va;
220 | int pagenum;
221 | uint64 abitsaddr;
222 | argaddr(0, &va);
223 | argint(1, &pagenum);
224 | argaddr(2, &abitsaddr);
225 |
226 | uint64 maskbits = 0;
227 | struct proc *proc = myproc();
228 | for (int i = 0; i < pagenum; i++) {
229 | pte_t *pte = walk(proc->pagetable, va+i*PGSIZE, 0);
230 | if (pte == 0)
231 | panic("page not exist.");
232 | if (PTE_FLAGS(*pte) & PTE_A) {
233 | maskbits = maskbits | (1L << i);
234 | }
235 | // clear PTE_A, set PTE_A bits zero
236 | *pte = ((*pte&PTE_A) ^ *pte) ^ 0 ;
237 | }
238 | if (copyout(proc->pagetable, abitsaddr, (char *)&maskbits, sizeof(maskbits)) < 0)
239 | panic("sys_pgacess copyout error");
240 |
241 | return 0;
242 | }
243 | ```
244 |
245 | 全部文件改动见 [github commit](https://github.com/flyto2035/xv6-labs-2022-solutions/commit/4489995876d00a8b4de22b7e06d93daef25d09e5).
246 |
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221029213304626.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221029213304626.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221029213416970.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221029213416970.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221029213443990.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221029213443990.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221029213635688.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221029213635688.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221029214535825.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221029214535825.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030190809519.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030190809519.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030191403194.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030191403194.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030192658267.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030192658267.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030200810283.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030200810283.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030200839704.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030200839704.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030201332014.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030201332014.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030201953431.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030201953431.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030203515710.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030203515710.png
--------------------------------------------------------------------------------
/doc/syscall.assets/image-20221030204838501.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/syscall.assets/image-20221030204838501.png
--------------------------------------------------------------------------------
/doc/syscall.md:
--------------------------------------------------------------------------------
1 | # Lab: system calls
2 |
3 | 本实验将会学习如何使用 GDB 进行调试,以及实现两个系统调用函数(System Call)。
4 |
5 | 在开始之前,应该将 git 切换到 `syscall` 分支。
6 |
7 | ~~~bash
8 | cd xv6-labs-2022
9 | git checkout syscall
10 | make clean
11 | ~~~
12 |
13 | ## Using gdb
14 |
15 | 首先应该学习如何使用 GDB 进行调试本 xv6。查看[此页面](https://pdos.csail.mit.edu/6.828/2022/labs/gdb.html)获得信息。
16 |
17 | 需要注意的是,在 Ubuntu 20.04.5 LTS 中,需要安装并使用 `gdb-multiarch`,只有这样才可以调试 `riscv64`程序。
18 |
19 | 进入 `xv6-labs-2022`文件夹中。
20 |
21 | 在终端中输入 `make qemu-gdb` ,这将运行 qemu 并开启调试功能,在这里,端口为本地的 **26000**。此时再打开一个终端,运行 `gdb-multiarch -x .gdbinit`。这将运行 `.gdbinit` 中的命令,也就是开启远程调试功能,并设置`arch`架构为 `riscv64`。具体可以查看此 `.gdbinit`文件。
22 |
23 | 
24 |
25 | 
26 |
27 | 
28 |
29 | 需要注意的是,如果 gdb 重新运行,那么 qemu 也应该重新运行。否则可能会出现意想不到的问题。
30 |
31 | ### 0x1
32 |
33 | 在 GDB 中运行以下指令。
34 |
35 | ~~~bash
36 | (gdb) b syscall
37 | Breakpoint 1 at 0x80001fe0: file kernel/syscall.c, line 133.
38 | (gdb) c
39 | Continuing.
40 | [Switching to Thread 1.3]
41 |
42 | Thread 3 hit Breakpoint 1, syscall () at kernel/syscall.c:133
43 | 133 {
44 | (gdb) layout src
45 | (gdb) backtrace
46 | ~~~
47 |
48 | `b syscall` 将在函数 `syscall` 处设置断点; `c` 将会运行到此断点时等待调试指令;`layout src` 将会开启一个窗口展示调试时的源代码;`backtrace` 将会打印堆栈回溯(stack backtrace)。
49 |
50 | 
51 |
52 | 那么第一个问题:
53 |
54 | > Looking at the backtrace output, which function called `syscall`?
55 |
56 | 通过堆栈回溯可以看到,函数 `usertrap()` 调用了 `syscall()` 函数。
57 |
58 | ### 0x2
59 |
60 | 输入几个`n` 命令,使其执行 `struct proc *p = myproc();` 并打印 `*p` 的值,它是一个 `proc` 结构体。
61 |
62 | ~~~bash
63 | (gdb) n
64 | (gdb) n
65 | (gdb) p/x *p
66 | ~~~
67 |
68 | 
69 |
70 | 那么第二个问题:
71 |
72 | > What is the value of `p->trapframe->a7` and what does that value represent? (Hint: look `user/initcode.S`, the first user program xv6 starts.)
73 |
74 | 输入命令查看 `p->trapframe->a7` 的值是多少。
75 |
76 | ~~~bash
77 | (gdb) p p->trapframe->a7
78 | $2 = 7
79 | (gdb)
80 | ~~~
81 |
82 | 得到 `a7` 的值为 `7` 。根据参考教材 [xv6 book](https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev1.pdf) 第二章和 `user/initcode.S` 中的代码可知,这个 `a7` 寄存器中保存了将要执行的系统调用号。这里的系统调用号为 `7`,在 `kernel/syscall.h` 中可以找到,这个系统调用为 `SYS_exec` 。
83 |
84 | 
85 |
86 | ### 0x3
87 |
88 | 系统调用运行在内核模式(kernel mode),可以通过 **Supervisor Status Register (sstatus)** 来查看当前 CPU 的状态。具体参看官方 [RISC-V privileged instructions](https://github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf) 文档 4.1.1 章节。
89 |
90 | 输入 GDB 命令来查看 `sstatus` 寄存器。通过 `p/t` 以二进制打印。
91 |
92 | ~~~bash
93 | (gdb) p/t $sstatus
94 | $4 = 100010
95 | (gdb)
96 | ~~~
97 |
98 | 这是官方文档关于 `sstatus` 寄存器的图示。参考 [RISC-V Privilieged ISA](http://docs.keystone-enclave.org/en/latest/Getting-Started/How-Keystone-Works/RISC-V-Background.html#risc-v-privilieged-isa)。和以下解释:
99 |
100 | >The SPP bit indicates the privilege level at which a hart was executing before entering supervisor mode. When a trap is taken, SPP is set to 0 if the trap originated from user mode, or 1 otherwise. When an SRET instruction (see Section 3.3.2) is executed to return from the trap handler, the privilege level is set to user mode if the SPP bit is 0, or supervisor mode if the SPP bit is 1; SPP is then set to 0.
101 | >
102 | >SPP 位指示进入管理员模式之前 hart 执行的特权级别。 当采取陷阱时,如果陷阱源自用户模式,则 SPP 设置为 0,否则设置为 1。 当执行 SRET 指令(见第 3.3.2 节)从陷阱处理程序返回时,如果 SPP 位为 0,则特权级别设置为用户模式,如果 SPP 位为 1,则设置为超级用户模式; 然后将 SPP 设置为 0。
103 |
104 | 
105 |
106 | 根据 `sstatus` 的二进制值 `100010` 可知,SPP 位是 `0`,那么在执行系统调用陷入内核之前的特权级别就是 user mode.
107 |
108 | 所以问题:
109 |
110 | > What was the previous mode that the CPU was in?
111 |
112 | 答案就是:用户模式(User Mode)。
113 |
114 | ### 0x4
115 |
116 | 在后续的实验中,我们编写的代码可能会使内核崩溃(panic)。例如替换 `syscall()` 函数中的 `num = p->trapframe->a7;` 为 `num = * (int *) 0;`,然后运行 `make qemu`。这样会看到一个 panic 信息。
117 |
118 | > 注:`syscall` 函数位于 `kernel/syscall.c` 132行。
119 |
120 | 
121 |
122 | ~~~bash
123 | xv6 kernel is booting
124 |
125 | hart 2 starting
126 | hart 1 starting
127 | scause 0x000000000000000d
128 | sepc=0x0000000080001ff4 stval=0x0000000000000000
129 | panic: kerneltrap
130 | ~~~
131 |
132 | 这里的 `sepc` 指代内核发生 panic 的代码地址。可以在 `kernel/kernel.asm` 中查看编译后的完整内核汇编代码,在其中搜索这个地址既可以找到使内核 panic 的代码。`sepc` 的值不是固定不变的。
133 |
134 | 在这里是 `0x0000000080001ff4`,所以我在 `kernel/kernel.asm` 中搜索 `80001ff4`。
135 |
136 | 
137 |
138 | 可以看到,果然是 `num = * (int *) 0;` 使内核 panic。对应的汇编则是 `lw a3,0(zero)`。一些 risc v 的汇编指令的简单介绍看这里 [RISC-V Assembly Language](https://web.eecs.utk.edu/~smarz1/courses/ece356/notes/assembly/)。
139 |
140 | 
141 |
142 | 所以这条汇编代码代表:将内存中地址从 0 开始的一个字 word (2 bytes) 大小的数据加载到寄存器 `a3` 中。
143 |
144 | 那么问题:
145 |
146 | > Write down the assembly instruction the kernel is panicing at. Which register corresponds to the varialable `num`?
147 |
148 | 答案就是:内核 panic 在 `lw a3,0(zero)`。`num` 代表 `a3` 寄存器。
149 |
150 | 0x5
151 |
152 | 再次运行虚拟器和 GDB 调试。将断点设置在发生 panic 处。
153 |
154 | ~~~bash
155 | (gdb) b *0x0000000080001ff4
156 | Breakpoint 1 at 0x80001ff4: file kernel/syscall.c, line 138.
157 | (gdb) c
158 | Continuing.
159 |
160 | Thread 1 hit Breakpoint 1, syscall () at kernel/syscall.c:138
161 | (gdb) layout asm
162 | (gdb) n
163 | (gdb) Ctrl + C # 键盘输入结束Thread
164 | (gdb) p $scause
165 | $1 = 13
166 | ~~~
167 |
168 | 再次输入 `n` 之后会发生 panic,此时输入 `Ctrl + C` 结束。查看 `scase` 寄存器,它代指内核 panic 的原因,查看文档[RISC-V privileged instructions](https://pdos.csail.mit.edu/6.828/2022/labs/n//github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf) 4.1.8 章节。下面是 Exception Code 图标。
169 |
170 | 
171 |
172 | 所以这里的 `13` 代表 `Load page fault` 。就是从内存地址 0 中 加载数据到寄存器 `a3` 时出错。那么地址 0 处是什么数据呢?从本教材 [book-riscv-rev3.pdf](https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf) 的 **Figure 3.3** 中可以找到答案。
173 |
174 | 
175 |
176 | 可以看到,在左侧 Virtual Address 中的地址 0 处对应右侧 Physical Address 的 Unused,表示这个地址没有被使用。而 Kernel 是从虚拟地址的 `0x80000000` 处开始的。
177 |
178 | 那么问题:
179 |
180 | > Why does the kernel crash? Hint: look at figure 3-3 in the text; is address 0 mapped in the kernel address space? Is that confirmed by the value in `scause` above? (See description of `scause` in [RISC-V privileged instructions](https://pdos.csail.mit.edu/6.828/2022/labs/n//github.com/riscv/riscv-isa-manual/releases/download/Priv-v1.12/riscv-privileged-20211203.pdf))
181 |
182 | 答案:内核因为加载了一个未使用的地址 0 处的内存数据而崩溃(Load page fault)。地址 0 并不映射到内核空间中(从 `0x80000000` 开始)。`scause` 中的异常代码证实了上述观点。
183 |
184 | ### 0x5
185 |
186 | 上述 `scuase` 指明了内核 panic 的原因。但是有时候我们需要知道,是哪一个用户程序调用 syscall 时发生了 panic。这可以通过打印 `proc` 结构体中的 `name` 来查看。
187 |
188 | 重新启动 qemu 和 gdb。
189 |
190 | ~~~bash
191 | (gdb) b syscall
192 | Breakpoint 1 at 0x80001fe0: file kernel/syscall.c, line 133.
193 | (gdb) c
194 | Continuing.
195 | [Switching to Thread 1.3]
196 |
197 | Thread 3 hit Breakpoint 1, syscall () at kernel/syscall.c:133
198 | 133 {
199 | (gdb) layout src
200 | (gdb) n
201 | (gdb) n
202 | (gdb) p p->name
203 | $1 = "initcode\000\000\000\000\000\000\000"
204 | (gdb)
205 | ~~~
206 |
207 | 可以看到,这个用户程序是 `initcode` ,也是 xv6 第一个 process。
208 |
209 | 打印 `proc` 结构体可以查看这个进程的其他信息。
210 |
211 | ~~~bash
212 | (gdb) p *p
213 | $3 = {lock = {locked = 0, name = 0x80008178 "proc", cpu = 0x0}, state = RUNNING, chan = 0x0, killed = 0, xstate = 0,
214 | pid = 1, parent = 0x0, kstack = 274877894656, sz = 4096, pagetable = 0x87f73000, trapframe = 0x87f74000, context = {
215 | ra = 2147488870, sp = 274877898368, s0 = 274877898416, s1 = 2147519792, s2 = 2147518720, s3 = 1, s4 = 0, s5 = 3,
216 | s6 = 2147588560, s7 = 8, s8 = 2147588856, s9 = 4, s10 = 1, s11 = 0}, ofile = {0x0 },
217 | cwd = 0x80016e40 , name = "initcode\000\000\000\000\000\000\000"}
218 | (gdb)
219 | ~~~
220 |
221 | 可以看到,这个`initcode` 的 pid 为 1.
222 |
223 | 问题:
224 |
225 | > What is the name of the binary that was running when the kernel paniced? What is its process id (`pid`)?
226 |
227 | 答案:这个二进制的名字为 `initcode` ,其 process id 为 1.
228 |
229 | > 注:一些调试的方法可以看官网的PPT [Using the GNU Debugger](https://pdos.csail.mit.edu/6.828/2019/lec/gdb_slides.pdf) 和 [guidance page](https://pdos.csail.mit.edu/6.828/2022/labs/guidance.html) 。
230 |
231 | ## System call tracing
232 |
233 | 此任务会增加一个系统调用追踪功能,它将会在后续实验的调试时有所帮助。课程提供了一个 `trace` 程序,它将会运行并开始另一个程序的系统调用追踪功能(tracing enable),此程序位于 `user/trace.c`。其参数为一个掩码 mask ,用来指示其要追踪的系统调用。例如 `trace(1 << SYS_fork)`,`SYS_fork` 为系统调用号在文件 `kernel/syscall.h` 中。如果系统调用号被设置在掩码中,你必须修改 xv6 内核,当每一个追踪的系统调用将要返回的时候打印一行信息。这一行信息包含进程 id,系统调用的名字和要返回的值。你不需要打印系统调用的参数。`trace` 系统调用应该启用它调用的程序和它调用程序的每一个子程序的追踪功能,但是不能影响其他进程。
234 |
235 | 注意事项:
236 |
237 | 1. 将 `$U/_trace` 添加到 Makefile 的 UPROGS 中。
238 | 2. 运行 `make qemu` ,将会发现无法编译 `user/trace.c`,这是因为编译器无法找到这个 `trace` 系统调用的定义。在 `user/user.h` 中添加这个系统调用的函数原型;在 `user/usys.pl` 中添加一个 `entry` ,它将会生成 `user/usys.S` ,里面包含真实的汇编代码,它使用 Risc V 的 `ecall` 指令陷入内核,执行系统调用;在 `kernel/syscal.h` 中添加一个系统调用号。做完这些,`make qemu` 就不会报错了。
239 | 3. 在 `kernel/sysproc.c` 中添加一个 `sys_trace()` 函数作为系统调用。它通过将它的参数保存到 `proc` 结构体(见 `kernel/proc.h`)中的新变量中,来实现其功能。从用户空间获取系统调用参数的函数位于 `kernel/syscall.c` 中,用法例子见 `kernel/sysproc.c`。
240 | 4. 修改 `fork()` (位于 `kernel/proc.c`),从父程序拷贝追踪掩码(mask)到子进程。
241 | 5. 在 `kernel/syscall.c`修改 `syscall()`,以此来打印系统调用 trace。你需要添加一个系统调用名字数组,用来索引。
242 |
243 | 这个问题的关键在于,如何指定在 `syscall()` 函数中只打印指定 syscall number 的信息。那么这个掩码 mask 就很重要。
244 |
245 | 例如 `1 << SYS_read`,相当于 `1 << 5`,其二进制值为 `100000`,那么这个二进制的第五位(如果从 0 开始算)为 1,就证明 系统调用号为 5 的需要被打印。
246 |
247 | 又例如官网上的例子中 `trace trace 2147483647` 这里 `2147483647`就是二进制的 `01111111111111111111111111111111`,其低31为全部为 1 ,就证明 30号(包括30号)以下的系统调用都需要跟踪)。
248 |
249 | 可以使用一个 mask 来判断已知的系统调用是否需要跟踪打印。如下:
250 |
251 | ~~~c
252 | if ( (mask >> num) & 0b1 )
253 | output
254 | ~~~
255 |
256 | 也就是判断那一位是否为 1 ,来判断其是否需要打印。
257 |
258 | ### 答案
259 |
260 | 文件变动见 [commit lab2 trace](https://github.com/flyto2035/xv6-labs-2022-solutions/commit/b18aa26959504c9634cf1a9c67753a048911cdd7),共修改了 8 个文件。
261 |
262 | 可通过运行 `./grade-lab-syscall trace` 进行测试。
263 |
264 | ## Sysinfo
265 |
266 | 这个部分将会实现一个系统调用 `sysinfo`,它将收集正在运行的系统(xv6)的信息。此 system call 需要一个参数:一个指向 `struct sysinfo` 的指针(见 `kernel/sysinfo.h`)。在陷入内核后,内核将会填充这个结构体中的字段:`freemem` 使系统剩余内存(free memory)的数量(单位 bytes),`nproc` 将会被设置为进程状态 `state` 不是 `UNSED` 的进程数量。我们提供了一个测试程序 `sysinfotest`,如果它打印 "sysinfotest: OK" ,那么就通过了测试。
267 |
268 | 注意事项:
269 |
270 | 1. 具体一般信息看官方的 hints。
271 | 2. `sysinfo` 有一个参数来自于用户空间 `use space` ,所以需要将内核空间(kernel space)中的数据(`struct sysinfo`)填充(复制)到用户空间中的结构体中。这个需要用到 `copyout`,具体例子请看 `sys_fstat()`(`kernel/sysfile.c`) 和 `filestat()`(`kernel/file.c`)。
272 | 3. 在 `kernel/kalloc.c` 中添加一个函数用于计算未使用的空闲内存。
273 | 4. 在 `kernel/proc.c` 中添加一个函数用于收集进程数量。
274 | 5. 这些函数的实现,可以参考其他官方的函数,会有一些启发。
275 |
276 | ### 答案
277 |
278 | 文件改动见 [commit lab2 sysinfo](https://github.com/flyto2035/xv6-labs-2022-solutions/commit/ed66024c7020638bf201d0e11599c246a4db5f4f),共修改了 9 个文件。
279 |
280 | 可通过运行 `./grade-lab-syscall sysinfo 进行测试。
281 |
--------------------------------------------------------------------------------
/doc/thread.md:
--------------------------------------------------------------------------------
1 | # lab thread
2 |
3 | 本文不再像前文那样进行细致的翻译和讲解,我想一个有追求的人应该具备独立思考的基本能力。
4 |
5 | 开始前,应该切换到 `thread` 分支,注意需要阅读 xv6-book 的 Chapter 7: Scheduling 章节。我建议先阅读,后面再来做lab,这样你会具有一个更加具体的概念。
6 |
7 | ## Uthread: switching between threads
8 |
9 | 在这个练习中,你需要在用户级别的线程系统中设计一个上下文(context)切换机制。
10 |
11 | xv6 提供了两个文件 `user/uthread.c` 和 `user/uthread_switch.S`,首先阅读这两个文件,尤其是 `uthread.c`,知道它是如何运行的。
12 |
13 | > 你的任务:想出一个方法创建thread,并且在线程切换的时候保存和恢复CPU上下文。
14 |
15 | ### 思路
16 |
17 | #### 0x1
18 |
19 | 根据xv6-book,切换线程需要保存和恢复CPU上下文(也就是寄存器)的相关信息,所以在 struct thread 中,需要有CPU寄存器的相关信息,你手动添加它。我们不需要保存全部的寄存器,只需要保存 **[callee-save registers](https://riscv.org/wp-content/uploads/2015/01/riscv-calling.pdf)**,你可以在 `kernel/proc.h` 中找到相关的内容,不过还是建议你阅读前面的超链接,你便你更好的了解。
20 |
21 | 所以在 `user/uthread.c` 中定义一个 struct context 来保存 callee-save registers 的信息。
22 |
23 | ```cpp
24 | struct context {
25 | uint64 ra; // return address
26 | uint64 sp; // stack pointer
27 |
28 | // callee-saved
29 | uint64 s0;
30 | uint64 s1;
31 | uint64 s2;
32 | uint64 s3;
33 | uint64 s4;
34 | uint64 s5;
35 | uint64 s6;
36 | uint64 s7;
37 | uint64 s8;
38 | uint64 s9;
39 | uint64 s10;
40 | uint64 s11;
41 | };
42 |
43 | struct thread {
44 | char stack[STACK_SIZE]; /* the thread's stack */
45 | int state; /* FREE, RUNNING, RUNNABLE */
46 | struct context context; // register context
47 | };
48 | ```
49 |
50 | #### 0x2
51 |
52 | `user/uthread_switch.S` 中,需要用asm编写保存和恢复寄存器的汇编代码,如果你做过前面的lab,或者认真读了xv6 book,那么这个函数 `thread_switch(uint64, uint64)` 和 `kernel/swtch.S` 中的`swtch`函数不能说一模一样,简直是没什么区别。
53 |
54 |
55 | #### 0x3
56 |
57 | 在 `thread_schedule` 中,很显然需要调用 `thread_swtich` 函数来保存寄存器和恢复CPU上下文为即将运行的进程的上下文信息。
58 |
59 | xv6 book 中说道,thread switch 的原理就是通过保存原本的CPU上下文,然后恢复想要调度运行的进程的CPU上下文信息,其中最重要的就是寄存器 `ra` 的值,因为它保存着函数将要返回的地址 return address. 所以此时 `ra` 中的地址是什么,CPU就会跳转到这个地址进行运行。这就是所谓的 thread switch. 不过为了保持原本线程中的数据的完整性,需要一并恢复它所需要的寄存器的信息,也就是 callee-save resigers.
60 |
61 | ```cpp
62 | void
63 | thread_schedule(void)
64 | {
65 | ...
66 | if (current_thread != next_thread) { /* switch threads? */
67 | ...
68 | /* YOUR CODE HERE
69 | * Invoke thread_switch to switch from t to next_thread:
70 | * thread_switch(??, ??);
71 | */
72 | // switch old to new thread
73 | // save old thread context and restore new thread context
74 | thread_switch(&t->context, ¤t_thread->context);
75 | } else
76 | next_thread = 0;
77 | }
78 | ```
79 |
80 | 这里为了方便,我修改了 `thread_switch` 函数的参数原型:
81 |
82 | ```cpp
83 | extern void thread_switch(struct context *old, struct context *new);
84 | ```
85 |
86 | #### 0x4
87 |
88 | 通过上述0x3的讲解,你应该清楚了线程切换的根本原理。如何在一开始 `thread_schedule` 函数调度的时候,切换的新的CPU上下文信息中的 `ra` 寄存器为我们想要调转的那个函数的地址呢?答案就是在 `thread_create` 的时候,提前将对应的 thread 的 `ra` 寄存器设置为对应的函数地址。
89 |
90 | 同时需要注意,每个 thread 拥有一个独立的 stack 空间。所以需要同时将寄存器 `sp` (stack pointer) 设置为 `thread.stack` 的地址。
91 |
92 | ```cpp
93 | void
94 | thread_create(void (*func)())
95 | {
96 | struct thread *t;
97 |
98 | for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
99 | if (t->state == FREE) break;
100 | }
101 | t->state = RUNNABLE;
102 | // YOUR CODE HERE
103 | // save stack pointer to stack address
104 | t->context.sp = (uint64)&t->stack[0];
105 | t->context.ra = (uint64)(*func);
106 | }
107 | ```
108 | 是不是挺有道理的?不过就是结果不正确,你会发现,经过一次两次 thread_switch 之后 `thread[0\1\2..]` 中的某些数据被修改为一个随机值了。比如 `thread[0].state` 在调度到 `thread_a` 之后,莫名的被修改为了一个随机的值,它正确的值应该为 `1`,因为它是 main 函数的线程,后面不会再进行调度。
109 |
110 | 其实原因就是 `sp` 的值错误了。学过操作系统原理的同学应该清楚,栈空间是从高往低填充的,也就是说 stack pointer 应该指向这个栈内存空间的高地址,而不是低地址。
111 |
112 | 在 C 语言中,一个数组的空间是从低地址向高地址分配的,所以此时 `t->stack` 是这个数组的低地址,我们需要将它的高地址传给 `sp` resiger. 也就是 `&t->stack[STACK_SIZE-1];`。
113 | 所以正确的应该是:
114 |
115 | ```cpp
116 | // save stack pointer to stack address
117 | t->context.sp = (uint64)&t->stack[STACK_SIZE-1];
118 | t->context.ra = (uint64)(*func);
119 | ```
120 |
121 | 我们应该掌握最基本的调式方法,详细的GDB调式方法见 https://sourceware.org/gdb/current/onlinedocs/gdb.html/
122 |
123 | 例如希望显示源代码 `layout src` ,希望显示asm代码 `layout asm`,二者都显示 `layout split`,见 https://sourceware.org/gdb/current/onlinedocs/gdb.html/TUI-Commands.html#TUI-Commands
124 |
125 | 在源代码中,如果执行 `next` 会执行多行asm代码,如果向单行调试asm代码,需要执行 `si` 。
126 |
127 | 详细的代码改动见 [GitHub Commit](https://github.com/relaxcn/xv6-labs-2022-solutions/commit/a965c2be0089a9fe8c827b2e873d76ade95a5008).
128 |
129 | ## Using threads
130 |
131 | 详见 [GitHub commit](https://github.com/relaxcn/xv6-labs-2022-solutions/commit/b95669c94e27e50427d105007d3ed1dae60b9e46),注意我的注释即可,最好画图分析一下指针,知道为什么会丢失数据。
132 |
133 | ```cpp
134 | static
135 | void put(int key, int value)
136 | {
137 | int i = key % NBUCKET;
138 |
139 | // is the key already present?
140 | struct entry *e = 0;
141 | for (e = table[i]; e != 0; e = e->next) {
142 | if (e->key == key)
143 | break;
144 | }
145 | if(e){
146 | // update the existing key.
147 | e->value = value;
148 | } else {
149 | pthread_mutex_lock(&lock[i]);
150 | // the new is new.
151 | // 重要的是 table[i] 的值,如果 thread_1 刚进入,但是 thread_2 刚好完成修改了 table[i] 的操作,此时就会丢失后面的所有node
152 | insert(key, value, &table[i], table[i]);
153 | pthread_mutex_unlock(&lock[i]);
154 | }
155 |
156 | }
157 | ```
158 |
159 | ## Barrier
160 |
161 | Barrier 是一种机制,可以让所有线程阻塞,直到所有线程都到达这个位置,然后继续运行。详见 [Wikipedia](https://en.wikipedia.org/wiki/Barrier_(computer_science)).
162 |
163 | `pthread_cond_wait` 在进入之后,会释放 mutex lock,返回之前会重新获取 mutex lock. 我们希望 `bstate.nthread` 记录阻塞的线程的数量仅在一个 thread 中递增一次,而不是多次增加。在所有线程到达之后,重置 `bstate.nthread` ,递增 `bstate.round` 表示一次回合。
164 |
165 | 首先我们需要使用 mutex lock 保护 `bstate.nthread` 的递增。因为如何不保护它,那么在多线程中会导致其结果最后不正确。如果其本来是 0,此时刚好计算到 `bstate.nthread + 1` 结束,结果是 1,但是还没有被赋值给 `bstate.nthread`,但是此时另一个线程刚好在此之前完成了对 `bstate.nthread` 的递增和赋值,此时 `bstate.nthread` = 1,然后回到刚才线程,执行赋值操作 `bstate.nthread = 1` 最终, `bstate.nthread` 的值为 1,显然是错误的,应该是 2.
166 |
167 | ```cpp
168 | static void
169 | barrier()
170 | {
171 | pthread_mutex_lock(&bstate.barrier_mutex); // Lock the mutex
172 |
173 | // Increment the number of threads that have reached this round
174 | bstate.nthread++;
175 |
176 | // If not all threads have reached the barrier, wait
177 | if (bstate.nthread < nthread) {
178 | pthread_cond_wait(&bstate.barrier_cond, &bstate.barrier_mutex);
179 | }
180 |
181 | // If all threads have reached the barrier, increment the round and reset the counter
182 | if (bstate.nthread == nthread) {
183 | bstate.round++;
184 | bstate.nthread = 0;
185 | pthread_cond_broadcast(&bstate.barrier_cond); // Notify all waiting threads
186 | }
187 |
188 | pthread_mutex_unlock(&bstate.barrier_mutex); // Unlock the mutex
189 | }
190 | ```
191 |
192 | 详细的代码改动见 [GitHub Commit](https://github.com/relaxcn/xv6-labs-2022-solutions/commit/098ee563a1f34897b0667981277adf30a743fa21).
--------------------------------------------------------------------------------
/doc/traps.assets/image-20221119182846614.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/traps.assets/image-20221119182846614.png
--------------------------------------------------------------------------------
/doc/traps.assets/image-20221119183441521.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/traps.assets/image-20221119183441521.png
--------------------------------------------------------------------------------
/doc/traps.assets/image-20221119183727421.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/traps.assets/image-20221119183727421.png
--------------------------------------------------------------------------------
/doc/traps.assets/image-20221119183826612.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/traps.assets/image-20221119183826612.png
--------------------------------------------------------------------------------
/doc/traps.assets/image-20221119184248121.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/traps.assets/image-20221119184248121.png
--------------------------------------------------------------------------------
/doc/traps.assets/image-20221119184424152.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/traps.assets/image-20221119184424152.png
--------------------------------------------------------------------------------
/doc/traps.assets/image-20221119185052056.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/traps.assets/image-20221119185052056.png
--------------------------------------------------------------------------------
/doc/traps.md:
--------------------------------------------------------------------------------
1 | # Lab: traps
2 |
3 | 这个实验将会探索系统调用是如何使用陷阱(trap)实现的。首先将会利用栈做一个热身练习,接下来你将会实现一个用户级陷阱处理(user-level trap handling)的例子。
4 |
5 | > 阅读 [xv6 book](https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf) 第四章节和以下相关文件:
6 | >
7 | > - `kernel/trampoline.S` :从用户空间到内核空间并返回的汇编代码。
8 | > - `kernel/trap.c`:处理所有中断的代码。
9 |
10 | 开始之前,切换到 `trap` 分支。
11 |
12 | ```bash
13 | git fetch
14 | git checkout traps
15 | make clean
16 | ```
17 |
18 | ## RISC-V assembly
19 |
20 | 理解 RISC-V 的汇编代码很重要。使用命令 `make fs.img` 编译 `user/call.c` ,这将会生成一个可读的汇编代码文件 `user/call.asm` 。
21 |
22 | 阅读 `call.asm` 中的 `g` ,`f` ,和 `main` 函数。参考这些材料:[reference page](https://pdos.csail.mit.edu/6.828/2022/reference.html)。
23 |
24 | ### 0x1
25 |
26 | > Which registers contain arguments to functions? For example, which register holds 13 in main's call to `printf`?
27 |
28 | 通过之前的阅读可知,调用函数时的参数传递使用寄存器 `a1`, `a2` 等通用寄存器。
29 |
30 | 阅读 `call.asm` 文件的第 45 行。
31 |
32 | 
33 |
34 | 通过阅读 `call.asm` 文件中的 `main` 函数可知,调用 `printf` 函数时,`13` 被寄存器 `a2` 保存。
35 |
36 | 答案:
37 |
38 | `a1`, `a2`, `a3` 等通用寄存器;`13` 被寄存器 `a2` 保存。
39 |
40 | ### 0x2
41 |
42 | > Where is the call to function `f` in the assembly code for main? Where is the call to `g`? (Hint: the compiler may inline functions.)
43 |
44 | 通过阅读函数 `f` 和 `g` 得知:函数 `f` 调用函数 `g` ;函数 `g` 使传入的参数加 3 后返回。
45 |
46 | 
47 |
48 | 所以总结来说,函数 `f` 就是使传入的参数加 3 后返回。考虑到编译器会进行内联优化,这就意味着一些显而易见的,编译时可以计算的数据会在编译时得出结果,而不是进行函数调用。
49 |
50 | 查看 `main` 函数可以发现,`printf` 中包含了一个对 `f` 的调用。
51 |
52 | 
53 |
54 | 但是对应的会汇编代码却是直接将 `f(8)+1` 替换为 `12` 。这就说明编译器对这个函数调用进行了优化,所以对于 `main` 函数的汇编代码来说,其并没有调用函数 `f` 和 `g` ,而是在运行之前由编译器对其进行了计算。
55 |
56 | 答案:
57 |
58 | `main` 的汇编代码没有调用 `f` 和 `g` 函数。编译器对其进行了优化。
59 |
60 | ### 0x3
61 |
62 | > At what address is the function `printf` located?
63 |
64 | 通过搜索容易得到 `printf` 函数的位置。
65 |
66 | 
67 |
68 | 得到其地址在 `0x642`。
69 |
70 | 答案:
71 |
72 | `0x642`
73 |
74 | ### 0x4
75 |
76 | > What value is in the register `ra` just after the `jalr` to `printf` in `main`?
77 |
78 | `auipc` 和 `jalr` 的配合,可以跳转到任意 32 位的地址。
79 |
80 | 
81 |
82 | 具体相关命令介绍请看参考链接:[reference1](https://xiayingp.gitbook.io/build_a_os/hardware-device-assembly/risc-v-assembly), [RISC-V unprivileged instructions](https://github.com/riscv/riscv-isa-manual/releases/download/Ratified-IMAFDQC/riscv-spec-20191213.pdf).
83 |
84 | 第 49 行,使用 `auipc ra,0x0` 将当前程序计数器 `pc` 的值存入 `ra` 中。
85 |
86 | 第 50 行,`jalr 1554(ra)` 跳转到偏移地址 `printf` 处,也就是 `0x642` 的位置。
87 |
88 | 根据 [reference1](https://xiayingp.gitbook.io/build_a_os/hardware-device-assembly/risc-v-assembly) 中的信息,在执行完这句命令之后, 寄存器 `ra` 的值设置为 `pc + 4` ,也就是 `return address` 返回地址 `0x38`。
89 |
90 | 答案:
91 |
92 | `jalr` 指令执行完毕之后,`ra` 的值为 `0x38`.
93 |
94 | ### 0x5
95 |
96 | > Run the following code.
97 | >
98 | > ```c
99 | > unsigned int i = 0x00646c72;
100 | > printf("H%x Wo%s", 57616, &i);
101 | > ```
102 | >
103 | > What is the output? [Here's an ASCII table](https://www.asciitable.com/) that maps bytes to characters.
104 | >
105 | > The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set `i` to in order to yield the same output? Would you need to change `57616` to a different value?
106 | >
107 | > [Here's a description of little- and big-endian](http://www.webopedia.com/TERM/b/big_endian.html) and [a more whimsical description](https://www.rfc-editor.org/ien/ien137.txt).
108 |
109 | 请查看在线 C Compiler 的运行结果 [c++ sell](https://cpp.sh/?source=%2F%2F+Example+program%0A%23include+%3Cstdio.h%3E%0A%0Aint+main()%0A%7B%0A++unsigned+int+i+%3D+0x00646c72%3B%0Aprintf(%22x%3D%25d+y%3D%25d%22%2C+3)%3B%0Areturn+0%3B%0A%7D),它打印出了 `He110 World`。
110 |
111 | 首先,`57616` 转换为 16 进制为 `e110`,所以格式化描述符 `%x` 打印出了它的 16 进制值。
112 |
113 | 其次,如果在小端(little-endian)处理器中,数据`0x00646c72` 的**高字节存储在内存的高位**,那么从**内存低位**,也就是**低字节**开始读取,对应的 ASCII 字符为 `rld`。
114 |
115 | 如果在 大端(big-endian)处理器中,数据 `0x00646c72` 的**高字节存储在内存的低位**,那么从**内存低位**,也就是**高字节**开始读取其 ASCII 码为 `dlr`。
116 |
117 | 所以如果大端序和小端序输出相同的内容 `i` ,那么在其为大端序的时候,`i` 的值应该为 `0x726c64`,这样才能保证从内存低位读取时的输出为 `rld` 。
118 |
119 | 无论 `57616` 在大端序还是小端序,它的二进制值都为 `e110` 。大端序和小端序只是改变了多字节数据在内存中的存放方式,并不改变其真正的值的大小,所以 `57616` 始终打印为二进制 `e110` 。
120 |
121 | 关于大小端,参考:[CSDN](https://blog.csdn.net/wwwlyj123321/article/details/100066463)
122 |
123 | 答案:
124 |
125 | 如果在大端序,`i` 的值应该为 `0x00646c72` 才能保证与小端序输出的内容相同。不用该变 `57616` 的值。
126 |
127 | ### 0x6
128 |
129 | > In the following code, what is going to be printed after `'y='`? (note: the answer is not a specific value.) Why does this happen?
130 | >
131 | > ```c
132 | > printf("x=%d y=%d", 3);
133 | > ```
134 |
135 | 通过之前的章节可知,函数的参数是通过寄存器`a1`, `a2` 等来传递。如果 `prinf` 少传递一个参数,那么其仍会从一个确定的寄存器中读取其想要的参数值,但是我们并没有给出这个确定的参数并将其存储在寄存器中,所以函数将从此寄存器中获取到一个随机的不确定的值作为其参数。故而此例中,`y=`后面的值我们不能够确定,它是一个垃圾值。
136 |
137 | 答案:
138 |
139 | `y=` 之后的值为一个不确定的垃圾值。
140 |
141 | ## Backtrace
142 |
143 | 打印出 `backtrace`,这是一个在错误发生时存在于栈中的函数调用列表,有利于调试。寄存器 `s0` 包含一个指向当前栈帧 `stack frame` 的指针,我们的任务就是使用栈帧遍历整个栈,打印每一个栈帧中的返回地址 `return address`。
144 |
145 | 实现一个 `backtrace()` 函数在 `kernel/printf.c` 中,并且在 `sys_sleep` 中调用它。之后运行 `bttest`,它将会调用 `sys_sleep` 。你和输出应该是一个**返回地址**列表,就像下面那样(可能数字会不同):
146 |
147 | ```bash
148 | backtrace:
149 | 0x0000000080002cda
150 | 0x0000000080002bb6
151 | 0x0000000080002898
152 | ```
153 |
154 | 注意事项:
155 |
156 | 1. 在 `kernel/defs.h` 中添加函数 `backtrace()` 的函数声明,以便在 `sys_sleep` 中调用 `backtrace`。
157 |
158 | 2. GCC 编译器将当前正在执行的函数的帧指针(frame pointer)存储到寄存器 `s0` 中。在 `kernel/riscv.h` 中添加以下代码:
159 |
160 | 1. ```bash
161 | static inline uint64
162 | r_fp()
163 | {
164 | uint64 x;
165 | asm volatile("mv %0, s0" : "=r" (x) );
166 | return x;
167 | }
168 | ```
169 |
170 | 2. 在 `backtrace` 中调用此函数,将会读取当前帧指针。`r_fp()` 使用[内联汇编](https://gcc.gnu.org/onlinedocs/gcc/Using-Assembly-Language-with-C.html)读取 `s0`。
171 |
172 | 3. [课堂笔记](https://pdos.csail.mit.edu/6.1810/2022/lec/l-riscv.txt)中有关于栈帧的布局图片。注意,返回地址在帧指针的 -8 偏移量处;前一个帧指针位于当前帧指针的固定偏移量 (-16) 处。
173 |
174 | 4. 遍历栈帧需要一个停止条件。有用的信息是:每个内核栈由一整个页(4k对其)组成,所有的栈帧都在同一个页上面。你可以使用`PGROUNDDOWN(fp)` 来定位帧指针所在的页面,从而确定循环停止的条件。
175 |
176 | > `PGROUNDDOWN(fp)` 总是表示 `fp` 所在的这一页的起始位置。
177 |
178 | 所以要在 `printf` 中添加该函数:
179 |
180 | ```c
181 | void
182 | backtrace(void)
183 | {
184 | uint64 fp_address = r_fp();
185 | while(fp_address != PGROUNDDOWN(fp_address)) {
186 | printf("%p\n", *(uint64*)(fp_address-8));
187 | fp_address = *(uint64*)(fp_address - 16);
188 | }
189 | }
190 | ```
191 |
192 | 在 `kernel/defs.h` 中添加该函数声明:
193 |
194 | ```c
195 | void backtrace(void);
196 | ```
197 |
198 | 在 `kerne/riscv.h` 中添加 `r_sp`函数。
199 |
200 | ```c
201 | static inline uint64
202 | r_sp()
203 | {
204 | uint64 x;
205 | asm volatile("mv %0, sp" : "=r" (x) );
206 | return x;
207 | }
208 | ```
209 |
210 | 在 `kernel/sysproc.c` 中的 `sys_sleep` 函数中添加该函数调用:
211 |
212 | ```c
213 | void sys_sleep(void){
214 | ...
215 | backtrace();
216 | ...
217 | }
218 | ```
219 |
220 | 具体文件变动见 [github commit](https://github.com/flyto2035/xv6-labs-2022-solutions/commit/c636291e238bc849a6ac9638dfd2a8e922c4febe).
221 |
222 | ## Alarm
223 |
224 | 这个练习将会添加一个特性:当一个进程使用 cpu 时,每隔一个特定的时间就提醒进程。如果我们想要限制一个进程使用 cpu 的时间,那么这个练习将会有帮助。
225 |
226 | 你应该添加一个新的系统调用 `sigalarm(interval, handler)`。如果一个应用调用了 `sigalarm(n, fn)`那么这个进程每消耗 `n` 个 ticks,内核应该确保函数 `fn` 被调用。当 `fn` 返回的时候,内核应该恢复现场,确保该进程在它刚才离开的地方继续执行。一个 tick 在 xv6 中是一个相当随意的单位时间,它取决于硬件时钟产生中断的快慢。如果一个应用调用 `sigalarm(0, 0)` ,内核应该停止产生周期性的警报调用。
227 |
228 | 在代码库中有 `user/alarmtest.c` 程序用于检测实验的真确性。将它添加到 Makefile 文件中以便编译它。
229 |
230 | `alarmtest` 在 `test0` 中调用 `sigalarm(2, periodic)` ,使内核每隔 2 个 ticks 就调用 `periodic` 函数。
231 |
232 | 通过修改内核,使得内核可以调转到位于用户空间的处理函数(alarm handler),它将打印出 "alarm!" 字符。
233 |
234 | 回忆一下之前的内容以及 xv6 book 中的第四章节的内容。当使用 trap 方式陷入内核的时候,会首先执行 `kernel/trampoline.S` 中的 `uservec` ,保存寄存器中的值以便返回时恢复现场,包括 `sepc` 中断时保存的用户程序的程序计数器(pc);然后跳转到 `kernel/trap.c` 中的 `usertrap(void)` ,检测该中断的类型(是否是系统调用或者是 timer 时钟中断);然后跳转到 `kernel/trap.c` 中的 `usertrapret(void)` ,它将从之前保存的栈帧(trapframe) 中恢复寄存器,其中一个重要的就是 `pec` ,CPU 从 特权模式 返回 用户模式 ,将使用 `spec` 的值恢复 `pc` 的值,它决定了返回时,CPU 将要执行的用户代码。这一点很重要,我们的代码也是利用这一点,使 CPU 执行我们定义的用户空间中的 alarm handler 函数。
235 |
236 | 需要注意的是,为了使得从 alarm handler 中返回之后,仍继续执行原用户程序,我们需要保存之前保存在 `trapframe` 中的寄存器值,并且在 alarm handler 调用 `sys_sigreturn` 时恢复这些寄存器。
237 |
238 | 另外,为了保证实验说明中的要求:在 alarm handler 函数未返回之前,不能重复调用 alarm handler。我们需要一个控制这个状态的变量`have_return`,它将会添加到 `struct proc` 中。
239 |
240 | 首先在 `kernel/proc.h` 中的 `proc` 结构体中添加需要的内容。
241 |
242 | ```c
243 | struct proc {
244 | ...
245 | // the virtual address of alarm handler function in user page
246 | uint64 handler_va;
247 | int alarm_interval;
248 | int passed_ticks;
249 | // save registers so that we can re-store it when return to interrupted code.
250 | struct trapframe saved_trapframe;
251 | // the bool value which show that is or not we have returned from alarm handler.
252 | int have_return;
253 | ...
254 | }
255 | ```
256 |
257 | 在 `kernel/sysproc.c` 中实现 `sys_sigalarm` 和 `sys_sigreturn` 。
258 |
259 | ```c
260 | uint64
261 | sys_sigreturn(void)
262 | {
263 | struct proc* proc = myproc();
264 | // re-store trapframe so that it can return to the interrupt code before.
265 | *proc->trapframe = proc->saved_trapframe;
266 | proc->have_return = 1; // true
267 | return proc->trapframe->a0;
268 | }
269 |
270 | uint64
271 | sys_sigalarm(void)
272 | {
273 | int ticks;
274 | uint64 handler_va;
275 |
276 | argint(0, &ticks);
277 | argaddr(1, &handler_va);
278 | struct proc* proc = myproc();
279 | proc->alarm_interval = ticks;
280 | proc->handler_va = handler_va;
281 | proc->have_return = 1; // true
282 | return 0;
283 | }
284 | ```
285 |
286 | 注意到一点,`sys_sigreturn(void)` 的返回值不是 0,而是 `proc->trapframe->a0`。这是因为我们想要完整的恢复所有寄存器的值,包括 `a0`。但是一个系统调用返回的时候,它会将其返回值存到 `a0` 寄存器中,那这样就改变了之前 `a0` 的值。所以,我们干脆让其返回之前想要恢复的 `a0` 的值,那这样在其返回之后 `a0` 的值仍没有改变。
287 |
288 | 然后修改 `kernel/trap.c` 中的 `usertrap` 函数。
289 |
290 | ```c
291 | void
292 | usertrap(void) {
293 | ...
294 | // give up the CPU if this is a timer interrupt.
295 | if(which_dev == 2) {
296 | struct proc *proc = myproc();
297 | // if proc->alarm_interval is not zero
298 | // and alarm handler is returned.
299 | if (proc->alarm_interval && proc->have_return) {
300 | if (++proc->passed_ticks == 2) {
301 | proc->saved_trapframe = *p->trapframe;
302 | // it will make cpu jmp to the handler function
303 | proc->trapframe->epc = proc->handler_va;
304 | // reset it
305 | proc->passed_ticks = 0;
306 | // Prevent re-entrant calls to the handler
307 | proc->have_return = 0;
308 | }
309 | }
310 | yield();
311 | }
312 | ...
313 | }
314 | ```
315 |
316 | 从内核跳转到用户空间中的 alarm handler 函数的关键一点就是:修改 `epc` 的值,使 trap 在返回的时候将 pc 值修改为该 alarm handler 函数的地址。这样,我们就完成了从内核调转到用户空间中的 alarm handler 函数。但是同时,我们也需要保存之前寄存器栈帧,因为后来 alarm handler 调用系统调用 `sys_sigreturn` 时会破坏之前保存的寄存器栈帧(p->trapframe)。
317 |
318 | 具体代码改动见:[github commit](https://github.com/flyto2035/xv6-labs-2022-solutions/commit/8dd68907b38ac6dbecfc93c4a452e6acb07313bd).
319 |
--------------------------------------------------------------------------------
/doc/utils.assets/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/relaxcn/xv6-labs-2022-solutions/e3fd4bcc271b3de6a5afb43a86ee3782cfc193b5/doc/utils.assets/1.png
--------------------------------------------------------------------------------
/doc/utils.md:
--------------------------------------------------------------------------------
1 | # Lab: Xv6 and Unix utilities
2 |
3 | 这个实验将会熟悉 xv6 操作系统和它的系统调用。
4 |
5 | ## Boot xv6
6 |
7 | 首先参考 [Tools Used in 6.1810](https://pdos.csail.mit.edu/6.828/2022/tools.html) 配置以下系统环境。这里我用的是 Windows 下的 WSL2 Ubuntu 20.04.
8 |
9 | ~~~bash
10 | sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu
11 | ~~~
12 |
13 | 克隆 xv6 代码,并测试。
14 |
15 | ~~~bash
16 | git clone git://g.csail.mit.edu/xv6-labs-2022
17 | cd xv6-labs-2022
18 | make qemu
19 | ~~~
20 |
21 | 可以运行以下 `ls` 命令查看其文件。
22 |
23 | xv6 中没有 `ps` 命令,但是你可以输入 `Ctrl-P`,内核将会打印出每一个进程(process)的信息。
24 |
25 | 想要退出 qemu ,输入 `Ctrl-a x` (同时按住 `Ctrl`和 `x` ,然后松开再按 `x`)。
26 |
27 | ## sleep
28 |
29 | 注意事项:
30 |
31 | 1. 你的程序名为 `sleep`,其文件名为 `sleep.c`,保存在 `user/sleep.c` 中。
32 | 2. 开始之前,你应该阅读 [xv6 book](https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf) 的第一章。
33 | 3. 使用系统调用 `sleep`。
34 | 4. `main` 函数应该以 `exit(0)` 结束。
35 | 5. 将你的`sleep` 函数添加到 `Makefile` 中的 `UPROGS`中,只有这样 `make qemu`时 才会编译它们。
36 |
37 | ~~~c
38 | #include "kernel/types.h"
39 | #include "kernel/stat.h"
40 | #include "user/user.h"
41 |
42 | int
43 | main(int argc, char* argv[])
44 | {
45 | if (argc != 2) {
46 | fprintf(2, "Usage: sleep times\n");
47 | }
48 | int time = atoi(*++argv);
49 | if (sleep(time) != 0) {
50 | fprintf(2, "Error in sleep sys_call!\n");
51 | }
52 | exit(0);
53 | }
54 | ~~~
55 |
56 | 将 `$U/_sleep\` 添加到 `Makefile` 中,如下图:
57 |
58 | 
59 |
60 | 然后运行 `make qemu` ,之后执行 `sleep` 函数。
61 |
62 | ~~~bash
63 | make qemu
64 | sleep 10
65 | (nothing happens for a little while)
66 | ~~~
67 |
68 | 你可以运行以下命令来测试你写的程序:
69 |
70 | ~~~bash
71 | ./grade-lab-util sleep
72 | == Test sleep, no arguments == sleep, no arguments: OK (3.1s)
73 | == Test sleep, returns == sleep, returns: OK (0.6s)
74 | == Test sleep, makes syscall == sleep, makes syscall: OK (0.6s)
75 | ~~~
76 |
77 | 在此时前你可能需要赋予 `grade-lab-util ` 可执行权限。
78 |
79 | ~~~bash
80 | sudo chmod +x ./grade-lab-util
81 | ~~~
82 |
83 | ## pingpong
84 |
85 | ~~~c
86 | #include "kernel/types.h"
87 | #include "kernel/stat.h"
88 | #include "user/user.h"
89 |
90 | int
91 | main(int argc, char* argv[])
92 | {
93 | // parent to child
94 | int fd[2];
95 |
96 | if (pipe(fd) == -1) {
97 | fprintf(2, "Error: pipe(fd) error.\n");
98 | }
99 | // child process
100 | if (fork() == 0){
101 | char buffer[1];
102 | read(fd[0], buffer, 1);
103 | close(fd[0]);
104 | fprintf(0, "%d: received ping\n", getpid());
105 | write(fd[1], buffer, 1);
106 | }
107 | // parent process
108 | else {
109 | char buffer[1];
110 | buffer[0] = 'a';
111 | write(fd[1], buffer, 1);
112 | close(fd[1]);
113 | read(fd[0], buffer, 1);
114 | fprintf(0, "%d: received pong\n", getpid());
115 |
116 | }
117 | exit(0);
118 | }
119 | ~~~
120 |
121 | 参考:https://www.geeksforgeeks.org/pipe-system-call/
122 |
123 | ## primes
124 |
125 | 这个比较难,理解递归会稍微好一点。其本质上是一个素数筛,使用的递归。需要看一个文章:http://swtch.com/~rsc/thread/
126 |
127 | ~~~c
128 | #include "kernel/types.h"
129 | #include "kernel/stat.h"
130 | #include "user/user.h"
131 |
132 | void new_proc(int p[2]) {
133 | int prime;
134 | int n;
135 | // close the write side of p
136 | close(p[1]);
137 | if (read(p[0], &prime, 4) != 4) {
138 | fprintf(2, "Error in read.\n");
139 | exit(1);
140 | }
141 | printf("prime %d\n", prime);
142 | // if read return not zero
143 | // if it still need next process
144 | if (read(p[0], &n, 4) == 4){
145 | int newfd[2];
146 | pipe(newfd);
147 | // father
148 | if (fork() != 0) {
149 | close(newfd[0]);
150 | if (n % prime) write(newfd[1], &n, 4);
151 | while (read(p[0], &n, 4) == 4) {
152 | if (n % prime) write(newfd[1], &n, 4);
153 | }
154 | close(p[0]);
155 | close(newfd[1]);
156 | wait(0);
157 | }
158 | // child
159 | else {
160 | new_proc(newfd);
161 | }
162 | }
163 | }
164 |
165 | int
166 | main(int argc, char* argv[])
167 | {
168 | int p[2];
169 | pipe(p);
170 | // child process
171 | if (fork() == 0) {
172 | new_proc(p);
173 | }
174 | // father process
175 | else {
176 | // close read port of pipe
177 | close(p[0]);
178 | for (int i = 2; i <= 35; ++i) {
179 | if (write(p[1], &i, 4) != 4) {
180 | fprintf(2, "failed write %d into the pipe\n", i);
181 | exit(1);
182 | }
183 | }
184 | close(p[1]);
185 | wait(0);
186 | exit(0);
187 | }
188 | return 0;
189 | }
190 | ~~~
191 |
192 | ## find
193 |
194 | 注意事项:
195 |
196 | 1. 查看 `user/ls.c` 的实现,看看它如何读取文件夹(本质是一个文件)。
197 | 2. 使用递归去读取子文件夹。
198 | 3. 不要递归 `.` 和 `..` 这两个文件。(这个特别重要,不然会无限循环了)
199 | 4. 文件系统中的更改在 qemu 中是一直保持的,所以使用 `make clean && make qemu` 清理文件系统。
200 | 5. 字符串的比较不能使用(==),要使用 `strcmp`。(这是 C,所以 == 字符串比较的是地址)
201 |
202 | 强烈建议弄明白 `user/ls.c` ,读取文件本质上没什么区别。程序中的 `printf` 只是为了方便我调试,lab2 将学习如何使用 GDB 进行调试。
203 |
204 | ~~~c
205 | #include "kernel/types.h"
206 | #include "kernel/stat.h"
207 | #include "kernel/fs.h"
208 | #include "user/user.h"
209 |
210 | // 去除字符串后面的空格
211 | char*
212 | rtrim(char* path)
213 | {
214 | static char newStr[DIRSIZ+1];
215 | int whiteSpaceSize = 0;
216 | int bufSize = 0;
217 | for(char* p = path + strlen(path) - 1; p >= path && *p == ' '; --p) {
218 | ++whiteSpaceSize;
219 | }
220 | bufSize = DIRSIZ - whiteSpaceSize;
221 | memmove(newStr, path, bufSize);
222 | newStr[bufSize] = '\0';
223 | return newStr;
224 | }
225 |
226 | void
227 | find(char* path, char* name)
228 | {
229 | char buf[512], *p;
230 | int fd;
231 | // dir descriptor
232 | struct dirent de;
233 | // file descriptor
234 | struct stat st;
235 |
236 | if ((fd = open(path, 0)) < 0) {
237 | fprintf(2, "find: cannot open %s\n", path);
238 | return;
239 | }
240 |
241 | if (fstat(fd, &st) == -1) {
242 | fprintf(2, "find: cannot fstat %s\n", path);
243 | close(fd);
244 | return;
245 | }
246 | // printf("switch to '%s'\n", path);
247 | switch (st.type) {
248 |
249 | case T_DEVICE:
250 | case T_FILE:
251 | fprintf(2, "find: %s not a path value.\n", path);
252 | close(fd);
253 | // printf("==='%s' is a File\n", path);
254 | break;
255 | case T_DIR:
256 | // printf("==='%s' is a dir\n", path);
257 | if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
258 | printf("ls: path too long\n");
259 | break;
260 | }
261 | // create full path
262 | strcpy(buf, path);
263 | p = buf + strlen(buf);
264 | *p++ = '/';
265 | // read dir infomation for file and dirs
266 | while (read(fd, &de, sizeof(de)) == sizeof de) {
267 | if (de.inum == 0)
268 | continue;
269 | if (strcmp(".", rtrim(de.name)) == 0 || strcmp("..", rtrim(de.name)) == 0)
270 | continue;
271 | // copy file name to full path
272 | memmove(p, de.name, DIRSIZ);
273 | // create a string with zero ending.
274 | p[DIRSIZ] = '\0';
275 | // stat each of files
276 | if (stat(buf, &st) == -1) {
277 | fprintf(2, "find: cannot stat '%s'\n", buf);
278 | continue;
279 | }
280 | // printf("===file:'%s'\n", buf);
281 | if (st.type == T_DEVICE || st.type == T_FILE) {
282 | if (strcmp(name, rtrim(de.name)) == 0) {
283 | printf("%s\n", buf);
284 | // for (int i = 0; buf[i] != '\0'; ++i) {
285 | // printf("'%d'\n", buf[i]);
286 | // }
287 | }
288 | }
289 | else if (st.type == T_DIR) {
290 | find(buf, name);
291 | }
292 | }
293 |
294 | }
295 | }
296 |
297 | int
298 | main(int argc, char* argv[])
299 | {
300 | if (argc != 3) {
301 | fprintf(2, "Usage: find path file.\n");
302 | exit(0);
303 | }
304 | char* path = argv[1];
305 | char* name = argv[2];
306 | // printf("path is '%s'\n", path);
307 | find(path, name);
308 | exit(0);
309 | }
310 | ~~~
311 |
312 | ## xargs
313 |
314 | `xargs` 程序将从 pipe (管道)中传递的值转换为其他程序的命令行参数(command line arguments)。
315 |
316 | 那么如何 `xargs` 如何从管道 (pipe) 中读取数据呢?其本质上就是从文件描述符 0 也就是 file descriptor 0 (standard input) 标准输入读取数据。这在 [xv6 book](https://pdos.csail.mit.edu/6.828/2022/xv6/book-riscv-rev3.pdf) 1.2章节有说明。
317 |
318 | `xargs` 程序将从管道中读取的参数作为其他程序的命令行参数。
319 |
320 | 例如 `echo a | xargs echo b` ,它将输出 `b a`。`xargs` 将前面程序的输出 `a` 作为后面 `echo` 程序的命令行参数。所以上述命令等价于 `echo b a`。
321 |
322 | 注意事项:
323 |
324 | 1. 每一行输出都使用 `fork` 和 `exec` ,并且使用 `wait` 等待子程序完成。
325 | 2. 通过每次读取一个字符直到一个新行(`'\n'`) 出现,来读取每一行输入。
326 | 3. `kernel/param.h` 定义了 `MAXARG`,这在你定义一个参数数组时会有用。
327 | 4. 运行 `make clean` 清理文件系统。然后再运行 `make qemu`。
328 |
329 | 要测试你的程序,运行一个 shell 脚本。正确的程序将会显示如下结果:
330 |
331 | ~~~bash
332 | make qemu
333 | ...
334 | xv6 kernel is booting
335 |
336 | hart 1 starting
337 | hart 2 starting
338 | init: starting sh
339 | $ sh < xargstest.sh
340 | $ $ $ $ $ $ hello
341 | hello
342 | hello
343 | $ $
344 | ~~~
345 |
346 | 这个脚本实际就是 `find . b | xargs grep hello` ,寻找所有文件 b 中包含 `hello` 的行。
347 |
348 | ~~~c
349 | #include "kernel/types.h"
350 | #include "kernel/stat.h"
351 | #include "kernel/param.h"
352 | #include "user/user.h"
353 |
354 | // 1 为打印调试信息
355 | #define DEBUG 0
356 |
357 | // 宏定义
358 | #define debug(codes) if(DEBUG) {codes}
359 |
360 | void xargs_exec(char* program, char** paraments);
361 |
362 | void
363 | xargs(char** first_arg, int size, char* program_name)
364 | {
365 | char buf[1024];
366 | debug(
367 | for (int i = 0; i < size; ++i) {
368 | printf("first_arg[%d] = %s\n", i, first_arg[i]);
369 | }
370 | )
371 | char *arg[MAXARG];
372 | int m = 0;
373 | while (read(0, buf+m, 1) == 1) {
374 | if (m >= 1024) {
375 | fprintf(2, "xargs: arguments too long.\n");
376 | exit(1);
377 | }
378 | if (buf[m] == '\n') {
379 | buf[m] = '\0';
380 | debug(printf("this line is %s\n", buf);)
381 | memmove(arg, first_arg, sizeof(*first_arg)*size);
382 | // set a arg index
383 | int argIndex = size;
384 | if (argIndex == 0) {
385 | arg[argIndex] = program_name;
386 | argIndex++;
387 | }
388 | arg[argIndex] = malloc(sizeof(char)*(m+1));
389 | memmove(arg[argIndex], buf, m+1);
390 | debug(
391 | for (int j = 0; j <= argIndex; ++j)
392 | printf("arg[%d] = *%s*\n", j, arg[j]);
393 | )
394 | // exec(char*, char** paraments): paraments ending with zero
395 | arg[argIndex+1] = 0;
396 | xargs_exec(program_name, arg);
397 | free(arg[argIndex]);
398 | m = 0;
399 | } else {
400 | m++;
401 | }
402 | }
403 | }
404 |
405 | void
406 | xargs_exec(char* program, char** paraments)
407 | {
408 | if (fork() > 0) {
409 | wait(0);
410 | } else {
411 | debug(
412 | printf("child process\n");
413 | printf(" program = %s\n", program);
414 |
415 | for (int i = 0; paraments[i] != 0; ++i) {
416 | printf(" paraments[%d] = %s\n", i, paraments[i]);
417 | }
418 | )
419 | if (exec(program, paraments) == -1) {
420 | fprintf(2, "xargs: Error exec %s\n", program);
421 | }
422 | debug(printf("child exit");)
423 | }
424 | }
425 |
426 | int
427 | main(int argc, char* argv[])
428 | {
429 | debug(printf("main func\n");)
430 | char *name = "echo";
431 | if (argc >= 2) {
432 | name = argv[1];
433 | debug(
434 | printf("argc >= 2\n");
435 | printf("argv[1] = %s\n", argv[1]);
436 | )
437 | }
438 | else {
439 | debug(printf("argc == 1\n");)
440 | }
441 | xargs(argv + 1, argc - 1, name);
442 | exit(0);
443 | }
444 | ~~~
--------------------------------------------------------------------------------
/doc/如何将一个二进制值的指定位设置为指定的值.md:
--------------------------------------------------------------------------------
1 | # 如何将一个二进制值的指定位设置为指定的值
2 |
3 | 如何将一串二进制的制定位设置为指定的值。假如有一串二进制值 `1010` ,要将第二位设置为 `0`。
4 |
5 | ## 异或 ^
6 |
7 | 首先需要了解异或 `^` 运算符的概念和性质。和 0 异或等于它本身,相同的值异或等于 1。另外一个值得注意的是,一个数和同一个值异或两次等于它本身。
8 |
9 | ## 公式
10 |
11 | 公式:`x = ((x&(1 << n)) ^ x) ^ (a << n)`。 `x` 为原值,`n` 为第 `n` 个值,`a` 为想要设置的值(0或1)。
12 |
13 | 首先 `(x&(1 << n))` 的值为:保留第 `n` 位原来的值,其他位置零。再将此值与原值 `x` 异或,得到一个值:除了第 `n` 个值为零,其他位置为原值。(这是因为,与 `0` 异或的那一位为原值,与相同值异或的那一位为 `0`)。然后此时,再与 `(a << n)` 异或,将第 `n` 位设置为 `a` (这是因为与 `0` 异或为其本身)。
14 |
15 | ## 过程
16 |
17 | ```bash
18 | 1、原值: 1 0 | 1 | 0 x
19 | 2、其他位为0: 0 0 | 1 | 0 x & ( 1 << n )
20 | 3、待设置的值 0 0 | 0 | 0 a << n
21 | 4、将 1 和 2 异或得到: 1 0 | 0 | 0 x & ( 1 << n ) ^ x
22 | 5、将 3 和 4 异或得到: 1 0 | 0 | 0 ((x&(1 << n)) ^ x) ^ (a << n)
23 | ```
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/mkfs/mkfs.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 |
8 | #define stat xv6_stat // avoid clash with host struct stat
9 | #include "kernel/types.h"
10 | #include "kernel/fs.h"
11 | #include "kernel/stat.h"
12 | #include "kernel/param.h"
13 |
14 | #ifndef static_assert
15 | #define static_assert(a, b) do { switch (0) case 0: case (a): ; } while (0)
16 | #endif
17 |
18 | #define NINODES 200
19 |
20 | // Disk layout:
21 | // [ boot block | sb block | log | inode blocks | free bit map | data blocks ]
22 |
23 | int nbitmap = FSSIZE/(BSIZE*8) + 1;
24 | int ninodeblocks = NINODES / IPB + 1;
25 | int nlog = LOGSIZE;
26 | int nmeta; // Number of meta blocks (boot, sb, nlog, inode, bitmap)
27 | int nblocks; // Number of data blocks
28 |
29 | int fsfd;
30 | struct superblock sb;
31 | char zeroes[BSIZE];
32 | uint freeinode = 1;
33 | uint freeblock;
34 |
35 |
36 | void balloc(int);
37 | void wsect(uint, void*);
38 | void winode(uint, struct dinode*);
39 | void rinode(uint inum, struct dinode *ip);
40 | void rsect(uint sec, void *buf);
41 | uint ialloc(ushort type);
42 | void iappend(uint inum, void *p, int n);
43 | void die(const char *);
44 |
45 | // convert to riscv byte order
46 | ushort
47 | xshort(ushort x)
48 | {
49 | ushort y;
50 | uchar *a = (uchar*)&y;
51 | a[0] = x;
52 | a[1] = x >> 8;
53 | return y;
54 | }
55 |
56 | uint
57 | xint(uint x)
58 | {
59 | uint y;
60 | uchar *a = (uchar*)&y;
61 | a[0] = x;
62 | a[1] = x >> 8;
63 | a[2] = x >> 16;
64 | a[3] = x >> 24;
65 | return y;
66 | }
67 |
68 | int
69 | main(int argc, char *argv[])
70 | {
71 | int i, cc, fd;
72 | uint rootino, inum, off;
73 | struct dirent de;
74 | char buf[BSIZE];
75 | struct dinode din;
76 |
77 |
78 | static_assert(sizeof(int) == 4, "Integers must be 4 bytes!");
79 |
80 | if(argc < 2){
81 | fprintf(stderr, "Usage: mkfs fs.img files...\n");
82 | exit(1);
83 | }
84 |
85 | assert((BSIZE % sizeof(struct dinode)) == 0);
86 | assert((BSIZE % sizeof(struct dirent)) == 0);
87 |
88 | fsfd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0666);
89 | if(fsfd < 0)
90 | die(argv[1]);
91 |
92 | // 1 fs block = 1 disk sector
93 | nmeta = 2 + nlog + ninodeblocks + nbitmap;
94 | nblocks = FSSIZE - nmeta;
95 |
96 | sb.magic = FSMAGIC;
97 | sb.size = xint(FSSIZE);
98 | sb.nblocks = xint(nblocks);
99 | sb.ninodes = xint(NINODES);
100 | sb.nlog = xint(nlog);
101 | sb.logstart = xint(2);
102 | sb.inodestart = xint(2+nlog);
103 | sb.bmapstart = xint(2+nlog+ninodeblocks);
104 |
105 | printf("nmeta %d (boot, super, log blocks %u inode blocks %u, bitmap blocks %u) blocks %d total %d\n",
106 | nmeta, nlog, ninodeblocks, nbitmap, nblocks, FSSIZE);
107 |
108 | freeblock = nmeta; // the first free block that we can allocate
109 |
110 | for(i = 0; i < FSSIZE; i++)
111 | wsect(i, zeroes);
112 |
113 | memset(buf, 0, sizeof(buf));
114 | memmove(buf, &sb, sizeof(sb));
115 | wsect(1, buf);
116 |
117 | rootino = ialloc(T_DIR);
118 | assert(rootino == ROOTINO);
119 |
120 | bzero(&de, sizeof(de));
121 | de.inum = xshort(rootino);
122 | strcpy(de.name, ".");
123 | iappend(rootino, &de, sizeof(de));
124 |
125 | bzero(&de, sizeof(de));
126 | de.inum = xshort(rootino);
127 | strcpy(de.name, "..");
128 | iappend(rootino, &de, sizeof(de));
129 |
130 | for(i = 2; i < argc; i++){
131 | // get rid of "user/"
132 | char *shortname;
133 | if(strncmp(argv[i], "user/", 5) == 0)
134 | shortname = argv[i] + 5;
135 | else
136 | shortname = argv[i];
137 |
138 | assert(index(shortname, '/') == 0);
139 |
140 | if((fd = open(argv[i], 0)) < 0)
141 | die(argv[i]);
142 |
143 | // Skip leading _ in name when writing to file system.
144 | // The binaries are named _rm, _cat, etc. to keep the
145 | // build operating system from trying to execute them
146 | // in place of system binaries like rm and cat.
147 | if(shortname[0] == '_')
148 | shortname += 1;
149 |
150 | inum = ialloc(T_FILE);
151 |
152 | bzero(&de, sizeof(de));
153 | de.inum = xshort(inum);
154 | strncpy(de.name, shortname, DIRSIZ);
155 | iappend(rootino, &de, sizeof(de));
156 |
157 | while((cc = read(fd, buf, sizeof(buf))) > 0)
158 | iappend(inum, buf, cc);
159 |
160 | close(fd);
161 | }
162 |
163 | // fix size of root inode dir
164 | rinode(rootino, &din);
165 | off = xint(din.size);
166 | off = ((off/BSIZE) + 1) * BSIZE;
167 | din.size = xint(off);
168 | winode(rootino, &din);
169 |
170 | balloc(freeblock);
171 |
172 | exit(0);
173 | }
174 |
175 | void
176 | wsect(uint sec, void *buf)
177 | {
178 | if(lseek(fsfd, sec * BSIZE, 0) != sec * BSIZE)
179 | die("lseek");
180 | if(write(fsfd, buf, BSIZE) != BSIZE)
181 | die("write");
182 | }
183 |
184 | void
185 | winode(uint inum, struct dinode *ip)
186 | {
187 | char buf[BSIZE];
188 | uint bn;
189 | struct dinode *dip;
190 |
191 | bn = IBLOCK(inum, sb);
192 | rsect(bn, buf);
193 | dip = ((struct dinode*)buf) + (inum % IPB);
194 | *dip = *ip;
195 | wsect(bn, buf);
196 | }
197 |
198 | void
199 | rinode(uint inum, struct dinode *ip)
200 | {
201 | char buf[BSIZE];
202 | uint bn;
203 | struct dinode *dip;
204 |
205 | bn = IBLOCK(inum, sb);
206 | rsect(bn, buf);
207 | dip = ((struct dinode*)buf) + (inum % IPB);
208 | *ip = *dip;
209 | }
210 |
211 | void
212 | rsect(uint sec, void *buf)
213 | {
214 | if(lseek(fsfd, sec * BSIZE, 0) != sec * BSIZE)
215 | die("lseek");
216 | if(read(fsfd, buf, BSIZE) != BSIZE)
217 | die("read");
218 | }
219 |
220 | uint
221 | ialloc(ushort type)
222 | {
223 | uint inum = freeinode++;
224 | struct dinode din;
225 |
226 | bzero(&din, sizeof(din));
227 | din.type = xshort(type);
228 | din.nlink = xshort(1);
229 | din.size = xint(0);
230 | winode(inum, &din);
231 | return inum;
232 | }
233 |
234 | void
235 | balloc(int used)
236 | {
237 | uchar buf[BSIZE];
238 | int i;
239 |
240 | printf("balloc: first %d blocks have been allocated\n", used);
241 | assert(used < BSIZE*8);
242 | bzero(buf, BSIZE);
243 | for(i = 0; i < used; i++){
244 | buf[i/8] = buf[i/8] | (0x1 << (i%8));
245 | }
246 | printf("balloc: write bitmap block at sector %d\n", sb.bmapstart);
247 | wsect(sb.bmapstart, buf);
248 | }
249 |
250 | #define min(a, b) ((a) < (b) ? (a) : (b))
251 |
252 | void
253 | iappend(uint inum, void *xp, int n)
254 | {
255 | char *p = (char*)xp;
256 | uint fbn, off, n1;
257 | struct dinode din;
258 | char buf[BSIZE];
259 | uint indirect[NINDIRECT];
260 | uint x;
261 |
262 | rinode(inum, &din);
263 | off = xint(din.size);
264 | // printf("append inum %d at off %d sz %d\n", inum, off, n);
265 | while(n > 0){
266 | fbn = off / BSIZE;
267 | assert(fbn < MAXFILE);
268 | if(fbn < NDIRECT){
269 | if(xint(din.addrs[fbn]) == 0){
270 | din.addrs[fbn] = xint(freeblock++);
271 | }
272 | x = xint(din.addrs[fbn]);
273 | } else {
274 | if(xint(din.addrs[NDIRECT]) == 0){
275 | din.addrs[NDIRECT] = xint(freeblock++);
276 | }
277 | rsect(xint(din.addrs[NDIRECT]), (char*)indirect);
278 | if(indirect[fbn - NDIRECT] == 0){
279 | indirect[fbn - NDIRECT] = xint(freeblock++);
280 | wsect(xint(din.addrs[NDIRECT]), (char*)indirect);
281 | }
282 | x = xint(indirect[fbn-NDIRECT]);
283 | }
284 | n1 = min(n, (fbn + 1) * BSIZE - off);
285 | rsect(x, buf);
286 | bcopy(p, buf + off - (fbn * BSIZE), n1);
287 | wsect(x, buf);
288 | n -= n1;
289 | off += n1;
290 | p += n1;
291 | }
292 | din.size = xint(off);
293 | winode(inum, &din);
294 | }
295 |
296 | void
297 | die(const char *s)
298 | {
299 | perror(s);
300 | exit(1);
301 | }
302 |
--------------------------------------------------------------------------------