Skip to content

Commit

Permalink
Merge branch 'main' of github.com:rcore-os/rCore-Tutorial-Book-v3 int…
Browse files Browse the repository at this point in the history
…o main
  • Loading branch information
chyyuu committed Apr 5, 2023
2 parents c77dd3b + 0305613 commit 41ff56d
Show file tree
Hide file tree
Showing 9 changed files with 1,246 additions and 23 deletions.
2 changes: 1 addition & 1 deletion source/chapter0/2os-interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,4 @@ API与ABI
ret
}
这里我们看到,API中的各个参数和返回值分别被RISC-V通用寄存器 `x10` (即存放系统调用号,也保存返回值)、 `x11`(存放 `fd` ) 、 `x12` (存放 `buf` )和 `x17` (存放 `len` )保存。
这里我们看到,API中的各个参数和返回值分别被RISC-V通用寄存器 `x17` (即存放系统调用号)、 `x10` (存放 `fd` ,也保存返回值) 、 `x11` (存放 `buf` )和 `x12` (存放 `len` )保存。
16 changes: 7 additions & 9 deletions source/chapter1/7exercise.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@

1. `*` 应用程序在执行过程中,会占用哪些计算机资源?
2. `*` 请用相关工具软件分析并给出应用程序A的代码段/数据段/堆/栈的地址空间范围。
3. `*` 请用分析并给出应用程序C的代码段/数据段/堆/栈的地址空间范围。
4. `*` 请结合编译器的知识和编写的应用程序B,说明应用程序B是如何建立调用栈链信息的。
5. `*` 请简要说明应用程序与操作系统的异同之处。
6. `**` 请基于QEMU模拟RISC—V的执行过程和QEMU源代码,说明RISC-V硬件加电后的几条指令在哪里?完成了哪些功能?
7. `*` RISC-V中的SBI的含义和功能是啥?
8. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?
9. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。
10. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间?
11. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。
3. `*` 请简要说明应用程序与操作系统的异同之处。
4. `**` 请基于QEMU模拟RISC—V的执行过程和QEMU源代码,说明RISC-V硬件加电后的几条指令在哪里?完成了哪些功能?
5. `*` RISC-V中的SBI的含义和功能是啥?
6. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?
7. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。
8. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间?
9. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。

我们可以手动阅读汇编代码和栈上的数据,体验一下这个过程。例如,对如下两个互相递归调用的函数:

Expand Down
62 changes: 53 additions & 9 deletions source/chapter1/8answer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -132,11 +132,43 @@
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

1. `*` 应用程序在执行过程中,会占用哪些计算机资源?

占用 CPU 计算资源(CPU 流水线,缓存等),内存(内存不够还会占用外存)等

2. `*` 请用相关工具软件分析并给出应用程序A的代码段/数据段/堆/栈的地址空间范围。
3. `*` 请用分析并给出应用程序C的代码段/数据段/堆/栈的地址空间范围。
4. `*` 请结合编译器的知识和编写的应用程序B,说明应用程序B是如何建立调用栈链信息的。
5. `*` 请简要说明应用程序与操作系统的异同之处。
6. `**` 请基于QEMU模拟RISC—V的执行过程和QEMU源代码,说明RISC-V硬件加电后的几条指令在哪里?完成了哪些功能?

简便起见,我们静态编译该程序生成可执行文件。使用 ``readelf`` 工具查看地址空间:

..
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
...
[ 7] .text PROGBITS 00000000004011c0 000011c0
0000000000095018 0000000000000000 AX 0 0 64
...
[10] .rodata PROGBITS 0000000000498000 00098000
000000000001cadc 0000000000000000 A 0 0 32
...
[21] .data PROGBITS 00000000004c50e0 000c40e0
00000000000019e8 0000000000000000 WA 0 0 32
...
[25] .bss NOBITS 00000000004c72a0 000c6290
0000000000005980 0000000000000000 WA 0 0 32
数据段(.data)和代码段(.text)的起止地址可以从输出信息中看出。

应用程序的堆栈是由内核为其动态分配的,需要在运行时查看。将 A 程序置于后台执行,通过查看 ``/proc/[pid]/maps`` 得到堆栈空间的分布:

..
01bc9000-01beb000 rw-p 00000000 00:00 0 [heap]
7ffff8e60000-7ffff8e82000 rw-p 00000000 00:00 0 [stack]
3. `*` 请简要说明应用程序与操作系统的异同之处。

这个问题相信大家完成了实验的学习后一定会有更深的理解。

4. `**` 请基于QEMU模拟RISC—V的执行过程和QEMU源代码,说明RISC-V硬件加电后的几条指令在哪里?完成了哪些功能?

在 QEMU 源码 [#qemu_bootrom]_ 中可以找到“上电”的时候刚执行的几条指令,如下:

Expand Down Expand Up @@ -167,11 +199,23 @@
- (我们还没有用到:将 FDT (Flatten device tree) 在物理内存中的地址写入 ``a1``)
- 跳转到 ``start_addr`` ,在我们实验中是 RustSBI 的地址

7. `*` RISC-V中的SBI的含义和功能是啥?
8. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?
9. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。
10. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间?
11. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。(题目剩余部分省略)
5. `*` RISC-V中的SBI的含义和功能是啥?

详情见 `SBI 官方文档 <https://github.com/riscv-non-isa/riscv-sbi-doc/blob/master/riscv-sbi.adoc>`_

6. `**` 为了让应用程序能在计算机上执行,操作系统与编译器之间需要达成哪些协议?

编译器依赖操作系统提供的程序库,操作系统执行应用程序需要编译器提供段位置、符号表、依赖库等信息。 `ELF <https://en.wikipedia.org/wiki/Executable_and_Linkable_Format>`_ 就是比较常见的一种文件格式。

7. `**` 请简要说明从QEMU模拟的RISC-V计算机加电开始运行到执行应用程序的第一条指令这个阶段的执行过程。

接第 5 题,跳转到 RustSBI 后,SBI 会对部分硬件例如串口等进行初始化,然后通过 mret 跳转到 payload 也就是 kernel 所在的起始地址。kernel 进行一系列的初始化后(内存管理,虚存管理,线程(进程)初始化等),通过 sret 跳转到应用程序的第一条指令开始执行。

8. `**` 为何应用程序员编写应用时不需要建立栈空间和指定地址空间?

应用程度对内存的访问需要通过 MMU 的地址翻译完成,应用程序运行时看到的地址和实际位于内存中的地址是不同的,栈空间和地址空间需要内核进行管理和分配。应用程序的栈指针在 trap return 过程中初始化。此外,应用程序可能需要动态加载某些库的内容,也需要内核完成映射。

9. `***` 现代的很多编译器生成的代码,默认情况下不再严格保存/恢复栈帧指针。在这个情况下,我们只要编译器提供足够的信息,也可以完成对调用栈的恢复。(题目剩余部分省略)

* 首先,我们当前的 ``pc`` 在 ``flip`` 函数的开头,这是我们正在运行的函数。返回给调用者处的地址在 ``ra`` 寄存器里,是 ``0x10742`` 。因为我们还没有开始操作栈指针,所以调用处的 ``sp`` 与我们相同,都是 ``0x40007f1310`` 。
* ``0x10742`` 在 ``flap`` 函数内。根据 ``flap`` 函数的开头可知,这个函数的栈帧大小是 16 个字节,所以调用者处的栈指针应该是 ``sp + 16 = 0x40007f1320``。调用 ``flap`` 的调用者返回地址保存在栈上 ``8(sp)`` ,可以读出来是 ``0x10750`` ,还在 ``flap`` 函数内。
Expand Down
2 changes: 1 addition & 1 deletion source/chapter3/2task-switching.rst
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Trap 控制流在调用 ``__switch`` 之前就需要明确知道即将切换到
# 阶段 [4]
ret
我们手写汇编代码来实现 ``__switch`` 。在阶段 [1] 可以看到它的函数原型中的两个参数分别是当前 A 任务上下文指针 ``current_task_cx_ptr`` 和即将被切换到的 B 任务上下文指针 ``next_task_cx_ptr`` ,从 :ref:`RISC-V 调用规范 <term-calling-convention>` 可以知道它们分别通过寄存器 ``a0/a1`` 传入。阶段 [2] 体现在第 19~27 行,即根据 B 任务上下文保存的内容来恢复 ``ra`` 寄存器、``s0~s11`` 寄存器以及 ``sp`` 寄存器。从中我们也能够看出 ``TaskContext`` 里面究竟包含哪些寄存器:
我们手写汇编代码来实现 ``__switch`` 。在阶段 [1] 可以看到它的函数原型中的两个参数分别是当前 A 任务上下文指针 ``current_task_cx_ptr`` 和即将被切换到的 B 任务上下文指针 ``next_task_cx_ptr`` ,从 :ref:`RISC-V 调用规范 <term-calling-convention>` 可以知道它们分别通过寄存器 ``a0/a1`` 传入。阶段 [2] 体现在第 19~27 行,即将当前 CPU 状态(包括 ``ra`` 寄存器、 ``s0~s11`` 寄存器以及 ``sp`` 寄存器)保存到 A 任务上下文。相对的,阶段 [3] 体现在第 29~37 行,即根据 B 任务上下文保存的内容来恢复上述 CPU 状态。从中我们也能够看出 ``TaskContext`` 里面究竟包含哪些寄存器:

.. code-block:: rust
:linenos:
Expand Down
6 changes: 5 additions & 1 deletion source/chapter4/6multitasking-based-on-as.rst
Original file line number Diff line number Diff line change
Expand Up @@ -693,7 +693,11 @@
vpn.step();
let mut end_va: VirtAddr = vpn.into();
end_va = end_va.min(VirtAddr::from(end));
v.push(&ppn.get_bytes_array()[start_va.page_offset()..end_va.page_offset()]);
if end_va.page_offset() == 0 {
v.push(&mut ppn.get_bytes_array()[start_va.page_offset()..]);
} else {
v.push(&mut ppn.get_bytes_array()[start_va.page_offset()..end_va.page_offset()]);
}
start = end_va.into();
}
v
Expand Down
2 changes: 1 addition & 1 deletion source/chapter5/3implement-process-mechanism.rst
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@
- 第 11 行我们解析应用的 ELF 执行文件得到应用地址空间 ``memory_set`` ,用户栈在应用地址空间中的位置 ``user_sp`` 以及应用的入口点 ``entry_point`` 。
- 第 12 行我们手动查页表找到位于应用地址空间中新创建的Trap 上下文被实际放在哪个物理页帧上,用来做后续的初始化。
- 第 16~18 行我们为该进程分配 PID 以及内核栈,并记录下内核栈在内核地址空间的位置 ``kernel_stack_top`` 。
- 第 16~19 行我们为该进程分配 PID 以及内核栈,并记录下内核栈在内核地址空间的位置 ``kernel_stack_top`` 。
- 第 20 行我们在该进程的内核栈上压入初始化的任务上下文,使得第一次任务切换到它的时候可以跳转到 ``trap_return`` 并进入用户态开始执行。
- 第 21 行我们整合之前的部分信息创建进程控制块 ``task_control_block`` 。
- 第 37 行我们初始化位于该进程应用地址空间中的 Trap 上下文,使得第一次进入用户态的时候时候能正确跳转到应用入口点并设置好用户栈,同时也保证在 Trap 的时候用户态能正确进入内核态。
Expand Down
2 changes: 1 addition & 1 deletion source/chapter5/4scheduling.rst
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ MLFQ调度策略的关键在于如何设置优先级。一旦设置进程的好

但这样就彻底解决问题了吗?其实还不够,比如对于优先级低且处于I/O密集型任务的进程,必须等待一段时间后,才能重新加入到最高优先级,才能减少响应时间。难道这样的进程不能不用等待一段时间吗?

而对于长进程,如果又不少长进程位于最低优先级,一下子把它们都提升为最高优先级,就可能影响本来处于最高优先级的交互式进程的响应时间。看来,第5条规则还有进一步改进的空间,提升优先级的方法可以更灵活一些。
而对于长进程,如果有不少长进程位于最低优先级,一下子把它们都提升为最高优先级,就可能影响本来处于最高优先级的交互式进程的响应时间。看来,第5条规则还有进一步改进的空间,提升优先级的方法可以更灵活一些。

先看长进程,可以发现,所谓长进程“饥饿”,是指它有很长时间没有得到执行了。如果我们能够统计其在就绪态没有被执行的等待时间长度,就可以基于这个动态变量来逐步提升其优先级。比如每过一段时间,查看就绪进程的等待时间(进程在就绪态的等待时间)长度,让其等待时间长度与其优先级成反比,从而能够逐步第动态提升长进程的优先级。

Expand Down
Loading

0 comments on commit 41ff56d

Please sign in to comment.