Skip to content

Commit

Permalink
update ch4
Browse files Browse the repository at this point in the history
  • Loading branch information
chyyuu committed Jul 19, 2022
1 parent 71d602f commit 3f849a9
Show file tree
Hide file tree
Showing 5 changed files with 24 additions and 16 deletions.
4 changes: 2 additions & 2 deletions source/chapter4/1rust-dynamic-allocation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Rust 的标准库中提供了很多开箱即用的堆数据结构,利用它们
.. _term-reference-counting:
.. _term-garbage-collection:

- C 语言仅支持 ``malloc/free`` 这一对操作,它们必须恰好成对使用,否则就会出现各种内存错误。比如分配了之后没有回收,则会导致内存溢出;回收之后再次 free 相同的指针,则会造成 Double-Free 问题;又如回收之后再尝试通过指针访问它指向的区域,这属于 Use-After-Free 问题。总之,这样的内存安全问题层出不穷,毕竟人总是会犯错的。
- C 语言仅支持 ``malloc/free`` 这一对操作,它们必须恰好成对使用,否则就会出现各种内存错误。比如分配了之后没有回收,则会导致内存泄漏;回收之后再次 free 相同的指针,则会造成 Double-Free 问题;又如回收之后再尝试通过指针访问它指向的区域,这属于 Use-After-Free 问题。总之,这样的内存安全问题层出不穷,毕竟人总是会犯错的。
- Python/Java 通过 **引用计数** (Reference Counting) 对所有的对象进行运行时的动态管理,一套 **垃圾回收** (GC, Garbage Collection) 机制会被自动定期触发,每次都会检查所有的对象,如果其引用计数为零则可以将该对象占用的内存从堆上回收以待后续其他的对象使用。这样做完全杜绝了内存安全问题,但是性能开销则很大,而且 GC 触发的时机和每次 GC 的耗时都是无法预测的,还使得软件的执行性能不够确定。
- C++ 的智能指针(shared_ptr、unique_ptr、weak_ptr、auto_ptr等)和 **资源获取即初始化** (RAII, Resource Acquisition Is Initialization,指将一个使用前必须获取的资源的生命周期绑定到一个变量上,变量释放时,对应的资源也一并释放。) 风格都是致力于解决内存安全问题。但这些编程方式是“建议”而不是“强制”。

Expand Down Expand Up @@ -203,7 +203,7 @@ Rust 的标准库中提供了很多开箱即用的堆数据结构,利用它们
panic!("Heap allocation error, layout = {:?}", layout);
}
最后,让我们尝试一下动态内存分配吧!感兴趣的同学可以在 ``rust_main`` 中尝试调用下面的 ``heap_test`` 函数。
最后,让我们尝试一下动态内存分配吧!感兴趣的同学可以在 ``rust_main`` 中尝试调用下面的 ``heap_test`` 函数(调用 ``heap_test()`` 前要记得先调用 ``init_heap()`` )

.. code-block:: rust
:linenos:
Expand Down
6 changes: 4 additions & 2 deletions source/chapter4/3sv39-implementation-1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ SV39 多级页表的硬件机制
内存控制相关的CSR寄存器
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

默认情况下 MMU 未被使能,此时无论 CPU 位于哪个特权级,访存的地址都会作为一个物理地址交给对应的内存控制单元来直接访问物理内存。我们可以通过修改 S 特权级的一个名为 ``satp`` 的 CSR 来启用分页模式,在这之后 S 和 U 特权级的访存地址会被视为一个虚拟地址,它需要经过 MMU 的地址转换变为一个物理地址,再通过它来访问物理内存;而 M 特权级的访存地址,我们可设定是内存的物理地址。
默认情况下 MMU 未被使能(启用),此时无论 CPU 位于哪个特权级,访存的地址都会作为一个物理地址交给对应的内存控制单元来直接访问物理内存。我们可以通过修改 S 特权级的一个名为 ``satp`` 的 CSR 来启用分页模式,在这之后 S 和 U 特权级的访存地址会被视为一个虚拟地址,它需要经过 MMU 的地址转换变为一个物理地址,再通过它来访问物理内存;而 M 特权级的访存地址,我们可设定是内存的物理地址。


.. note::
Expand Down Expand Up @@ -169,6 +169,8 @@ SV39 多级页表的硬件机制

.. image:: sv39-pte.png

.. chyyuu 页表项的RSW的解释,pec中提到RSW是留给S特权级软件(也就是内核)自行决定如何使用的,比如可以用它实现一些页面置换算法。; U的进一步解释:在Risc-v的特权级文档中U位还有其他的补充描述,当sstatus寄存器中的SUM位置1,S 特权级可以访问U位为1的页,但是S特权级的程序常运行在SUM位清空的条件下,如果S特权级直接访问会出现page fault
上图为 SV39 分页模式下的页表项,其中 :math:`[53:10]` 这 :math:`44` 位是物理页号,最低的 :math:`8` 位 :math:`[7:0]` 则是标志位,它们的含义如下(请注意,为方便说明,下文我们用 *页表项的对应虚拟页面* 来表示索引到一个页表项的虚拟页号对应的虚拟页面):

- V(Valid):仅当位 V 为 1 时,页表项才是合法的;
Expand Down Expand Up @@ -337,7 +339,7 @@ SV39 多级页表的硬件机制

**分析 SV39 多级页表的内存占用**

我们知道,多级页表的总内存消耗取决于节点的数目,每个节点则需要一个大小为 :math:`4\text{KiB}` 物理页帧存放。考虑一个地址空间,除了根节点的一个物理页帧之外,地址空间中的每个实际用到的大小为 :math:`T` 字节的 *连续* 区间会让多级页表额外消耗不超过 :math:`4\text{KiB}\times(\lceil\frac{T}{2\text{MiB}}\rceil+\lceil\frac{T}{1\text{GiB}}\rceil)` 的内存。这是因为,括号中的两项分别对应为了映射这段连续区间所需要新分配的最深层和次深层节点的数目,前者(对应第二级页表)每连续映射 :math:`2\text{MiB}` 才会新分配一个 :math:`4\text{Kib}` 的第一级页表,而后者(对应根页表,第三极页表)每连续映射 :math:`1\text{GiB}` 才会新分配一个 :math:`4\text{Kib}` 的第二级页表。由于后者远小于前者,可以将后者忽略,最后得到的结果近似于 :math:`\frac{T}{512}` 。而一般情况下我们对于地址空间的使用方法都是在其中放置少数几个连续的逻辑段,因此当一个地址空间实际使用的区域大小总和为 :math:`S` 字节的时候,我们可以认为为此多级页表消耗的内存在 :math:`\frac{S}{512}` 左右。相比线性表固定消耗 :math:`1\text{GiB}` 的内存,这已经相当可以接受了。
我们知道,多级页表的总内存消耗取决于节点的数目,每个节点则需要一个大小为 :math:`4\text{KiB}` 物理页帧存放。考虑一个地址空间,除了根节点的一个物理页帧之外,地址空间中的每个实际用到的大小为 :math:`T` 字节的 *连续* 区间会让多级页表额外消耗不超过 :math:`4\text{KiB}\times(\lceil\frac{T}{2\text{MiB}}\rceil+\lceil\frac{T}{1\text{GiB}}\rceil)` 的内存。这是因为,括号中的两项分别对应为了映射这段连续区间所需要新分配的最深层和次深层节点的数目,前者(对应第二级页表)每连续映射 :math:`2\text{MiB}` 才会新分配一个 :math:`4\text{Kib}` 的第一级页表,而后者(对应根页表,第三级页表)每连续映射 :math:`1\text{GiB}` 才会新分配一个 :math:`4\text{Kib}` 的第二级页表。由于后者远小于前者,可以将后者忽略,最后得到的结果近似于 :math:`\frac{T}{512}` 。而一般情况下我们对于地址空间的使用方法都是在其中放置少数几个连续的逻辑段,因此当一个地址空间实际使用的区域大小总和为 :math:`S` 字节的时候,我们可以认为为此多级页表消耗的内存在 :math:`\frac{S}{512}` 左右。相比线性表固定消耗 :math:`1\text{GiB}` 的内存,这已经相当可以接受了。

然而,从理论上来说,不妨设某个应用地址空间中的实际用到的总空间大小为 :math:`S` 字节,则对于这个应用的 SV39 多级页表所需的内存量,有两个更加严格的上限:

Expand Down
10 changes: 5 additions & 5 deletions source/chapter4/4sv39-implementation-2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
--------------------------


上一节更多的是站在硬件的角度来分析SV39多级页表的硬件机制,本节我们主要讲解基于 SV39 多级页表机制的操作系统内存管理。这还需进一步管理计算机系统中当前已经使用或是空闲的物理页帧,这样操作系统才能给应用程序动态分配或回收物理地址空间。有了有效的物理内存空间的管理,操作系统就能够在物理内存空间中建立多级页表(页表占用物理内存),为应用程序和操作系统自身建立虚实地址映射关系,从而实现虚拟内存空间,即给应用“看到”的地址空间。
上一节更多的是站在硬件的角度来分析SV39多级页表的硬件机制,本节我们主要讲解基于 SV39 多级页表机制的操作系统内存管理。这还需进一步管理计算机系统中当前已经使用的或空闲的物理页帧,这样操作系统才能给应用程序动态分配或回收物理地址空间。有了有效的物理内存空间的管理,操作系统就能够在物理内存空间中建立多级页表(页表占用物理内存),为应用程序和操作系统自身建立虚实地址映射关系,从而实现虚拟内存空间,即给应用“看到”的地址空间。

物理页帧管理
-----------------------------------

从前面的介绍可以看出物理页帧的重要性:它既可以用来实际存放应用/内核的数据/代码,也能够用来存储应用/内核的多级页表。但Bootloader把操作系统内核加载到物理内存中后,物理内存上已经有一部分用于放置内核的代码和数据。我们需要将剩下的空闲内存以单个物理页帧为单位管理起来,当需要存放应用数据或扩展应用的多级页表时分配空闲的物理页帧,并在应用出错或退出的时候回收应用占有的所有物理页帧。
从前面的介绍可以看出物理页帧的重要性:它既可以用来实际存放应用/内核的数据/代码,也能够用来存储应用/内核的多级页表。当Bootloader把操作系统内核加载到物理内存中后,物理内存上已经有一部分用于放置内核的代码和数据。我们需要将剩下的空闲内存以单个物理页帧为单位管理起来,当需要存放应用数据或扩展应用的多级页表时分配空闲的物理页帧,并在应用出错或退出的时候回收应用占有的所有物理页帧。

可用物理页的分配与回收
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down Expand Up @@ -238,7 +238,7 @@
实例。由于这个物理页帧之前可能被分配过并用做其他用途,我们在这里直接将这个物理页帧上的所有字节清零。这一过程并不
那么显然,我们后面再详细介绍。

当一个 ``FrameTracker`` 生命周期结束被编译器回收的时候,我们需要将它控制的物理页帧回收掉 ``FRAME_ALLOCATOR`` 中:
当一个 ``FrameTracker`` 生命周期结束被编译器回收的时候,我们需要将它控制的物理页帧回收到 ``FRAME_ALLOCATOR`` 中:

.. code-block:: rust
Expand Down Expand Up @@ -341,7 +341,7 @@

.. _modify-page-table:

在上述操作的过程中,内核需要能访问或修改多级页表节点的内容。即在操作某个多级页表或是管理物理页帧的时候,操作系统要能够读写与一个给定的物理页号对应的物理页帧上的数据。这是因为,在多级页表的架构中,每个节点都被保存在一个物理页帧中,一个节点所在物理页帧的物理页号其实就是指向该节点的“指针”。
在上述操作的过程中,内核需要能访问或修改多级页表节点的内容。即在操作某个多级页表或管理物理页帧的时候,操作系统要能够读写与一个给定的物理页号对应的物理页帧上的数据。这是因为,在多级页表的架构中,每个节点都被保存在一个物理页帧中,一个节点所在物理页帧的物理页号其实就是指向该节点的“指针”。

在尚未启用分页模式之前,内核和应用的代码都可以通过物理地址直接访问内存。而在打开分页模式之后,运行在 S 特权级的内核与运行在 U 特权级的应用在访存上都会受到影响,它们的访存地址会被视为一个当前地址空间( ``satp`` CSR 给出当前多级页表根节点的物理页号)中的一个虚拟地址,需要 MMU 查相应的多级页表完成地址转换变为物理地址,即地址空间中虚拟地址指向的数据真正被内核放在的物理内存中的位置,然后才能访问相应的数据。此时,如果想要访问一个特定的物理地址 ``pa`` 所指向的内存上的数据,就需要 **构造** 对应的一个虚拟地址 ``va`` ,使得当前地址空间的页表存在映射 :math:`\text{va}\rightarrow\text{pa}` ,且页表项中的保护位允许这种访问方式。于是,在代码中我们只需访问地址 ``va`` ,它便会被 MMU 通过地址转换变成 ``pa`` ,这样我们就做到了在启用分页模式的情况下也能正常访问内存。

Expand Down Expand Up @@ -408,7 +408,7 @@

这里简要从内存安全的角度来分析一下 ``PhysPageNum`` 的 ``get_*`` 系列方法的实现中 ``unsafe`` 的使用。首先需要指出的是,当需要访问一个物理页帧的时候,我们需要从它被绑定到的 ``FrameTracker`` 中获得其物理页号 ``PhysPageNum`` 随后再调用 ``get_*`` 系列方法才能访问物理页帧。因此, ``PhysPageNum`` 介于 ``FrameTracker`` 和物理页帧之间,也可以看做拥有部分物理页帧的所有权。由于 ``get_*`` 返回的是引用,我们可以尝试检查引用引发的常见问题:第一个问题是 use-after-free 的问题,即是否存在 ``get_*`` 返回的引用存在期间被引用的物理页帧已被回收的情形;第二个问题则是注意到 ``get_*`` 返回的是可变引用,那么就需要考虑对物理页帧的访问读写冲突的问题。

为了解决这些问题,我们在编写代码的时候需要额外当心。对于每一段 unsafe 代码,我们都需要认真考虑它会对其他无论是 unsafe 还是 safe 的代码造成的潜在影响。比如为了避免第一个问题,我们需要保证当完成物理页帧访问之后便立即回收掉 ``get_*`` 返回的引用,至少使它不能超出 ``FrameTracker`` 的生命周期;考虑第二个问题,目前每个 ``FrameTracker`` 仅会出现一次(在它所属的进程中),因此它只会出现在一个上下文中,也就不会产生冲突。但是当内核态打开中断,或是内核支持在单进程中存在多个线程,情况也许又会发生变化。
为了解决这些问题,我们在编写代码的时候需要额外当心。对于每一段 unsafe 代码,我们都需要认真考虑它会对其他无论是 unsafe 还是 safe 的代码造成的潜在影响。比如为了避免第一个问题,我们需要保证当完成物理页帧访问之后便立即回收掉 ``get_*`` 返回的引用,至少使它不能超出 ``FrameTracker`` 的生命周期;考虑第二个问题,目前每个 ``FrameTracker`` 仅会出现一次(在它所属的进程中),因此它只会出现在一个上下文中,也就不会产生冲突。但是当内核态打开(允许)中断时,或内核支持在单进程中存在多个线程时,情况也许又会发生变化。

当编译器不能介入的时候,我们很难完美的解决这些问题。因此重新设计数据结构和接口,特别是考虑数据的所有权关系,将建模进行转换,使得 Rust 有能力检查我们的设计会是一种更明智的选择。这也可以说明为什么要尽量避免使用 unsafe 。事实上,我们目前 ``PhysPageNum::get_*`` 接口并非一个好的设计,如果同学有兴趣可以试着对设计进行改良,让 Rust 编译器帮助我们解决上述与引用相关的问题。

Expand Down
3 changes: 2 additions & 1 deletion source/chapter4/5kernel-app-spaces.rst
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@
- 第 4 行, ``new_bare`` 方法可以新建一个空的地址空间;
- 第 10 行, ``push`` 方法可以在当前地址空间插入一个新的逻辑段 ``map_area`` ,如果它是以 ``Framed`` 方式映射到物理内存,还可以可选地在那些被映射到的物理页帧上写入一些初始化数据 ``data`` ;
- 第 18 行, ``insert_framed_area`` 方法调用 ``push`` ,可以在当前地址空间插入一个 ``Framed`` 方式映射到物理内存的逻辑段。注意该方法的调用者要保证同一地址空间内的任意两个逻辑段不能存在交集,从后面即将分别介绍的内核和应用的地址空间布局可以看出这一要求得到了保证;
- 第 29 行, ``new_kernel`` 可以生成内核的地址空间,而第 32 行的 ``from_elf`` 则可以应用的 ELF 格式可执行文件解析出各数据段并对应生成应用的地址空间。它们的实现我们将在后面讨论。
- 第 29 行, ``new_kernel`` 可以生成内核的地址空间;具体实现将在后面讨论;
- 第 32 行, ``from_elf`` 分析应用的 ELF 文件格式的内容,解析出各数据段并生成对应的地址空间;具体实现将在后面讨论。

在实现 ``push`` 方法在地址空间中插入一个逻辑段 ``MapArea`` 的时候,需要同时维护地址空间的多级页表 ``page_table`` 记录的虚拟页号到页表项的映射关系,也需要用到这个映射关系来找到向哪些物理页帧上拷贝初始数据。这用到了 ``MapArea`` 提供的另外几个方法:

Expand Down
Loading

0 comments on commit 3f849a9

Please sign in to comment.