This commit is contained in:
DeathWish5 2021-09-08 23:06:22 +08:00
parent 938f5cafe0
commit ac8bbdcbf0
18 changed files with 719 additions and 578 deletions

View File

@ -7,7 +7,6 @@
.. ..
chyyuu有一个ascii图画出我们做的OS。 chyyuu有一个ascii图画出我们做的OS。
本章展现了操作系统一系列功能: 本章展现了操作系统一系列功能:
- 通过动态内存分配,提高了应用程序对内存的动态使用效率 - 通过动态内存分配,提高了应用程序对内存的动态使用效率
@ -57,6 +56,11 @@
本章的应用和上一章相同,只不过由于内核提供给应用的访存接口被替换,应用的构建方式发生了变化,这方面在下面会深入介绍。 本章的应用和上一章相同,只不过由于内核提供给应用的访存接口被替换,应用的构建方式发生了变化,这方面在下面会深入介绍。
因此应用运行起来的效果与上一章是一致的。 因此应用运行起来的效果与上一章是一致的。
.. warning::
我们不会在以后的实验中用到优先级调度,而 ch3 实现的内存检查也会被虚存直接覆盖。所以你可以直接基本框架代码继续实验,无需 merge ch3 的修改。
获取本章代码: 获取本章代码:
.. code-block:: console .. code-block:: console

View File

@ -209,7 +209,7 @@ mappages的perm是用于控制页表项的flags的。请注意它具体指向哪
} }
// 注意 kalloc() 分配的页为脏页,这里需要先清空。 // 注意 kalloc() 分配的页为脏页,这里需要先清空。
memset(pagetable, 0, PGSIZE); memset(pagetable, 0, PGSIZE);
// 映射 trapoline也就是 uservec 和 userret注意这里的权限! // 映射 trapoline也就是 uservec 和 userret 的代码),注意这里的权限!
if (mappages(pagetable, TRAMPOLINE, PAGE_SIZE, (uint64)trampoline, if (mappages(pagetable, TRAMPOLINE, PAGE_SIZE, (uint64)trampoline,
PTE_R | PTE_X) < 0) { PTE_R | PTE_X) < 0) {
kfree(pagetable); kfree(pagetable);
@ -221,32 +221,33 @@ mappages的perm是用于控制页表项的flags的。请注意它具体指向哪
PTE_R | PTE_W) < 0) { PTE_R | PTE_W) < 0) {
panic("mappages fail"); panic("mappages fail");
} }
// 接下来映射用户实际地址空间,也就是把 physics address [start, end)
// 接下来映射用户实际地址空间,也就是把 physics address [start, end) 
// 映射到虚拟地址 [BASE_ADDRESS, BASE_ADDRESS + length)
// riscv 指令有对齐要求,同时,如果不对齐直接映射的话会把部分内核地址映射到用户态,很不安全
// ch5我们就不需要这个限制了。
if (!PGALIGNED(start)) { if (!PGALIGNED(start)) {
// Fix in ch5
panic("user program not aligned, start = %p", start); panic("user program not aligned, start = %p", start);
} }
if (!PGALIGNED(end)) {
// Fix in ch5
warnf("Some kernel data maybe mapped to user, start = %p, end = %p",
start, end);
}
end = PGROUNDUP(end); end = PGROUNDUP(end);
// 实际的 map 指令。
uint64 length = end - start; uint64 length = end - start;
if (mappages(pg, BASE_ADDRESS, length, start, if (mappages(pg, BASE_ADDRESS, length, start,
PTE_U | PTE_R | PTE_W | PTE_X) != 0) { PTE_U | PTE_R | PTE_W | PTE_X) != 0) {
panic("mappages fail"); panic("mappages fail");
} }
p->pagetable = pg; p->pagetable = pg;
// 接下来 map user stack 注意这里的虚存选择,想想为何要这样?
uint64 ustack_bottom_vaddr = BASE_ADDRESS + length + PAGE_SIZE; uint64 ustack_bottom_vaddr = BASE_ADDRESS + length + PAGE_SIZE;
if (USTACK_SIZE != PAGE_SIZE) {
// Fix in ch5
panic("Unsupported");
}
mappages(pg, ustack_bottom_vaddr, USTACK_SIZE, (uint64)kalloc(), mappages(pg, ustack_bottom_vaddr, USTACK_SIZE, (uint64)kalloc(),
PTE_U | PTE_R | PTE_W | PTE_X); PTE_U | PTE_R | PTE_W | PTE_X);
p->ustack = ustack_bottom_vaddr; p->ustack = ustack_bottom_vaddr;
// 设置 trapframe
p->trapframe->epc = BASE_ADDRESS; p->trapframe->epc = BASE_ADDRESS;
p->trapframe->sp = p->ustack + USTACK_SIZE; p->trapframe->sp = p->ustack + USTACK_SIZE;
// exit 的时候会 unmap 页表中 [BASE_ADDRESS, max_page * PAGE_SIZE) 的页
p->max_page = PGROUNDUP(p->ustack + USTACK_SIZE - 1) / PAGE_SIZE; p->max_page = PGROUNDUP(p->ustack + USTACK_SIZE - 1) / PAGE_SIZE;
return pg; return pg;
} }

View File

@ -1,74 +1,71 @@
chapter4练习 chapter4练习
============================================ ============================================
- 本节难度: **看懂代码就和lab1一样** - 本节难度: **有一定困难,尽早开始**
本章任务
-------------------------------------------
- 我们不会在以后的实验中用到优先级调度,而 ch3 实现的内存检查也会被虚存直接覆盖。所以你可以直接基本框架代码继续实验,无需 merge ch3 的修改。
- ``make test BASE=1``
- 理解 vm.c 中的几个函数的大致功能,通过 bin_loader 理解当前用户程序的虚存布局。
- 结合课堂内容,完成本章问答作业。
- 完成本章编程作业。
- 最终,完成实验报告并 push 你的 ch4 分支到远程仓库。
编程作业 编程作业
--------------------------------------------- ---------------------------------------------
申请内存 mmap 匿名映射
++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++
你有没有想过,当你在 C 语言中写下的 ``new int[100];`` 执行时可能会发生哪些事情?你可能已经发现,目前我们给用户程序的内存都是固定的并没有增长的能力,这些程序是不能执行 ``new`` 这类导致内存使用增加的操作。libc 中通过 `sbrk <https://linux.die.net/man/2/sbrk>`_ 系统调用增加进程可使用的堆空间这也是本来的题目设计但是一位热心的往年助教J学长表示这一点也不酷他推荐了另一个申请内存的系统调用。 你有没有想过,当你在 C 语言中写下的 ``new int[100];`` 执行时可能会发生哪些事情?你可能已经发现,目前我们给用户程序的内存都是固定的并没有增长的能力,这些程序是不能执行 ``new`` 这类导致内存使用增加的操作。libc 中通过 `sbrk <https://linux.die.net/man/2/sbrk>`_ 系统调用增加进程可使用的堆空间这也是本来的题目设计但是一位热心的往年助教J学长表示这一点也不酷他推荐了另一个申请内存的系统调用。
`mmap <https://man7.org/linux/man-pages/man2/mmap.2.html>`_ 本身主要使用来在内存中映射文件的,这里我们简化它的功能,仅仅用来提供申请内存的功能。 `mmap <https://man7.org/linux/man-pages/man2/mmap.2.html>`_ 本身主要使用来在内存中映射文件的,这里我们简化它的功能,仅仅使用匿名映射
mmap 系统调用新定义: mmap 系统调用新定义:
- syscall ID222 - syscall ID222
- C接口 ``int mmap(void* start, unsigned long long len, int port)`` - 接口: ``int mmap(void* start, unsigned long long len, int port int flag, int fd)``
- Rust接口 ``fn mmap(start: usize, len: usize, port: usize) -> i32`` - 功能:申请长度为 len 字节的匿名物理内存(不要求实际物理内存位置,可以随便找一块),并映射到 addr 开始的虚存,内存页属性为 port。
- 功能:申请长度为 len 字节的物理内存(不要求实际物理内存位置,可以随便找一块),并映射到 addr 开始的虚存,内存页属性为 port。
- 参数: - 参数:
- start需要映射的虚存起始地址。 - start需要映射的虚存起始地址。
- len映射字节长度可以为 0 (如果是则直接返回),不可过大(上限 1GiB )。 - len映射字节长度可以为 0 (如果是则直接返回),不可过大(上限 1GiB )。
- port第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效(必须为 0 )。 - port第 0 位表示是否可读,第 1 位表示是否可写,第 2 位表示是否可执行。其他位无效(必须为 0 )。
- flag目前始终为 0忽略该参数。
- fd目前始终为 0, 忽略该参数。
- 说明: - 说明:
- 正确时返回实际 map size为 4096 的倍数),错误返回 -1 。 - 正确时返回实际 map size为 4096 的倍数),错误返回 -1 。
- 为了简单addr 要求按页对齐(否则报错)len 可直接按页上取整。 - 为了简单addr 要求按页对齐(否则报错)len 可直接按页上取整。
- 为了简单,不考虑分配失败时的页回收(也就是内存泄漏)。 - 为了简单,不考虑分配失败时的页回收。
- flag, fd 参数留待后续实验拓展。
- 错误: - 错误:
- [addr, addr + len) 存在已经被映射的页。 - [addr, addr + len) 存在已经被映射的页。
- 物理内存不足。 - 物理内存不足。
- port & !0x7 != 0 (port 其余位必须为0)。 - port & ~0x7 == 0port 其他位必须为 0
- port & 0x7 = 0 (这样的内存无意义)。 - port & 0x7 != 0不可读不可写不可执行的内存无意义
munmap 系统调用新定义: munmap 系统调用新定义:
- syscall ID215 - syscall ID215
- C接口 ``int munmap(void* start, unsigned long long len)`` - 接口: ``int munmap(void* start, unsigned long long len)``
- Rust接口 ``fn munmap(start: usize, len: usize) -> i32``
- 功能:取消一块虚存的映射。 - 功能:取消一块虚存的映射。
- 参数:同 mmap - 参数:同 mmap
- 说明: - 说明:
- 为了简单,参数错误时不考虑内存的恢复和回收。 - 为了简单,参数错误时不考虑内存的恢复和回收。
- 错误: - 错误:
- [start, start + len) 中存在未被映射的虚存。 - [start, start + len) 中存在未被映射的虚存。
实验要求
++++++++++++++++++++++++++++++++++++++++++
- 实现分支ch4。
- 完成实验指导书中的内容实现虚拟内存可以运行过去几个lab的程序。
- 更新 sys_write 的范围检查,改为基于页表的检查方法。
- 实现 mmap 和 munmap 两个自定义系统调用,并通过 `Rust测例 <https://github.com/DeathWish5/rCore_tutorial_tests>`_ 中 chapter4 对应的所有测例,测例详情见对应仓库,系统调用具体要求参考 `guide.md <https://github.com/DeathWish5/rCore_tutorial_tests/blob/master/guide.md>`_ 中chapter4对应的所有测例。
注意:记得删除 lab3 关于程序时间片上界的规定 正确实现后,你的 os 应该能够正确运行 ch4_* 对应的一些测试用例,``make test BASE=0`` 来执行测试。
challenge: 支持多核。 tips:
- 匿名映射的页可以使用 kalloc() 得到。
- 注意 kalloc 不支持连续物理内存分配,所以你必须把多个页的 mmap 逐页进行映射。
- 一定要注意 mmap 是的页表项,注意 riscv 页表项的格式与 port 的区别。
- 你增加 PTE_U 了吗?
实验检查
+++++++++++++++++++++++++++++++++++++++++++++
- 实验目录要求
目录要求不变(参考 lab1 目录或者示例代码目录结构)。同样在 os 目录下 `make run` 之后可以正确加载用户程序并执行。
加载的用户测例位置: ``../user/build/bin``
- 检查
可以正确 `make run` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。
问答作业 问答作业
------------------------------------------------- -------------------------------------------------
@ -97,7 +94,7 @@ challenge: 支持多核。
- 此时页面失效如何表现在页表项(PTE)上? - 此时页面失效如何表现在页表项(PTE)上?
3. 双页表与单页表 3. [选做,不占分]双页表与单页表
为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说,用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。(备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 `KPTI <https://en.wikipedia.org/wiki/Kernel_page-table_isolation>`_ ) 为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说,用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。(备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 `KPTI <https://en.wikipedia.org/wiki/Kernel_page-table_isolation>`_ )
@ -108,8 +105,7 @@ challenge: 支持多核。
报告要求 报告要求
-------------------------------------------------------- --------------------------------------------------------
- pdf 格式CI 网站提交,注明姓名学号。
* 简单总结本次实验与上个实验相比你增加的东西。控制在5行以内不要贴代码 - 完成 ch4 问答作业。
* 完成问答问题。 - [可选,不占分]你对本次实验设计及难度的看法。
* (optional) 你对本次实验设计及难度的看法。

View File

@ -9,4 +9,4 @@
2address-space 2address-space
3sv39-implementation-1 3sv39-implementation-1
4sv39-implementation-2 4sv39-implementation-2
5exercise 7exercise

View File

@ -31,83 +31,42 @@
.. code-block:: console .. code-block:: console
$ cd os $ make test BASE=1
$ make run
将 Maix 系列开发板连接到 PC并在上面运行本章代码 # ....
app list:
ch2b_exit
ch2b_hello_world
ch2b_power
ch2b_write1
ch3b_sleep
ch3b_sleep1
ch3b_yield0
ch3b_yield1
ch3b_yield2
ch5b_exec_simple
ch5b_exit
ch5b_forktest0
ch5b_forktest1
ch5b_forktest2
ch5b_getpid
ch5b_usertest
usershell
C user shell
>>
.. code-block:: console 不出意外,你将最终运行进入 C suer shell这里你可以输入 app list 中的一个应用,敲击回车之后就可以运行。其中 ``ch5b_usertest`` 打包了很多应用,只要执行它就能够自动执行所有基础测试:
$ cd os .. code-block:: bash
$ make run BOARD=k210
待内核初始化完毕之后将在屏幕上打印可用的应用列表并进入shell程序以 K210 平台为例): >> ch2b_exit
Shell: Process 2 exited with code 1234
>> ch2b_hello_world
Hello world from user mode program!
Test hello_world OK!
Shell: Process 3 exited with code 0
.. code-block:: 当应用执行完毕后将继续回到shell程序的命令输入模式。另外这个命令行支持退格键。
[rustsbi] RustSBI version 0.1.1
.______ __ __ _______.___________. _______..______ __
| _ \ | | | | / | | / || _ \ | |
| |_) | | | | | | (----`---| |----`| (----`| |_) || |
| / | | | | \ \ | | \ \ | _ < | |
| |\ \----.| `--' |.----) | | | .----) | | |_) || |
| _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|
[rustsbi] Platform: K210 (Version 0.1.0)
[rustsbi] misa: RV64ACDFIMSU
[rustsbi] mideleg: 0x22
[rustsbi] medeleg: 0x1ab
[rustsbi] Kernel entry: 0x80020000
[kernel] Hello, world!
last 808 Physical Frames.
.text [0x80020000, 0x8002e000)
.rodata [0x8002e000, 0x80032000)
.data [0x80032000, 0x800c7000)
.bss [0x800c7000, 0x802d8000)
mapping .text section
mapping .rodata section
mapping .data section
mapping .bss section
mapping physical memory
remap_test passed!
after initproc!
/**** APPS ****
exit
fantastic_text
forktest
forktest2
forktest_simple
forktree
hello_world
initproc
matrix
sleep
sleep_simple
stack_overflow
user_shell
usertests
yield
**************/
Rust user shell
>>
其中 ``usertests`` 打包了很多应用,只要执行它就能够自动执行一系列应用。
只需输入应用的名称并回车即可在系统中执行该应用。如果输入错误的话可以使用退格键 (Backspace) 。以应用 ``exit`` 为例:
.. code-block::
>> exit
I am the parent. Forking the child...
I am the child.
I am parent, fork a child pid 3
I am the parent, waiting now..
waitpid 3 ok.
exit pass.
Shell: Process 2 exited with code 0
>>
当应用执行完毕后将继续回到shell程序的命令输入模式。
本章代码导读 本章代码导读

View File

@ -62,40 +62,33 @@ fork 系统调用
.. code-block:: c .. code-block:: c
int int fork()
fork(void)
{ {
int pid;
struct proc *np;
struct proc *p = curr_proc(); struct proc *p = curr_proc();
// Allocate process. struct proc *np = allocproc();
if((np = allocproc()) == 0){
panic("allocproc\n");
}
// Copy user memory from parent to child. // Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){ uvmcopy(p->pagetable, np->pagetable, p->max_page);
panic("uvmcopy\n"); np->max_page = p->max_page;
}
np->sz = p->sz;
// copy saved user registers. // copy saved user registers.
*(np->trapframe) = *(p->trapframe); *(np->trapframe) = *(p->trapframe);
// Cause fork to return 0 in the child. // Cause fork to return 0 in the child.
np->trapframe->a0 = 0; np->trapframe->a0 = 0;
pid = np->pid;
np->parent = p; np->parent = p;
np->state = RUNNABLE; np->state = RUNNABLE;
return pid; return np->pid;
} }
首先fork调用allocproc分配一个新的进程PCB具体内容请见lab3lab4注意页表的初始化也在alloc时完成了。之后根据fork的规定我们需要把进程A的内存拷贝至B的进程使得二者一样。我们不能仅仅拷贝一份一模一样的页表那么父子进程就会修改同样的物理内存发生数据冲突不符合进程隔离的要求。需要把页表对应的页先拷贝一份然后建立一个对这些新页有同样映射的页表。这一工作由一个 uvmcopy 的函数去做。uvmcopy函数会遍历A进程的页表以页为单位将对应的内存复制到B进程页表中新kalloc的空闲地址之中。注意由于mmap系统调用的存在我们不能简单直接复制A进程虚拟地址[0x0, memory size)对应的物理地址到B这样会产生遗漏。 首先fork调用allocproc分配一个新的进程PCB具体内容请见lab3lab4注意页表的初始化也在alloc时完成了。之后根据fork的规定我们需要把进程A的内存拷贝至B的进程使得二者一样。我们不能仅仅拷贝一份一模一样的页表那么父子进程就会修改同样的物理内存发生数据冲突不符合进程隔离的要求。需要把页表对应的页先拷贝一份然后建立一个对这些新页有同样映射的页表。这一工作由一个 uvmcopy 的函数去做。uvmcopy函数会遍历A进程的页表以页为单位将对应的内存复制到B进程页表中新kalloc的空闲地址之中。
.. warning::
注意 mmap 对于进程 max_page 的影响。在 ch4 中,即便实现错误导致了内存泄漏也不会有直接致命的影响,但在 lab5 就不是这样了!修复你的 mmap 实现!
之后我们把A的trapframe也复制给B确保了B能继续A的执行流。但是我们设定a0寄存器的值为a这是因为fork要求子进程的fork返回值是0。之后就是对于PCB的状态设定。 之后我们把A的trapframe也复制给B确保了B能继续A的执行流。但是我们设定a0寄存器的值为a这是因为fork要求子进程的fork返回值是0。之后就是对于PCB的状态设定。
全部处理完之后我们就得到了fork的新进程并且父进程此时的返回值就是子进程的pid。 全部处理完之后我们就得到了fork的新进程并且父进程此时的返回值就是子进程的pid。
这里大家要仔细思考一下当调度的我们新生成的子进程B的时候它的执行流具体是什么样子的这个问题对于理解OS框架十分重要。 这里大家要仔细思考一下当调度的我们新生成的子进程B的时候它的执行流具体是什么样子的这个问题对于理解OS框架十分重要。提示:新进程的 context 是怎样的allocproc 会在进程池中新增一个进程,那么调度到的这个进程会从哪里开始执行?
wait 系统调用 wait 系统调用
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -154,35 +147,23 @@ exec 系统调用
.. code-block:: c .. code-block:: c
int exec(char* name) { int exec(char *name)
{
int id = get_id_by_name(name); int id = get_id_by_name(name);
if(id < 0) if (id < 0)
return -1; return -1;
struct proc *p = curr_proc(); struct proc *p = curr_proc();
proc_freepagetable(p->pagetable, p->sz); uvmunmap(p->pagetable, 0, p->max_page, 1);
p->sz = 0; p->max_page = 0;
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
panic("");
}
loader(id, p); loader(id, p);
return 0; return 0;
} }
我们exec的设计是传入待执行测例的文件名。之后会找到文件名对应的id。如果存在对应文件就会执行内存的释放。 我们exec的设计是传入待执行测例的文件名。之后会找到文件名对应的id。如果存在对应文件就会执行内存的释放。
.. code-block:: c 由于 trapframe 和 trampoline 是可以复用的(每个进程都一样),所以我们并不会把他们 unmap。而对于用户真正的数据就会删掉映射的同时把物理页面也 free 掉。
void proc_freepagetable(pagetable_t pagetable, uint64 sz) 之后就是执行 loader 函数这个loader函数相较前面的章节有比较大的修改我们会在下一节说明。
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
}
由于 trapframe 和 trampoline 是可以复用的(每个进程都一样),所以我们并不会把他们删掉,而仅仅是 unmap。而对于用户真正的数据就会删掉映射的同时把物理页面也 free 掉。(其实 trapframe 和 trampoline 也可以不 unmap 直接用,但我们想复用 loader.c 中的代码,所以先 unmap 掉。)
之后重新创建一个新的页表并进行trapframe 和 trampoline的新的映射并将测例加载进入新的进程。这个loader函数相较前面的章节有比较大的修改我们会在下一节说明。
支持了fork和exec之后我们就用拥有了支持shell的基本能力。 支持了fork和exec之后我们就用拥有了支持shell的基本能力。

View File

@ -9,143 +9,184 @@ shell与测例的加载
新的bin_loader 新的bin_loader
------------------------------------------------------------------------ ------------------------------------------------------------------------
exec会调用bin_loader,将对应文件名的测例加载到指定的进程p之中。 exec会调用bin_loader,将对应文件名的测例加载到指定的进程p之中。请结合注释理解 bin_loader 的变化:
.. code-block:: c .. code-block:: c
:linenos: :linenos:
void bin_loader(uint64 start, uint64 end, struct proc *p) { int bin_loader(uint64 start, uint64 end, struct proc *p)
uint64 s = PGROUNDDOWN(start), e = PGROUNDUP(end), length = e - s; {
// proc_pagetable 完成 trapframe 和 trampoline 的映射 void *page;
p->pagetable = proc_pagetable(p); // 注意现在我们不要求对其了,代码的核心逻辑还是把 [start, end)
// 完成 .bin 数据的映射 // 映射到虚拟内存的 [BASE_ADDRESS, BASE_ADDRESS + length)
for(uint64 va = BASE_ADDRESS, pa = s; pa < e; va += PGSIZE, pa += PGSIZE) { uint64 pa_start = PGROUNDDOWN(start);
void* page = kalloc(); uint64 pa_end = PGROUNDUP(end);
memmove(page, (const void*)pa, PGSIZE); uint64 length = pa_end - pa_start;
mappages(p->pagetable, va, PGSIZE, (uint64)page, PTE_U | PTE_R | PTE_W | PTE_X); uint64 va_start = BASE_ADDRESS;
} uint64 va_end = BASE_ADDRESS + length;
// 完成用户栈的映射 // 不再一次 map 很多页面,而是逐页 map为什么
alloc_ustack(p); for (uint64 va = va_start, pa = pa_start; pa < pa_end;
va += PGSIZE, pa += PGSIZE) {
p->trapframe->epc = BASE_ADDRESS; // 这里我们不会直接映射,而是新分配一个页面,然后使用 memmove 进行拷贝
p->sz = USTACK_SIZE + length; // 这样就不会有对其的问题了,但为何这么做其实有更深层的原因。
} page = kalloc();
memmove(page, (const void *)pa, PGSIZE);
// 这个 if 就是为了防止 start end 不对其导致拷贝了多余的内核数据
// 我们需要手动把它们清空
if (pa < start) {
memset(page, 0, start - va);
} else if (pa + PAGE_SIZE > end) {
memset(page + (end - pa), 0, PAGE_SIZE - (end - pa));
}
mappages(p->pagetable, va, PGSIZE, (uint64)page, PTE_U | PTE_R | PTE_W | PTE_X);
}
// 同 lab4 map user stack
p->ustack = va_end + PAGE_SIZE;
for (uint64 va = p->ustack; va < p->ustack + USTACK_SIZE;
va += PGSIZE) {
page = kalloc();
memset(page, 0, PGSIZE);
mappages(p->pagetable, va, PGSIZE, (uint64)page, PTE_U | PTE_R | PTE_W);
}
// 设置 trapframe
p->trapframe->sp = p->ustack + USTACK_SIZE;
p->trapframe->epc = va_start;
p->max_page = PGROUNDUP(p->ustack + USTACK_SIZE - 1) / PAGE_SIZE;
p->state = RUNNABLE;
return 0;
}
其中对于用户栈、trapframe、trampoline 的映射没有变化,但是对 .bin 数据的映射似乎面目全非了,竟然由一个循环完成。其实,这个循环的逻辑十分简单,就是对于 .bin 的每一页,都申请一个新页并进行内容拷贝,最后建立这一页的映射。之所以这么麻烦完全是由于我们的物理内存管理过于简陋,一次只能分配一个页,如果能够分配连续的物理页,那么这个循环可以被一个 mappages 替代。 其中对于用户栈、trapframe、trampoline 的映射没有变化,但是对 .bin 数据的映射似乎面目全非了,竟然由一个循环完成。其实,这个循环的逻辑十分简单,就是对于 .bin 的每一页,都申请一个新页并进行内容拷贝,最后建立这一页的映射。之所以这么麻烦完全是由于我们的物理内存管理过于简陋,一次只能分配一个页,如果能够分配连续的物理页,那么这个循环可以被一个 mappages 替代。
那么另一个问题是,为什么要拷贝呢?想想 lab4 我们是怎么干的,直接把虚存和物理内存映射就好了,根本没有拷贝。那么,拷贝是为了什么呢?其实,按照 lab4 的做法,程序运行之后就会修改仅有一份的程序"原像"你会发现lab4 的程序都是一次性的,如果第二次执行,会发现 .data 和 .bss 段数据都被上一次执行改掉了,不是初始化的状态。但是 lab4 的时候,每个程序最多执行一次,所以这么做是可以的。但在 lab5 所有程序都可能被无数次的执行,我们就必须对“程序原像”做保护,在“原像”的拷贝上运行程序了。 那么另一个更重要的问题是,为什么要拷贝呢?想想 lab4 我们是怎么干的,直接把虚存和物理内存映射就好了,根本没有拷贝。那么,拷贝是为了什么呢?其实,按照 lab4 的做法,程序运行之后就会修改仅有一份的程序"原像"你会发现lab4 的程序都是一次性的,如果第二次执行,会发现 .data 和 .bss 段数据都被上一次执行改掉了,不是初始化的状态。但是 lab4 的时候,每个程序最多执行一次,所以这么做是可以的。但在 lab5 所有程序都可能被无数次的执行,我们就必须对“程序原像”做保护,在“原像”的拷贝上运行程序了。
测例的执行 测例的执行
------------------------------------------------------------------------ ------------------------------------------------------------------------
从本章开始大家可以发现我们的run_all_app函数有所改变: 从本章开始,大家可以发现我们的 run_all_app 函数被 load_init_app 取代了:
.. code-block:: c .. code-block:: c
:linenos: :linenos:
// os/loader.c // os/loader.c
int run_all_app() {
// load all apps and init the corresponding `proc` structure.
int load_init_app()
{
int id = get_id_by_name(INIT_PROC);
if (id < 0)
panic("Cannpt find INIT_PROC %s", INIT_PROC);
struct proc *p = allocproc(); struct proc *p = allocproc();
p->parent = 0; if (p == NULL) {
int id = get_id_by_name("user_shell"); panic("allocproc\n");
if(id < 0) }
panic("no user shell"); debugf("load init proc %s", INIT_PROC);
loader(id, p); loader(id, p);
p->state = RUNNABLE;
return 0; return 0;
} }
练习修改我们这里只加载user_shell的bin程序。如果大家打开user_shell的测例源码可以发现它是通过spwan系统调用来执行其他测例的 这个 load_init_app load 的 INIT_PROC 一般来说就是我们在本章第一节展示的那个 usershell不过可以通过在 Makefile 中传入 INIT_PROC 参数而改变,大部分情况下,不推荐修改,这是由于 usershell 具有不错的灵活性
usershell usershell
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
我们也提供了一个十分简单的支持交互的usershell测例。 ``user/src/usershell.c`` 就是 usershell 的代码了,有兴趣的同学可以研究下这个 shell:
.. code-block:: c .. code-block:: c
const unsigned char LF = 0x0a;
const unsigned char CR = 0x0d;
const unsigned char DL = 0x7f;
const unsigned char BS = 0x08;
// 手搓了一个极简的 stack用来维护用户输入保存一行的输入 // 手搓了一个极简的 stack用来维护用户输入保存一行的输入
char line[100] = {}; char line[100] = {};
int top = 0; int top = 0;
void push(char c) { line[top++] = c; } void push(char c){ line[top++] = c; }
void pop() { --top; } void pop() { --top; }
int is_empty() { return top == 0; } int is_empty() { return top == 0;}
void clear() { top = 0; } void clear() { top = 0; }
int main() { int main()
{
printf("C user shell\n"); printf("C user shell\n");
printf(">> "); printf(">> ");
// shell 是不会结束的 fflush(stdout);
while (1) { while (1) {
// 读取一个字符
char c = getchar(); char c = getchar();
switch (c) { switch (c) {
// 敲了回车,将输入内容解析位一个程序名,通过 fork + exec 执行 // 回车,执行当前 stack 中字符串对应的程序
case LF: case LF:
case CR: case CR:
printf("\n"); printf("\n");
if (!is_empty()) { if (!is_empty()) {
push('\0'); push('\0');
int pid = fork(); int pid = fork();
if (pid == 0) { if (pid == 0) {
// child process // child process
if (exec(line) < 0) { if (exec(line, NULL) < 0) {
printf("no such program\n"); printf("no such program: %s\n",
exit(0); line);
} exit(0);
panic("unreachable!");
} else {
// 父进程 wait 执行的函数
int xstate = 0;
int exit_pid = 0;
exit_pid = wait(pid, &xstate);
assert(pid == exit_pid, -1);
printf("Shell: Process %d exited with code %d\n", pid, xstate);
} }
// 无论如何,清空输入 buffer panic("unreachable!");
clear(); } else {
int xstate = 0;
int exit_pid = 0;
exit_pid = waitpid(pid, &xstate);
assert(pid == exit_pid);
printf("Shell: Process %d exited with code %d\n",
pid, xstate);
} }
printf(">> "); clear();
break; }
case BS: printf(">> ");
case DL: fflush(stdout);
// 退格键 break;
if (!is_empty()) { // 退格建pop一个char
putchar(BS); case BS:
printf(" "); case DL:
putchar(BS); if (!is_empty()) {
pop(); putchar(BS);
} printf(" ");
break; putchar(BS);
default: fflush(stdout);
// 普通输入,回显 pop();
putchar(c); }
push(c); break;
break; // 普通输入,回显并 push 一个 char
default:
putchar(c);
fflush(stdout);
push(c);
break;
} }
} }
return 0; return 0;
} }
可以看到这个测例实际上就是实现了一个简单的字符串处理的函数并且针对解析得到的不同的指令调用不同的系统调用。要注意这需要shell支持read的系统调用。当读入用户的输入时它会死循环的等待用户输入一个代表程序名称的字符串(通过sys_read)当用户按下空格之后shell 会使用 fork 和 exec 创建并执行这个程序,然后通过 sys_wait 来等待程序执行结束,并输出 exit_code。有了 shell 之后,我们可以只执行自己希望的程序,也可以执行某一个程序很多次来观察输出,这对于使用体验是极大的提升!可以说,第五章的所有努力都是为了支持 shell。 可以看到这个测例实际上就是实现了一个简单的字符串处理的函数并且针对解析得到的不同的指令调用不同的系统调用。要注意这需要shell支持read的系统调用。当读入用户的输入时它会死循环的等待用户输入一个代表程序名称的字符串(通过sys_read)当用户按下空格之后shell 会使用 fork 和 exec 创建并执行这个程序,然后通过 sys_wait 来等待程序执行结束,并输出 exit_code。有了 shell 之后,我们可以只执行自己希望的程序,也可以执行某一个程序很多次来观察输出,这对于使用体验是极大的提升!可以说,第五章的所有努力都是为了支持 shell。
我们简单看一下sys_read的实现 我们简单看一下sys_read的实现,它与 sys_write 有点相似
.. code-block:: c .. code-block:: c
uint64 sys_read(int fd, uint64 va, uint64 len) { uint64 sys_read(int fd, uint64 va, uint64 len)
if (fd != 0) {
if (fd != STDIN)
return -1; return -1;
struct proc *p = curr_proc(); struct proc *p = curr_proc();
char str[200]; char str[MAX_STR_LEN];
for(int i = 0; i < len; ++i) { len = MIN(len, MAX_STR_LEN);
int c = console_getchar(); for (int i = 0; i < len; ++i) {
// consgetc() 会阻塞式的等待读取一个 char
int c = consgetc();
str[i] = c; str[i] = c;
} }
copyout(p->pagetable, va, str, len); copyout(p->pagetable, va, str, len);
return len; return len;
} }
目前我们只支持标准输入stdin的输入对应fd = 0。console_getchar和putchar一样在sbi.c之中实现了其系统调用的过程 目前我们只支持标准输入stdin的输入对应fd = STDIN

View File

@ -1,74 +1,22 @@
chapter5练习 chapter5练习
============================================== ==============================================
- 本节难度: **一定比lab4简单** - 本节难度: **看似唬人,其实就那样**
本章任务
----------------------------------------
- ``make test BASE=1`` 执行 usershell然后运行 ``ch5b_usertest``
- 看懂 fork, exec, wait 的逻辑。结合课堂内容回答本章问答问题(最终写进 ch6 的报告)。
编程作业 编程作业
--------------------------------------------- ---------------------------------------------
进程创建 无。
+++++++++++++++++++++++++++++++++++++++++++++
大家一定好奇过为啥进程创建要用 fork + execve 这么一个奇怪的系统调用,就不能直接搞一个新进程吗?思而不学则殆,我们就来试一试!这章的编程练习请大家实现一个完全 DIY 的系统调用 spawn用以创建一个新进程。
spawn 系统调用定义( `标准spawn看这里 <https://man7.org/linux/man-pages/man3/posix_spawn.3.html>`_ )
- syscall ID: 400
- C 接口: ``int spawn(char *filename)``
- Rust 接口: ``fn spawn(file: *const u8) -> isize``
- 功能:相当于 fork + exec新建子进程并执行目标程序。
- 说明成功返回子进程id否则返回 -1。
- 可能的错误:
- 无效的文件名。
- 进程池满/内存不足等资源错误。
实验要求
+++++++++++++++++++++++++++++++++++++++++++++
- 实现分支ch5。
- 完成实验指导书中的内容,实现进程控制,可以运行 usershell。
- 实现自定义系统调用 spawn并通过 `Rust测例 <https://github.com/DeathWish5/rCore_tutorial_tests>`_ 中chapter5对应的所有测例。
challenge: 支持多核。
实验检查
+++++++++++++++++++++++++++++++++++++++++++++
- 实验目录要求
目录要求不变(参考 lab1 目录或者示例代码目录结构)。同样在 os 目录下 ``make run`` 之后可以正确加载用户程序并执行。
加载的用户测例位置: ``../user/build/bin``
- 检查
可以正确 ``make run`` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。
问答作业 问答作业
-------------------------------------------- --------------------------------------------
(1) fork + exec 的一个比较大的问题是 fork 之后的内存页/文件等资源完全没有使用就废弃了,针对这一点,有什么改进策略? 1. fork + exec 的一个比较大的问题是 fork 之后的内存页/文件等资源完全没有使用就废弃了,针对这一点,有什么改进策略?
(2) 其实使用了题(1)的策略之后fork + exec 所带来的无效资源的问题已经基本被解决了,但是今年来 fork 还是在被不断的批判,那么到底是什么正在"杀死"fork可以参考 `论文 <https://www.microsoft.com/en-us/research/uploads/prod/2019/04/fork-hotos19.pdf>`_ **注意**:回答无明显错误就给满分,出这题只是想引发大家的思考,完全不要求看论文,球球了,别卷了。 2. [选做] 其实使用了题(1)的策略之后fork + exec 所带来的无效资源的问题已经基本被解决了,但是今年来 fork 还是在被不断的批判,那么到底是什么正在"杀死"fork可以参考 `论文 <https://www.microsoft.com/en-us/research/uploads/prod/2019/04/fork-hotos19.pdf>`_ **注意**:回答无明显错误就给满分,出这题只是想引发大家的思考,完全不要求看论文,球球了,别卷了。
(3) fork 当年被设计并称道肯定是有其好处的。请使用 **带初始参数** 的 spawn 重写如下 fork 程序,然后描述 fork 有那些好处。注意:使用"伪代码"传达意思即可spawn 接口可以自定义。可以写多个文件。
.. code-block:: rust
fn main() {
let a = get_a();
if fork() == 0 {
let b = get_b();
println!("a + b = {}", a + b);
exit(0);
}
println!("a = {}", a);
0
}
4. 描述进程执行的几种状态,以及 fork/exec/wait/exit 对于状态的影响。
报告要求
------------------------------------------------------------
* 简单总结本次实验与上个实验相比你增加的东西。控制在5行以内不要贴代码
* 完成问答问题
* (optional) 你对本次实验设计及难度的看法。

View File

@ -29,104 +29,64 @@
.. code-block:: console .. code-block:: console
$ cd os $ make test BASE=1
$ make run # 在 shell 中执行:
>> ch6b_usertest
本章代码树 本章代码树
----------------------------------------- -----------------------------------------
.. code-block:: .. code-block:: bash
./os/src .
Rust 28 Files 2061 Lines ├── bootloader
Assembly 3 Files 88 Lines │ └── rustsbi-qemu.bin
├── LICENSE
├── bootloader ├── Makefile
│   ├── rustsbi-k210.bin ├── os
│   └── rustsbi-qemu.bin │ ├── console.c
├── LICENSE │ ├── console.h
├── os │ ├── const.h
│   ├── build.rs │ ├── defs.h
│   ├── Cargo.lock │ ├── entry.S
│   ├── Cargo.toml │ ├── file.c
│   ├── Makefile │ ├── file.h
│   └── src │ ├── kalloc.c
│   ├── config.rs │ ├── kalloc.h
│   ├── console.rs │ ├── kernel.ld
│   ├── entry.asm │ ├── kernelld.py
│   ├── fs(新增:文件系统子模块 fs) │ ├── loader.c
│   │   ├── mod.rs(包含已经打开且可以被进程读写的文件的抽象 File Trait) │ ├── loader.h
│   │   ├── pipe.rs(实现了 File Trait 的第一个分支——可用来进程间通信的管道) │ ├── log.h
│   │   └── stdio.rs(实现了 File Trait 的第二个分支——标准输入/输出) │ ├── main.c
│   ├── lang_items.rs │ ├── pack.py
│   ├── link_app.S │ ├── pipe.c
│   ├── linker-k210.ld │ ├── printf.c
│   ├── linker-qemu.ld │ ├── printf.h
│   ├── loader.rs │ ├── proc.c
│   ├── main.rs │ ├── proc.h
│   ├── mm │ ├── riscv.h
│   │   ├── address.rs │ ├── sbi.c
│   │   ├── frame_allocator.rs │ ├── sbi.h
│   │   ├── heap_allocator.rs │ ├── string.c
│   │   ├── memory_set.rs │ ├── string.h
│   │   ├── mod.rs │ ├── switch.S
│   │   └── page_table.rs(新增:应用地址空间的缓冲区抽象 UserBuffer 及其迭代器实现) │ ├── syscall.c
│   ├── sbi.rs │ ├── syscall.h
│   ├── syscall │ ├── syscall_ids.h
│   │   ├── fs.rs(修改:调整 sys_read/write 的实现,新增 sys_close/pipe) │ ├── timer.c
│   │   ├── mod.rs(修改:调整 syscall 分发) │ ├── timer.h
│   │   └── process.rs │ ├── trampoline.S
│   ├── task │ ├── trap.c
│   │   ├── context.rs │ ├── trap.h
│   │   ├── manager.rs │ ├── types.h
│   │   ├── mod.rs │ ├── vm.c
│   │   ├── pid.rs │ └── vm.h
│   │   ├── processor.rs ├── README.md
│   │   ├── switch.rs ├── scripts
│   │   ├── switch.S │ ├── kernelld.py
│   │   └── task.rs(修改:在任务控制块中加入文件描述符表相关机制) │ └── pack.py
│   ├── timer.rs └── user
│   └── trap
│   ├── context.rs
│   ├── mod.rs
│   └── trap.S
├── README.md
├── rust-toolchain
├── tools
│   ├── kflash.py
│   ├── LICENSE
│   ├── package.json
│   ├── README.rst
│   └── setup.py
└── user
├── Cargo.lock
├── Cargo.toml
├── Makefile
└── src
├── bin
│   ├── exit.rs
│   ├── fantastic_text.rs
│   ├── forktest2.rs
│   ├── forktest.rs
│   ├── forktest_simple.rs
│   ├── forktree.rs
│   ├── hello_world.rs
│   ├── initproc.rs
│   ├── matrix.rs
│   ├── pipe_large_test.rs(新增)
│   ├── pipetest.rs(新增)
│   ├── run_pipe_test.rs(新增)
│   ├── sleep.rs
│   ├── sleep_simple.rs
│   ├── stack_overflow.rs
│   ├── user_shell.rs
│   ├── usertests.rs
│   └── yield.rs
├── console.rs
├── lang_items.rs
├── lib.rs(新增两个系统调用sys_close/sys_pipe)
├── linker.ld
└── syscall.rs(新增两个系统调用sys_close/sys_pipe)

View File

@ -68,6 +68,7 @@
注意文件对于进程而言也是其需要记录的一种资源因此我们在进程对应的PCB结构体之中也需要记录进程打开的文件信息。我们给PCB增加文件指针数组。 注意文件对于进程而言也是其需要记录的一种资源因此我们在进程对应的PCB结构体之中也需要记录进程打开的文件信息。我们给PCB增加文件指针数组。
.. code-block:: c .. code-block:: c
// proc.h // proc.h
// Per-process state // Per-process state
struct proc { struct proc {
@ -78,15 +79,15 @@
// os/proc.c // os/proc.c
int fdalloc(struct file* f) { int fdalloc(struct file* f) {
struct proc* p = curr_proc(); struct proc* p = curr_proc();
// fd = 0,1,2 is reserved for stdio/stdout/stderr // fd = 0,1,2 is reserved for stdio/stdout/stderr
for(int i = 3; i < FD_MAX; ++i) { for(int i = 3; i < FD_MAX; ++i) {
if(p->files[i] == 0) { if(p->files[i] == 0) {
p->files[i] = f; p->files[i] = f;
return i; return i;
}
} }
} return -1;
return -1;
} }
一个进程能打开的文件是有限的我们设置为16。一个进程如果要打开某一个文件其文件指针数组必须有空位。如果有就把下标做为文件的fd并把指定文件指针存入数组之中。 一个进程能打开的文件是有限的我们设置为16。一个进程如果要打开某一个文件其文件指针数组必须有空位。如果有就把下标做为文件的fd并把指定文件指针存入数组之中。
@ -101,6 +102,7 @@ pipe管道的实现
首先,看一下管道的结构体。 首先,看一下管道的结构体。
.. code-block:: c .. code-block:: c
// file.h抽象成一个文件了。 // file.h抽象成一个文件了。
#define PIPESIZE 512 #define PIPESIZE 512
@ -117,6 +119,7 @@ pipe管道的实现
我们来看一下如何创建一个管道。 我们来看一下如何创建一个管道。
.. code-block:: c .. code-block:: c
:linenos: :linenos:
int pipealloc(struct file *f0, struct file *f1) int pipealloc(struct file *f0, struct file *f1)
@ -142,11 +145,16 @@ pipe管道的实现
return 0; return 0;
} }
.. note::
在内核中,我们是不能 new 一个结构体的,这是由于我们没有实现堆内存管理。但我们可以用一种略显浪费的方式,也就是直接 kalloc() 一个页,只要不大于一整个页的数据结构都可以这样 new 出来。
管道两端的输入和输出被我们抽象成了两个文件。这两个文件的创建由sys_pipe调用完成。我们在分配时就会设置管道两端哪一端可写哪一端可读并初始化管道本身的nread和nwrite记录buffer的指针。 管道两端的输入和输出被我们抽象成了两个文件。这两个文件的创建由sys_pipe调用完成。我们在分配时就会设置管道两端哪一端可写哪一端可读并初始化管道本身的nread和nwrite记录buffer的指针。
关闭pipe比较简单。函数其实只关闭了读写端中的一个如果两个都被关闭释放 pipe。 关闭pipe比较简单。函数其实只关闭了读写端中的一个如果两个都被关闭释放 pipe。
.. code-block:: c .. code-block:: c
:linenos: :linenos:
void pipeclose(struct pipe *pi, int writable) void pipeclose(struct pipe *pi, int writable)
@ -164,7 +172,9 @@ pipe管道的实现
重点是管道的读写. 重点是管道的读写.
.. code-block:: c .. code-block:: c
:linenos: :linenos:
int pipewrite(struct pipe *pi, uint64 addr, int n) int pipewrite(struct pipe *pi, uint64 addr, int n)
{ {
// w 记录已经写的字节数 // w 记录已经写的字节数
@ -233,6 +243,7 @@ pipe 相关系统调用
首先是sys_pipe. 首先是sys_pipe.
.. code-block:: c .. code-block:: c
:linenos: :linenos:
// os/syscall.c // os/syscall.c
@ -257,29 +268,37 @@ pipe 相关系统调用
sys_close比较简单。就只是释放掉进程的fd并且清空对应file并且设置其种类为FD_NONE. sys_close比较简单。就只是释放掉进程的fd并且清空对应file并且设置其种类为FD_NONE.
.. code-block:: c .. code-block:: c
:linenos: :linenos:
uint64 sys_close(int fd) { uint64 sys_close(int fd)
// stdio/stdout/stderr can't be closed for now {
if(fd <= 2) // 目前不支持 stdio 的关闭ch7会支持这个
return 0; if (fd <= 2 || fd > FD_BUFFER_SIZE)
return -1;
struct proc *p = curr_proc(); struct proc *p = curr_proc();
fileclose(p->files[fd]); struct file *f = p->files[fd];
// 目前仅支持关闭 pipe
if (f->type == FD_PIPE) {
fileclose(f);
} else {
panic("fileclose: unsupported file type %d fd = %d\n", f->type, fd);
}
p->files[fd] = 0; p->files[fd] = 0;
return 0; return 0;
} }
void fileclose(struct file *f) void fileclose(struct file *f)
{ {
if(f->ref < 1) // ref == 0 才真正关闭
panic("fileclose");
if(--f->ref > 0) { if(--f->ref > 0) {
return; return;
} }
// pipe 类型需要关闭对应的 pipe
if(f->type == FD_PIPE){ if(f->type == FD_PIPE){
pipeclose(f->pipe, f->writable); pipeclose(f->pipe, f->writable);
} }
// 清空其他数据
f->off = 0; f->off = 0;
f->readable = 0; f->readable = 0;
f->writable = 0; f->writable = 0;
@ -292,30 +311,36 @@ sys_close比较简单。就只是释放掉进程的fd并且清空对应file
.. code-block:: c .. code-block:: c
:linenos: :linenos:
uint64 sys_write(int fd, uint64 va, uint64 len) { uint64 sys_write(int fd, uint64 va, uint64 len)
if(fd <= 2) { {
if (fd == STDOUT || fd == STDERR) {
return console_write(va, len); return console_write(va, len);
} }
if (fd <= 2 || fd > FD_BUFFER_SIZE)
return -1;
struct proc *p = curr_proc(); struct proc *p = curr_proc();
struct file *f = p->files[fd]; struct file *f = p->files[fd];
if(f->type == FD_PIPE) { if (f->type == FD_PIPE) {
return pipewrite(f->pipe, va, len); return pipewrite(f->pipe, va, len);
} else {
panic("unknown file type %d\n", f->type);
} }
error("unknown file type %d\n", f->type);
return -1;
} }
uint64 sys_read(int fd, uint64 va, uint64 len) { uint64 sys_read(int fd, uint64 va, uint64 len)
if(fd <= 2) { {
if (fd == STDIN) {
return console_read(va, len); return console_read(va, len);
} }
if (fd <= 2 || fd > FD_BUFFER_SIZE)
return -1;
struct proc *p = curr_proc(); struct proc *p = curr_proc();
struct file *f = p->files[fd]; struct file *f = p->files[fd];
if(f->type == FD_PIPE) { if (f->type == FD_PIPE) {
return piperead(f->pipe, va, len); return piperead(f->pipe, va, len);
} else {
panic("unknown file type %d fd = %d\n", f->type, fd);
} }
error("unknown file type %d\n", f->type);
return -1;
} }
注意一个文件目前fd最大就是15。 注意一个文件目前fd最大就是15。

View File

@ -20,10 +20,27 @@ fork 为什么是毒瘤呢?因为你总是要在新增加一个东西以后考
可以看到创建子进程时会遍历父进程继承其所有打开的文件并且给指定文件的ref + 1。因为我们记录的本身就只是一个指针只需用ref来记录一个文件还有没有进程使用。 可以看到创建子进程时会遍历父进程继承其所有打开的文件并且给指定文件的ref + 1。因为我们记录的本身就只是一个指针只需用ref来记录一个文件还有没有进程使用。
此外,进程结束需要清理的资源除了内存之外增加了文件:
.. code-block:: c
void freeproc(struct proc *p)
{
// ...
+ for (int i = 3; i < FD_BUFFER_SIZE; i++) {
+ if (p->files[i] != NULL) {
+ fileclose(p->files[i]);
+ }
+ }
// ...
}
你会发现 exec 的实现竟然没有修改,注意 exec 仅仅重新加载进程执行的测例文件镜像不会改变其他属性比如文件。也就是说fork 出的子进程打开了与父进程相同的文件,但是 exec 并不会把打开的文件刷掉,基于这一点,我们可以利用 pipe 进行进程间通信。 你会发现 exec 的实现竟然没有修改,注意 exec 仅仅重新加载进程执行的测例文件镜像不会改变其他属性比如文件。也就是说fork 出的子进程打开了与父进程相同的文件,但是 exec 并不会把打开的文件刷掉,基于这一点,我们可以利用 pipe 进行进程间通信。
.. code-block:: c .. code-block:: c
// user/src/ch6b_pipetest
char STR[] = "hello pipe!"; char STR[] = "hello pipe!";
int main() { int main() {
@ -45,4 +62,4 @@ fork 为什么是毒瘤呢?因为你总是要在新增加一个东西以后考
return 0; return 0;
} }
由于父子进程的fd列表一致可以直接使用创建好的pipe进行通信。 由于 fork 会拷贝所有文件而 exec 不会改变文件,所以父子进程的fd列表一致可以直接使用创建好的pipe进行通信。

View File

@ -1,89 +1,47 @@
chapter6练习 chapter6练习
=========================================== ===========================================
- 本节难度: **也就和lab3一样吧** - 本节难度: **大魔王!!**
本章任务
------------------------------------------
- 运行 ``ch6b_usertest``
- merge ch4 的修改, ``git merge ch4`` 并处理冲突。
- 检查 ``ch6_4_usertest``,确保 ch4 的功能依然正确。
- 结合代码、指导书已经课堂所学,理解文件系统的几个概念:全局文件表 / 进程文件描述符等
- 完成本章编程作业。
- 最终,完成实验报告并 push 你的 ch6 分支到远程仓库。
增加 ch6_4_usertest
编程作业 编程作业
------------------------------------------- -------------------------------------------
进程通信:邮件 进程通信:共享内存
+++++++++++++++++++++++++++++++++++++++++++ +++++++++++++++++++++++++++++++++++++++++++
这一章我们实现了基于 pipe 的进程间通信,但是看测例就知道了,管道不太自由,我们来实现一套乍一看更靠谱的通信 syscall吧本节要求实现邮箱机制以及对应的 syscall。 // TODO
- 邮箱说明每个进程拥有唯一一个邮箱基于“数据报”收发字节信息利用环形buffer存储读写顺序为 FIFO不记录来源进程。每次读写单位必须为一个报文如果用于接收的缓冲区长度不够舍弃超出的部分截断报文。为了简单邮箱中最多拥有16条报文每条报文最大长度256字节。当邮箱满时发送邮件也就是写邮箱会失败。不考虑读写邮箱的权限也就是所有进程都能够随意给其他进程的邮箱发报。 正确实现后,你的 os 应该能够正确运行 ch6_* 对应的一些测试用例,在 shell 中执行 ch6_usertest 来执行测试
**mailread**: tips:
- 自求多福吧。。。。
* syscall ID401
* C接口 ``int mailread(void* buf, int len)``
* Rust接口: ``fn mailread(buf: *mut u8, len: usize)``
* 功能:读取一个报文,如果成功返回报文长度.
* 参数:
* buf: 缓冲区头。
* len缓冲区长度。
* 说明:
* len > 256 按 256 处理len < 队首报文长度且不为0则截断报文。
* len = 0则不进行读取如果没有报文读取返回-1否则返回0这是用来测试是否有报文可读。
* 可能的错误:
* 邮箱空。
* buf 无效。
**mailwrite**:
* syscall ID402
* C接口 ``int mailwrite(int pid, void* buf, int len)``
* Rust接口: ``fn mailwrite(pid: usize, buf: *mut u8, len: usize)``
* 功能:向对应进程邮箱插入一条报文.
* 参数:
* pid: 目标进程id。
* buf: 缓冲区头。
* len缓冲区长度。
* 说明:
* len > 256 按 256 处理,
* len = 0则不进行写入如果邮箱满返回-1否则返回0这是用来测试是否可以发报。
* 可以向自己的邮箱写入报文。
* 可能的错误:
* 邮箱满。
* buf 无效。
实验要求
+++++++++++++++++++++++++++++++++++++++++++++
- 实现分支ch6。
- 完成实验指导书中的内容,实现进程控制,可以基于 pipe 进行进程通信。
- 实现邮箱机制及系统调用,并通过 `Rust测例 <https://github.com/DeathWish5/rCore_tutorial_tests>`_ 中 chapter6 对应的所有测例。
challenge: 支持多核。
实验检查
++++++++++++++++++++++++++++++++++++++++++++++
- 实验目录要求
目录要求不变(参考 lab1 目录或者示例代码目录结构)。同样在 os 目录下 ``make run`` 之后可以正确加载用户程序并执行。
加载的用户测例位置: ``../user/build/bin``
- 检查
可以正确 ``make run`` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。
问答作业 问答作业
------------------------------------------- -------------------------------------------
(1) 举出使用 pipe 的一个实际应用的例子。 1. 举出使用 pipe 的一个实际应用的例子。
(2) 假设我们的邮箱现在有了更加强大的功能,容量大幅增加而且记录邮件来源,可以实现“回信”。考虑一个多核场景,有 m 个核为消费者n 个为生产者,消费者通过邮箱向生产者提出订单,生产者通过邮箱回信给出产品。 tips:
- 想想你平时咋使用 linux terminal 的?
- 假设你的邮箱实现没有使用锁等机制进行保护,在多核情景下可能会发生哪些问题?单核一定不会发生问题吗?为什么? - 如何使用 cat 和 wc 完成一个文件的行数统计?
- 请结合你在课堂上学到的内容,描述读者写者问题的经典解决方案,必要时提供伪代码。
- 由于读写是基于报文的,不是随机读写,你有什么点子来优化邮箱的实现吗?
报告要求 报告要求
--------------------------------------- ---------------------------------------
* 简单总结本次实验与上个实验相比你增加的东西。控制在5行以内不要贴代码 - pdf 格式CI 网站提交,注明姓名学号。
* 完成问答问题 - 完成 ch5 ch6 问答作业。
* (optional) 你对本次实验设计及难度的看法。 - [可选,不占分]你对本次实验设计及难度的看法。

View File

@ -31,45 +31,19 @@
.. code-block:: console .. code-block:: console
$ cd os $ make test BASE=1
$ make run >> ch7b_usertest
若要在 k210 平台上运行,首先需要将 microSD 通过读卡器插入 PC ,然后将打包应用 ELF 的文件系统镜像烧写到 microSD 中: .. code-block:: bash
.. code-block:: console >> ch7b_filetest
$ cd os
$ make sdcard
Are you sure write to /dev/sdb ? [y/N]
y
16+0 records in
16+0 records out
16777216 bytes (17 MB, 16 MiB) copied, 1.76044 s, 9.5 MB/s
8192+0 records in
8192+0 records out
4194304 bytes (4.2 MB, 4.0 MiB) copied, 3.44472 s, 1.2 MB/s
途中需要输入 ``y`` 确认将文件系统烧写到默认的 microSD 所在位置 ``/dev/sdb`` 中。这个位置可以在 ``os/Makefile`` 中的 ``SDCARD`` 处进行修改,在烧写之前请确认它被正确配置为 microSD 的实际位置,否则可能会造成数据损失。
烧写之后,将 microSD 插入到 Maix 系列开发板并连接到 PC然后在开发板上运行本章代码
.. code-block:: console
$ cd os
$ make run BOARD=k210
内核初始化完成之后就会进入shell程序在这里我们运行一下本章的测例 ``filetest_simple``
.. code-block::
>> filetest_simple
file_test passed! file_test passed!
Shell: Process 2 exited with code 0 Shell: Process 2 exited with code 0
>> >>
它会将 ``Hello, world!`` 输出到另一个文件 ``filea`` ,并读取里面的内容确认输出正确。我们也可以通过命令行工具 ``cat`` 来更直观的查看 ``filea`` 中的内容: 它会将 ``Hello, world!`` 输出到另一个文件 ``filea`` ,并读取里面的内容确认输出正确。我们也可以通过命令行工具 ``cat`` 来更直观的查看 ``filea`` 中的内容:
.. code-block:: .. code-block:: bash
>> cat filea >> cat filea
Hello, world! Hello, world!
@ -78,7 +52,7 @@
此外在本章我们为shell程序支持了输入/输出重定向功能,可以将一个应用的输出保存到一个指定的文件。例如,下面的命令可以将 ``yield`` 应用的输出保存在文件 ``fileb`` 当中,并在应用执行完毕之后确认它的输出: 此外在本章我们为shell程序支持了输入/输出重定向功能,可以将一个应用的输出保存到一个指定的文件。例如,下面的命令可以将 ``yield`` 应用的输出保存在文件 ``fileb`` 当中,并在应用执行完毕之后确认它的输出:
.. code-block:: .. code-block:: bash
>> yield > fileb >> yield > fileb
Shell: Process 2 exited with code 0 Shell: Process 2 exited with code 0
@ -94,12 +68,81 @@
Shell: Process 2 exited with code 0 Shell: Process 2 exited with code 0
>> >>
本章代码树
-----------------------------------------
.. code-block:: bash
.
├── bootloader
│ └── rustsbi-qemu.bin
├── LICENSE
├── Makefile
├── nfs (新增,辅助程序,要来将 .bin 打包为 os 可以识别的文件镜像)
│ ├── fs.c
│ ├── fs.h
│ ├── Makefile
│ └── types.h
├── os
│ ├── bio.c (新增IO buffer 的实现)
│ ├── bio.h
│ ├── console.c
│ ├── console.h
│ ├── const.h
│ ├── defs.h
│ ├── entry.S
│ ├── fcntl.h (新增,文件相关的一些抽象)
│ ├── file.c (更加完成的文件操作)
│ ├── file.h (更加完成的文件定义)
│ ├── fs.c (新增,文件系统实际逻辑)
│ ├── fs.h
│ ├── kalloc.c
│ ├── kalloc.h
│ ├── kernel.ld
│ ├── kernelvec.S
│ ├── link_app.S
│ ├── loader.c
│ ├── loader.h
│ ├── log.h
│ ├── main.c
│ ├── pipe.c
│ ├── plic.c (新增,用来处理磁盘中断)
│ ├── plic.h (新增,用来处理磁盘中断)
│ ├── printf.c
│ ├── printf.h
│ ├── proc.c
│ ├── proc.h
│ ├── riscv.h
│ ├── sbi.c
│ ├── sbi.h
│ ├── string.c
│ ├── string.h
│ ├── switch.S
│ ├── syscall.c
│ ├── syscall.h
│ ├── syscall_ids.h
│ ├── timer.c
│ ├── timer.h
│ ├── trampoline.S
│ ├── trap.c
│ ├── trap.h
│ ├── types.h
│ ├── virtio_disk.c (新增,用来处理磁盘中断)
│ ├── virtio.h (新增,用来处理磁盘中断)
│ ├── vm.c
│ └── vm.h
├── README.md
├── scripts
│ └── initproc.py (弱化的 pack.py仅仅用来插入 INIT_PROC 符号)
└── user
本章代码导读 本章代码导读
----------------------------------------------------- -----------------------------------------------------
本章涉及的代码量相对较多且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统也就是说本章。本章采用的文件系统和ext4文件系统比较类似。其中也涉及到了inode这个概念。进入本章之后我们的测例文件一开始是存放在我们生成的“磁盘”上的需要我们实现磁盘的读写来进行操作了。我们实现了一个简单的nfs文件系统具体的结构将在下面的章节中说明。大家可以看一看我们本章对makefile文件的改动. 本章涉及的代码量相对较多且与进程执行相关的管理还有直接的关系。其实我们是参考经典的UNIX基于索引的文件系统设计了一个简化的有一级目录并支持创建/打开/读写/关闭文件一系列操作的文件系统也就是说本章。本章采用的文件系统和ext4文件系统比较类似。其中也涉及到了inode这个概念。进入本章之后我们的测例文件一开始是存放在我们生成的“磁盘”上的需要我们实现磁盘的读写来进行操作了。我们实现了一个简单的 nfs 文件系统,具体的结构将在下面的章节中说明。大家可以看一看我们本章对 makefile 文件的改动.
.. code-block:: Makefile .. code-block:: Makefile
QEMU = qemu-system-riscv64 QEMU = qemu-system-riscv64
QEMUOPTS = \ QEMUOPTS = \
-nographic \ -nographic \
@ -107,9 +150,7 @@
-machine virt \ -machine virt \
-bios $(BOOTLOADER) \ -bios $(BOOTLOADER) \
-kernel kernel \ -kernel kernel \
+ -drive file=$(U)/fs-copy.img,if=none,format=raw,id=x0 \ # 以 user/fs-copy.img 作为磁盘镜像 + -drive file=$(U)/fs.img,if=none,format=raw,id=x0 \ # 以 user/fs.img 作为磁盘镜像
+ -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 # 虚拟 virtio 磁盘设备 + -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0 # 虚拟 virtio 磁盘设备
需要注意一定要确保user测例生成的img文件在对应的位置否则会make run失败。
我们OS的读写文件操作均在内核态进行由于不确定读写磁盘的结束时间这意味着我们需要新的中断方式——外部中断来提醒OS读写结束了。而要在内核态引入中断意味着我们不得不短暂开启在内核态的嵌套中断。一旦OS打开了文件那么我们就可以获得文件对应的fd了(实际上lab6中我们做了类似的事情就可以使用sys_write/sys_read对文件进行读写操作。 我们OS的读写文件操作均在内核态进行由于不确定读写磁盘的结束时间这意味着我们需要新的中断方式——外部中断来提醒OS读写结束了。而要在内核态引入中断意味着我们不得不短暂开启在内核态的嵌套中断。一旦OS打开了文件那么我们就可以获得文件对应的fd了(实际上lab6中我们做了类似的事情就可以使用sys_write/sys_read对文件进行读写操作。

View File

@ -4,7 +4,7 @@
本节导读 本节导读
------------------------------------------------- -------------------------------------------------
本节我们首先以Linux 上的常规文件和目录为例,站在访问文件的应用的角度,介绍文件中值得注意的地方及文件使用方法。由于 Linux 上的文件系统模型还是比较复杂,在我们的内核实现中对它进行了很大程度的简化,我们会对简化的具体情形进行介绍。最后,我们介绍我们内核上应用的开发者应该如何使用我们简化后的文件系统和一些相关知识。 本节我们首先以 Linux 上的常规文件和目录为例,站在访问文件的应用的角度,介绍文件中值得注意的地方及文件使用方法。由于 Linux 上的文件系统模型还是比较复杂,在我们的内核实现中对它进行了很大程度的简化,我们会对简化的具体情形进行介绍。最后,我们介绍我们内核上应用的开发者应该如何使用我们简化后的文件系统和一些相关知识。
文件和目录 文件和目录
------------------------------------------------- -------------------------------------------------
@ -18,21 +18,21 @@
.. code-block:: console .. code-block:: console
$ cd os/src/ $ stat os/main.c
$ stat main.rs
File: main.rs File: os/main.c
Size: 940 Blocks: 8 IO Block: 4096 regular file Size: 491 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 4975 Links: 1 Device: 805h/2053d Inode: 4726542 Links: 1
Access: (0644/-rw-r--r--) Uid: ( 1000/ oslab) Gid: ( 1000/ oslab) Access: (0664/-rw-rw-r--) Uid: ( 1000/deathwish) Gid: ( 1000/deathwish)
Access: 2021-02-28 23:32:50.289925450 +0800 Access: 2021-09-08 17:52:06.915389371 +0800
Modify: 2021-02-28 23:32:50.133927136 +0800 Modify: 2021-09-08 17:52:06.127425836 +0800
Change: 2021-02-28 23:32:50.133927136 +0800 Change: 2021-09-08 17:52:06.127425836 +0800
Birth: - Birth: -
``stat`` 工具展示了 ``main.c`` 的如下信息: ``stat`` 工具展示了 ``main.c`` 的如下信息:
- File 表明它的文件名为 ``main.c`` - File 表明它的文件名为 ``main.c``
- Size 表明它的字节大小为 940 字节。 - Size 表明它的字节大小为 491 字节。
- Blocks 表明它占据 8 个 **块** (Block) 来存储。在文件系统中,文件的数据以块为单位进行存储,在 IO Block 可以看出在 Ubuntu 系统中每个块的大小为 4096 字节。 - Blocks 表明它占据 8 个 **块** (Block) 来存储。在文件系统中,文件的数据以块为单位进行存储,在 IO Block 可以看出在 Ubuntu 系统中每个块的大小为 4096 字节。
- regular file 表明这个文件是一个常规文件。事实上,其他类型的文件也可以通过文件名来进行访问。 - regular file 表明这个文件是一个常规文件。事实上,其他类型的文件也可以通过文件名来进行访问。
- 当文件是一个特殊文件如块设备文件或者字符设备文件的时候Device 将指出该特殊文件的 major/minor ID 。对于一个常规文件,我们无需关心它。 - 当文件是一个特殊文件如块设备文件或者字符设备文件的时候Device 将指出该特殊文件的 major/minor ID 。对于一个常规文件,我们无需关心它。
@ -53,13 +53,14 @@
.. code-block:: console .. code-block:: console
$ stat os $ stat os
File: os
Size: 4096 Blocks: 8 IO Block: 4096 directory File: os/main.c
Device: 801h/2049d Inode: 4982 Links: 5 Size: 491 Blocks: 8 IO Block: 4096 regular file
Access: (0755/drwxr-xr-x) Uid: ( 1000/ oslab) Gid: ( 1000/ oslab) Device: 805h/2053d Inode: 4726542 Links: 1
Access: 2021-02-28 23:32:50.133927136 +0800 Access: (0664/-rw-rw-r--) Uid: ( 1000/deathwish) Gid: ( 1000/deathwish)
Modify: 2021-02-28 23:32:50.129927180 +0800 Access: 2021-09-08 17:52:06.915389371 +0800
Change: 2021-02-28 23:32:50.129927180 +0800 Modify: 2021-09-08 17:52:06.127425836 +0800
Change: 2021-09-08 17:52:06.127425836 +0800
Birth: - Birth: -
directory 表明 ``os`` 是一个目录,从 Access 字符串的首位 ``d`` 也可以看出这一点。对于目录而言, Access 的 ``rwx`` 含义有所不同: directory 表明 ``os`` 是一个目录,从 Access 字符串的首位 ``d`` 也可以看出这一点。对于目录而言, Access 的 ``rwx`` 含义有所不同:
@ -72,8 +73,8 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
有了目录之后,我们就可以将所有的文件和目录组织为一种被称为 **目录树** (Directory Tree) 的有根树结构(不考虑软链接)。树中的每个节点都是一个文件或目录,一个目录下面的所有的文件和子目录都是它的孩子。可以看出所有的文件都是目录树的叶子节点。目录树的根节点也是一个目录,它被称为 **根目录** (Root Directory)。目录树中的每个目录和文件都可以用它的 **绝对路径** (Absolute Path) 来进行索引,该绝对路径是目录树上的根节点到待索引的目录和文件所在的节点之间自上而下的路径上的所有节点的文件或目录名两两之间加上路径分隔符拼接得到的。例如,在 Linux 上,根目录的绝对路径是 ``/`` ,路径分隔符也是 ``/`` ,因此: 有了目录之后,我们就可以将所有的文件和目录组织为一种被称为 **目录树** (Directory Tree) 的有根树结构(不考虑软链接)。树中的每个节点都是一个文件或目录,一个目录下面的所有的文件和子目录都是它的孩子。可以看出所有的文件都是目录树的叶子节点。目录树的根节点也是一个目录,它被称为 **根目录** (Root Directory)。目录树中的每个目录和文件都可以用它的 **绝对路径** (Absolute Path) 来进行索引,该绝对路径是目录树上的根节点到待索引的目录和文件所在的节点之间自上而下的路径上的所有节点的文件或目录名两两之间加上路径分隔符拼接得到的。例如,在 Linux 上,根目录的绝对路径是 ``/`` ,路径分隔符也是 ``/`` ,因此:
- ``main.rs`` 的绝对路径是 ``/home/oslab/workspace/v3/rCore-Tutorial-v3/os/src/main.rs`` - ``main.c`` 的绝对路径是 ``/home/oslab/workspace/UCORE/uCore-Tutorial-v2/os/main.c``
- ``os`` 目录的绝对路径则是 ``/home/oslab/workspace/v3/rCore-Tutorial-v3/os/`` - ``os`` 目录的绝对路径则是 ``/home/oslab/workspace/UCORE/uCore-Tutorial-v2/os``
上面的绝对路径因具体环境而异。 上面的绝对路径因具体环境而异。

View File

@ -12,7 +12,7 @@ nfs文件系统
导言中提到我们的nfs文件系统十分类似ext4文件系统下面我们可以看一下nfs文件系统的布局:: 导言中提到我们的nfs文件系统十分类似ext4文件系统下面我们可以看一下nfs文件系统的布局::
// 基本信息:块大小 BSIZE = 1024B总容量 FSSIZE = 1000 个 block = 1000 * 1024 B。 // 基本信息:块大小 BSIZE = 1024B总容量 FSSIZE = 1000 个 block = 1000 * 1024 B。
// Layout: // Layout:
// 0号块目前不起作用可以忽略。superblock 固定为 1 号块size 固定为一个块。 // 0号块留待后续拓展可以忽略。superblock 固定为 1 号块size 固定为一个块。
// 其后是储存 inode 的若干个块,占用块数 = inode 上限 / 每个块上可以容纳的 inode 数量, // 其后是储存 inode 的若干个块,占用块数 = inode 上限 / 每个块上可以容纳的 inode 数量,
// 其中 inode 上限固定为 200每个块的容量 = BSIZE / sizeof(struct disk_inode) // 其中 inode 上限固定为 200每个块的容量 = BSIZE / sizeof(struct disk_inode)
// 再之后是数据块相关内容,包含一个 储存空闲块位置的 bitmap 和 实际的数据块bitmap 块 // 再之后是数据块相关内容,包含一个 储存空闲块位置的 bitmap 和 实际的数据块bitmap 块
@ -89,7 +89,7 @@ virtio 磁盘驱动
注意:这一部分代码不需要同学们详细了解细节,但需要知道大概的过程。 注意:这一部分代码不需要同学们详细了解细节,但需要知道大概的过程。
在 ucore-tutorial 中磁盘块的读写是通过中断处理的。在 virtio.h 和 virtio-disk.c 中我们按照 qemu 对 virtio 的定义,实现了 virtio_disk_init 和 virtio_disk_rw 两个函数前者完成磁盘设备的初始化和对其管理的初始化。virtio_disk_rw 实际完成磁盘IO当设定好读写信息后会通过 MMIO 的方式通知磁盘开始写。然后os 会开启中断并开始死等磁盘读写完成。当磁盘完成 IO 后,磁盘会触发一个外部中断,在中断处理中会把死循环条件解除。内核态只会在处理磁盘读写的时候短暂开启中断,之后会马上关闭。 在 uCore-Tutorial 中磁盘块的读写是通过中断处理的。在 virtio.h 和 virtio-disk.c 中我们按照 qemu 对 virtio 的定义,实现了 virtio_disk_init 和 virtio_disk_rw 两个函数前者完成磁盘设备的初始化和对其管理的初始化。virtio_disk_rw 实际完成磁盘IO当设定好读写信息后会通过 MMIO 的方式通知磁盘开始写。然后os 会开启中断并开始死等磁盘读写完成。当磁盘完成 IO 后,磁盘会触发一个外部中断,在中断处理中会把死循环条件解除。内核态只会在处理磁盘读写的时候短暂开启中断,之后会马上关闭。
.. code-block:: c .. code-block:: c
@ -551,6 +551,7 @@ ialloc 干的事情:遍历 inode blocks 找到一个空闲的inode初始化
文件读写结束后需要fclose释放掉其inode同时释放OS中对应的file结构体和fd。其实 inode 文件的关闭只需要调用 iput 就好了iput 的实现简单到让人感觉迷惑,就是 inode 引用计数减一。诶?为什么没有计数为 0 就写回然后释放 inode 的操作?和 buf 的释放同理,这里会等 inode 池满了之后自行被替换出去,重新读磁盘实在太太太太慢了。对了,千万记得 iput 和 iget 数量相同,一定要一一对应,否则你懂的。 文件读写结束后需要fclose释放掉其inode同时释放OS中对应的file结构体和fd。其实 inode 文件的关闭只需要调用 iput 就好了iput 的实现简单到让人感觉迷惑,就是 inode 引用计数减一。诶?为什么没有计数为 0 就写回然后释放 inode 的操作?和 buf 的释放同理,这里会等 inode 池满了之后自行被替换出去,重新读磁盘实在太太太太慢了。对了,千万记得 iput 和 iget 数量相同,一定要一一对应,否则你懂的。
.. code-block:: c .. code-block:: c
void void
fileclose(struct file *f) fileclose(struct file *f)
{ {

View File

@ -0,0 +1,222 @@
基于磁盘的 OS
================================================
本节导读
------------------------------------------------
在有了对磁盘的控制能力后,我们的 os 也随之发生了一系列的改变,包括:
- 从磁盘加载文件(之前一直通过奇怪的操作打包进内存)
- 更加完善的文件管理
- 支持命令行参数
- 支持 IO 重定向
这一节我们介绍前两点,下一节介绍后两点的实现。
从磁盘加载文件
------------------------------------------------
之前,我们都通过 pack.py 和 kernelld.py 将用户软件的镜像打包进内存。但内存并不是持久化的存储介质,一旦掉电就会失效,因此把用户程序存储在磁盘上,有需要的时候从磁盘加载才是科学的方式,我们来看看要办到这一点需要那些修改。
首先,我们不再需要 pack.py kernelld.py 等脚本,仅仅保留了一个为了测试方便的 initproc.py这个脚本的唯一作用就是插入 INIT_PROC 这个 symbol。
接下来os 主要的修改就主要集中在 bin_loader 这个函数了,:
.. code-block:: c
// 首先,不在需要传入 start, end 只需要传入对应的 inode 指针
// 所有的用户程序镜像已经打包到了磁盘里,这部分之后介绍
int bin_loader(struct inode *ip, struct proc *p)
{
ivalid(ip);
void *page;
// 不需要 pa 相关的东西,其实 start = 文件开头length = 文件大小
uint64 length = ip->size;
// va 相关设定不变
uint64 va_start = BASE_ADDRESS;
uint64 va_end = PGROUNDUP(BASE_ADDRESS + length);
for (uint64 va = va_start, off = 0; va < va_end; va += PGSIZE, off += PAGE_SIZE) {
page = kalloc();
// 之前的 memmove 变为了 readi也就是从磁盘读取
readi(ip, 0, (uint64)page, off, PAGE_SIZE);
// 由于 kalloc 会写入垃圾数据,不对其的部分还是需要手动清空
if (off + PAGE_SIZE > length) {
memset(page + (length - off), 0, PAGE_SIZE - (length - off));
}
mappages(p->pagetable, va, PGSIZE, (uint64)page, PTE_U | PTE_R | PTE_W | PTE_X);
}
// 其余部分不变
// map stack
// set trapframe
}
那么,用户程序是如何被打包成一个遵循上一节所说设定的磁盘镜像的呢?这就是 ``nfs/fs.c`` 的功劳了。我们可以使用 ``make -C nfs`` 来得到一个 fs.img 的镜像,对应的 ``nfs/Makefile`` 如下:
.. code-block:: Makefile
FS_FUSE := fs
$(FS_FUSE): fs.c fs.h types.h
fs.img: $(FS_FUSE)
./$(FS_FUSE) $@ $(wildcard $(U)/$(USER_BIN_DIR)/*)
可以看到 fs.c 会被编译为可执行程序 fs, 使用格式如下:
.. code-block:: console
fs 输出镜像名称 输入文件1 输入文件2 输入文件3 ...
Makfile 中,我们把 user 产生的所有 bin 文件作为输入文件。接下来我们简要解析 fs.c 的具体逻辑,同学们不需要完整的理解这段程序的逻辑,但想要正确的完成 lab7 的实验,必须对 fs.c 进行对应的修改。在 exercise 章节会有一定提示。
.. code-block:: c
// 一些全局常量定义
int NINODES = 200; // 文件系统设定的 inode 数量
int nbitmap = FSSIZE / (BSIZE * 8) + 1; // bitmap block 的数量
int ninodeblocks = NINODES / IPB + 1; // inode block 的数量
int nmeta; // meta 数据块的数量(含义下方解释)
int nblocks;
// 一些全局变量定义
int fsfd; // 输出镜像文件的 fd
struct superblock sb; // 超级块
char zeroes[BSIZE]; // BSIZE 大小的空 buf用来清空某一个磁盘块
uint freeinode = 1; // 表示还空闲的 inode每使用一个 inode 需要 +1
uint freeblock; // 表示还空闲的 block每使用一个 block 需要 +1
int main(int argc, char *argv[])
{
int i, cc, fd;
uint rootino, inum, off;
struct dirent de;
char buf[BSIZE];
struct dinode din;
// 至少需要输入镜像的名称
if (argc < 2) {
fprintf(stderr, "Usage: mkfs fs.img files...\n");
exit(1);
}
// 创建输出文件
fsfd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, 0666);
// [ boot block | sb block | inode blocks | free bit map | data blocks ]
// meta data 包括: boot block, superblock, inode block, free bitmap
nmeta = 2 + ninodeblocks + nbitmap;
// nblocks 表示存放数据剩余的块数
nblocks = FSSIZE - nmeta;
// superblock 初始化
// xint() 的功能仅仅是转换字节顺序,大家可以忽略这个细节
sb.magic = FSMAGIC;
sb.size = xint(FSSIZE);
sb.nblocks = xint(nblocks);
sb.ninodes = xint(NINODES);
sb.inodestart = xint(2);
sb.bmapstart = xint(2 + ninodeblocks);
// 目前还空闲的的块,目前我们只占用了 meta data 的那些块
// 之后没使用一块freeblock 需要 +1。
freeblock = nmeta;
// wsect 会写输出镜像,把第 i 个块写为 buf这里首先清空一遍
for (i = 0; i < FSSIZE; i++)
wsect(i, zeroes);
memset(buf, 0, sizeof(buf));
memmove(buf, &sb, sizeof(sb));
// 0 号块不处理
// 1 号快写为之前设定好的 superblock
wsect(1, buf);
// ialloc() 会分配一个空的 inode并初始化 type返回 inode id
rootino = ialloc(T_DIR);
// 从第二个参数开始都是需要打包的用户程序
for (i = 2; i < argc; i++) {
// 获得 basename这是为了去掉前缀 "../user/target/bin/"
char *shortname = basename(argv[i]);
assert(index(shortname, '/') == 0);
// 打开对应的用户文件
if ((fd = open(argv[i], 0)) < 0) {
perror(argv[i]);
exit(1);
}
// 为每一个用户程序分配一个 inode
inum = ialloc(T_FILE);
// 为每一个用户程序分配一个根目录中的目录项
bzero(&de, sizeof(de));
de.inum = xshort(inum);
strncpy(de.name, shortname, DIRSIZ);
// 把该目录项写入根目录的数据块
// iappend 会像某一个 inode 对应的数据块后 append 数据
iappend(rootino, &de, sizeof(de));
// 读取该程序的数据并写入对应 inode 的数据块
while ((cc = read(fd, buf, sizeof(buf))) > 0)
iappend(inum, buf, cc);
close(fd);
}
// 更新 rootdir inode 对应的 size 数据,按数据快大小取整
rinode(rootino, &din);
off = xint(din.size);
off = ((off / BSIZE) + 1) * BSIZE;
din.size = xint(off);
winode(rootino, &din);
// balloc 完成 bitmap 的填写,把 [0, freeblock) 的块标记为已经使用
balloc(freeblock);
return 0;
}
更加完善的文件管理
----------------------------------------
ch7 规范化了对于 fd 的管理,比如 0, 1, 2 不再是保留给 stdio 文件的 fd 参数,而是会在进程创建的时候给进程 3 个默认文件。为什么要这么做?这使得不需要的时候我们可以关闭 stdio 文件:
.. code-block:: c
// allocproc() 之后必须执行 init_stdio_files 来初始化 files
int init_stdio_files(struct proc *p)
{
for (int i = 0; i < 3; i++) {
p->files[i] = stdio_init(i);
}
return 0;
}
struct file *stdio_init(int fd)
{
struct file *f = filealloc();
f->type = FD_STDIO;
f->ref = 1;
f->readable = (fd == STDIN || fd == STDERR);
f->writable = (fd == STDOUT || fd == STDERR);
return f;
}
同时 close 0,1,2 号文件不会失败并返回 -1文件读写也不会直接根据 fd 判断,例如 sys_write:
.. code-block:: c
uint64 sys_write(int fd, uint64 va, uint64 len)
{
if (fd < 0 || fd > FD_BUFFER_SIZE)
return -1;
struct proc *p = curr_proc();
struct file *f = p->files[fd];
switch (f->type) {
case FD_STDIO:
return console_write(va, len);
case FD_PIPE:
return pipewrite(f->pipe, va, len);
case FD_INODE:
return inodewrite(f, va, len);
default:
panic("unknown file type %d\n", f->type);
}
}
之后我们会看到,这使得文件重定向成为了可能, // TODO

View File

@ -0,0 +1,5 @@
命令行参数与 IO 重定向
==================================================
通过上述努力,我们可以支持文件重定向,当然这需要 usershell 做一些字符串解析的工作。但我们尴尬的发现,我们的 os 甚至还不支持带参数的 exec也就是 ``main(int argc, char** argv)``,所以我们在 lab7 通过修改 exec 支持了这一点。

View File

@ -3,6 +3,14 @@ chapter7练习
- 本节难度: **理解文件系统比较费事,编程难度适中** - 本节难度: **理解文件系统比较费事,编程难度适中**
本章任务
-----------------------------------------------
- ch7b_usertest
- merge ch6 的改动,然后再次测试 ch6_usertest 和 ch7b_usertest。
- 完成本章问答作业。
- 完成本章编程作业。
- 最终,完成实验报告并 push 你的 ch7 分支到远程仓库。
编程作业 编程作业
------------------------------------------------- -------------------------------------------------
@ -110,43 +118,16 @@ chapter7练习
const FILE = 0o100000; const FILE = 0o100000;
} }
} }
实验要求
+++++++++++++++++++++++++++++++++++++++++++++++++++++
- 实现分支ch7。
- 完成实验指导书中的内容,实现基本的文件操作。
- 实现硬链接及相关系统调用,并通过 `Rust测例 <https://github.com/DeathWish5/rCore_tutorial_tests>`_ 中 chapter7 对应的所有测例。
challenge: 支持多核。
.. note::
**如何调试 easy-fs**
如果你在第一章练习题中已经借助 ``log`` crate 实现了日志功能,那么你可以直接在 ``easy-fs`` 中引入 ``log`` crate通过 ``log::info!/debug!`` 等宏即可进行调试并在内核中看到日志输出。具体来说,在 ``easy-fs`` 中的修改是:在 ``easy-fs/Cargo.toml`` 的依赖中加入一行 ``log = "0.4.0"``,然后在 ``easy-fs/src/lib.rs`` 中加入一行 ``extern crate log``
你也可以完全在用户态进行调试。仿照 ``easy-fs-fuse`` 建立一个在当前操作系统中运行的应用程序,将测试逻辑写在 ``main`` 函数中。这个时候就可以将它引用的 ``easy-fs````no_std`` 去掉并使用 ``println!`` 进行调试。
实验检查
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
- 实验目录要求
目录要求不变(参考 lab1 目录或者示例代码目录结构)。同样在 os 目录下 `make run` 之后可以正确加载用户程序并执行。
加载的用户测例位置: `../user/build/bin`
- 检查
可以正确 `make run` 执行,可以正确执行目标用户测例,并得到预期输出(详见测例注释)。
正确实现后,你的 os 应该能够正确运行 ch7_* 对应的一些测试用例,在 shell 中执行 ch7_usertest 来执行测试。
Tips Tips
++++++++++++++++++++++++++++++++++++++++++++++++++++++++ ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
- 注意 ``sys_linkat`` 有 5 个参数,而原有的系统调用分发函数 ``syscall`` (位于 ``os/src/syscall/mod.rs`` 中)最多仅支持 3 个参数,因此我们需要进行拓展。这需要将 ``syscall`` 的函数签名中的 ``args`` 拓展为 ``[usize; 5]`` ,还需要对应调整 ``trap_handler`` 中对 ``syscall`` 的调用,从 Trap 上下文中获取更多通用寄存器放入参数 ``args`` 中。 - 需要给 inode 和 dinode 都增加 link 的计数,但强烈建议不要改变整个数据结构的大小,事实上,推荐你修改一个 pad。
- os 和 nfs 的修改需要同步,只不过 nfs 比较简单,只需要初始化 link 计数为 1 就行(可以通过修改 ``ialloc``来实现)。
- 理论上讲unlink 有删除文件的语义,如果 link 计数为 0需要删除 inode 和对应的数据块,但我们没有设置相关的测试,如果仅仅是想拿到测试分数,可以不实现这一点。
- 理论上讲,想要测试文件系统的属性需要重启机器,但我们没有这样做。一方面是为了测试的方便,另一方面也留了一条后路。。。。。。。。。
问答作业 问答作业