Skip to content

Commit 1b7ee94

Browse files
committed
ch8-2 implement blocking mechanism
1 parent 73beab8 commit 1b7ee94

File tree

1 file changed

+37
-2
lines changed

1 file changed

+37
-2
lines changed

source/chapter8/2lock.rst

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -735,7 +735,7 @@ RISC-V 并不原生支持 CAS/TAS 原子指令,但我们可以通过 LR/SC 指
735735
在内核态操作系统支持下实现锁机制
736736
---------------------------------------------------------------------
737737

738-
在编程时,常常遇到必须满足某些条件才能进行接下来的流程的情况。如果一开始这些条件并不成立,那么必须通过某种方式暂时在原地 **等待** 这些条件满足之后才能继续前进。我们可以用多种不同的方式进行等待,最为常见的几种包括:忙等、通过 yield 暂时让权以及后面重点介绍的阻塞。
738+
在编程时,常常遇到必须满足某些条件(或是遇到某些事件)才能进行接下来的流程的情况。如果一开始这些条件并不成立,那么必须通过某种方式暂时在原地 **等待** 这些条件满足之后才能继续前进。我们可以用多种不同的方式进行等待,最为常见的几种包括:忙等、通过 yield 暂时让权以及后面重点介绍的阻塞。
739739

740740
忙等
741741
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -746,7 +746,7 @@ RISC-V 并不原生支持 CAS/TAS 原子指令,但我们可以通过 LR/SC 指
746746

747747
总体上说,若要以忙等方式进行等待,首先要保证忙等是有意义的,不然就只是单纯的在浪费 CPU 资源。怎样才算是有意义的忙等呢?那就是 **在忙等的时候被等待的条件有可能从不满足变为满足** 。比如说,一个线程占据 CPU 资源进行忙等的同时,另一个线程可以在另一个 CPU 上执行,外设也在工作,它们都可以修改内存使得条件得到满足。这种情况才有等待的价值。于是可以知道, **在单核环境下且等待条件不涉及外设的时候,一个线程的忙等是没有意义的** ,因为被等待的条件的状态不可能发生变化。
748748

749-
在忙等有意义的前提下,忙等的优势是在条件成立的第一时间就能够进行响应,对于事件的响应延迟更低,实时性更好。它的缺点则是不可避免的会浪费一部分 CPU 资源在忙等上。因此,如果我们能够预测到条件将很快得到满足,在这种情况下使用忙等是一个好主意。如果条件成立的时间无法预测或者所需时间比较长,那还是及时交出 CPU 资源更好。
749+
在忙等有意义的前提下,忙等的优势是在条件成立的第一时间就能够进行响应,对于事件的响应延迟更低,实时性更好,而且不涉及开销很大的上下文切换。它的缺点则是不可避免的会浪费一部分 CPU 资源在忙等上。因此,如果我们能够预测到条件将很快得到满足,在这种情况下使用忙等是一个好主意。如果条件成立的时间无法预测或者所需时间比较长,那还是及时交出 CPU 资源更好。
750750

751751
通过 yield 暂时让权
752752
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -815,6 +815,41 @@ RISC-V 并不原生支持 CAS/TAS 原子指令,但我们可以通过 LR/SC 指
815815
阻塞
816816
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
817817

818+
.. _term-blocking:
819+
820+
在操作系统的协助下,我们可以对于等待进行更加精细的控制。为了避免等待事件的线程在事件到来之前被调度到而产生大量上下文切换开销,我们可以新增一种 **阻塞** (Blocking) 机制。当线程需要等待事件到来的时候,操作系统可以将该线程标记为阻塞状态 (Blocked) 并将其从调度器的就绪队列中移除。由于操作系统每次只会从就绪队列中选择一个线程分配 CPU 资源,被阻塞的线程就不再会获得 CPU 使用权,也就避免了上下文切换。相对的,在线程要等待的事件到来之后,我们需要解除线程的阻塞状态,将线程状态改成就绪状态,并将线程重新加入到就绪队列,使其有资格得到 CPU 资源。这就是与阻塞机制配套的唤醒机制。在线程被唤醒之后,由于它所等待的事件已经出现,在操作系统调度到它之后它就可以继续向下运行了。
821+
822+
阻塞与唤醒机制相配合就可以实现精确且高效的等待。阻塞机制保证在线程等待的事件到来之前,线程不会参与调度,因此不会浪费任何时间片或产生上下文切换。唤醒机制则在事件到来之后允许线程正常继续执行。注意到,操作系统能够感知到事件以及等待该事件的线程,因此根据事件的实时性要求以及线程上任务的重要程度,操作系统可以在对于调度策略进行调整。比如,当事件为键盘或鼠标输入时,操作系统可以在唤醒之后将对应线程的优先级调高,让其能够被尽量早的调度到,这样就能够降低响应延迟并提升用户体验。也就是说,相比 yield ,这种做法的可控性更好。
823+
824+
阻塞机制的缺点在于会不可避免的产生两次上下文切换。站在等待的线程的视角,它会被切换出去再切换回来然后再继续执行。在事件产生频率较低、事件到来速度比较慢的情况下这不是问题,但当事件产生频率很高的时候直接忙等也许是更好的选择。此外,阻塞机制相对比较复杂,需要操作系统的支持。
825+
826+
下面介绍我们的操作系统如何实现阻塞机制以及阻塞机制的若干应用。
827+
828+
实现阻塞机制
829+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
830+
831+
在 ``suspend/exit_current_and_run_next`` 之外,我们新增第三种任务管理接口 ``block_current_and_run_next`` :
832+
833+
.. code-block:: rust
834+
:linenos:
835+
836+
// os/src/task/mod.rs
837+
838+
pub fn block_current_and_run_next() {
839+
let task = take_current_task().unwrap();
840+
let mut task_inner = task.inner_exclusive_access();
841+
let task_cx_ptr = &mut task_inner.task_cx as *mut TaskContext;
842+
task_inner.task_status = TaskStatus::Blocked;
843+
drop(task_inner);
844+
schedule(task_cx_ptr);
845+
}
846+
847+
基于阻塞机制实现 sleep 系统调用
848+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
849+
850+
基于阻塞机制实现锁机制
851+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
852+
818853
参考文献
819854
----------------------------------------------------------------------
820855

0 commit comments

Comments
 (0)