Skip to content

Commit

Permalink
Merge pull request #191 from h888866j/main
Browse files Browse the repository at this point in the history
  • Loading branch information
wyfcyx authored Jul 22, 2023
2 parents 942680f + 1b93f38 commit 2e993ce
Show file tree
Hide file tree
Showing 12 changed files with 608 additions and 608 deletions.
4 changes: 2 additions & 2 deletions source/chapter1/5support-func-call.rst
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@

它的开头和结尾分别在 sp(x2) 和 fp(s0) 所指向的地址。按照地址从高到低分别有以下内容,它们都是通过 ``sp`` 加上一个偏移量来访问的:

- ``ra`` 寄存器保存其返回之后的跳转地址,是一个调用者保存寄存器
- ``ra`` 寄存器保存其返回之后的跳转地址,是一个被调用者保存寄存器
- 父亲栈帧的结束地址 ``fp`` ,是一个被调用者保存寄存器;
- 其他被调用者保存寄存器 ``s1`` ~ ``s11`` ;
- 函数所使用到的局部变量。
Expand Down Expand Up @@ -361,4 +361,4 @@

C 语言中的指针相当于 Rust 中的裸指针,它无所不能但又太过于灵活,程序员对其不谨慎的使用常常会引起很多内存不安全问题,最常见的如悬垂指针和多次回收的问题,Rust 编译器没法确认程序员对它的使用是否安全,因此将其划到 unsafe Rust 的领域。在 safe Rust 中,我们有引用 ``&/&mut`` 以及各种功能各异的智能指针 ``Box<T>/RefCell<T>/Rc<T>`` 可以使用,只要按照 Rust 的规则来使用它们便可借助编译器在编译期就解决很多潜在的内存不安全问题。

本节我们介绍了函数调用和栈的背景知识,通过分配栈空间并正确设置栈指针在内核中使能了函数调用并成功将控制权转交给 Rust 代码,从此我们终于可以利用强大的 Rust 语言来编写内核的各项功能了。下一节中我们将进行构建“三叶虫”操作系统的最后一个步骤:即基于 RustSBI 提供的服务成功在屏幕上打印 ``Hello, world!`` 。
本节我们介绍了函数调用和栈的背景知识,通过分配栈空间并正确设置栈指针在内核中使能了函数调用并成功将控制权转交给 Rust 代码,从此我们终于可以利用强大的 Rust 语言来编写内核的各项功能了。下一节中我们将进行构建“三叶虫”操作系统的最后一个步骤:即基于 RustSBI 提供的服务成功在屏幕上打印 ``Hello, world!`` 。
2 changes: 1 addition & 1 deletion source/chapter3/3multiprogramming.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

- 任务运行状态:任务从开始到结束执行过程中所处的不同运行状态:未初始化、准备执行、正在执行、已退出
- 任务控制块:管理程序的执行过程的任务上下文,控制程序的执行与暂停
- 任务相关系统调用:应用程序和操作系统直接的接口,用于程序主动暂停 ``sys_yield`` 和主动退出 ``sys_exit``
- 任务相关系统调用:应用程序和操作系统之间的接口,用于程序主动暂停 ``sys_yield`` 和主动退出 ``sys_exit``

这些都是三叠纪“始初龙”协作式操作系统 [#eoraptor]_ 需要具有的功能。本节的代码可以在 ``ch3-coop`` 分支上找到。

Expand Down
4 changes: 2 additions & 2 deletions source/chapter4/6multitasking-based-on-as.rst
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@
TASK_MANAGER.get_current_trap_cx()
}
通过 ``current_user_token`` ``current_trap_cx`` 分别可以获得当前正在执行的应用的地址空间的 token 和可以在内核地址空间中修改位于该应用地址空间中的 Trap 上下文的可变引用
通过 ``current_user_token`` 可以获得当前正在执行的应用的地址空间的 token 。同时,该应用地址空间中的 Trap 上下文很关键,内核需要访问它来拿到应用进行系统调用的参数并将系统调用返回值写回,通过 ``current_trap_cx`` 内核可以拿到它访问这个 Trap 上下文的可变引用并进行读写

改进 Trap 处理的实现
------------------------------------
Expand Down Expand Up @@ -737,4 +737,4 @@

如果同学能想明白如何插入/删除页表;如何在 ``trap_handler`` 下处理 ``LoadPageFault`` ;以及 ``sys_get_time`` 在使能页机制下如何实现,那就会发现下一节的实验练习也许 **就和lab1一样** 。

.. [#tutus] 头甲龙最早出现在1.8亿年以前的侏罗纪中期,是身披重甲的食素恐龙,尾巴末端的尾锤,是防身武器。
.. [#tutus] 头甲龙最早出现在1.8亿年以前的侏罗纪中期,是身披重甲的食素恐龙,尾巴末端的尾锤,是防身武器。
1,180 changes: 590 additions & 590 deletions source/chapter5/4scheduling.rst

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion source/chapter5/5exercise.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ challenge: 支持多核。

(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) 请阅读下列代码,并分析程序的输出,假定不发生运行错误,不考虑行缓冲:

Expand Down
2 changes: 1 addition & 1 deletion source/chapter6/1fs-interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ Blocks 给出 ``os`` 目录也占用 8 个块进行存储。实际上目录也
文件关闭
++++++++++++++++++++++++++++++++++++++++++++++++++

在打开文件,对文件完成了读写操作后,还需要关闭文件,这样才让进程释放被这个文件所占用的内核资源。 ``close`` 的调用参数是文件描述符,但文件被关闭后,文件在内核中的资源会被释放,文件描述符会被回收。这样,进程就不能继续使用该文件描述符进行文件读写了。
在打开文件,对文件完成了读写操作后,还需要关闭文件,这样才让进程释放被这个文件占用的内核资源。 ``close`` 的调用参数是文件描述符,当文件被关闭后,该文件在内核中的资源会被释放,文件描述符会被回收。这样,进程就不能继续使用该文件描述符进行文件读写了。

.. code-block:: rust
Expand Down
2 changes: 1 addition & 1 deletion source/chapter7/2pipe.rst
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@
``PipeRingBuffer::read_byte`` 方法可以从管道中读取一个字节,注意在调用它之前需要确保管道缓冲区中不是空的。它会更新循环队列队头的位置,并比较队头和队尾是否相同,如果相同的话则说明管道的状态变为空 ``EMPTY`` 。仅仅通过比较队头和队尾是否相同不能确定循环队列是否为空,因为它既有可能表示队列为空,也有可能表示队列已满。因此我们需要在 ``read_byte`` 的同时进行状态更新。

``PipeRingBuffer::available_read`` 可以计算管道中还有多少个字符可以读取。我们首先需要需要判断队列是否为空,因为队头和队尾相等可能表示队列为空或队列已满,两种情况 ``available_read`` 的返回值截然不同。如果队列为空的话直接返回 0,否则根据队头和队尾的相对位置进行计算。
``PipeRingBuffer::available_read`` 可以计算管道中还有多少个字符可以读取。我们首先需要判断队列是否为空,因为队头和队尾相等可能表示队列为空或队列已满,两种情况 ``available_read`` 的返回值截然不同。如果队列为空的话直接返回 0,否则根据队头和队尾的相对位置进行计算。

``PipeRingBuffer::all_write_ends_closed`` 可以判断管道的所有写端是否都被关闭了,这是通过尝试将管道中保存的写端的弱引用计数升级为强引用计数来实现的。如果升级失败的话,说明管道写端的强引用计数为 0 ,也就意味着管道所有写端都被关闭了,从而管道中的数据不会再得到补充,待管道中仅剩的数据被读取完毕之后,管道就可以被销毁了。

Expand Down
2 changes: 1 addition & 1 deletion source/chapter8/1thread-kernel.rst
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
- 第 4~7 行我们声明线程函数接受的参数类型为一个名为 ``FuncArguments`` 的结构体,内含 ``x`` 和 ``y`` 两个字段。
- 第 15 行我们创建并默认初始化三个 ``pthread_t`` 实例 ``t0`` 、 ``t1`` 和 ``t2`` ,分别代表我们接下来要创建的三个线程。
- 第 16 行在主线程的栈上给出三个线程接受的参数。
- 第 9~12 行实现线程运行的函数 ``func`` ,可以看到它的函数签名符合要求。它实际接受的参数类型应该为我们之前定义的 ``FuncArguments`` 类型的指针,但是在函数签名中是一个 ``void *`` ,所以在第 10 行我们我们首先进行类型转换得到 ``FuncArguments*`` ,而后才能访问 ``x`` 和 ``y`` 两个字段并打印出来。
- 第 9~12 行实现线程运行的函数 ``func`` ,可以看到它的函数签名符合要求。它实际接受的参数类型应该为我们之前定义的 ``FuncArguments`` 类型的指针,但是在函数签名中是一个 ``void *`` ,所以在第 10 行我们首先进行类型转换得到 ``FuncArguments*`` ,而后才能访问 ``x`` 和 ``y`` 两个字段并打印出来。
- 第 17~19 行使用 ``pthread_create`` 创建三个线程,分别绑定到 ``t0~t2`` 三个 ``pthread_t`` 实例上。它们均执行 ``func`` 函数,但接受的参数有所不同。

编译运行,一种可能的输出为:
Expand Down
2 changes: 1 addition & 1 deletion source/chapter8/5concurrency-problem.rst
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ A是共享变量。粗略地看,可以估计执行流程为:线程thr1先被

状态是安全的,是指存在一个资源分配/线程执行序列使得所有的线程都能获取其所需资源并完成线程的工作。如果找不到这样的资源分配/线程执行序列,那么状态是不安全的。这里把线程的执行过程简化为:申请资源、释放资源的一系列资源操作。这意味这线程执行完毕后,会释放其占用的所有资源。

我们需要知道,不安全状态并不等于死锁,而是指有死锁的可能性。安全全状态和不安全状态的区别是:从安全状态出发,操作系统通过调度线程执行序列,能够保证所有线程都能完成,一定不会出现死锁;而从不安全状态出发,就没有这样的保证,可能出现死锁。
我们需要知道,不安全状态并不等于死锁,而是指有死锁的可能性。安全状态和不安全状态的区别是:从安全状态出发,操作系统通过调度线程执行序列,能够保证所有线程都能完成,一定不会出现死锁;而从不安全状态出发,就没有这样的保证,可能出现死锁。

.. chyyuu 有一个安全,不安全,死锁的图???
Expand Down
2 changes: 1 addition & 1 deletion source/chapter9/2device-driver-1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
.. chyyuu 在我们的具体实现中,与上述的一般中断处理过程不太一样。首先操作系统通过自定义的 ``SBI_DEVICE_HANDLER`` SBI调用,告知RustSBI在收到外部中断后,要跳转到的操作系统中处理外部中断的函数 ``device_trap_handler`` 。这样,在外部中断产生后,先由RustSBI在M Mode下接收的,并转到S Mode,交由 ``device_trap_handler`` 内核函数进一步处理。
在以往的操作系统实现中,当一个进程通过 ``sys_read`` 系统调用来获取串口字符时,并没有用上中断机制。但一个进程读不到字符的时候,将会被操作系统调度到就绪队列的尾部,等待下一次执行的时刻。这其实就是一种变相的轮询方式来获取串口的输入字符。这里其实是可以对进程管理做的一个改进,来避免进程通过轮询的方式检查串口字符输入。既然我们已经在上一章设计实现了让用户态线程挂起的同步互斥机制,我们就可以把自然地把这种机制也用来内核中,在外设不能及时提供资源的情况下,让想获取资源的线程或进程挂起,知道外设提供了资源,再唤醒进程继续执行
在以往的操作系统实现中,当一个进程通过 ``sys_read`` 系统调用来获取串口字符时,并没有用上中断机制。但一个进程读不到字符的时候,将会被操作系统调度到就绪队列的尾部,等待下一次执行的时刻。这其实就是一种变相的轮询方式来获取串口的输入字符。这里其实是可以对进程管理做的一个改进,来避免进程通过轮询的方式检查串口字符输入。既然我们已经在上一章设计实现了让用户态线程挂起的同步互斥机制,我们就可以把这种机制也用在内核中,在外设不能及时提供资源的情况下,让想获取资源的线程或进程挂起,直到外设提供了资源,再唤醒线程或进程继续执行

目前,支持中断的驱动可有效地支持等待的进程唤醒的操作。以串口为例,如果一个进程通过系统调用想获取串口输入,但此时串口还没有输入的字符,那么操作系统就设置一个进程等待串口输入的条件变量(条件变量包含一个等待队列),然后把当前进程设置等待状态,并挂在这个等待队列上,再把CPU让给其它就绪进程执行。对于串口输入的处理,由于要考虑中断,相对就要复杂一些。读字符串的代码如下所示:

Expand Down
10 changes: 5 additions & 5 deletions source/chapter9/2device-driver-2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,15 +176,15 @@ virtio设备状态表示包括在设备初始化过程中用到的设备状态

virtio设备进行I/O传输过程中,设备驱动会指出 `I/O请求` 队列的当前位置状态信息,这样设备能查到I/O请求的信息,并根据 `I/O请求` 进行I/O传输;而设备会指出 `I/O完成` 队列的当前位置状态信息,这样设备驱动通过读取 `I/O完成` 数据结构中的状态信息,就知道设备是否完成I/O请求的相应操作,并进行后续事务处理。

比如,virtio_blk设备驱动发出一个读设备块的I/O请求,并在某确定位置给出这个I/O请求的地址,然后给设备发出'kick'通知(读或写相关I/O寄存器映射的内存地址),此时处于I/O请求状态;设备在得到通知后,此时处于 `I/O处理` 状态,它解析个I/O请求,完成个I/O请求的处理,即把磁盘块内容读入到内存中,并给出读出的块数据的内存地址,再通过中断通知设备驱动,此时处于 `I/O完成` 状态;如果磁盘块读取发生错误,此时处于 `I/O错误` 状态;设备驱动通过中断处理例程,此时处于 `I/O后续处理` 状态,设备驱动知道设备已经完成读磁盘块操作,会根据磁盘块数据所在内存地址,把数据传递给文件系统进行进一步处理;如果设备驱动发现磁盘块读错误,则会进行错误恢复相关的后续处理。
比如,virtio_blk设备驱动发出一个读设备块的I/O请求,并在某确定位置给出这个I/O请求的地址,然后给设备发出'kick'通知(读或写相关I/O寄存器映射的内存地址),此时处于I/O请求状态;设备在得到通知后,此时处于 `I/O处理` 状态,它解析这个I/O请求,完成这个I/O请求的处理,即把磁盘块内容读入到内存中,并给出读出的块数据的内存地址,再通过中断通知设备驱动,此时处于 `I/O完成` 状态;如果磁盘块读取发生错误,此时处于 `I/O错误` 状态;设备驱动通过中断处理例程,此时处于 `I/O后续处理` 状态,设备驱动知道设备已经完成读磁盘块操作,会根据磁盘块数据所在内存地址,把数据传递给文件系统进行进一步处理;如果设备驱动发现磁盘块读错误,则会进行错误恢复相关的后续处理。




virtio设备交互机制
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

virtio设备交互机制包括基于Notifications的事件通知和基于virtqueue虚拟队列的数据传输。事件通知是指设备和驱动程序必须通知对方,它们有数据需要对方处理。数据传输是指设备和驱动程序之间进行进行I/O数据(如磁盘块数据、网络包)传输。
virtio设备交互机制包括基于Notifications的事件通知和基于virtqueue虚拟队列的数据传输。事件通知是指设备和驱动程序必须通知对方,它们有数据需要对方处理。数据传输是指设备和驱动程序之间进行I/O数据(如磁盘块数据、网络包)传输。

**Notification通知**

Expand All @@ -205,7 +205,7 @@ virtio协议中一个关键部分是virtqueue,在virtio规范中,virtqueue

操作系统在Qemu上运行时,virtqueue是 virtio 驱动程序和 virtio 设备访问的同一块内存区域。

当涉及到 virtqueue 的描述时,有很多不一致的地方。有将其与vring(virtio-rings或VRings)等同表示,也有将二者分别单独描述为不同的对象。我们将在这里单独描述它们,因为vring是virtueues的主要组成部分,是达成virtio设备和驱动程序之间数据传输的数据结构, vring本质是virtio设备和驱动程序之间的共享内存,但 virtqueue 不仅仅只有vring。
当涉及到 virtqueue 的描述时,有很多不一致的地方。有将其与vring(virtio-rings或VRings)等同表示,也有将二者分别单独描述为不同的对象。我们将在这里单独描述它们,因为vring是virtqueues的主要组成部分,是达成virtio设备和驱动程序之间数据传输的数据结构, vring本质是virtio设备和驱动程序之间的共享内存,但 virtqueue 不仅仅只有vring。



Expand All @@ -214,7 +214,7 @@ virtqueue由三部分组成(如下图所示):

- 描述符表 Descriptor Table:描述符表是描述符为组成元素的数组,每个描述符描述了一个内存buffer 的address/length。而内存buffer中包含I/O请求的命令/数据(由virtio设备驱动填写),也可包含I/O完成的返回结果(由virtio设备填写)等。
- 可用环 Available Ring:一种vring,记录了virtio设备驱动程序发出的I/O请求索引,即被virtio设备驱动程序更新的描述符索引的集合,需要virtio设备进行读取并完成相关I/O操作;
- 已用环 Used Ring:另一种vring,记录了virtio设备发出的I/O完成索引,即被virtio设备更新的描述符索引d 集合,需要vrtio设备驱动程序进行读取并对I/O操作结果进行进一步处理。
- 已用环 Used Ring:另一种vring,记录了virtio设备发出的I/O完成索引,即被virtio设备更新的描述符索引的集合,需要vrtio设备驱动程序进行读取并对I/O操作结果进行进一步处理。


.. image:: ../../os-lectures/lec13/figs/virtqueue-arch.png
Expand Down Expand Up @@ -509,7 +509,7 @@ idx总是递增,并在到达 ``qsz`` 后又回到0:
**接收设备I/O响应的操作**
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

一旦设备完成了I/O请求,形成I/O响应,就会更新描述符所指向的缓冲区,并向驱动程序发送已用缓冲区通知(used buffer notification)。一般会采用中断这种更加高效的通知机制。设备驱动程序在收到中断后,就会更加I/O响应信息进行后续处理。相关的伪代码如下所示:
一旦设备完成了I/O请求,形成I/O响应,就会更新描述符所指向的缓冲区,并向驱动程序发送已用缓冲区通知(used buffer notification)。一般会采用中断这种更加高效的通知机制。设备驱动程序在收到中断后,就会对I/O响应信息进行后续处理。相关的伪代码如下所示:

.. code-block:: Rust
:linenos:
Expand Down
Loading

0 comments on commit 2e993ce

Please sign in to comment.