Skip to content

Commit

Permalink
feat(lab/5): add docs for block & wake_up
Browse files Browse the repository at this point in the history
  • Loading branch information
GZTimeWalker committed May 23, 2024
1 parent 5a1b458 commit 52b9fdd
Show file tree
Hide file tree
Showing 3 changed files with 155 additions and 7 deletions.
2 changes: 1 addition & 1 deletion docs/labs/0x04/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -838,7 +838,7 @@ Syscall::WaitPid => { /* ... */},

但是`waitpid` 需要返回特殊状态,以区分进程正在运行还是已经退出。这非常糟糕,当前进程的返回值也是一个 `isize` 类型的值,这意味着如果按照现在的设计,势必存在一些返回值和“正在运行”的状态冲突。不过在本次实验中,这并不会造成太大的问题。

此问题的更好解决方案将留作 Lab 5 的加分项供大家探索
此问题的更好解决方案将在 Lab 5 中进行讨论

## 运行 Shell

Expand Down
158 changes: 153 additions & 5 deletions docs/labs/0x05/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,135 @@ fn main() -> isize {
entry!(main);
```

## 进程的阻塞与唤醒

进程的阻塞与唤醒是非常重要的功能,它可以用于控制进程的执行顺序、资源的分配、进程的同步等。在先前的实现中,已经实现了 `wait_pid` 系统调用,它通过轮询的方式来等待一个进程的退出,并返回其退出状态。

相对而言,轮询会消耗大量的 CPU 时间,因此需要一种更为高效的方式来进行进程的阻塞与唤醒:**能否让进程在等待某个事件发生时进入阻塞状态,而在事件发生时唤醒进程呢?**

在本实验的设计中,对于 `wait_pid` 系统调用,进程在发出调用后,会进入一个**等待队列**,当被等待的进程退出时,会唤醒等待队列中的进程。

### 等待队列

`pkg/kernel/src/proc/manager.rs` 中,修改 `ProcessManager` 并添加等待队列:

```rust
pub struct ProcessManager {
// ...
wait_queue: Mutex<BTreeMap<ProcessId, BTreeSet<ProcessId>>>,
}
```

其中,`BTreeMap` 的键值对应于被等待的进程 ID,`BTreeSet` 用于存储等待的进程 ID 的集合。

### 阻塞进程

`ProcessManager` 添加 `block` 函数,用于将进程设置为阻塞状态:

```rust
/// Block the process with the given pid
pub fn block(&self, pid: ProcessId) {
if let Some(proc) = self.get_proc(&pid) {
// FIXME: set the process as blocked
}
}
```

!!! tip "设置进程的私有字段"

`ProcessInner` 中的 `status` 字段是私有的,对于一系列的状态变更,你可以添加相应的函数来进行设置。

例如,你可以添加 `block` 函数,来设置进程的状态:

```rust
pub fn block(&mut self) {
self.status = ProcessStatus::Blocked;
}
```

`pkg/kernel/src/proc/mod.rs` 中,修改 `wait_pid` 系统调用的实现,添加 `ProcessContext` 参数来确保可以进行可能切换上下文操作(意味着当前进程被阻塞,需要切换到下一个进程):

```rust
pub fn wait_pid(pid: ProcessId, context: &mut ProcessContext) {
x86_64::instructions::interrupts::without_interrupts(|| {
let manager = get_process_manager();
if let Some(ret) = manager.get_exit_code(pid) {
context.set_rax(ret as usize);
} else {
manager.wait_pid(pid);
manager.save_current(context);
manager.current().write().block();
manager.switch_next(context);
}
})
}
```

同时为 `ProcessManager` 添加 `wait_pid` 函数:

```rust
pub fn wait_pid(&self, pid: ProcessId) {
let mut wait_queue = self.wait_queue.lock();
// FIXME: push the current process to the wait queue
// `processor::current_pid()` is waiting for `pid`
}
```

!!! tip "使用 `BTreeMap``entry` 方法"

`BTreeMap` 的 `entry` 方法可以用于获取一个键对应的值的可变引用,并可以通过 `or_default` 或者 `or_insert` 来插入一个默认值。

```rust
let mut map: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
let entry = map.entry(42).or_default();
entry.insert(2333);
```

### 唤醒进程

在阻塞进程后,还需要对进程进行唤醒。对于本处的 `wait_pid` 系统调用,当被等待的进程退出时,需要唤醒等待队列中的进程。

首先,为 `ProcessManager` 添加 `wake_up` 函数:

```rust
/// Wake up the process with the given pid
///
/// If `ret` is `Some`, set the return value of the process
pub fn wake_up(&self, pid: ProcessId, ret: Option<isize>) {
if let Some(proc) = self.get_proc(&pid) {
let mut inner = proc.write();
if let Some(ret) = ret {
// FIXME: set the return value of the process
// like `context.set_rax(ret as usize)`
}
// FIXME: set the process as ready
// FIXME: push to ready queue
}
}
```

在进程退出时,也即 `kill` 系统调用中,需要唤醒等待队列中的进程。修改 `ProcessManager` 中的 `kill` 函数:

```rust
pub fn kill(pid: ProcessId, ret: isize) {
// ...

if let Some(pids) = self.wait_queue.lock().remove(&pid) {
for pid in pids {
self.wake_up(pid, Some(ret));
}
}
}
```

这样,就实现了一个无需轮询的进程阻塞与唤醒机制。

!!! success "阶段性成果"

尝试在你的 Shell 中启动另一个 Shell,然后在其中利用 `ps` 打印进程信息:

**前一个 Shell 应当被阻塞 (Blocked),直到后一个 Shell 退出。**

## 并发与锁机制

由于并发执行时,线程的调度顺序无法预知,进而造成的执行顺序不确定,**持有共享资源的进程之间的并发执行可能会导致数据的不一致**,最终导致相同的程序产生一系列不同的结果,这样的情况被称之为**竞态条件(race condition)**
Expand Down Expand Up @@ -602,7 +731,7 @@ pub fn sem_wait(key: u32, context: &mut ProcessContext) {
SemaphoreResult::NotExist => context.set_rax(1),
SemaphoreResult::Block(pid) => {
// FIXME: save, block it, then switch to next
// maybe use `save_current` and `switch_next`
// use `save_current` and `switch_next`
}
_ => unreachable!(),
}
Expand All @@ -624,6 +753,12 @@ pub fn sys_sem(args: &SyscallArgs, context: &mut ProcessContext) {
}
```

!!! tip "进程的唤醒"

与 `wait_pid` 系统调用类似,你需要在 `sem_signal` 中对进程进行唤醒。

但是无需为进程设置返回值,因此在调用 `wake_up` 时,传入 `None` 即可。

!!! tip "完善用户库"

完善 `pkg/lib/src/sync.rs` 中有关信号量的操作,使用不同的 `op` 参数来进行信号量的用户态函数的分配,系统调用宏需要将参数转换为 `usize` 类型,可以参考如下声明:
Expand Down Expand Up @@ -805,13 +940,26 @@ pub fn sys_sem(args: &SyscallArgs, context: &mut ProcessContext) {

## 加分项

1. 🤔 参考信号量相关系统调用的实现,尝试修改 `waitpid` 系统调用,在进程等待另一个进程退出时进行阻塞,并在目标进程退出后携带返回值唤醒进程。

2. 🤔 尝试实现如下用户程序任务,完成用户程序 `fish`
1. 🤔 尝试实现如下用户程序任务,完成用户程序 `fish`

- 创建三个子进程,让它们分别能输出且只能输出 `>``<``_`
- 使用学到的方法对这些子进程进行同步,使得打印出的序列总是 `<><_``><>_` 的组合。

在完成这一任务的基础上,其他细节可以自行决定如何实现,包括输出长度等。

3. 🤔 尝试和前文不同的其他方法解决哲学家就餐问题,并验证你的方法能够正确解决它,简要介绍你的方法,并给出程序代码和测试结果。
2. 🤔 尝试和前文不同的其他方法解决哲学家就餐问题,并验证你的方法能够正确解决它,简要介绍你的方法,并给出程序代码和测试结果。

3. 🔥 尝试使用符合 Rust 做法的方式处理互斥锁,使用 RAII 的方式来保证锁的释放:

RAII(Resource Acquisition Is Initialization)是一种资源获取即初始化的技术,它通过在对象的构造函数中获取资源,然后在析构函数中释放资源,来保证资源的正确释放。

对于 Rust,也即实现 `MutexGuard` 类似的结构,它在构造时获取锁,然后在此结构体被移出作用域时释放锁。

-`acquire` 时候返回 `MutexGuard` 对象。
- 移除 `release` 函数,使用 `MutexGuard``Drop` trait 来释放锁。

!!! danger "本项实现难度较大,不建议初学者尝试。"

本加分项涉及到生命周期、unsafe 构造、`mem::forget`、`Deref` 等内容,需要对 Rust 的底层实现有一定的了解。

!!! note "作为本加分项的备选方案,可以尝试查看 `spin` crate 及其依赖的的源码,了解其实现方式,并进行一些描述、记录和学习。"
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ nav:
- 实验四:用户程序与系统调用:
- labs/0x04/index.md
- 实验任务: labs/0x04/tasks.md
- 实验五:fork 与并发:
- 实验五:fork、阻塞与并发:
- labs/0x05/index.md
- 实验任务: labs/0x05/tasks.md
- 实验六:硬盘驱动与文件系统:
Expand Down

0 comments on commit 52b9fdd

Please sign in to comment.