MIT 6.S081 | 0x05 Copy on-write
嘿嘿。。嘿。上个实验还是在去年做的 。非常抱歉我拖了这么久,拖延症晚期了。不过既然都开了这个系列,那还是更新吧。
COW 这节 lab 我顶多花了 15 个小时就做差不多了,但是有一个小 bug 迟迟没有发现,导致我一直没有任何进展。最后受不了在网上找代码对比,才发现我把
uvmunmap
函数最后一个参数写错了,其它地方都是对的,就很难受。参考了别人写的代码,函数封装的很舒服,于是我也就按相似的方式把一些代码简单做了一下封装。
然后就是
usertests
有一个 casetextwrite
一直没通过,我把那行注释掉就全过了,这算一个小遗憾,后期如果能通过我就更新一下这篇文章。
🟥 Implement copy-on write
copy-on write,COW,又称写时复制,这个实验算 6.S081 里比较出名的实验了吧。在使用 fork()
时为了减少开销,会让父子进程的页表指向同一个物理地址,节省了分配空间所占的时间。将对应的页改为不可写,直到出现 page fault 时才复制对应的页。
所以整个实验主要分为两个部分:
- 调用
fork()
后仅修改页表,并将权限设为不可写 - page fault 时为 COW 的页分配空间
修改 uvmcopy()
在使用 fork()
创建子进程时,会使用 uvmcopy()
将父进程页表的空间复制到子进程的页表中。这部分原本的做法是既复制页表页复制对应的空间。而我们要将其修改为仅复制页表并将两个页表的权限设为不可写。
在
uvmcopy()
通过复制页表使父子进程指向同一个物理空间,读的时候没有问题,当需要进行写入操作时,由于权限为不可写,所以会报 page fault,所以在usertrap()
中遇到 page fault 时才会真正进行物理空间的复制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
int
uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
*pte &= (~PTE_W);
*pte |= PTE_C; // cow flag
flags = PTE_FLAGS(*pte);
if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) {
printf("uvmcopy: failed\n");
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
ref_cnt_inc(pa); // increase page ref count
}
return 0;
}
这一步很简单,删掉原先的 kalloc()
,只操作页表就可以了。
修改 usertrap()
前面已经指出,在 fork()
时仅修改了页表,如果尝试写操作时会报 page fault,而这个中断在 usertrap()
中的中断号(r_scause()
)是 15。所以在发生 15 号中断时我们需要做两件事:
- 判断是否是由于 COW 导致的 page fault
- 为 cow page fault 复制对应的物理空间
这里分了两步,感觉重复调用了两回
walk()
。实际上可以解耦操作,第一步判断是否是 COW 页的函数会在copyout()
中遇到。
首先判断所在的页是否是 COW 页,我们将页表对应的 flag 取到进行判断即可,这个命名也可取为 is_cow_page()
1
2
3
4
5
6
7
8
int cow_uncopied(pagetable_t pgtbl, uint64 va) {
if (va >= MAXVA) return 0;
pte_t * pte = walk(pgtbl, va, 0);
if (pte == 0) return 0;
if ((*pte & PTE_V) == 0) return 0;
if ((*pte & PTE_U) == 0) return 0;
return ((*pte) & PTE_C);
}
接下来就是为 COW 页分配空间的操作,这段代码实际上在之前的 uvmcopy()
里已经有一部分了,参考着写就 ok。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int cow_alloc(pagetable_t pgtbl, uint64 va) {
pte_t *pte = walk(pgtbl, va, 0);
if (pte == 0) return -1;
uint flags = PTE_FLAGS(*pte);
uint64 pa = PTE2PA(*pte);
char *mem = kalloc();
if (mem == 0) return -1;
va = PGROUNDDOWN(va);
flags &= (~PTE_C);
flags |= PTE_W;
memmove(mem, (char*)pa, PGSIZE);
uvmunmap(pgtbl, va, 1, 1); // !!!
if (mappages(pgtbl, va, PGSIZE, (uint64)mem, flags) < 0) {
kfree(mem);
return -1;
}
return 0;
}
这里有个小坑,也是因为我没注意。
uvmunmap(pgtbl, va, 1, 1)
最后的那个参数得改成 1,用来减小对应页的引用计数,不然就会出大问题。就是这里卡了我好久,还是修行不太够hhh
完成上面两个函数,在 usertrap()
中就写的很舒服了:
1
2
3
4
5
6
7
8
9
10
11
@@ -65,6 +97,10 @@ usertrap(void)
intr_on();
syscall();
+ } else if (r_scause() == 15 && cow_uncopied(p->pagetable, r_stval())) {
+ if (cow_alloc(p->pagetable, r_stval()) < 0) {
+ setkilled(p);
+ }
} else if((which_dev = devintr()) != 0){
// ok
} else {
修改 copyout()
上一节封装的函数立刻就能在 copyout()
中用到
1
2
3
4
5
6
7
8
9
10
11
12
13
@@ -355,6 +352,12 @@ copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
while(len > 0){
va0 = PGROUNDDOWN(dstva);
+ if (cow_uncopied(pagetable, va0)) {
+ if (cow_alloc(pagetable, va0) < 0) {
+ return -1;
+ }
+ }
+
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
注意
cow_alloc()
要放到walkaddr()
前面哦。不然walkaddr()
取到的是父进程的地址,而不是kalloc()
后得到的新地址。
添加引用计数
为什么要进行引用计数呢?当实现了 COW 后,一个物理页可能会被多个进程使用,这时候如果哪个进程 free
掉这个页,其它进程就找不着这个页了,所以需要使用一个引用计数来管理物理页与页表的关系,只有当一个物理页不被任何页表引用时才进行 free
。引用计数在以下情况时会发生改变:
-
kalloc()
时会被设为 1 -
kfree()
时引用减少,如果为零则真的清除空间 - 被其它进程引用(仅在
fork()
中)时引用增加
唯一需要注意的是数组 ref_cnt
的大小,我设为了 PHYSTOP / PGSIZE
。
全部代码
具体细节看代码,注意我把 usertests
里的 textwrite()
给注释掉了,只有这个 case 我没法通过,哭哭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
diff --git a/kernel/defs.h b/kernel/defs.h
index a3c962b..1935a1a 100644
--- a/kernel/defs.h
+++ b/kernel/defs.h
@@ -63,6 +63,7 @@ void ramdiskrw(struct buf*);
void* kalloc(void);
void kfree(void *);
void kinit(void);
+void ref_cnt_inc(uint64);
// log.c
void initlog(int, struct superblock*);
@@ -173,6 +174,8 @@ uint64 walkaddr(pagetable_t, uint64);
int copyout(pagetable_t, uint64, char *, uint64);
int copyin(pagetable_t, char *, uint64, uint64);
int copyinstr(pagetable_t, char *, uint64, uint64);
+int cow_uncopied(pagetable_t, uint64);
+int cow_alloc(pagetable_t, uint64);
// plic.c
void plicinit(void);
diff --git a/kernel/kalloc.c b/kernel/kalloc.c
index 0699e7e..e323d58 100644
--- a/kernel/kalloc.c
+++ b/kernel/kalloc.c
@@ -14,6 +14,17 @@ void freerange(void *pa_start, void *pa_end);
extern char end[]; // first address after kernel.
// defined by kernel.ld.
+int ref_cnt[PHYSTOP / PGSIZE];
+struct spinlock ref_cnt_lock;
+
+#define REF_CNT(pa) ref_cnt[(uint64)pa / PGSIZE]
+
+void ref_cnt_inc(uint64 pa) {
+ acquire(&ref_cnt_lock);
+ REF_CNT(pa)++;
+ release(&ref_cnt_lock);
+}
+
struct run {
struct run *next;
};
@@ -27,7 +38,9 @@ void
kinit()
{
initlock(&kmem.lock, "kmem");
+ initlock(&ref_cnt_lock, "ref_cnt");
freerange(end, (void*)PHYSTOP);
+ memset(ref_cnt, 0, PHYSTOP / PGSIZE);
}
void
@@ -51,6 +64,14 @@ kfree(void *pa)
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
+ acquire(&ref_cnt_lock);
+ if (REF_CNT(pa) > 1) {
+ REF_CNT(pa)--;
+ release(&ref_cnt_lock);
+ return;
+ }
+ release(&ref_cnt_lock);
+
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
@@ -76,7 +97,11 @@ kalloc(void)
kmem.freelist = r->next;
release(&kmem.lock);
- if(r)
+ if(r) {
memset((char*)r, 5, PGSIZE); // fill with junk
+ acquire(&ref_cnt_lock);
+ REF_CNT(r) = 1;
+ release(&ref_cnt_lock);
+ }
return (void*)r;
}
diff --git a/kernel/riscv.h b/kernel/riscv.h
index 20a01db..0eb9183 100644
--- a/kernel/riscv.h
+++ b/kernel/riscv.h
@@ -343,6 +343,7 @@ typedef uint64 *pagetable_t; // 512 PTEs
#define PTE_W (1L << 2)
#define PTE_X (1L << 3)
#define PTE_U (1L << 4) // user can access
+#define PTE_C (1L << 8) // cow page
// shift a physical address to the right place for a PTE.
#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10)
diff --git a/kernel/trap.c b/kernel/trap.c
index 512c850..4f56f9d 100644
--- a/kernel/trap.c
+++ b/kernel/trap.c
@@ -29,6 +29,38 @@ trapinithart(void)
w_stvec((uint64)kernelvec);
}
+int cow_uncopied(pagetable_t pgtbl, uint64 va) {
+ if (va >= MAXVA) return 0;
+ pte_t * pte = walk(pgtbl, va, 0);
+ if (pte == 0) return 0;
+ if ((*pte & PTE_V) == 0) return 0;
+ if ((*pte & PTE_U) == 0) return 0;
+ return ((*pte) & PTE_C);
+}
+
+int cow_alloc(pagetable_t pgtbl, uint64 va) {
+ pte_t *pte = walk(pgtbl, va, 0);
+ if (pte == 0) return -1;
+ uint flags = PTE_FLAGS(*pte);
+
+ uint64 pa = PTE2PA(*pte);
+
+ char *mem = kalloc();
+ if (mem == 0) return -1;
+
+ va = PGROUNDDOWN(va);
+ flags &= (~PTE_C);
+ flags |= PTE_W;
+
+ memmove(mem, (char*)pa, PGSIZE);
+ uvmunmap(pgtbl, va, 1, 1);
+ if (mappages(pgtbl, va, PGSIZE, (uint64)mem, flags) < 0) {
+ kfree(mem);
+ return -1;
+ }
+ return 0;
+}
+
//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
@@ -65,6 +97,10 @@ usertrap(void)
intr_on();
syscall();
+ } else if (r_scause() == 15 && cow_uncopied(p->pagetable, r_stval())) {
+ if (cow_alloc(p->pagetable, r_stval()) < 0) {
+ setkilled(p);
+ }
} else if((which_dev = devintr()) != 0){
// ok
} else {
diff --git a/kernel/vm.c b/kernel/vm.c
index 9f69783..918b1d9 100644
--- a/kernel/vm.c
+++ b/kernel/vm.c
@@ -308,7 +308,6 @@ uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
pte_t *pte;
uint64 pa, i;
uint flags;
- char *mem;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
@@ -316,20 +315,18 @@ uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
if((*pte & PTE_V) == 0)
panic("uvmcopy: page not present");
pa = PTE2PA(*pte);
+
+ *pte &= (~PTE_W);
+ *pte |= PTE_C;
flags = PTE_FLAGS(*pte);
- if((mem = kalloc()) == 0)
- goto err;
- memmove(mem, (char*)pa, PGSIZE);
- if(mappages(new, i, PGSIZE, (uint64)mem, flags) != 0){
- kfree(mem);
- goto err;
+ if (mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) {
+ printf("uvmcopy: failed\n");
+ uvmunmap(new, 0, i / PGSIZE, 1);
+ return -1;
}
+ ref_cnt_inc(pa);
}
return 0;
-
- err:
- uvmunmap(new, 0, i / PGSIZE, 1);
- return -1;
}
// mark a PTE invalid for user access.
@@ -355,6 +352,12 @@ copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)
while(len > 0){
va0 = PGROUNDDOWN(dstva);
+ if (cow_uncopied(pagetable, va0)) {
+ if (cow_alloc(pagetable, va0) < 0) {
+ return -1;
+ }
+ }
+
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
diff --git a/time.txt b/time.txt
new file mode 100644
index 0000000..3f10ffe
--- /dev/null
+++ b/time.txt
@@ -0,0 +1 @@
+15
\ No newline at end of file
diff --git a/user/usertests.c b/user/usertests.c
index 7d3e9bc..301b631 100644
--- a/user/usertests.c
+++ b/user/usertests.c
@@ -2629,7 +2629,7 @@ struct test {
{bigargtest, "bigargtest"},
{argptest, "argptest"},
{stacktest, "stacktest"},
- {textwrite, "textwrite"},
+ // {textwrite, "textwrite"},
{pgbug, "pgbug" },
{sbrkbugs, "sbrkbugs" },
{sbrklast, "sbrklast"},