mirror of
https://github.com/LearningOS/rust-based-os-comp2022.git
synced 2026-05-08 23:01:31 +08:00
add os[1-8]-ref for os refereces, add guide, add README
This commit is contained in:
239
guide/source/chapter8/0intro.rst
Normal file
239
guide/source/chapter8/0intro.rst
Normal file
@@ -0,0 +1,239 @@
|
||||
引言
|
||||
=========================================
|
||||
|
||||
本章导读
|
||||
-----------------------------------------
|
||||
|
||||
到本章开始之前,我们好像已经完成了组成应用程序执行环境的操作系统的三个重要抽象:进程、地址空间和文件,
|
||||
让应用程序开发、运行和存储数据越来越方便和灵活。有了进程以后,可以让操作系统从宏观层面实现多个应用的并发执行,
|
||||
而并发是通过操作系统基于处理器的时间片不断地切换进程来达到的。到目前为止的并发,仅仅是进程间的并发,
|
||||
对于一个进程内部还没有并发性的体现。而这就是线程(Thread)出现的起因:提高一个进程内的并发性。
|
||||
|
||||
.. chyyuu
|
||||
https://en.wikipedia.org/wiki/Per_Brinch_Hansen 关于操作系统并发 Binch Hansen 和 Hoare ???
|
||||
https://en.wikipedia.org/wiki/Thread_(computing) 关于线程
|
||||
http://www.serpentine.com/blog/threads-faq/the-history-of-threads/ The history of threads
|
||||
https://en.wikipedia.org/wiki/Core_War 我喜欢的一种早期游戏
|
||||
[Dijkstra, 65] Dijkstra, E. W., Cooperating sequential processes, in Programming Languages, Genuys, F. (ed.), Academic Press, 1965.
|
||||
[Saltzer, 66] Saltzer, J. H., Traffic control in a multiplexed computer system, MAC-TR-30 (Sc.D. Thesis), July, 1966.
|
||||
https://en.wikipedia.org/wiki/THE_multiprogramming_system
|
||||
http://www.cs.utexas.edu/users/EWD/ewd01xx/EWD196.PDF
|
||||
https://en.wikipedia.org/wiki/Edsger_W._Dijkstra
|
||||
https://en.wikipedia.org/wiki/Per_Brinch_Hansen
|
||||
https://en.wikipedia.org/wiki/Tony_Hoare
|
||||
https://en.wikipedia.org/wiki/Mutual_exclusion
|
||||
https://en.wikipedia.org/wiki/Semaphore_(programming)
|
||||
https://en.wikipedia.org/wiki/Monitor_(synchronization)
|
||||
Dijkstra, Edsger W. The structure of the 'THE'-multiprogramming system (EWD-196) (PDF). E.W. Dijkstra Archive. Center for American History, University of Texas at Austin. (transcription) (Jun 14, 1965)
|
||||
|
||||
|
||||
有了进程以后,为什么还会出现线程呢?考虑如下情况,对于很多应用(以单一进程的形式运行)而言,
|
||||
逻辑上存在多个可并行执行的任务,如果其中一个任务被阻塞,将会引起不依赖该任务的其他任务也被阻塞。
|
||||
举个具体的例子,我们平常用编辑器来编辑文本内容的时候,都会有一个定时自动保存的功能,
|
||||
这个功能的作用是在系统或应用本身出现故障的情况前,已有的文档内容会被提前保存。
|
||||
假设编辑器自动保存时由于磁盘性能导致写入较慢,导致整个进程被操作系统挂起,这就会影响到用户编辑文档的人机交互体验:
|
||||
即软件的及时响应能力不足,用户只有等到磁盘写入完成后,操作系统重新调度该进程运行后,用户才可编辑。
|
||||
如果我们把一个进程内的多个可并行执行任务通过一种更细粒度的方式让操作系统进行调度,
|
||||
那么就可以通过处理器时间片切换实现这种细粒度的并发执行。这种细粒度的调度对象就是线程。
|
||||
|
||||
|
||||
.. _term-thread-define:
|
||||
|
||||
线程定义
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
简单地说,线程是进程的组成部分,进程可包含1 -- n个线程,属于同一个进程的线程共享进程的资源,
|
||||
比如地址空间、打开的文件等。基本的线程由线程ID、执行状态、当前指令指针 (PC)、寄存器集合和栈组成。
|
||||
线程是可以被操作系统或用户态调度器独立调度(Scheduling)和分派(Dispatch)的基本单位。
|
||||
|
||||
在本章之前,进程是程序的基本执行实体,是程序关于某数据集合上的一次运行活动,是系统进行资源(处理器、
|
||||
地址空间和文件等)分配和调度的基本单位。在有了线程后,对进程的定义也要调整了,进程是线程的资源容器,
|
||||
线程成为了程序的基本执行实体。
|
||||
|
||||
|
||||
同步互斥
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在上面提到了同步互斥和数据一致性,它们的含义是什么呢?当多个线程共享同一进程的地址空间时,
|
||||
每个线程都可以访问属于这个进程的数据(全局变量)。如果每个线程使用到的变量都是其他线程不会读取或者修改的话,
|
||||
那么就不存在一致性问题。如果变量是只读的,多个线程读取该变量也不会有一致性问题。但是,当一个线程修改变量时,
|
||||
其他线程在读取这个变量时,可能会看到一个不一致的值,这就是数据不一致性的问题。
|
||||
|
||||
.. note::
|
||||
|
||||
**并发相关术语**
|
||||
|
||||
- 共享资源(shared resource):不同的线程/进程都能访问的变量或数据结构。
|
||||
- 临界区(critical section):访问共享资源的一段代码。
|
||||
- 竞态条件(race condition):多个线程/进程都进入临界区时,都试图更新共享的数据结构,导致产生了不期望的结果。
|
||||
- 不确定性(indeterminate): 多个线程/进程在执行过程中出现了竞态条件,导致执行结果取决于哪些线程在何时运行,
|
||||
即执行结果不确定,而开发者期望得到的是确定的结果。
|
||||
- 互斥(mutual exclusion):一种操作原语,能保证只有一个线程进入临界区,从而避免出现竞态,并产生确定的执行结果。
|
||||
- 原子性(atomic):一系列操作要么全部完成,要么一个都没执行,不会看到中间状态。在数据库领域,
|
||||
具有原子性的一系列操作称为事务(transaction)。
|
||||
- 同步(synchronization):多个并发执行的进程/线程在一些关键点上需要互相等待,这种相互制约的等待称为进程/线程同步。
|
||||
- 死锁(dead lock):一个线程/进程集合里面的每个线程/进程都在等待只能由这个集合中的其他一个线程/进程
|
||||
(包括他自身)才能引发的事件,这种情况就是死锁。
|
||||
- 饥饿(hungry):指一个可运行的线程/进程尽管能继续执行,但由于操作系统的调度而被无限期地忽视,导致不能执行的情况。
|
||||
|
||||
在后续的章节中,会大量使用上述术语,如果现在还不够理解,没关系,随着后续的一步一步的分析和实验,
|
||||
相信大家能够掌握上述术语的实际含义。
|
||||
|
||||
|
||||
|
||||
实践体验
|
||||
-----------------------------------------
|
||||
|
||||
获取本章代码:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ git clone https://github.com/LearningOS/rCore-Tutorial-Code-2022S.git
|
||||
$ cd rCore-Tutorial-Code-2022S
|
||||
$ git checkout ch8
|
||||
$ git clone https://github.com/LearningOS/rCore-Tutorial-Test-2022S.git user
|
||||
|
||||
记得更新测例仓库的代码。
|
||||
|
||||
在 qemu 模拟器上运行本章代码:
|
||||
|
||||
.. code-block:: console
|
||||
|
||||
$ cd os
|
||||
$ make run
|
||||
|
||||
内核初始化完成之后就会进入 shell 程序,我们可以体会一下线程的创建和执行过程。在这里我们运行一下本章的测例 ``ch8b_threads`` :
|
||||
|
||||
.. code-block::
|
||||
|
||||
>> ch8b_threads
|
||||
aaa....bbb...ccc...
|
||||
thread#1 exited with code 1
|
||||
thread#2 exited with code 2
|
||||
thread#3 exited with code 3
|
||||
main thread exited.
|
||||
Shell: Process 2 exited with code 0
|
||||
>>
|
||||
|
||||
它会有4个线程在执行,等前3个线程执行完毕后,主线程退出,导致整个进程退出。
|
||||
|
||||
此外,在本章的操作系统支持通过互斥来执行“哲学家就餐问题”这个应用程序:
|
||||
|
||||
.. code-block::
|
||||
|
||||
>> ch8b_phil_din_mutex
|
||||
Here comes 5 philosophers!
|
||||
time cost = 720
|
||||
'-' -> THINKING; 'x' -> EATING; ' ' -> WAITING
|
||||
#0: ------- xxxxxxxx---------- xxxx----- xxxxxx--xxx
|
||||
#1: ---xxxxxx-- xxxxxxx---------- x---xxxxxx
|
||||
#2: ----- xx---------xx----xxxxxx------------ xxxx
|
||||
#3: -----xxxxxxxxxx------xxxxx-------- xxxxxx-- xxxxxxxxx
|
||||
#4: ------ x------ xxxxxx-- xxxxx------ xx
|
||||
#0: ------- xxxxxxxx---------- xxxx----- xxxxxx--xxx
|
||||
Shell: Process 2 exited with code 0
|
||||
>>
|
||||
|
||||
我们可以看到5个代表“哲学家”的线程通过操作系统的 **信号量** 互斥机制在进行 “THINKING”、“EATING”、“WAITING” 的日常生活。
|
||||
没有哲学家由于拿不到筷子而饥饿,也没有两个哲学家同时拿到一个筷子。
|
||||
|
||||
.. note::
|
||||
|
||||
**哲学家就餐问题**
|
||||
|
||||
计算机科学家 Dijkstra 提出并解决的哲学家就餐问题是经典的进程同步互斥问题。哲学家就餐问题描述如下:
|
||||
|
||||
有5个哲学家共用一张圆桌,分别坐在周围的5张椅子上,在圆桌上有5个碗和5只筷子,他们的生活方式是交替地进行思考和进餐。
|
||||
平时,每个哲学家进行思考,饥饿时便试图拿起其左右最靠近他的筷子,只有在他拿到两只筷子时才能进餐。进餐完毕,放下筷子继续思考。
|
||||
|
||||
|
||||
本章代码树
|
||||
-----------------------------------------
|
||||
|
||||
.. code-block::
|
||||
:linenos:
|
||||
|
||||
.
|
||||
├── bootloader
|
||||
│ └── rustsbi-qemu.bin
|
||||
├── Dockerfile
|
||||
├── easy-fs
|
||||
│ ├── Cargo.lock
|
||||
│ ├── Cargo.toml
|
||||
│ └── src
|
||||
│ ├── bitmap.rs
|
||||
│ ├── block_cache.rs
|
||||
│ ├── block_dev.rs
|
||||
│ ├── efs.rs
|
||||
│ ├── layout.rs
|
||||
│ ├── lib.rs
|
||||
│ └── vfs.rs
|
||||
├── easy-fs-fuse
|
||||
│ ├── Cargo.lock
|
||||
│ ├── Cargo.toml
|
||||
│ └── src
|
||||
│ └── main.rs
|
||||
├── LICENSE
|
||||
├── Makefile
|
||||
├── os
|
||||
│ ├── build.rs
|
||||
│ ├── Cargo.lock
|
||||
│ ├── Cargo.toml
|
||||
│ ├── Makefile
|
||||
│ └── src
|
||||
│ ├── config.rs (修改:扩大了内核堆空间)
|
||||
│ ├── console.rs
|
||||
│ ├── drivers
|
||||
│ │ ├── block
|
||||
│ │ │ ├── mod.rs
|
||||
│ │ │ └── virtio_blk.rs
|
||||
│ │ └── mod.rs
|
||||
│ ├── entry.asm
|
||||
│ ├── fs
|
||||
│ │ ├── inode.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── pipe.rs
|
||||
│ │ └── stdio.rs
|
||||
│ ├── lang_items.rs
|
||||
│ ├── linker.ld
|
||||
│ ├── logging.rs
|
||||
│ ├── main.rs
|
||||
│ ├── mm
|
||||
│ │ ├── address.rs
|
||||
│ │ ├── frame_allocator.rs
|
||||
│ │ ├── heap_allocator.rs
|
||||
│ │ ├── memory_set.rs (修改:去除了构建进程地址空间时分配用户栈和映射陷入上下文的逻辑)
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── page_table.rs
|
||||
│ ├── sbi.rs
|
||||
│ ├── sync (新增:互斥锁、信号量和条件变量三种同步互斥机制的实现)
|
||||
│ │ ├── condvar.rs
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── mutex.rs
|
||||
│ │ ├── semaphore.rs
|
||||
│ │ └── up.rs
|
||||
│ ├── syscall
|
||||
│ │ ├── fs.rs (修改:将原先对 task 的调用改为对 process 的调用)
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── process.rs (修改:将原先对 task 的调用改为对 process 的调用)
|
||||
│ │ ├── sync.rs (新增:三种同步互斥机制相关的系统调用,以及基于定时器条件变量的 sleep 调用)
|
||||
│ │ └── thread.rs (新增:线程相关系统调用)
|
||||
│ ├── task
|
||||
│ │ ├── context.rs (修改:将任务上下文的成员变量改为 pub 类型)
|
||||
│ │ ├── id.rs (新增:由 pid.rs 修改而来,提供 pid/tid 、 kstack/ustack 的分配和回收机制)
|
||||
│ │ ├── kthread.rs (新增:完全在内核态运行的线程,仅供参考,在实验中未使用)
|
||||
│ │ ├── manager.rs
|
||||
│ │ ├── mod.rs (修改:增加阻塞线程的功能,将 exit 扩展到多线程,并在主线程退出时一并退出进程)
|
||||
│ │ ├── processor.rs (修改:增加获取当前线程的中断上下文虚拟地址及获取当前进程的功能)
|
||||
│ │ ├── process.rs (新增:将原先 Task 中的地址空间、文件等机制拆分为进程)
|
||||
│ │ ├── stackless_coroutine.rs (新增:完全在内核态运行的无栈协程,仅供参考,在实验中未使用)
|
||||
│ │ ├── switch.rs
|
||||
│ │ ├── switch.S
|
||||
│ │ └── task.rs (修改:将进程相关的功能移至 process.rs 中)
|
||||
│ ├── timer.rs (修改:增加定时器条件变量的实现)
|
||||
│ └── trap
|
||||
│ ├── context.rs
|
||||
│ ├── mod.rs (修改:使用线程对应的中断上下文地址而非固定的 TRAP_CONTEXT)
|
||||
│ └── trap.S
|
||||
├── README.md
|
||||
└── rust-toolchain
|
||||
485
guide/source/chapter8/1thread-kernel.rst
Normal file
485
guide/source/chapter8/1thread-kernel.rst
Normal file
@@ -0,0 +1,485 @@
|
||||
内核态的线程管理
|
||||
=========================================
|
||||
|
||||
线程概念
|
||||
---------------------------------------------
|
||||
|
||||
这里会结合与进程的比较来说明线程的概念。到本章之前,我们看到了进程这一抽象,操作系统让进程拥有相互隔离的虚拟的地址空间,
|
||||
让进程感到在独占一个虚拟的处理器。其实这只是操作系统通过时分复用和空分复用技术来让每个进程复用有限的物理内存和物理CPU。
|
||||
而线程是在进程内中的一个新的抽象。在没有线程之前,一个进程在一个时刻只有一个执行点(即程序计数器 (PC)
|
||||
寄存器保存的要执行指令的指针)。但线程的引入把进程内的这个单一执行点给扩展为多个执行点,即在进程中存在多个线程,
|
||||
每个线程都有一个执行点。而且这些线程共享进程的地址空间,所以可以不必采用相对比较复杂的 IPC 机制(一般需要内核的介入),
|
||||
而可以很方便地直接访问进程内的数据。
|
||||
|
||||
在线程的具体运行过程中,需要有程序计数器寄存器来记录当前的执行位置,需要有一组通用寄存器记录当前的指令的操作数据,
|
||||
需要有一个栈来保存线程执行过程的函数调用栈和局部变量等,这就形成了线程上下文的主体部分。
|
||||
这样如果两个线程运行在一个处理器上,就需要采用类似两个进程运行在一个处理器上的调度/切换管理机制,
|
||||
即需要在一定时刻进行线程切换,并进行线程上下文的保存与恢复。这样在一个进程中的多线程可以独立运行,
|
||||
取代了进程,成为操作系统调度的基本单位。
|
||||
|
||||
由于把进程的结构进行了细化,通过线程来表示对处理器的虚拟化,使得进程成为了管理线程的容器。
|
||||
在进程中的线程没有父子关系,大家都是兄弟,但还是有个老大。这个代表老大的线程其实就是创建进程(比如通过
|
||||
``fork`` 系统调用创建进程)时,建立的第一个线程,它的线程标识符(TID)为 ``0`` 。
|
||||
|
||||
|
||||
线程模型与重要系统调用
|
||||
----------------------------------------------
|
||||
|
||||
目前,我们只介绍本章实现的内核中采用的一种非常简单的线程模型。这个线程模型有三个运行状态:
|
||||
就绪态、运行态和等待态;共享所属进程的地址空间和其他共享资源(如文件等);可被操作系统调度来分时占用CPU执行;
|
||||
可以动态创建和退出;可通过系统调用获得操作系统的服务。我们实现的线程模型建立在进程的地址空间抽象之上:
|
||||
每个线程都共享进程的代码段和和可共享的地址空间(如全局数据段、堆等),但有自己的独占的栈。
|
||||
线程模型需要操作系统支持一些重要的系统调用:创建线程、等待子线程结束等,来支持灵活的多线程应用。
|
||||
接下来会介绍这些系统调用的基本功能和设计思路。
|
||||
|
||||
|
||||
线程创建系统调用
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在一个进程的运行过程中,进程可以创建多个属于这个进程的线程,每个线程有自己的线程标识符(TID,Thread Identifier)。
|
||||
系统调用 ``thread_create`` 的原型如下:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
/// 功能:当前进程创建一个新的线程
|
||||
/// 参数:entry 表示线程的入口函数地址
|
||||
/// 参数:arg:表示线程的一个参数
|
||||
pub fn sys_thread_create(entry: usize, arg: usize) -> isize
|
||||
|
||||
当进程调用 ``thread_create`` 系统调用后,内核会在这个进程内部创建一个新的线程,这个线程能够访问到进程所拥有的代码段,
|
||||
堆和其他数据段。但内核会给这个新线程分配一个它专有的用户态栈,这样每个线程才能相对独立地被调度和执行。
|
||||
另外,由于用户态进程与内核之间有各自独立的页表,所以二者需要有一个跳板页 ``TRAMPOLINE``
|
||||
来处理用户态切换到内核态的地址空间平滑转换的事务。所以当出现线程后,在进程中的每个线程也需要有一个独立的跳板页
|
||||
``TRAMPOLINE`` 来完成同样的事务。
|
||||
|
||||
相比于创建进程的 ``fork`` 系统调用,创建线程不需要要建立新的地址空间,这是二者之间最大的不同。
|
||||
另外属于同一进程中的线程之间没有父子关系,这一点也与进程不一样。
|
||||
|
||||
等待子线程系统调用
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
当一个线程执行完代表它的功能后,会通过 ``exit`` 系统调用退出。内核在收到线程发出的 ``exit`` 系统调用后,
|
||||
会回收线程占用的部分资源,即用户态用到的资源,比如用户态的栈,用于系统调用和异常处理的跳板页等。
|
||||
而该线程的内核态用到的资源,比如内核栈等,需要通过进程/主线程调用 ``waittid`` 来回收了,
|
||||
这样整个线程才能被彻底销毁。系统调用 ``waittid`` 的原型如下:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
/// 参数:tid表示线程id
|
||||
/// 返回值:如果线程不存在,返回-1;如果线程还没退出,返回-2;其他情况下,返回结束线程的退出码
|
||||
pub fn sys_waittid(tid: usize) -> i32
|
||||
|
||||
|
||||
一般情况下进程/主线程要负责通过 ``waittid`` 来等待它创建出来的线程(不是主线程)结束并回收它们在内核中的资源
|
||||
(如线程的内核栈、线程控制块等)。如果进程/主线程先调用了 ``exit`` 系统调用来退出,那么整个进程
|
||||
(包括所属的所有线程)都会退出,而对应父进程会通过 ``waitpid`` 回收子进程剩余还没被回收的资源。
|
||||
|
||||
|
||||
进程相关的系统调用
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在引入了线程机制后,进程相关的重要系统调用: ``fork`` 、 ``exec`` 、 ``waitpid`` 虽然在接口上没有变化,
|
||||
但在它要完成的功能上需要有一定的扩展。首先,需要注意到把以前进程中与处理器执行相关的部分拆分到线程中。这样,在通过
|
||||
``fork`` 创建进程其实也意味着要单独建立一个主线程来使用处理器,并为以后创建新的线程建立相应的线程控制块向量。
|
||||
相对而言, ``exec`` 和 ``waitpid`` 这两个系统调用要做的改动比较小,还是按照与之前进程的处理方式来进行。总体上看,
|
||||
进程相关的这三个系统调用还是保持了已有的进程操作的语义,并没有由于引入了线程,而带来大的变化。
|
||||
|
||||
|
||||
应用程序示例
|
||||
----------------------------------------------
|
||||
|
||||
我们刚刚介绍了 thread_create/waittid 两个重要系统调用,我们可以借助它们和之前实现的系统调用,
|
||||
开发出功能更为灵活的应用程序。下面我们通过描述一个多线程应用程序 ``threads`` 的开发过程来展示这些系统调用的使用方法。
|
||||
|
||||
|
||||
系统调用封装
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
同学可以在 user/src/syscall.rs 中看到以 sys_* 开头的系统调用的函数原型,它们后续还会在 user/src/lib.rs 中被封装成方便应用程序使用的形式。如 ``sys_thread_create`` 被封装成 ``thread_create`` ,而 ``sys_waittid`` 被封装成 ``waittid`` :
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
pub fn thread_create(entry: usize, arg: usize) -> isize { sys_thread_create(entry, arg) }
|
||||
|
||||
pub fn waittid(tid: usize) -> isize {
|
||||
loop {
|
||||
match sys_waittid(tid) {
|
||||
-2 => { yield_(); }
|
||||
exit_code => return exit_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waittid 等待一个线程标识符的值为tid 的线程结束。在具体实现方面,我们看到当 sys_waittid 返回值为 -2 ,即要等待的线程存在但它却尚未退出的时候,主线程调用 yield_ 主动交出 CPU 使用权,待下次 CPU 使用权被内核交还给它的时候再次调用 sys_waittid 查看要等待的线程是否退出。这样做是为了减小 CPU 资源的浪费。这种方法是为了尽可能简化内核的实现。
|
||||
|
||||
|
||||
多线程应用程序 -- threads
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
多线程应用程序 -- threads 开始执行后,先调用 ``thread_create`` 创建了三个线程,加上进程自带的主线程,其实一共有四个线程。每个线程在打印了1000个字符后,会执行 ``exit`` 退出。进程通过 ``waittid`` 等待这三个线程结束后,最终结束进程的执行。下面是多线程应用程序 -- threads 的源代码:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
//usr/src/bin/ch8b_threads.rs
|
||||
|
||||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
#[macro_use]
|
||||
extern crate user_lib;
|
||||
extern crate alloc;
|
||||
|
||||
use user_lib::{thread_create, waittid, exit};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub fn thread_a() -> ! {
|
||||
for _ in 0..1000 { print!("a"); }
|
||||
exit(1)
|
||||
}
|
||||
|
||||
pub fn thread_b() -> ! {
|
||||
for _ in 0..1000 { print!("b"); }
|
||||
exit(2)
|
||||
}
|
||||
|
||||
pub fn thread_c() -> ! {
|
||||
for _ in 0..1000 { print!("c"); }
|
||||
exit(3)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn main() -> i32 {
|
||||
let mut v = Vec::new();
|
||||
v.push(thread_create(thread_a as usize, 0));
|
||||
v.push(thread_create(thread_b as usize, 0));
|
||||
v.push(thread_create(thread_c as usize, 0));
|
||||
for tid in v.iter() {
|
||||
let exit_code = waittid(*tid as usize);
|
||||
println!("thread#{} exited with code {}", tid, exit_code);
|
||||
}
|
||||
println!("main thread exited.");
|
||||
0
|
||||
}
|
||||
|
||||
线程管理的核心数据结构
|
||||
-----------------------------------------------
|
||||
|
||||
为了在现有进程管理的基础上实现线程管理,我们需要改进一些数据结构包含的内容及接口。
|
||||
基本思路就是把进程中与处理器相关的部分分拆出来,形成线程相关的部分。
|
||||
|
||||
本节将按照如下顺序来进行介绍:
|
||||
|
||||
- 任务控制块 TaskControlBlock :表示线程的核心数据结构。
|
||||
- 任务管理器 TaskManager :管理线程集合的核心数据结构。
|
||||
- 处理器管理结构 Processor :用于线程调度,维护线程的处理器状态。
|
||||
|
||||
线程控制块
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
在内核中,每个线程的执行状态和线程上下文等均保存在一个被称为线程控制块 (TCB, Task Control Block)
|
||||
的结构中,它是内核对线程进行管理的核心数据结构。在内核看来,它就等价于一个线程。
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
pub struct TaskControlBlock {
|
||||
// immutable
|
||||
pub process: Weak<ProcessControlBlock>,
|
||||
pub kernel_stack: KernelStack,
|
||||
// mutable
|
||||
inner: UPSafeCell<TaskControlBlockInner>,
|
||||
}
|
||||
|
||||
pub struct TaskControlBlockInner {
|
||||
pub trap_cx_ppn: PhysPageNum,
|
||||
pub task_cx: TaskContext,
|
||||
pub task_status: TaskStatus,
|
||||
pub exit_code: Option<i32>,
|
||||
pub res: Option<TaskUserRes>,
|
||||
}
|
||||
|
||||
线程控制块就是任务控制块(TaskControlBlock),主要包括在线程初始化之后就不再变化的元数据:
|
||||
线程所属的进程和线程的内核栈,以及在运行过程中可能发生变化的元数据: UPSafeCell<TaskControlBlockInner> 。
|
||||
大部分的细节放在 ``TaskControlBlockInner`` 中:
|
||||
|
||||
之前进程中的定义不存在的:
|
||||
|
||||
- ``res: Option<TaskUserRes>`` 指出了用户态的线程代码执行需要的信息,这些在线程初始化之后就不再变化:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
pub struct TaskUserRes {
|
||||
pub tid: usize,
|
||||
pub ustack_base: usize,
|
||||
pub process: Weak<ProcessControlBlock>,
|
||||
}
|
||||
|
||||
- tid:线程标识符
|
||||
- ustack_base:线程的栈顶地址
|
||||
- process:线程所属的进程
|
||||
|
||||
与之前进程中的定义相同/类似的部分:
|
||||
|
||||
- ``trap_cx_ppn`` 指出了应用地址空间中线程的 Trap 上下文被放在的物理页帧的物理页号。
|
||||
- ``task_cx`` 保存暂停线程的线程上下文,用于线程切换。
|
||||
- ``task_status`` 维护当前线程的执行状态。
|
||||
- ``exit_code`` 线程退出码。
|
||||
|
||||
|
||||
包含线程的进程控制块
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
把线程相关数据单独组织成数据结构后,进程的结构也需要进行一定的调整:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
pub struct ProcessControlBlock {
|
||||
// immutable
|
||||
pub pid: PidHandle,
|
||||
// mutable
|
||||
inner: UPSafeCell<ProcessControlBlockInner>,
|
||||
}
|
||||
|
||||
pub struct ProcessControlBlockInner {
|
||||
...
|
||||
pub tasks: Vec<Option<Arc<TaskControlBlock>>>,
|
||||
pub task_res_allocator: RecycleAllocator,
|
||||
}
|
||||
|
||||
从中可以看出,进程把与处理器执行相关的部分都移到了 ``TaskControlBlock`` 中,并组织为一个线程控制块向量中,
|
||||
这就自然对应到多个线程的管理上了。而 ``RecycleAllocator`` 是对之前的 ``PidAllocator`` 的一个升级版,
|
||||
即一个相对通用的资源分配器,可用于分配进程标识符(PID)和线程的内核栈(KernelStack)。
|
||||
|
||||
.. chyyuu 加一个PidAllocator的链接???
|
||||
|
||||
线程与处理器管理结构
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
线程管理的结构是线程管理器,即任务管理器,位于 ``os/src/task/manager.rs`` 中,
|
||||
其数据结构和方法与之前章节中进程的任务管理器完全一样,仅负责管理所有线程。而处理器管理结构 ``Processor``
|
||||
负责维护 CPU 状态、调度和特权级切换等事务。其数据结构与之前章节中进程的处理器管理结构完全一样。
|
||||
但在相关方法上面,由于多个线程有各自的用户栈和跳板页,所以有些不同,下面会进一步分析。
|
||||
|
||||
.. chyyuu 加一个taskmanager,processor的链接???
|
||||
|
||||
线程管理机制的设计与实现
|
||||
-----------------------------------------------
|
||||
|
||||
在上述线程模型和内核数据结构的基础上,我们还需完成线程管理的基本实现,从而构造出一个完整的“达科塔盗龙”操作系统。
|
||||
本节将分析如何实现线程管理:
|
||||
|
||||
- 线程创建、线程退出与等待线程结束
|
||||
- 线程执行中的特权级切换
|
||||
.. - 进程管理中与线程相关的处理
|
||||
|
||||
|
||||
线程创建、线程退出与等待线程结束
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
线程创建
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
当一个进程执行中发出了创建线程的系统调用 ``sys_thread_create`` 后,操作系统就需要在当前进程的基础上创建一个线程了,
|
||||
这里重点是需要了解创建线程控制块,在线程控制块中初始化各个成员变量,建立好进程和线程的关系等。
|
||||
只有建立好这些成员变量,才能给线程建立一个灵活方便的执行环境。这里列出支持线程正确运行所需的重要的执行环境要素:
|
||||
|
||||
- 线程的用户态栈:确保在用户态的线程能正常执行函数调用;
|
||||
- 线程的内核态栈:确保线程陷入内核后能正常执行函数调用;
|
||||
- 线程的跳板页:确保线程能正确的进行用户态<-->内核态切换;
|
||||
- 线程上下文:即线程用到的寄存器信息,用于线程切换。
|
||||
|
||||
线程创建的具体实现如下:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
// os/src/syscall/thread.rs
|
||||
|
||||
pub fn sys_thread_create(entry: usize, arg: usize) -> isize {
|
||||
let task = current_task().unwrap();
|
||||
let process = task.process.upgrade().unwrap();
|
||||
// create a new thread
|
||||
let new_task = Arc::new(TaskControlBlock::new(
|
||||
Arc::clone(&process),
|
||||
task.inner_exclusive_access().res.as_ref().unwrap().ustack_base,
|
||||
true,
|
||||
));
|
||||
// add new task to scheduler
|
||||
add_task(Arc::clone(&new_task));
|
||||
let new_task_inner = new_task.inner_exclusive_access();
|
||||
let new_task_res = new_task_inner.res.as_ref().unwrap();
|
||||
let new_task_tid = new_task_res.tid;
|
||||
let mut process_inner = process.inner_exclusive_access();
|
||||
// add new thread to current process
|
||||
let tasks = &mut process_inner.tasks;
|
||||
while tasks.len() < new_task_tid + 1 {
|
||||
tasks.push(None);
|
||||
}
|
||||
tasks[new_task_tid] = Some(Arc::clone(&new_task));
|
||||
let new_task_trap_cx = new_task_inner.get_trap_cx();
|
||||
*new_task_trap_cx = TrapContext::app_init_context(
|
||||
entry,
|
||||
new_task_res.ustack_top(),
|
||||
kernel_token(),
|
||||
new_task.kernel_stack.get_top(),
|
||||
trap_handler as usize,
|
||||
);
|
||||
(*new_task_trap_cx).x[10] = arg;
|
||||
new_task_tid as isize
|
||||
}
|
||||
|
||||
上述代码主要完成了如下事务:
|
||||
|
||||
- 第4-5行,找到当前正在执行的线程 ``task`` 和此线程所属的进程 ``process`` 。
|
||||
- 第7-11行,调用 ``TaskControlBlock::new`` 方法,创建一个新的线程 ``new_task`` ,在创建过程中,建立与进程
|
||||
``process`` 的所属关系,分配了线程用户态栈、内核态栈、用于异常/中断的跳板页。
|
||||
- 第13行,把线程挂到调度队列中。
|
||||
- 第19-22行,把线程接入到所需进程的线程列表 ``tasks`` 中。
|
||||
- 第25~32行,初始化位于该线程在用户态地址空间中的 Trap 上下文:设置线程的函数入口点和用户栈,
|
||||
使得第一次进入用户态时能从线程起始位置开始正确执行;设置好内核栈和陷入函数指针 ``trap_handler`` ,
|
||||
保证在 Trap 的时候用户态的线程能正确进入内核态。
|
||||
|
||||
线程退出
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
当一个非主线程的其他线程发出 ``sys_exit`` 系统调用时,内核会调用 ``exit_current_and_run_next``
|
||||
函数退出当前线程并切换到下一个线程,但不会导致其所属进程的退出。当 **主线程** 即进程发出这个系统调用,
|
||||
内核会回收整个进程(这包括了其管理的所有线程)资源,并退出。具体实现如下:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
// os/src/syscall/process.rs
|
||||
|
||||
pub fn sys_exit(exit_code: i32) -> ! {
|
||||
exit_current_and_run_next(exit_code);
|
||||
panic!("Unreachable in sys_exit!");
|
||||
}
|
||||
|
||||
// os/src/task/mod.rs
|
||||
|
||||
pub fn exit_current_and_run_next(exit_code: i32) {
|
||||
let task = take_current_task().unwrap();
|
||||
let mut task_inner = task.inner_exclusive_access();
|
||||
let process = task.process.upgrade().unwrap();
|
||||
let tid = task_inner.res.as_ref().unwrap().tid;
|
||||
// record exit code
|
||||
task_inner.exit_code = Some(exit_code);
|
||||
task_inner.res = None;
|
||||
// here we do not remove the thread since we are still using the kstack
|
||||
// it will be deallocated when sys_waittid is called
|
||||
drop(task_inner);
|
||||
drop(task);
|
||||
// however, if this is the main thread of current process
|
||||
// the process should terminate at once
|
||||
if tid == 0 {
|
||||
let mut process_inner = process.inner_exclusive_access();
|
||||
// mark this process as a zombie process
|
||||
process_inner.is_zombie = true;
|
||||
// record exit code of main process
|
||||
process_inner.exit_code = exit_code;
|
||||
{
|
||||
// move all child processes under init process
|
||||
let mut initproc_inner = INITPROC.inner_exclusive_access();
|
||||
for child in process_inner.children.iter() {
|
||||
child.inner_exclusive_access().parent = Some(Arc::downgrade(&INITPROC));
|
||||
initproc_inner.children.push(child.clone());
|
||||
}
|
||||
}
|
||||
let mut recycle_res = Vec::<TaskUserRes>::new();
|
||||
// deallocate user res (including tid/trap_cx/ustack) of all threads
|
||||
// it has to be done before we dealloc the whole memory_set
|
||||
// otherwise they will be deallocated twice
|
||||
for task in process_inner.tasks.iter().filter(|t| t.is_some()) {
|
||||
let task = task.as_ref().unwrap();
|
||||
let mut task_inner = task.inner_exclusive_access();
|
||||
if let Some(res) = task_inner.res.take() {
|
||||
recycle_res.push(res);
|
||||
}
|
||||
}
|
||||
drop(process_inner);
|
||||
recycle_res.clear();
|
||||
let mut process_inner = process.inner_exclusive_access();
|
||||
process_inner.children.clear();
|
||||
// deallocate other data in user space i.e. program code/data section
|
||||
process_inner.memory_set.recycle_data_pages();
|
||||
}
|
||||
drop(process);
|
||||
// we do not have to save task context
|
||||
let mut _unused = TaskContext::zero_init();
|
||||
schedule(&mut _unused as *mut _);
|
||||
}
|
||||
|
||||
上述代码主要完成了如下事务:
|
||||
|
||||
- 第11-21行,回收线程的各种资源。
|
||||
- 第24-56行,如果是主线程发出的退去请求,则回收整个进程的部分资源,并退出进程。第 33~37
|
||||
行所做的事情是将当前进程的所有子进程挂在初始进程 INITPROC 下面,其做法是遍历每个子进程,
|
||||
修改其父进程为初始进程,并加入初始进程的孩子向量中。第 49 行将当前进程的孩子向量清空。
|
||||
- 第58-59行,进行线程调度切换。
|
||||
|
||||
上述实现中很大一部分与第五章讲解的 进程的退出 的功能实现大致相同。
|
||||
|
||||
.. chyyuu 加上链接???
|
||||
|
||||
等待线程结束
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
主线程通过系统调用 ``sys_waittid`` 来等待其他线程的结束。具体实现如下:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
// os/src/syscall/ch8b_thread.rs
|
||||
|
||||
pub fn sys_waittid(tid: usize) -> i32 {
|
||||
let task = current_task().unwrap();
|
||||
let process = task.process.upgrade().unwrap();
|
||||
let task_inner = task.inner_exclusive_access();
|
||||
let mut process_inner = process.inner_exclusive_access();
|
||||
// a thread cannot wait for itself
|
||||
if task_inner.res.as_ref().unwrap().tid == tid {
|
||||
return -1;
|
||||
}
|
||||
let mut exit_code: Option<i32> = None;
|
||||
let waited_task = process_inner.tasks[tid].as_ref();
|
||||
if let Some(waited_task) = waited_task {
|
||||
if let Some(waited_exit_code) = waited_task.inner_exclusive_access().exit_code {
|
||||
exit_code = Some(waited_exit_code);
|
||||
}
|
||||
} else {
|
||||
// waited thread does not exist
|
||||
return -1;
|
||||
}
|
||||
if let Some(exit_code) = exit_code {
|
||||
// dealloc the exited thread
|
||||
process_inner.tasks[tid] = None;
|
||||
exit_code
|
||||
} else {
|
||||
// waited thread has not exited
|
||||
-2
|
||||
}
|
||||
}
|
||||
|
||||
上述代码主要完成了如下事务:
|
||||
|
||||
- 第9-10行,如果是线程等自己,返回错误.
|
||||
- 第12-21行,如果找到 ``tid`` 对应的退出线程,则收集该退出线程的退出码 ``exit_tid`` ,否则返回错误(退出线程不存在)。
|
||||
- 第22-29行,如果退出码存在,则清空进程中对应此退出线程的线程控制块(至此,线程所占资源算是全部清空了),否则返回错误(线程还没退出)。
|
||||
|
||||
|
||||
线程执行中的特权级切换和调度切换
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
线程执行中的特权级切换与第三章中 **任务切换的设计与实现** 小节中讲解的过程是一致的。而线程执行中的调度切换过程与第五章的 **进程调度机制** 小节中讲解的过程是一致的。
|
||||
这里就不用再赘述一遍了。
|
||||
|
||||
|
||||
.. [#dak] 达科塔盗龙是一种生存于距今6700万-6500万年前白垩纪晚期的兽脚类驰龙科恐龙,它主打的并不是霸王龙的力量路线,而是利用自己修长的后肢来提高敏捷度和奔跑速度。它全身几乎都长满了羽毛,可能会滑翔或者其他接近飞行行为的行动模式。
|
||||
379
guide/source/chapter8/2lock.rst
Normal file
379
guide/source/chapter8/2lock.rst
Normal file
@@ -0,0 +1,379 @@
|
||||
锁机制
|
||||
=========================================
|
||||
|
||||
本节导读
|
||||
-----------------------------------------
|
||||
|
||||
.. chyyuu https://en.wikipedia.org/wiki/Lock_(computer_science)
|
||||
|
||||
到目前为止,我们已经实现了进程和线程,也能够理解在一个时间段内,会有多个线程在执行,这就是并发。
|
||||
而且,由于线程的引入,多个线程可以共享进程中的全局数据。如果多个线程都想读和更新全局数据,
|
||||
那么谁先更新取决于操作系统内核的抢占式调度和分派策略。在一般情况下,每个线程都有可能先执行,
|
||||
且可能由于中断等因素,随时被操作系统打断其执行,而切换到另外一个线程运行,
|
||||
形成在一段时间内,多个线程交替执行的现象。如果没有一些保障机制(比如互斥、同步等),
|
||||
那么这些对共享数据进行读写的交替执行的线程,其期望的共享数据的正确结果可能无法达到。
|
||||
|
||||
所以,我们需要研究一种保障机制 --- 锁 ,确保无论操作系统如何抢占线程,调度和切换线程的执行,
|
||||
都可以保证对拥有锁的线程,可以独占地对共享数据进行读写,从而能够得到正确的共享数据结果。
|
||||
这种机制的能力来自于处理器的指令、操作系统系统调用的基本支持,从而能够保证线程间互斥地读写共享数据。
|
||||
下面各个小节将从为什么需要锁、锁的基本思路、锁的不同实现方式等逐步展开讲解。
|
||||
|
||||
为什么需要锁
|
||||
-----------------------------------------
|
||||
|
||||
上一小节已经提到,没有保障机制的多个线程,在对共享数据进行读写的过程中,可能得不到预期的结果。
|
||||
我们来看看这个简单的例子:
|
||||
|
||||
.. code-block:: c
|
||||
:linenos:
|
||||
:emphasize-lines: 4
|
||||
|
||||
// 线程的入口函数
|
||||
int a=0;
|
||||
void f() {
|
||||
a = a + 1;
|
||||
}
|
||||
|
||||
对于上述函数中的第 4 行代码,一般人理解处理器会一次就执行完这条简单的语句,但实际情况并不是这样。
|
||||
我们可以用 GCC 编译出上述函数的汇编码:
|
||||
|
||||
.. code-block:: shell
|
||||
:linenos:
|
||||
|
||||
$ riscv64-unknown-elf-gcc -o f.s -S f.c
|
||||
|
||||
|
||||
可以看到生成的汇编代码如下:
|
||||
|
||||
.. code-block:: asm
|
||||
:linenos:
|
||||
:emphasize-lines: 18-23
|
||||
|
||||
//f.s
|
||||
.text
|
||||
.globl a
|
||||
.section .sbss,"aw",@nobits
|
||||
.align 2
|
||||
.type a, @object
|
||||
.size a, 4
|
||||
a:
|
||||
.zero 4
|
||||
.text
|
||||
.align 1
|
||||
.globl f
|
||||
.type f, @function
|
||||
f:
|
||||
addi sp,sp,-16
|
||||
sd s0,8(sp)
|
||||
addi s0,sp,16
|
||||
lui a5,%hi(a)
|
||||
lw a5,%lo(a)(a5)
|
||||
addiw a5,a5,1
|
||||
sext.w a4,a5
|
||||
lui a5,%hi(a)
|
||||
sw a4,%lo(a)(a5)
|
||||
nop
|
||||
ld s0,8(sp)
|
||||
addi sp,sp,16
|
||||
jr ra
|
||||
|
||||
|
||||
.. chyyuu 可以给上面的汇编码添加注释???
|
||||
|
||||
从中可以看出,对于高级语言的一条简单语句(C 代码的第 4 行,对全局变量进行读写),很可能是由多条汇编代码
|
||||
(汇编代码的第 18~23 行)组成。如果这个函数是多个线程要执行的函数,那么在上述汇编代码第
|
||||
18 行到第 23 行中的各行之间,可能会发生中断,从而导致操作系统执行抢占式的线程调度和切换,
|
||||
就会得到不一样的结果。由于执行这段汇编代码(第 18~23 行))的多个线程在访问全局变量过程中可能导致竞争状态,
|
||||
因此我们将此段代码称为临界区(critical section)。临界区是访问共享变量(或共享资源)的代码片段,
|
||||
不能由多个线程同时执行,即需要保证互斥。
|
||||
|
||||
下面是有两个线程T0、T1在一个时间段内的一种可能的执行情况:
|
||||
|
||||
===== ===== ======= ======= =========== =========
|
||||
时间 T0 T1 OS 共享变量a 寄存器a5
|
||||
===== ===== ======= ======= =========== =========
|
||||
1 L18 -- -- 0 a的高位地址
|
||||
2 -- -- 切换 0 0
|
||||
3 -- L18 -- 0 a的高位地址
|
||||
4 L20 -- -- 0 1
|
||||
5 -- -- 切换 0 a的高位地址
|
||||
6 -- L20 -- 0 1
|
||||
7 -- -- 切换 0 1
|
||||
8 L23 -- -- 1 1
|
||||
9 -- -- 切换 1 1
|
||||
10 -- L23 -- 1 1
|
||||
===== ===== ======= ======= =========== =========
|
||||
|
||||
一般情况下,线程 T0 执行完毕后,再执行线程 T1,那么共享全局变量 ``a`` 的值为 2 。但在上面的执行过程中,
|
||||
可以看到在线程执行指令的过程中会发生线程切换,这样在时刻 10 的时候,共享全局变量 ``a`` 的值为 1 ,
|
||||
这不是我们预期的结果。出现这种情况的原因是两个线程在操作系统的调度下(在哪个时刻调度具有不确定性),
|
||||
交错执行 ``a = a + 1`` 的不同汇编指令序列,导致虽然增加全局变量 ``a`` 的代码被执行了两次,
|
||||
但结果还是只增加了 1 。这种多线程的最终执行结果不确定(indeterminate),取决于由于调度导致的、
|
||||
不确定指令执行序列的情况就是竞态条件(race condition)。
|
||||
|
||||
如果每个线程在执行 ``a = a + 1`` 这个 C 语句所对应多条汇编语句过程中,不会被操作系统切换,
|
||||
那么就不会出现多个线程交叉读写全局变量的情况,也就不会出现结果不确定的问题了。
|
||||
|
||||
所以,访问(特指写操作)共享变量代码片段,不能由多个线程同时执行(即并行)或者在一个时间段内都去执行
|
||||
(即并发)。要做到这一点,需要互斥机制的保障。从某种角度上看,这种互斥性也是一种原子性,
|
||||
即线程在临界区的执行过程中,不会出现只执行了一部分,就被打断并切换到其他线程执行的情况。即,
|
||||
要么线程执行的这一系列操作/指令都完成,要么这一系列操作/指令都不做,不会出现指令序列执行中被打断的情况。
|
||||
|
||||
|
||||
锁的基本思路
|
||||
-----------------------------------------
|
||||
|
||||
要保证多线程并发执行中的临界区的代码具有互斥性或原子性,我们可以建立一种锁,
|
||||
只有拿到锁的线程才能在临界区中执行。这里的锁与现实生活中的锁的含义很类似。比如,我们可以写出如下的伪代码:
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
|
||||
lock(mutex); // 尝试取锁
|
||||
a = a + 1; // 临界区,访问临界资源 a
|
||||
unlock(mutex); // 是否锁
|
||||
... // 剩余区
|
||||
|
||||
对于一个应用程序而言,它的执行是受到其执行环境的管理和限制的,而执行环境的主要组成就是用户态的系统库、
|
||||
操作系统和更底层的处理器,这说明我们需要有硬件和操作系统来对互斥进行支持。一个自然的想法是,这个
|
||||
``lock/unlock`` 互斥操作就是CPU提供的机器指令,那上面这一段程序就很容易在计算机上执行了。
|
||||
但需要注意,这里互斥的对象是线程的临界区代码,而临界区代码可以访问各种共享变量(简称临界资源)。
|
||||
只靠两条机器指令,难以识别各种共享变量,不太可能约束可能在临界区的各种指令执行共享变量操作的互斥性。
|
||||
所以,我们还是需要有一些相对更灵活和复杂一点的方法,能够设置一种所有线程能看到的标记,
|
||||
在一个能进入临界区的线程设置好这个标记后,其他线程都不能再进入临界区了。总体上看,
|
||||
对临界区的访问过程分为四个部分:
|
||||
|
||||
1. 尝试取锁: 查看锁是否可用,即临界区是否可访问(看占用临界区标志是否被设置),如果可以访问,
|
||||
则设置占用临界区标志(锁不可用)并转到步骤 2 ,否则线程忙等或被阻塞;
|
||||
2. 临界区: 访问临界资源的系列操作
|
||||
3. 释放锁: 清除占用临界区标志(锁可用),如果有线程被阻塞,会唤醒阻塞线程;
|
||||
4. 剩余区: 与临界区不相关部分的代码
|
||||
|
||||
根据上面的步骤,可以看到锁机制有两种:让线程忙等的忙等锁(spin lock),以及让线程阻塞的睡眠锁
|
||||
(sleep lock)。锁的实现大体上基于三类机制:用户态软件、机器指令硬件、内核态操作系统。
|
||||
下面我们介绍来 rCore 中基于内核态操作系统级方法实现的支持互斥的锁。
|
||||
|
||||
我们还需要知道如何评价各种锁实现的效果。一般我们需要关注锁的三种属性:
|
||||
|
||||
1. 互斥性(mutual exclusion),即锁是否能够有效阻止多个线程进入临界区,这是最基本的属性。
|
||||
2. 公平性(fairness),当锁可用时,每个竞争线程是否有公平的机会抢到锁。
|
||||
3. 性能(performance),即使用锁的时间开销。
|
||||
|
||||
|
||||
内核态操作系统级方法实现锁 --- mutex 系统调用
|
||||
-----------------------------------------
|
||||
|
||||
|
||||
使用 mutex 系统调用
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
如何能够实现轻量的可睡眠锁?一个自然的想法就是,让等待锁的线程睡眠,让释放锁的线程显式地唤醒等待锁的线程。
|
||||
如果有多个等待锁的线程,可以全部释放,让大家再次竞争锁;也可以只释放最早等待的那个线程。
|
||||
这就需要更多的操作系统支持,特别是需要一个等待队列来保存等待锁的线程。
|
||||
|
||||
我们先看看多线程应用程序如何使用mutex系统调用的:
|
||||
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
:emphasize-lines: 8,13,21,32,35,38
|
||||
|
||||
// user/src/bin/race_adder_mutex_blocking.rs
|
||||
|
||||
static mut A: usize = 0;
|
||||
...
|
||||
unsafe fn f() -> ! {
|
||||
let mut t = 2usize;
|
||||
for _ in 0..PER_THREAD {
|
||||
mutex_lock(0);
|
||||
let a = &mut A as *mut usize;
|
||||
let cur = a.read_volatile();
|
||||
for _ in 0..500 { t = t * t % 10007; }
|
||||
a.write_volatile(cur + 1);
|
||||
mutex_unlock(0);
|
||||
}
|
||||
exit(t as i32)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn main() -> i32 {
|
||||
let start = get_time();
|
||||
assert_eq!(mutex_blocking_create(), 0);
|
||||
let mut v = Vec::new();
|
||||
for _ in 0..THREAD_COUNT {
|
||||
v.push(thread_create(f as usize, 0) as usize);
|
||||
}
|
||||
...
|
||||
}
|
||||
|
||||
// usr/src/syscall.rs
|
||||
|
||||
pub fn sys_mutex_create(blocking: bool) -> isize {
|
||||
syscall(SYSCALL_MUTEX_CREATE, [blocking as usize, 0, 0])
|
||||
}
|
||||
pub fn sys_mutex_lock(id: usize) -> isize {
|
||||
syscall(SYSCALL_MUTEX_LOCK, [id, 0, 0])
|
||||
}
|
||||
pub fn sys_mutex_unlock(id: usize) -> isize {
|
||||
syscall(SYSCALL_MUTEX_UNLOCK, [id, 0, 0])
|
||||
}
|
||||
|
||||
|
||||
- 第21行,创建了一个ID为 ``0`` 的互斥锁,对应的是第32行 ``SYSCALL_MUTEX_CREATE`` 系统调用;
|
||||
- 第8行,尝试获取锁(对应的是第35行 ``SYSCALL_MUTEX_LOCK`` 系统调用),如果取得锁,
|
||||
将继续向下执行临界区代码;如果没有取得锁,将阻塞;
|
||||
- 第13行,释放锁(对应的是第38行 ``SYSCALL_MUTEX_UNLOCK`` 系统调用),如果有等待在该锁上的线程,
|
||||
则唤醒这些等待线程。
|
||||
|
||||
mutex 系统调用的实现
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
操作系统如何实现这些系统调用呢?首先考虑一下与此相关的核心数据结构,
|
||||
然后考虑与数据结构相关的相关函数/方法的实现。
|
||||
|
||||
在线程的眼里, **互斥** 是一种每个线程能看到的资源,且在一个进程中,可以存在多个不同互斥资源,
|
||||
所以我们可以把所有的互斥资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是:
|
||||
``mutex_list: Vec<Option<Arc<dyn Mutex>>>`` 表示的是实现了 ``Mutex`` trait 的一个“互斥资源”的向量。而
|
||||
``MutexBlocking`` 是会实现 ``Mutex`` trait 的内核数据结构,它就是我们提到的 **互斥资源** 即
|
||||
**互斥锁** 。操作系统需要显式地施加某种控制,来确定当一个线程释放锁时,等待的线程谁将能抢到锁。
|
||||
为了做到这一点,操作系统需要有一个等待队列来保存等待锁的线程,如下面代码的第 20 行所示。
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
:emphasize-lines: 9,20
|
||||
|
||||
pub struct ProcessControlBlock {
|
||||
// immutable
|
||||
pub pid: PidHandle,
|
||||
// mutable
|
||||
inner: UPSafeCell<ProcessControlBlockInner>,
|
||||
}
|
||||
pub struct ProcessControlBlockInner {
|
||||
...
|
||||
pub mutex_list: Vec<Option<Arc<dyn Mutex>>>,
|
||||
}
|
||||
pub trait Mutex: Sync + Send {
|
||||
fn lock(&self);
|
||||
fn unlock(&self);
|
||||
}
|
||||
pub struct MutexBlocking {
|
||||
inner: UPSafeCell<MutexBlockingInner>,
|
||||
}
|
||||
pub struct MutexBlockingInner {
|
||||
locked: bool,
|
||||
wait_queue: VecDeque<Arc<TaskControlBlock>>,
|
||||
}
|
||||
|
||||
|
||||
这样,在操作系统中,需要设计实现三个核心成员变量。互斥锁的成员变量有两个:表示是否锁上的 ``locked``
|
||||
和管理等待线程的等待队列 ``wait_queue``;进程的成员变量:锁向量 ``mutex_list`` 。
|
||||
|
||||
首先需要创建一个互斥锁,下面是应对 ``SYSCALL_MUTEX_CREATE`` 系统调用的创建互斥锁的函数:
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
:emphasize-lines: 14,18
|
||||
|
||||
// os/src/syscall/sync.rs
|
||||
pub fn sys_mutex_create(blocking: bool) -> isize {
|
||||
let process = current_process();
|
||||
let mut process_inner = process.inner_exclusive_access();
|
||||
if let Some(id) = process_inner
|
||||
.mutex_list
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, item)| item.is_none())
|
||||
.map(|(id, _)| id) {
|
||||
process_inner.mutex_list[id] = if !blocking {
|
||||
Some(Arc::new(MutexSpin::new()))
|
||||
} else {
|
||||
Some(Arc::new(MutexBlocking::new()))
|
||||
};
|
||||
id as isize
|
||||
} else {
|
||||
process_inner.mutex_list.push(Some(Arc::new(MutexSpin::new())));
|
||||
process_inner.mutex_list.len() as isize - 1
|
||||
}
|
||||
}
|
||||
|
||||
- 第 14 行,如果向量中有空的元素,就在这个空元素的位置创建一个可睡眠的互斥锁;
|
||||
- 第 18 行,如果向量满了,就在向量中添加新的可睡眠的互斥锁;
|
||||
|
||||
|
||||
有了互斥锁,接下来就是实现 ``Mutex`` trait的内核函数:对应 ``SYSCALL_MUTEX_LOCK`` 系统调用的
|
||||
``sys_mutex_lock`` 。操作系统主要工作是,在锁已被其他线程获取的情况下,把当前线程放到等待队列中,
|
||||
并调度一个新线程执行。主要代码如下:
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
:emphasize-lines: 8,16,17,19,21
|
||||
|
||||
// os/src/syscall/sync.rs
|
||||
pub fn sys_mutex_lock(mutex_id: usize) -> isize {
|
||||
let process = current_process();
|
||||
let process_inner = process.inner_exclusive_access();
|
||||
let mutex = Arc::clone(process_inner.mutex_list[mutex_id].as_ref().unwrap());
|
||||
drop(process_inner);
|
||||
drop(process);
|
||||
mutex.lock();
|
||||
0
|
||||
}
|
||||
|
||||
// os/src/sync/mutex.rs
|
||||
impl Mutex for MutexBlocking {
|
||||
fn lock(&self) {
|
||||
let mut mutex_inner = self.inner.exclusive_access();
|
||||
if mutex_inner.locked {
|
||||
mutex_inner.wait_queue.push_back(current_task().unwrap());
|
||||
drop(mutex_inner);
|
||||
block_current_and_run_next();
|
||||
} else {
|
||||
mutex_inner.locked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.. chyyuu drop的作用???
|
||||
|
||||
- 第 8 行,调用 ID 为 ``mutex_id`` 的互斥锁 ``mutex`` 的 ``lock`` 方法,具体工作由该方法来完成。
|
||||
- 第 16 行,如果互斥锁 ``mutex`` 已经被其他线程获取了,那么在第 17 行,将把当前线程放入等待队列中;
|
||||
在第 19 行,让当前线程处于等待状态,并调度其他线程执行。
|
||||
- 第 21 行,如果互斥锁 ``mutex`` 还没被获取,那么当前线程会获取给互斥锁,并返回系统调用。
|
||||
|
||||
|
||||
最后是实现 ``Mutex`` trait 的内核函数:对应 ``SYSCALL_MUTEX_UNLOCK`` 系统调用的 ``sys_mutex_unlock`` 。
|
||||
操作系统的主要工作是,如果有等待在这个互斥锁上的线程,需要唤醒最早等待的线程。主要代码如下:
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
:emphasize-lines: 8,17-18,20
|
||||
|
||||
// os/src/syscall/sync.rs
|
||||
pub fn sys_mutex_unlock(mutex_id: usize) -> isize {
|
||||
let process = current_process();
|
||||
let process_inner = process.inner_exclusive_access();
|
||||
let mutex = Arc::clone(process_inner.mutex_list[mutex_id].as_ref().unwrap());
|
||||
drop(process_inner);
|
||||
drop(process);
|
||||
mutex.unlock();
|
||||
0
|
||||
}
|
||||
|
||||
// os/src/sync/mutex.rs
|
||||
impl Mutex for MutexBlocking {
|
||||
fn unlock(&self) {
|
||||
let mut mutex_inner = self.inner.exclusive_access();
|
||||
assert!(mutex_inner.locked);
|
||||
if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
|
||||
add_task(waking_task);
|
||||
} else {
|
||||
mutex_inner.locked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- 第 8 行,调用 ID 为 ``mutex_id`` 的互斥锁 ``mutex`` 的 ``unlock`` 方法,具体工作由该方法来完成的。
|
||||
- 第 17-18 行,如果有等待的线程,唤醒等待最久的那个线程,相当于将锁的所有权移交给该线程。
|
||||
- 第 20 行,若没有线程等待,则释放锁。
|
||||
|
||||
|
||||
275
guide/source/chapter8/3semaphore.rst
Normal file
275
guide/source/chapter8/3semaphore.rst
Normal file
@@ -0,0 +1,275 @@
|
||||
信号量机制
|
||||
=========================================
|
||||
|
||||
本节导读
|
||||
-----------------------------------------
|
||||
|
||||
.. chyyuu https://en.wikipedia.org/wiki/Semaphore_(programming)
|
||||
|
||||
在上一节中,我们介绍了互斥锁(mutex 或 lock)的起因、使用和实现过程。通过互斥锁,
|
||||
可以让线程在临界区执行时,独占临界资源。当我们需要更灵活的互斥访问或同步操作方式,如提供了最多只允许
|
||||
N 个线程访问临界资源的情况,让某个线程等待另外一个线程执行完毕后再继续执行的同步过程等,
|
||||
互斥锁这种方式就有点力不从心了。
|
||||
|
||||
在本节中,将介绍功能更加强大和灵活的同步互斥机制 -- 信号量(Semaphore),它的设计思路、
|
||||
使用和在操作系统中的具体实现。可以看到,信号量的实现需要互斥锁和处理器原子指令的支持,
|
||||
它是一种更高级的同步互斥机制。
|
||||
|
||||
|
||||
信号量的起源和基本思路
|
||||
-----------------------------------------
|
||||
|
||||
1963 年前后,当时的数学家(其实是计算机科学家)Edsger Dijkstra 和他的团队在为 Electrologica X8
|
||||
计算机开发一个操作系统(称为 THE multiprogramming system,THE 多道程序系统)的过程中,提出了信号量
|
||||
(Semphore)是一种变量或抽象数据类型,用于控制多个线程对共同资源的访问。
|
||||
|
||||
信号量是对互斥锁的一种巧妙的扩展。上一节中的互斥锁的初始值一般设置为 1 的整型变量,
|
||||
表示临界区还没有被某个线程占用。互斥锁用 0 表示临界区已经被占用了,用 1 表示临界区为空,再通过
|
||||
``lock/unlock`` 操作来协调多个线程轮流独占临界区执行。而信号量的初始值可设置为 N 的整数变量, 如果 N
|
||||
大于 0, 表示最多可以有 N 个线程进入临界区执行,如果 N 小于等于 0 ,表示不能有线程进入临界区了,
|
||||
必须在后续操作中让信号量的值加 1 ,才能唤醒某个等待的线程。
|
||||
|
||||
Dijkstra 对信号量设计了两种操作:P(Proberen(荷兰语),尝试)操作和 V(Verhogen(荷兰语),增加)操作。
|
||||
P 操作是检查信号量的值是否大于 0,若该值大于 0,则将其值减 1 并继续(表示可以进入临界区了);若该值为
|
||||
0,则线程将睡眠。注意,此时 P 操作还未结束。而且由于信号量本身是一种临界资源(可回想一下上一节的锁,
|
||||
其实也是一种临界资源),所以在 P 操作中,检查/修改信号量值以及可能发生的睡眠这一系列操作,
|
||||
是一个不可分割的原子操作过程。通过原子操作才能保证,一旦 P 操作开始,则在该操作完成或阻塞睡眠之前,
|
||||
其他线程均不允许访问该信号量。
|
||||
|
||||
V 操作会对信号量的值加 1 ,然后检查是否有一个或多个线程在该信号量上睡眠等待。如有,
|
||||
则选择其中的一个线程唤醒并允许该线程继续完成它的 P 操作;如没有,则直接返回。注意,信号量的值加 1,
|
||||
并可能唤醒一个线程的一系列操作同样也是不可分割的原子操作过程。不会有某个进程因执行 V 操作而阻塞。
|
||||
|
||||
如果信号量是一个任意的整数,通常被称为计数信号量(Counting Semaphore),或一般信号量(General
|
||||
Semaphore);如果信号量只有0或1的取值,则称为二值信号量(Binary Semaphore)。可以看出,
|
||||
互斥锁是信号量的一种特例 --- 二值信号量,信号量很好地解决了最多允许 N 个线程访问临界资源的情况。
|
||||
|
||||
信号量的一种实现伪代码如下所示:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
fn P(S) {
|
||||
if S >= 1
|
||||
S = S - 1;
|
||||
else
|
||||
<block and enqueue the thread>;
|
||||
}
|
||||
fn V(S) {
|
||||
if <some threads are blocked on the queue>
|
||||
<unblock a thread>;
|
||||
else
|
||||
S = S + 1;
|
||||
}
|
||||
|
||||
在上述实现中,S 的取值范围为大于等于 0 的整数。S 的初值一般设置为一个大于 0 的正整数,
|
||||
表示可以进入临界区的线程数。当 S 取值为 1,表示是二值信号量,也就是互斥锁了。
|
||||
使用信号量实现线程互斥访问临界区的伪代码如下:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
let static mut S: semaphore = 1;
|
||||
|
||||
// Thread i
|
||||
fn foo() {
|
||||
...
|
||||
P(S);
|
||||
execute Cricital Section;
|
||||
V(S);
|
||||
...
|
||||
}
|
||||
|
||||
下面是另外一种信号量实现的伪代码:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
fn P(S) {
|
||||
S = S - 1;
|
||||
if S < 0 then
|
||||
<block and enqueue the thread>;
|
||||
}
|
||||
|
||||
fn V(S) {
|
||||
S = S + 1;
|
||||
if <some threads are blocked on the queue>
|
||||
<unblock a thread>;
|
||||
}
|
||||
|
||||
在这种实现中,S 的初值一般设置为一个大于 0 的正整数,表示可以进入临界区的线程数。但 S
|
||||
的取值范围可以是小于 0 的整数,表示等待进入临界区的睡眠线程数。
|
||||
|
||||
信号量的另一种用途是用于实现同步(synchronization)。比如,把信号量的初始值设置为 0 ,
|
||||
当一个线程 A 对此信号量执行一个 P 操作,那么该线程立即会被阻塞睡眠。之后有另外一个线程 B
|
||||
对此信号量执行一个 V 操作,就会将线程 A 唤醒。这样线程 B 中执行 V 操作之前的代码序列 B-stmts
|
||||
和线程 A 中执行 P 操作之后的代码 A-stmts 序列之间就形成了一种确定的同步执行关系,即线程 B 的
|
||||
B-stmts 会先执行,然后才是线程 A 的 A-stmts 开始执行。相关伪代码如下所示:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
let static mut S: semaphore = 0;
|
||||
|
||||
//Thread A
|
||||
...
|
||||
P(S);
|
||||
Label_2:
|
||||
A-stmts after Thread B::Label_1;
|
||||
...
|
||||
|
||||
//Thread B
|
||||
...
|
||||
B-stmts before Thread A::Label_2;
|
||||
Label_1:
|
||||
V(S);
|
||||
...
|
||||
|
||||
|
||||
实现信号量
|
||||
------------------------------------------
|
||||
|
||||
使用 semaphore 系统调用
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
我们通过例子来看看如何实际使用信号量。下面是面向应用程序对信号量系统调用的简单使用,
|
||||
可以看到对它的使用与上一节介绍的互斥锁系统调用类似。
|
||||
|
||||
在这个例子中,主线程先创建了信号量初值为 0 的信号量 ``SEM_SYNC`` ,然后再创建两个线程 First
|
||||
和 Second 。线程 First 会先睡眠 10ms,而当线程 Second 执行时,会由于执行信号量的 P
|
||||
操作而等待睡眠;当线程 First 醒来后,会执行 V 操作,从而能够唤醒线程 Second。这样线程 First
|
||||
和线程 Second 就形成了一种稳定的同步关系。
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
:emphasize-lines: 5,10,16,22,25,28
|
||||
|
||||
const SEM_SYNC: usize = 0; //信号量ID
|
||||
unsafe fn first() -> ! {
|
||||
sleep(10);
|
||||
println!("First work and wakeup Second");
|
||||
semaphore_up(SEM_SYNC); //信号量V操作
|
||||
exit(0)
|
||||
}
|
||||
unsafe fn second() -> ! {
|
||||
println!("Second want to continue,but need to wait first");
|
||||
semaphore_down(SEM_SYNC); //信号量P操作
|
||||
println!("Second can work now");
|
||||
exit(0)
|
||||
}
|
||||
pub fn main() -> i32 {
|
||||
// create semaphores
|
||||
assert_eq!(semaphore_create(0) as usize, SEM_SYNC); // 信号量初值为0
|
||||
// create first, second threads
|
||||
...
|
||||
}
|
||||
|
||||
pub fn sys_semaphore_create(res_count: usize) -> isize {
|
||||
syscall(SYSCALL_SEMAPHORE_CREATE, [res_count, 0, 0])
|
||||
}
|
||||
pub fn sys_semaphore_up(sem_id: usize) -> isize {
|
||||
syscall(SYSCALL_SEMAPHORE_UP, [sem_id, 0, 0])
|
||||
}
|
||||
pub fn sys_semaphore_down(sem_id: usize) -> isize {
|
||||
syscall(SYSCALL_SEMAPHORE_DOWN, [sem_id, 0, 0])
|
||||
}
|
||||
|
||||
|
||||
- 第 16 行,创建了一个初值为 0 ,ID 为 ``SEM_SYNC`` 的信号量,对应的是第 22 行
|
||||
``SYSCALL_SEMAPHORE_CREATE`` 系统调用;
|
||||
- 第 10 行,线程 Second 执行信号量 P 操作(对应第 28行 ``SYSCALL_SEMAPHORE_DOWN``
|
||||
系统调用),由于信号量初值为 0 ,该线程将阻塞;
|
||||
- 第 5 行,线程 First 执行信号量 V 操作(对应第 25 行 ``SYSCALL_SEMAPHORE_UP`` 系统调用),
|
||||
会唤醒等待该信号量的线程 Second。
|
||||
|
||||
实现 semaphore 系统调用
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
操作系统如何实现信号量系统调用呢?我们还是采用通常的分析做法:数据结构+方法,
|
||||
即首先考虑一下与此相关的核心数据结构,然后考虑与数据结构相关的相关函数/方法的实现。
|
||||
|
||||
在线程的眼里,信号量是一种每个线程能看到的共享资源,且在一个进程中,可以存在多个不同信号量资源,
|
||||
所以我们可以把所有的信号量资源放在一起让进程来管理,如下面代码第 9 行所示。这里需要注意的是:
|
||||
``semaphore_list: Vec<Option<Arc<Semaphore>>>`` 表示的是信号量资源的列表。而 ``Semaphore``
|
||||
是信号量的内核数据结构,由信号量值和等待队列组成。操作系统需要显式地施加某种控制,来确定当一个线程执行
|
||||
P 操作和 V 操作时,如何让线程睡眠或唤醒线程。在这里,P 操作是由 ``Semaphore`` 的 ``down``
|
||||
方法实现,而 V 操作是由 ``Semaphore`` 的 ``up`` 方法实现。
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
:emphasize-lines: 9,16,17,34-36,44-47
|
||||
|
||||
pub struct ProcessControlBlock {
|
||||
// immutable
|
||||
pub pid: PidHandle,
|
||||
// mutable
|
||||
inner: UPSafeCell<ProcessControlBlockInner>,
|
||||
}
|
||||
pub struct ProcessControlBlockInner {
|
||||
...
|
||||
pub semaphore_list: Vec<Option<Arc<Semaphore>>>,
|
||||
}
|
||||
|
||||
pub struct Semaphore {
|
||||
pub inner: UPSafeCell<SemaphoreInner>,
|
||||
}
|
||||
pub struct SemaphoreInner {
|
||||
pub count: isize,
|
||||
pub wait_queue: VecDeque<Arc<TaskControlBlock>>,
|
||||
}
|
||||
impl Semaphore {
|
||||
pub fn new(res_count: usize) -> Self {
|
||||
Self {
|
||||
inner: unsafe { UPSafeCell::new(
|
||||
SemaphoreInner {
|
||||
count: res_count as isize,
|
||||
wait_queue: VecDeque::new(),
|
||||
}
|
||||
)},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn up(&self) {
|
||||
let mut inner = self.inner.exclusive_access();
|
||||
inner.count += 1;
|
||||
if inner.count <= 0 {
|
||||
if let Some(task) = inner.wait_queue.pop_front() {
|
||||
add_task(task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn down(&self) {
|
||||
let mut inner = self.inner.exclusive_access();
|
||||
inner.count -= 1;
|
||||
if inner.count < 0 {
|
||||
inner.wait_queue.push_back(current_task().unwrap());
|
||||
drop(inner);
|
||||
block_current_and_run_next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
首先是核心数据结构:
|
||||
|
||||
- 第 9 行,进程控制块中管理的信号量列表。
|
||||
- 第 16-17 行,信号量的核心数据成员:信号量值和等待队列。
|
||||
|
||||
然后是重要的三个成员函数:
|
||||
|
||||
- 第 20 行,创建信号量,信号量初值为参数 ``res_count`` 。
|
||||
- 第 31 行,实现 V 操作的 ``up`` 函数,第 34 行,当信号量值小于等于 0 时,
|
||||
将从信号量的等待队列中弹出一个线程放入线程就绪队列。
|
||||
- 第 41 行,实现 P 操作的 ``down`` 函数,第 44 行,当信号量值小于 0 时,
|
||||
将把当前线程放入信号量的等待队列,设置当前线程为挂起状态并选择新线程执行。
|
||||
|
||||
|
||||
Dijkstra, Edsger W. Cooperating sequential processes (EWD-123) (PDF). E.W. Dijkstra Archive.
|
||||
Center for American History, University of Texas at Austin. (transcription) (September 1965)
|
||||
https://www.cs.utexas.edu/users/EWD/transcriptions/EWD01xx/EWD123.html
|
||||
|
||||
Downey, Allen B. (2016) [2005]. "The Little Book of Semaphores" (2nd ed.). Green Tea Press.
|
||||
|
||||
Leppäjärvi, Jouni (May 11, 2008). "A pragmatic, historically oriented survey on the universality
|
||||
of synchronization primitives" (pdf). University of Oulu, Finland.
|
||||
300
guide/source/chapter8/4condition-variable.rst
Normal file
300
guide/source/chapter8/4condition-variable.rst
Normal file
@@ -0,0 +1,300 @@
|
||||
条件变量机制
|
||||
=========================================
|
||||
|
||||
本节导读
|
||||
-----------------------------------------
|
||||
|
||||
到目前为止,我们已经了解了操作系统提供的互斥锁和信号量。但应用程序在使用这两者时需要非常小心,
|
||||
如果使用不当,就会产生效率低下、竞态条件、死锁或者其他一些不可预测的情况。为了简化编程、避免错误,
|
||||
计算机科学家针对某些情况设计了一种更高层的同步互斥原语。具体而言,在有些情况下,
|
||||
线程需要检查某一条件(condition)满足之后,才会继续执行。
|
||||
|
||||
我们来看一个例子,有两个线程 first 和 second 在运行,线程 first 会把全局变量 A 设置为
|
||||
1,而线程 second 在 ``A != 0`` 的条件满足后,才能继续执行,如下面的伪代码所示:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
static mut A: usize = 0;
|
||||
unsafe fn first() -> ! {
|
||||
A=1;
|
||||
...
|
||||
}
|
||||
|
||||
unsafe fn second() -> ! {
|
||||
while A==0 {
|
||||
// 忙等或睡眠等待 A==1
|
||||
};
|
||||
//继续执行相关事务
|
||||
}
|
||||
|
||||
在上面的例子中,如果线程 second 先执行,会忙等在 while 循环中,在操作系统的调度下,线程
|
||||
first 会执行并把 A 赋值为 1 后,然后线程 second 再次执行时,就会跳出 while 循环,进行接下来的工作。
|
||||
配合互斥锁,可以正确完成上述带条件的同步流程,如下面的伪代码所示:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
static mut A: usize = 0;
|
||||
unsafe fn first() -> ! {
|
||||
mutex.lock();
|
||||
A=1;
|
||||
mutex.unlock();
|
||||
...
|
||||
}
|
||||
|
||||
unsafe fn second() -> ! {
|
||||
mutex.lock();
|
||||
while A==0 {
|
||||
mutex.unlock();
|
||||
// give other thread a chance to lock
|
||||
mutex.lock();
|
||||
};
|
||||
mutex.unlock();
|
||||
//继续执行相关事务
|
||||
}
|
||||
|
||||
这种实现能执行,但效率低下,因为线程 second 会忙等检查,浪费处理器时间。我们希望有某种方式让线程
|
||||
second 休眠,直到等待的条件满足,再继续执行。于是,我们可以写出如下的代码:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
static mut A: usize = 0;
|
||||
unsafe fn first() -> ! {
|
||||
mutex.lock();
|
||||
A=1;
|
||||
wakup(second);
|
||||
mutex.unlock();
|
||||
...
|
||||
}
|
||||
|
||||
unsafe fn second() -> ! {
|
||||
mutex.lock();
|
||||
while A==0 {
|
||||
wait();
|
||||
};
|
||||
mutex.unlock();
|
||||
//继续执行相关事务
|
||||
}
|
||||
|
||||
粗略地看,这样就可以实现睡眠等待了。但请同学仔细想想,当线程 second 在睡眠的时候, ``mutex``
|
||||
是否已经上锁了? 确实,线程 second 是带着上锁的 ``mutex`` 进入等待睡眠状态的。
|
||||
如果这两个线程的调度顺序是先执行线程 second,再执行线程first,那么线程 second 会先睡眠且拥有
|
||||
``mutex`` 的锁;当线程 first 执行时,会由于没有 ``mutex`` 的锁而进入等待锁的睡眠状态。
|
||||
结果就是两个线程都睡了,都执行不下去,这就出现了 **死锁** 。
|
||||
|
||||
这里需要解决的两个关键问题: **如何等待一个条件?** 和 **在条件为真时如何向等待线程发出信号** 。
|
||||
我们的计算机科学家给出了 **管程(Monitor)** 和 **条件变量(Condition Variables)**
|
||||
这种巧妙的方法。接下来,我们就会深入讲解条件变量的设计与实现。
|
||||
|
||||
条件变量的基本思路
|
||||
-------------------------------------------
|
||||
|
||||
管程有一个很重要的特性,即任一时刻只能有一个活跃线程调用管程中的过程,
|
||||
这一特性使线程在调用执行管程中过程时能保证互斥,这样线程就可以放心地访问共享变量。
|
||||
管程是编程语言的组成部分,编译器知道其特殊性,因此可以采用与其他过程调用不同的方法来处理对管程的调用.
|
||||
因为是由编译器而非程序员来生成互斥相关的代码,所以出错的可能性要小。
|
||||
|
||||
管程虽然借助编译器提供了一种实现互斥的简便途径,但这还不够,还需要一种线程间的沟通机制。
|
||||
首先是等待机制:由于线程在调用管程中某个过程时,发现某个条件不满足,那就在无法继续运行而被阻塞。
|
||||
其次是唤醒机制:另外一个线程可以在调用管程的过程中,把某个条件设置为真,并且还需要有一种机制,
|
||||
及时唤醒等待条件为真的阻塞线程。为了避免管程中同时有两个活跃线程,
|
||||
我们需要一定的规则来约定线程发出唤醒操作的行为。目前有三种典型的规则方案:
|
||||
|
||||
- Hoare 语义:线程发出唤醒操作后,马上阻塞自己,让新被唤醒的线程运行。注:此时唤醒线程的执行位置还在管程中。
|
||||
- Hansen 语义:是执行唤醒操作的线程必须立即退出管程,即唤醒操作只可能作为一个管程过程的最后一条语句。
|
||||
注:此时唤醒线程的执行位置离开了管程。
|
||||
- Mesa 语义:唤醒线程在发出行唤醒操作后继续运行,并且只有它退出管程之后,才允许等待的线程开始运行。
|
||||
注:此时唤醒线程的执行位置还在管程中。
|
||||
|
||||
一般开发者会采纳 Brinch Hansen 的建议,因为它在概念上更简单,并且更容易实现。这种沟通机制的具体实现就是
|
||||
**条件变量** 和对应的操作:wait 和 signal。线程使用条件变量来等待一个条件变成真。
|
||||
条件变量其实是一个线程等待队列,当条件不满足时,线程通过执行条件变量的 wait
|
||||
操作就可以把自己加入到等待队列中,睡眠等待(waiting)该条件。另外某个线程,当它改变条件为真后,
|
||||
就可以通过条件变量的 signal 操作来唤醒一个或者多个等待的线程(通过在该条件上发信号),让它们继续执行。
|
||||
|
||||
早期提出的管程是基于 Concurrent Pascal 来设计的,其他语言如 C 和 Rust 等,并没有在语言上支持这种机制。
|
||||
我们还是可以用手动加入互斥锁的方式来代替编译器,就可以在 C 和 Rust 的基础上实现原始的管程机制了。
|
||||
在目前的 C 语言应用开发中,实际上也是这么做的。这样,我们就可以用互斥锁和条件变量,
|
||||
来重现上述的同步互斥例子:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
static mut A: usize = 0;
|
||||
unsafe fn first() -> ! {
|
||||
mutex.lock();
|
||||
A=1;
|
||||
condvar.wakup();
|
||||
mutex.unlock();
|
||||
...
|
||||
}
|
||||
|
||||
unsafe fn second() -> ! {
|
||||
mutex.lock();
|
||||
while A==0 {
|
||||
condvar.wait(mutex); //在睡眠等待之前,需要释放mutex
|
||||
};
|
||||
mutex.unlock();
|
||||
//继续执行相关事务
|
||||
}
|
||||
|
||||
有了上面的介绍,我们就可以实现条件变量的基本逻辑了。下面是条件变量的 wait 和 signal 操作的伪代码:
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
|
||||
fn wait(mutex) {
|
||||
mutex.unlock();
|
||||
<block and enqueue the thread>;
|
||||
mutex.lock();
|
||||
}
|
||||
|
||||
fn signal() {
|
||||
<unblock a thread>;
|
||||
}
|
||||
|
||||
条件变量的wait操作包含三步,1. 释放锁;2. 把自己挂起;3. 被唤醒后,再获取锁。条件变量的 signal
|
||||
操作只包含一步:找到挂在条件变量上睡眠的线程,把它唤醒。
|
||||
|
||||
注意,条件变量不像信号量那样有一个整型计数值的成员变量,所以条件变量也不能像信号量那样有读写计数值的能力。
|
||||
如果一个线程向一个条件变量发送唤醒操作,但是在该条件变量上并没有等待的线程,则唤醒操作实际上什么也没做。
|
||||
|
||||
实现条件变量
|
||||
-------------------------------------------
|
||||
|
||||
使用 condvar 系统调用
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
我们通过例子来看看如何实际使用条件变量。下面是面向应用程序对条件变量系统调用的简单使用,
|
||||
可以看到对它的使用与上一节介绍的信号量系统调用类似。 在这个例子中,主线程先创建了初值为 1
|
||||
的互斥锁和一个条件变量,然后再创建两个线程 First 和 Second。线程 First 会先睡眠 10ms,而当线程
|
||||
Second 执行时,会由于条件不满足执行条件变量的 wait 操作而等待睡眠;当线程 First 醒来后,通过设置
|
||||
A 为 1,让线程 second 等待的条件满足,然后会执行条件变量的 signal 操作,从而能够唤醒线程 Second。
|
||||
这样线程 First 和线程 Second 就形成了一种稳定的同步与互斥关系。
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
:emphasize-lines: 11,19,26,33,36,39
|
||||
|
||||
static mut A: usize = 0; //全局变量
|
||||
|
||||
const CONDVAR_ID: usize = 0;
|
||||
const MUTEX_ID: usize = 0;
|
||||
|
||||
unsafe fn first() -> ! {
|
||||
sleep(10);
|
||||
println!("First work, Change A --> 1 and wakeup Second");
|
||||
mutex_lock(MUTEX_ID);
|
||||
A=1;
|
||||
condvar_signal(CONDVAR_ID);
|
||||
mutex_unlock(MUTEX_ID);
|
||||
...
|
||||
}
|
||||
unsafe fn second() -> ! {
|
||||
println!("Second want to continue,but need to wait A=1");
|
||||
mutex_lock(MUTEX_ID);
|
||||
while A==0 {
|
||||
condvar_wait(CONDVAR_ID, MUTEX_ID);
|
||||
}
|
||||
mutex_unlock(MUTEX_ID);
|
||||
...
|
||||
}
|
||||
pub fn main() -> i32 {
|
||||
// create condvar & mutex
|
||||
assert_eq!(condvar_create() as usize, CONDVAR_ID);
|
||||
assert_eq!(mutex_blocking_create() as usize, MUTEX_ID);
|
||||
// create first, second threads
|
||||
...
|
||||
}
|
||||
|
||||
pub fn condvar_create() -> isize {
|
||||
sys_condvar_create(0)
|
||||
}
|
||||
pub fn condvar_signal(condvar_id: usize) {
|
||||
sys_condvar_signal(condvar_id);
|
||||
}
|
||||
pub fn condvar_wait(condvar_id: usize, mutex_id: usize) {
|
||||
sys_condvar_wait(condvar_id, mutex_id);
|
||||
}
|
||||
|
||||
- 第 26 行,创建了一个 ID 为 ``CONDVAR_ID`` 的条件量,对应第 33 行 ``SYSCALL_CONDVAR_CREATE`` 系统调用;
|
||||
- 第 19 行,线程 Second 执行条件变量 ``wait`` 操作(对应第 39 行 ``SYSCALL_CONDVAR_WAIT`` 系统调用),
|
||||
该线程将释放 ``mutex`` 锁并阻塞;
|
||||
- 第 5 行,线程 First 执行条件变量 ``signal`` 操作(对应第 36 行 ``SYSCALL_CONDVAR_SIGNAL`` 系统调用),
|
||||
会唤醒等待该条件变量的线程 Second。
|
||||
|
||||
|
||||
实现 condvar 系统调用
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
操作系统如何实现条件变量系统调用呢?在线程的眼里,条件变量是一种每个线程能看到的共享资源,
|
||||
且在一个进程中,可以存在多个不同条件变量资源,所以我们可以把所有的条件变量资源放在一起让进程来管理,
|
||||
如下面代码第9行所示。这里需要注意的是: ``condvar_list: Vec<Option<Arc<Condvar>>>``
|
||||
表示的是条件变量资源的列表。而 ``Condvar`` 是条件变量的内核数据结构,由等待队列组成。
|
||||
操作系统需要显式地施加某种控制,来确定当一个线程执行 ``wait`` 操作和 ``signal`` 操作时,
|
||||
如何让线程睡眠或唤醒线程。在这里, ``wait`` 操作是由 ``Condvar`` 的 ``wait`` 方法实现,而 ``signal``
|
||||
操作是由 ``Condvar`` 的 ``signal`` 方法实现。
|
||||
|
||||
.. code-block:: rust
|
||||
:linenos:
|
||||
:emphasize-lines: 9,15,18,27,33
|
||||
|
||||
pub struct ProcessControlBlock {
|
||||
// immutable
|
||||
pub pid: PidHandle,
|
||||
// mutable
|
||||
inner: UPSafeCell<ProcessControlBlockInner>,
|
||||
}
|
||||
pub struct ProcessControlBlockInner {
|
||||
...
|
||||
pub condvar_list: Vec<Option<Arc<Condvar>>>,
|
||||
}
|
||||
pub struct Condvar {
|
||||
pub inner: UPSafeCell<CondvarInner>,
|
||||
}
|
||||
pub struct CondvarInner {
|
||||
pub wait_queue: VecDeque<Arc<TaskControlBlock>>,
|
||||
}
|
||||
impl Condvar {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: unsafe { UPSafeCell::new(
|
||||
CondvarInner {
|
||||
wait_queue: VecDeque::new(),
|
||||
}
|
||||
)},
|
||||
}
|
||||
}
|
||||
pub fn signal(&self) {
|
||||
let mut inner = self.inner.exclusive_access();
|
||||
if let Some(task) = inner.wait_queue.pop_front() {
|
||||
add_task(task);
|
||||
}
|
||||
}
|
||||
pub fn wait(&self, mutex:Arc<dyn Mutex>) {
|
||||
mutex.unlock();
|
||||
let mut inner = self.inner.exclusive_access();
|
||||
inner.wait_queue.push_back(current_task().unwrap());
|
||||
drop(inner);
|
||||
block_current_and_run_next();
|
||||
mutex.lock();
|
||||
}
|
||||
}
|
||||
|
||||
首先是核心数据结构:
|
||||
|
||||
- 第 9 行,进程控制块中管理的条件变量列表。
|
||||
- 第 15 行,条件变量的核心数据成员:等待队列。
|
||||
|
||||
然后是重要的三个成员函数:
|
||||
|
||||
- 第 18 行,创建条件变量,即创建了一个空的等待队列。
|
||||
- 第 27 行,实现 ``signal`` 操作,将从条件变量的等待队列中弹出一个线程放入线程就绪队列。
|
||||
- 第 33 行,实现 ``wait`` 操作,释放 ``mutex`` 互斥锁,将把当前线程放入条件变量的等待队列,
|
||||
设置当前线程为挂起状态并选择新线程执行。在恢复执行后,再加上 ``mutex`` 互斥锁。
|
||||
|
||||
Hansen, Per Brinch (1993). "Monitors and concurrent Pascal: a personal history". HOPL-II:
|
||||
The second ACM SIGPLAN conference on History of programming languages. History of Programming
|
||||
Languages. New York, NY, USA: ACM. pp. 1–35. doi:10.1145/155360.155361. ISBN 0-89791-570-4.
|
||||
132
guide/source/chapter8/5exercise.rst
Normal file
132
guide/source/chapter8/5exercise.rst
Normal file
@@ -0,0 +1,132 @@
|
||||
chapter8 练习
|
||||
=======================================
|
||||
|
||||
编程作业
|
||||
--------------------------------------
|
||||
|
||||
.. warning::
|
||||
|
||||
本次实验框架变动较大,且改动较为复杂,为降低同学们的工作量,本次实验不要求合并之前的实验内容,
|
||||
只需通过 ch8 的全部测例和其他章节的基础测例即可。你可以直接在实验框架的 ch8 分支上完成以下作业。
|
||||
|
||||
.. note::
|
||||
|
||||
本次实验的工作量约为 100 行代码。
|
||||
|
||||
|
||||
死锁检测
|
||||
+++++++++++++++++++++++++++++++
|
||||
|
||||
目前的 mutex 和 semaphore 相关的系统调用不会分析资源的依赖情况,用户程序可能出现死锁。
|
||||
我们希望在系统中加入死锁检测机制,当发现可能发生死锁时拒绝对应的资源获取请求。
|
||||
一种检测死锁的算法如下:
|
||||
|
||||
定义如下三个数据结构:
|
||||
|
||||
- 可利用资源向量 Available :含有 m 个元素的一维数组,每个元素代表可利用的某一类资源的数目,
|
||||
其初值是该类资源的全部可用数目,其值随该类资源的分配和回收而动态地改变。
|
||||
Available[j] = k,表示第 j 类资源的可用数量为 k。
|
||||
- 分配矩阵 Allocation:n * m 矩阵,表示每类资源已分配给每个线程的资源数。
|
||||
Allocation[i,j] = g,则表示线程 i 当前己分得第 j 类资源的数量为 g。
|
||||
- 需求矩阵 Need:n * m 的矩阵,表示每个线程还需要的各类资源数量。
|
||||
Need[i,j] = d,则表示线程 i 还需要第 j 类资源的数量为 d 。
|
||||
|
||||
算法运行过程如下:
|
||||
|
||||
1. 设置两个向量: 工作向量 Work,表示操作系统可提供给线程继续运行所需的各类资源数目,它含有
|
||||
m 个元素。初始时,Work = Available ;结束向量 Finish,表示系统是否有足够的资源分配给线程,
|
||||
使之运行完成。初始时 Finish[0..n-1] = false,表示所有线程都没结束;当有足够资源分配给线程时,
|
||||
设置 Finish[i] = true。
|
||||
2. 从线程集合中找到一个能满足下述条件的线程
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
|
||||
Finish[i] == false;
|
||||
Need[i,j] ≤ Work[j];
|
||||
|
||||
若找到,执行步骤 3,否则执行步骤 4。
|
||||
|
||||
3. 当线程 thr[i] 获得资源后,可顺利执行,直至完成,并释放出分配给它的资源,故应执行:
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
|
||||
Work[j] = Work[j] + Allocation[i, j];
|
||||
Finish[i] = true;
|
||||
|
||||
跳转回步骤2
|
||||
|
||||
4. 如果 Finish[0..n-1] 都为 true,则表示系统处于安全状态;否则表示系统处于不安全状态,即出现死锁。
|
||||
|
||||
出于兼容性和灵活性考虑,我们允许进程按需开启或关闭死锁检测功能。为此我们将实现一个新的系统调用:
|
||||
``sys_enable_deadlock_detect`` 。
|
||||
|
||||
**enable_deadlock_detect**:
|
||||
|
||||
* syscall ID: 469
|
||||
* 功能:为当前进程启用或禁用死锁检测功能。
|
||||
* C 接口: ``int enable_deadlock_detect(int is_enable)``
|
||||
* Rust 接口: ``fn enable_deadlock_detect(is_enable: i32) -> i32``
|
||||
* 参数:
|
||||
* is_enable: 为 1 表示启用死锁检测, 0 表示禁用死锁检测。
|
||||
* 说明:
|
||||
* 开启死锁检测功能后, ``mutex_lock`` 和 ``semaphore_down`` 如果检测到死锁,
|
||||
应拒绝相应操作并返回 -0xDEAD (十六进制值)。
|
||||
* 简便起见可对 mutex 和 semaphore 分别进行检测,无需考虑二者 (以及 ``waittid`` 等)
|
||||
混合使用导致的死锁。
|
||||
* 返回值:如果出现了错误则返回 -1,否则返回 0。
|
||||
* 可能的错误
|
||||
* 参数不合法
|
||||
* 死锁检测开启失败
|
||||
|
||||
|
||||
实验要求
|
||||
+++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
- 完成分支: ch8。
|
||||
- 实验目录要求不变。
|
||||
- 通过所有测例。
|
||||
|
||||
问答作业
|
||||
--------------------------------------------
|
||||
|
||||
1. 在我们的多线程实现中,当主线程 (即 0 号线程) 退出时,视为整个进程退出,
|
||||
此时需要结束该进程管理的所有线程并回收其资源。
|
||||
- 需要回收的资源有哪些?
|
||||
- 其他线程的 TaskControlBlock 可能在哪些位置被引用,分别是否需要回收,为什么?
|
||||
2. 对比以下两种 ``Mutex.unlock`` 的实现,二者有什么区别?这些区别可能会导致什么问题?
|
||||
|
||||
.. code-block:: Rust
|
||||
:linenos:
|
||||
|
||||
impl Mutex for Mutex1 {
|
||||
fn unlock(&self) {
|
||||
let mut mutex_inner = self.inner.exclusive_access();
|
||||
assert!(mutex_inner.locked);
|
||||
mutex_inner.locked = false;
|
||||
if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
|
||||
add_task(waking_task);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mutex for Mutex2 {
|
||||
fn unlock(&self) {
|
||||
let mut mutex_inner = self.inner.exclusive_access();
|
||||
assert!(mutex_inner.locked);
|
||||
if let Some(waking_task) = mutex_inner.wait_queue.pop_front() {
|
||||
add_task(waking_task);
|
||||
} else {
|
||||
mutex_inner.locked = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
报告要求
|
||||
-------------------------------
|
||||
|
||||
- 简单总结你实现的功能(200字以内,不要贴代码)及你完成本次实验所用的时间。
|
||||
- 完成问答题。
|
||||
- (optional) 你对本次实验设计及难度/工作量的看法,以及有哪些需要改进的地方,欢迎畅所欲言。
|
||||
15
guide/source/chapter8/index.rst
Normal file
15
guide/source/chapter8/index.rst
Normal file
@@ -0,0 +1,15 @@
|
||||
第八章:并发
|
||||
==============================================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 4
|
||||
|
||||
0intro
|
||||
1thread-kernel
|
||||
2lock
|
||||
3semaphore
|
||||
4condition-variable
|
||||
5exercise
|
||||
|
||||
.. chyyuu
|
||||
扩展章节,添加其他类型同步互斥的介绍
|
||||
Reference in New Issue
Block a user