diff --git a/docs/labs/0x04/tasks.md b/docs/labs/0x04/tasks.md index 9f09343..5c803b0 100644 --- a/docs/labs/0x04/tasks.md +++ b/docs/labs/0x04/tasks.md @@ -838,7 +838,7 @@ Syscall::WaitPid => { /* ... */}, 但是`waitpid` 需要返回特殊状态,以区分进程正在运行还是已经退出。这非常糟糕,当前进程的返回值也是一个 `isize` 类型的值,这意味着如果按照现在的设计,势必存在一些返回值和“正在运行”的状态冲突。不过在本次实验中,这并不会造成太大的问题。 - 此问题的更好解决方案将留作 Lab 5 的加分项供大家探索。 + 此问题的更好解决方案将在 Lab 5 中进行讨论。 ## 运行 Shell diff --git a/docs/labs/0x05/tasks.md b/docs/labs/0x05/tasks.md index 895debe..81b4937 100644 --- a/docs/labs/0x05/tasks.md +++ b/docs/labs/0x05/tasks.md @@ -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` 的键值对应于被等待的进程 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> = 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) { + 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)**。 @@ -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!(), } @@ -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` 类型,可以参考如下声明: @@ -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 及其依赖的的源码,了解其实现方式,并进行一些描述、记录和学习。" diff --git a/mkdocs.yml b/mkdocs.yml index 2431c0e..1f1734f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -58,7 +58,7 @@ nav: - 实验四:用户程序与系统调用: - labs/0x04/index.md - 实验任务: labs/0x04/tasks.md - - 实验五:fork 与并发: + - 实验五:fork、阻塞与并发: - labs/0x05/index.md - 实验任务: labs/0x05/tasks.md - 实验六:硬盘驱动与文件系统: