diff --git a/docs/labs/0x00/tasks.md b/docs/labs/0x00/tasks.md index b3c4f15..e9ca4c9 100644 --- a/docs/labs/0x00/tasks.md +++ b/docs/labs/0x00/tasks.md @@ -14,9 +14,9 @@ 我们推荐在以下环境进行实验: -- Windows 10/11 -- Ubuntu 22.04 LTS -- Ubuntu 22.04 LTS on WSL 2 +- Ubuntu 22.04 LTS (jammy) on WSL 2 **(推荐 Windows 用户选择)** +- Windows 10/11 **(Windows 原生备选,GDB 相关功能无法使用)** +- Ubuntu 22.04 LTS (jammy) - macOS with Apple Silicon 以上环境经过我们的测试和验证,可以正常进行实验。对于其他常用的 Linux 发行版,通常也可以正常进行实验,但我们不提供技术支持。 @@ -27,15 +27,16 @@ 本实验在 Windows 上进行项目开发是**完全可行的**,但是我们提供的各种工具的选项可能有所出入。 - 在 Windows 平台上我们建议通过 VSCode + Python + CodeLLDB 插件进行开发、调试。 + 在 Windows + WSL 2 平台上,建议使用 VSCode (Remote WSL) 连接到 WSL2 进行开发、调试。 在 Linux 平台上我们建议通过 VSCode (Remote) + Python / make + GDB 结合 gef 进行开发、调试。 + 在 Windows 平台上我们建议通过 VSCode + Python + CodeLLDB 插件进行开发、调试。 + +- 对于选择使用 Linux 的同学,请参考 [Linux 环境配置](../../wiki/linux.md) 进行配置,**文档包含 Linux 相关安装指南**。 - 对于选择使用 Windows 的同学,请参考 [Windows 环境配置](../../wiki/windows.md) 进行配置。 -- 对于选择使用 Linux 的同学,请参考 [Linux 环境配置](../../wiki/linux.md) 进行配置。 - - 对于选择使用 macOS 的同学,请安装 `brew` 和相应工具,参考 [Linux 环境配置](../../wiki/linux.md) 进行配置。 ## 尝试使用 Rust 进行编程 @@ -166,7 +167,7 @@ 使得每次调用 `UniqueId::new()` 时总会得到一个新的不重复的 `UniqueId`。 - 你可以在函数体中定义 `static` 变量来存储一些全局状态 - - 你可以尝试使用 `std::sync::atomic::AtomicU16` 来确保多线程下的正确性(无需进行验证) + - 你可以尝试使用 `std::sync::atomic::AtomicU16` 来确保多线程下的正确性(无需进行验证,相关原理将在 Lab 5 介绍,此处不做要求) - 使得你的实现能够通过如下测试: ```rust diff --git a/docs/labs/0x01/tasks.md b/docs/labs/0x01/tasks.md index 194e21d..b5d2ebb 100644 --- a/docs/labs/0x01/tasks.md +++ b/docs/labs/0x01/tasks.md @@ -506,11 +506,7 @@ println!("{}", record.args()); 2. 😋 尝试在进入内核并初始化串口驱动后,使用 escape sequence 来清屏,并编辑 `get_ascii_header()` 中的字符串常量,输出你的学号信息。 -3. 🤔 尝试修改 `logger` 的初始化函数,使得日志等级能够根据编译时的环境变量 `LOG_LEVEL` 来决定编译产物的日志等级。 - - !!! note "提示" - - 你可以使用 `match option_env!("LOG_LEVEL")` 来判断环境变量的值,它将会在编译时被替换为环境变量的值。 +3. 🤔 尝试添加字符串型启动配置变量 `log_level`,并修改 `logger` 的初始化函数,使得内核能够根据启动参数进行日志输出。 3. 🤔 尝试使用调试器,在内核初始化之后中断,查看、记录并解释如下的信息: diff --git a/docs/labs/0x02/tasks.md b/docs/labs/0x02/tasks.md index 46bc016..e5dd119 100644 --- a/docs/labs/0x02/tasks.md +++ b/docs/labs/0x02/tasks.md @@ -56,6 +56,8 @@ lazy_static! { 你需要参考上下文,在 `src/memory/gdt.rs` 中补全 TSS 的中断栈表,为 Double Fault 和 Page Fault 准备独立的栈。 +!!! warning "仅修改 `interrupt_stack_table`,不要修改用作示例的 `privilege_stack_table`" + ## 注册中断处理程序 !!! note "请阅读 [CPU 中断处理](../../wiki/interrupts.md) 部分,学习中断基本知识。" @@ -551,7 +553,7 @@ static int init_serial() { } #[inline] - pub fn try_get_key() -> Option { + pub fn try_pop_key() -> Option { INPUT_BUF.pop() } ``` diff --git a/docs/labs/0x03/tasks.md b/docs/labs/0x03/tasks.md index 1b9fad8..0eef72e 100644 --- a/docs/labs/0x03/tasks.md +++ b/docs/labs/0x03/tasks.md @@ -122,6 +122,11 @@ as_handler!(teapot); 对于参数部分,`ProcessContext` 的简单声明如下: ```rust +#[repr(C)] +pub struct ProcessContext { + value: ProcessContextValue, +} + #[repr(C)] pub struct ProcessContextValue { pub regs: RegistersValue, @@ -129,7 +134,7 @@ pub struct ProcessContextValue { } ``` -这里的 `ProcessContextValue` 命名和相关的保护处理方法来自 `InterruptStackFrame` 的内部实现,用以防止意外的修改及其导致的非预期行为。 +`ProcessContext` 实现了内部 `value` 的 `Deref` trait,因此可以直接使用 `ProcessContextValue` 中的字段内容。而 `ProcessContextValue` 相关的保护处理方法参考并实现自 `InterruptStackFrame` 的内部实现,用以防止意外的修改及其导致的非预期行为。 `repr(C)` 用于指定使用 C 语言的结构体布局,以便于在汇编代码中正确处理结构体的字段。 @@ -292,7 +297,7 @@ pub fn new(init: Arc) -> Self { 内核栈的起始地址通过配置文件被定义在了 `0xFFFFFF0100000000`,距离内核起始地址 4GiB。默认大小为 512 个 4KiB 的页面,即 2MiB。 - 在虚拟内存的规划中,任意进程的栈地址空间大小为 4GiB。以内核为例,内核栈的起始地址为 `0xFFFFFF0100000000`,结束地址为 `0xFFFFFF0200000000`。 + 在虚拟内存的规划中,任意进程的栈地址空间大小为 4GiB。以内核为例,内核栈所对应的内存区域的起始地址为 `0xFFFFFF0100000000`,结束地址为 `0xFFFFFF0200000000`。 !!! note "缺页异常?" @@ -366,7 +371,7 @@ pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; +---------------------+ ``` -!!! tip "**以 PID 2 为例:
初始化分配的栈的页面为 `0x3FFEFFFFF000` 到 `0x3FFF00000000`
默认栈顶地址为 `0x3FFEFFFFFFF8`**" +!!! tip "**以 PID 2 为例:
初始化分配的栈的页面为 `0x3FFEFFFFF000` 到 `0x3FFF00000000`(区间左闭右开)
默认栈顶地址为 `0x3FFEFFFFFFF8`**" 有关于用户进程的其他部分内存布局的说明,将在下一次实验中详细讨论。 @@ -503,7 +508,7 @@ pub const STACK_INIT_TOP: u64 = STACK_MAX - 8; ```rust pub fn new_test_thread(id: &str) -> ProcessId { - let proc_data = ProcessData::new(); + let mut proc_data = ProcessData::new(); proc_data.set_env("id", id); crate::proc::spawn_kernel_thread( diff --git a/docs/labs/0x04/tasks.md b/docs/labs/0x04/tasks.md index 7d39616..6ecd20e 100644 --- a/docs/labs/0x04/tasks.md +++ b/docs/labs/0x04/tasks.md @@ -778,7 +778,7 @@ pub fn exit(ret: isize, context: &mut ProcessContext) { pub fn wait(init: proc::ProcessId) { loop { if proc::still_alive(init) { - x86_64::instructions::hlt(); + x86_64::instructions::hlt(); // Why? Check reflection question 5 } else { break; } @@ -797,11 +797,13 @@ pub fn still_alive(pid: ProcessId) -> bool { } ``` -对于具体的进程操作、目录操作等功能,将会移步到用户态程序进行实现。为了给予用户态程序操作进程、等待进程退出的能力,这里还缺少最后两个系统调用需要实现:`spawn` 和 `waitpid`,对这两个系统调用有如下约定: +对于具体的进程操作、目录操作等功能,将会移步到用户态程序进行实现。为了给予用户态程序操作进程、等待进程退出的能力,这里还缺少最后几个系统调用需要实现:`spawn`、`getpid` 和 `waitpid`,对这些系统调用有如下约定: ```rust // path: &str (arg0 as *const u8, arg1 as len) -> pid: u16 Syscall::Spawn => { /* ... */ }, +// None -> pid: u16 +Syscall::GetPid => { /* ... */ }, // pid: arg0 as u16 -> status: isize Syscall::WaitPid => { /* ... */}, ``` @@ -810,15 +812,13 @@ Syscall::WaitPid => { /* ... */}, > 你可以自由定义你的内核与用户态的交互方式了! -!!! note "关于 `WaitPid` 的问题" +!!! note "关于 `waitpid` 的问题" - 你可能会发现,`WaitPid` 需要返回特殊状态,以区分进程正在运行还是已经退出。 + 在这里的简单实现下,`waitpid` 不进行阻塞,应当立刻返回进程的当前状态。从而本次实验中,在用户态使用忙等待的方式判断进程是否退出。 - 不过非常糟糕,当前进程的返回值也是一个 `isize` 类型的值,这意味着如果按照现在的设计,势必存在一些返回值和“正在运行”的状态冲突。 + 但是`waitpid` 需要返回特殊状态,以区分进程正在运行还是已经退出。这非常糟糕,当前进程的返回值也是一个 `isize` 类型的值,这意味着如果按照现在的设计,势必存在一些返回值和“正在运行”的状态冲突。不过在本次实验中,这并不会造成太大的问题。 - 不过在简单的实验中,这并不会造成太大的问题,而此问题的解决方案也留作加分项供大家研究。 - - **另请注意,`WaitPid` 虽然名为 `wait` 但是并不会进行阻塞,应当立刻返回进程的当前状态。** + 此问题的更好解决方案将留作 Lab 5 的加分项供大家探索。 ## 运行 Shell @@ -923,19 +923,33 @@ The factorial of 999999 under modulo 1000000007 is 128233642. 从本次实验及先前实验的所学内容出发,结合进程的创建、链接、执行、退出的生命周期,参考系统调用的调用过程(可以仅以 Linux 为例),解释程序的运行。 +5. `x86_64::instructions::hlt` 做了什么?为什么这样使用? + +6. 有同学在某个回南天迷蒙的深夜遇到了奇怪的问题: + + 只有当进行用户输入(触发了串口输入中断)的时候,会触发奇怪的 Page Fault,然而进程切换、内存分配甚至 `fork` 等系统调用都很正常。 + + **经过近三个小时的排查,发现他将 TSS 中的 `privilege_stack_table` 相关设置注释掉了。** + + 请查阅资料,了解特权级栈的作用,实验说明这一系列中断的触发过程,尝试解释这个现象。 + + - 可以使用 `intdbg` 参数,或 `ysos.py -i` 进行数据捕获。 + - 留意包含 `0x80` 系统调用、`0x0e` 缺页异常的某三次中断的信息。 + - 注意到一个不应当存在的地址……? + + 或许你可以重新复习一下 Lab 2 的相关内容:[double-fault-exceptions](https://os.phil-opp.com/double-fault-exceptions/) + ## 加分项 1. 😋 尝试在 `ProcessData` 中记录代码段的占用情况,并统计当前进程所占用的页面数量,并在打印进程信息时,将进程的内存占用打印出来。 2. 😋 尝试在 `kernel/src/memory/frames.rs` 中实现帧分配器的回收功能 `FrameDeallocator`,作为一个最小化的实现,你可以在 `Allocator` 使用一个 `Vec` 存储被释放的页面,并在分配时从中取出。 -3. 🤔 基于帧回收器的实现,在 `elf` 中实现 `unmap_range` 函数,从页表中取消映射一段连续的页面,并使用帧回收器进行回收。利用它实现进程栈的回收(利用 `ProcessData` 中存储的页面信息)。**页表的回收将会在后续实现用实现,暂时不需要处理** - -4. 🤔 改进或重新设计进程返回值的相关内容,给予 `WaitPid` 更好的兼容性,描述你的设计和实现。 +3. 🤔 基于帧回收器的实现,在 `elf` 中实现 `unmap_range` 函数,从页表中取消映射一段连续的页面,并使用帧回收器进行回收。利用它实现进程栈的回收(利用 `ProcessData` 中存储的页面信息),页表的回收将会在后续实现用实现,暂时不需要处理。 -5. 🤔 尝试利用 `UefiRuntime` 和 `chrono` crate,获取当前时间,并将其暴露给用户态,以实现 `sleep` 函数。 +4. 🤔 尝试利用 `UefiRuntime` 和 `chrono` crate,获取当前时间,并将其暴露给用户态,以实现 `sleep` 函数。 - `UefiRuntime` 的实现: + `UefiRuntime` 的实现,它可能需要使用锁进行保护: ```rust pub struct UefiRuntime { @@ -955,7 +969,7 @@ The factorial of 999999 under modulo 1000000007 is 128233642. } ``` - 一个可能的 `sleep` 函数实现: + 这里提供一个可能的 `sleep` 函数实现: ```rust pub fn sleep(millisecs: i64) { @@ -968,6 +982,10 @@ The factorial of 999999 under modulo 1000000007 is 128233642. } ``` - 在实现后,写一个或更改现有用户程序,验证你的实现是否正确,尝试输出当前时间并等待一段时间。 + > 当前实现是纯用户态、采用轮询的,这种实现是很低效的。 + > + > 在现代操作系统中,进程会被挂起,并等待对应事件触发后重新被调度。 + > + > 虽然不是最好,但是在目前的需求下,这已经足够了。 - > 当前实现是纯用户态、采用轮询的,这种实现是很低效的。在现代操作系统中,进程会被挂起,并等待对应事件触发后重新被调度。 + 在实现后,写一个或更改现有用户程序,验证你的实现是否正确,尝试输出当前时间并使用 `sleep` 函数进行等待。 diff --git a/docs/labs/0x05/index.md b/docs/labs/0x05/index.md index 367799f..fa69ba8 100644 --- a/docs/labs/0x05/index.md +++ b/docs/labs/0x05/index.md @@ -20,9 +20,6 @@ 鼓励使用 Typst 来进行实验文档的编写,使用可以参考 [使用 Typst 编写报告](../../general/typst.md)。 -对于本次实验内容,你需要参考学习如下实验资料: - - ## 实验任务与要求 1. 请各位同学独立完成作业,任何抄袭行为都将使本次作业判为 0 分。 diff --git a/docs/labs/0x05/tasks.md b/docs/labs/0x05/tasks.md index 26f5406..453638b 100644 --- a/docs/labs/0x05/tasks.md +++ b/docs/labs/0x05/tasks.md @@ -1,5 +1,758 @@ # 实验五:fork 的实现、并发与锁机制 +!!! danger "在执行每一条命令前,请你对将要进行的操作进行思考" + + **为了你的数据安全和不必要的麻烦,请谨慎使用 `sudo`,并确保你了解每一条指令的含义。** + + **1. 实验文档给出的命令不需要全部执行** + + **2. 不是所有的命令都可以无条件执行** + + **3. 不要直接复制粘贴命令执行** + +## fork 的实现 + +在操作系统设计中,进程的控制除了创建、终止等基本操作之外,还包括了进程的**复制**。 + +这种复制的操作可以用于创建**子进程**,被称为 `fork`,它可以使得用户进程具有控制多个进程的能力,从而实现并发执行。 + +YSOS 的 `fork` 系统调用设计如下描述: + +!!! note "出于实验设计考量:
本实现与 Linux 或 [POSIX](https://pubs.opengroup.org/onlinepubs/9699919799/) 中所定义的 `fork` 有所不同,也结合了 Linux 中 `vfork` 的行为。" + +- `fork` 会创建一个新的进程,新进程称为子进程,原进程称为父进程。 +- 子进程在系统调用后将得到 `0` 的返回值,而父进程将得到子进程的 PID。如果创建失败,父进程将得到 `-1` 的返回值。 +- `fork` **不复制**父进程的内存空间,**不实现** Cow (Copy on Write) 机制,即父子进程将持有一定的共享内存:代码段、数据段、堆、bss 段等。 +- `fork` 子进程与父进程共享内存空间(页表),但**子进程拥有自己独立的寄存器和栈空间(在一个不同的栈的地址继承原来的数据)。** +- **由于上述内存分配机制的限制,`fork` 系统调用必须在任何 Rust 内存分配(堆内存分配)之前进行。** + +为了实现父子进程的资源共享,在先前的实验中,已经做了一些准备工作: + +比如 `pkg/kernel/src/proc/paging.rs` 中,`PageTableContext` 中的 `Cr3RegValue` 被 `Arc` 保护了起来;在 `pkg/kernel/src/proc/data.rs` 中,也存在 `Arc` 包装的共享数据的内容。 + +??? note "忘了 `Arc` 是什么?" + + `Arc` 是 `alloc::sync` 中的一个原子引用计数智能指针,它允许多个线程同时拥有对同一数据的所有权,且不会造成数据竞争。 + + `Arc` 的 `clone()` 方法会增加引用计数,`drop()` 方法会减少引用计数,当引用计数为 0 时,数据会被释放。`Arc` 本身是**不可变的**,但可以通过 `RwLock` 获取内部可变性,进而安全的修改一个被多个线程所持有的数据。 + +对于 Windows 等将进程抽象为资源容器的操作系统,这些需要共享的资源也就会被抽象为**进程对象**。在这种情况下,实验所设计的行为又更类似于 “新建一个执行线程” 的操作。 + +### 系统调用 + +有了上一次实验的经验,系统调用的新增、处理均已经有了一定的经验,此处不过多赘述。对 `fork` 系统调用有如下约定,别忘了在 `syscall_def` 中定义你的系统调用号: + +```rust +// None -> pid: u16 or 0 or -1 +Syscall::Fork => { /* ... */}, +``` + +!!! tip "如果你和笔者一样有强迫症,Linux 相关功能的系统调用号是 `58`" + +### 进程管理 + +!!! warning "关于具体的实现" + + 实验至此,你也应当积累了一些自己的项目管理经验,对于上述的 `FIXME`,你应当有一些自己的想法,用合适的方式进行实现。 + + 后续的完善将会给出一些提示、建议和注意事项,相关代码结构并不需要**完全按照文档进行**。 + + **请注意:每个 `FIXME` 并不代表此功能必须在对应的位置实现,你也应当自由管理相关函数的返回值、参数等。** + +在处理好用户态库和系统调用的对接后,参考如下代码,完善你的 `fork`: + +!!! note "往下翻翻,说明更多哦(为什么总有人不看完文档就开始写代码!)" + +```rust +pub fn fork(context: &mut ProcessContext) { + x86_64::instructions::interrupts::without_interrupts(|| { + let manager = get_process_manager(); + // FIXME: save_current as parent + // FIXME: fork to get child + // FIXME: push to child & parent to ready queue + // FIXME: switch to next process + }) +} +``` + +```rust +impl ProcessManager { + pub fn fork(&self) { + // FIXME: get current process + // FIXME: fork to get child + // FIXME: add child to process list + + // FOR DBG: maybe print the process ready queue? + } +} +``` + +```rust +impl Process { + pub fn fork(self: &Arc) -> Arc { + // FIXME: lock inner as write + // FIXME: inner fork with parent weak ref + + // FOR DBG: maybe print the child process info + // e.g. parent, name, pid, etc. + + // FIXME: make the arc of child + // FIXME: add child to current process's children list + // FIXME: set fork ret value for parent + // FIXME: mark the child as ready & return it + } +} +``` + +```rust +pub struct ProcessInner { + // ... + parent: Option>, + children: Vec>, + // ... +} + +impl ProcessInner { + pub fn fork(&mut self, parent: Weak) -> ProcessInner { + // FIXME: get current process's stack info + + // FIXME: clone the process data struct + // FIXME: clone the page table context (see instructions) + + // FIXME: alloc & map new stack for child (see instructions) + // FIXME: copy the *entire stack* from parent to child + + // FIXME: update child's stack frame(context) with new *stack pointer* + // > keep lower bits of rsp, update the higher bits + // > also update the stack record in process data + // FIXME: set the return value 0 for child with `context.set_rax` + + // FIXME: construct the child process inner + + // NOTE: return inner because there's no pid record in inner + } +} +``` + +关于具体的代码实现,参考如下的提示和说明: + +1. 将功能的具体实现委托至下一级进行,保持代码语义的简洁。 + + - 系统调用静态函数,并将其委托给 `ProcessManager::fork`。 + - `ProcessManager::fork` 将具体实现委托给当前进程的 `Process::fork`。 + - `Process::fork` 将具体实现委托给 `ProcessInner::fork`。 + + 每一层代码只关心自己层级的逻辑和数据,不关心持有自身的锁或其他外部数据的状态,进而提高代码可维护性。 + +2. 使用先前实现的 `save_current` 和 `switch_next` 等函数,提高代码复用性。 + + 如果使用时遇到了问题,很可能是你的代码过于相互耦合,尝试将逻辑进行分离,保证函数功能的单一性。 + +3. 利用好函数的返回值等机制,注意相关操作的执行顺序。 + +4. 使用 `Arc::downgrade` 获取 `Weak` 引用,从而避免循环引用。 + + 父进程持有子进程的强引用,子进程持有父进程的弱引用,这样可以避免循环引用导致的内存泄漏。 + +5. 为了复制栈空间,你可以使用 `core::intrinsics::copy_nonoverlapping` 函数。 + + 这个函数会使用底层 LLVM 所提供的内存复制相关指令,具有较高的性能。需要调用侧保证源和目标的内存空间不会重叠。可以封装为如下函数进行使用: + + ```rust + /// Clone a range of memory + /// + /// - `src_addr`: the address of the source memory + /// - `dest_addr`: the address of the target memory + /// - `size`: the count of pages to be cloned + fn clone_range(src_addr: u64, dest_addr: u64, size: usize) { + trace!("Clone range: {:#x} -> {:#x}", src_addr, dest_addr); + unsafe { + copy_nonoverlapping::( + src_addr as *mut u8, + dest_addr as *mut u8, + size * Size4KiB::SIZE as usize, + ); + } + } + ``` + +6. 记录父子进程共用的页表。 + + 可以使用 `Arc` 来提供引用计数,来确保进程逐个退出时,只有最后一个退出的进程会进行页表内容的释放。为此,你需要补充一些相关的函数调用: + + ```rust + impl PageTableContext { + // ... + pub fn using_count(&self) -> usize { + Arc::strong_count(&self.reg) + } + + pub fn fork(&self) -> Self { + // forked process shares the page table + Self { + reg: self.reg.clone(), + } + } + // ... + } + ``` + + 也可以补充一些相关的调试信息: + + ```rust + impl Debug for PageTableContext { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // ... + .field("refs", &self.using_count()) + // ... + } + } + ``` + +8. 为子进程分配合适的栈空间。 + + 通过子进程数量、页表引用计数、当前父进程的栈等信息,为子进程分配合适的栈空间。 + + 下面是一个比较常规的期望的栈空间分配结果: + + ```txt + +---------------------+ <- 0x400000000000 + | Parent Stack | + +---------------------+ <- 0x3FFF00000000 + | Child 1 Stack | + +---------------------+ <- 0x3FFE00000000 + | Child 2 Stack | + +---------------------+ <- 0x3FFD00000000 + | ... | + +---------------------+ + ``` + + 这样的栈布局在复杂情况下可能会造成栈复用,在这种情况下进行 `map_range` 会失败,从而你可以继续寻找合适的偏移: + + ```rust + while elf::map_range(/* page range to be mapped */).is_err() + { + trace!("Map thread stack to {:#x} failed.", new_stack_base); + new_stack_base -= STACK_MAX_SIZE; // stack grow down + } + ``` + + 你也可以用 bitmap 等结构体记录栈的释放,或使用其他方式进行合理的分配。 + + 此处的实现很灵活,也无需完全按照上述栈规划进行,你可以自行对算法或分布进行设计。 + + !!! note "更通用的实现" + + 在更好的实现中,`fork` 并不复制整个栈,操作系统会启用 `fork` 后全部页面的写保护。 + + 在任意进程尝试写入时,再对整个页面进行复制。这种策略被称为写时复制(Copy on Write,COW),它可以大大减少内存的使用和开销,提高性能。 + + +### 功能测试 + +在完成了 `fork` 的实现后,你需要通过如下功能测试来验证你的实现是否正确: + +```rust +#![no_std] +#![no_main] + +extern crate alloc; +extern crate lib; + +use lib::*; + +static mut M: u64 = 0xdeadbeef; + +fn main() -> usize { + let mut c = 32; + + let pid = sys_fork(); + + if pid == 0 { + println!("I am the child process"); + + assert_eq!(c, 32); + + unsafe { + println!("child read value of M: {:#x}", M); + M = 0x2333; + println!("child changed the value of M: {:#x}", M); + } + + c += 32; + } else { + println!("I am the parent process"); + + sys_stat(); + + assert_eq!(c, 32); + + println!("Waiting for child to exit..."); + + let ret = sys_wait_pid(pid); + + println!("Child exited with status {}", ret); + + assert_eq!(ret, 64); + + unsafe { + println!("parent read value of M: {:#x}", M); + assert_eq!(M, 0x2333); + } + + c += 1024; + + assert_eq!(c, 1056); + } + + c +} + +entry!(main); +``` + +## 并发与锁机制 + +由于并发执行时,线程的调度顺序无法预知,进而造成的执行顺序不确定,**持有共享资源的进程之间的并发执行可能会导致数据的不一致**,最终导致相同的程序产生一系列不同的结果,这样的情况被称之为**竞态条件(race condition)**。 + +!!! tip "条件竞争……?" + + 恶意程序利用类似的原理,通过不断地尝试,最终绕过检查,获得了一些不应该被访问的资源,这种对系统的攻击行为也被称为条件竞争。 + + > 它们的英文翻译都是 Race Condition,但在不同的领域内常用不同的翻译。 + + 一个著名的例子是 Linux 内核权限提升漏洞 Dirty COW (CVE-2016-5195),通过条件竞争使得普通用户可以写入原本只读的内存区域,从而提升权限。 + +考虑如下的代码: + +```rust +static mut COUNTER: usize = 0; + +fn main() { + let mut handles = vec![]; + + for _ in 0..10 { + handles.push(std::thread::spawn(|| { + for _ in 0..1000 { + unsafe { + COUNTER += 1; + } + } + })); + } + + for handle in handles { + handle.join().unwrap(); + } + + println!("Result: {}", unsafe { COUNTER }); +} +``` + +!!! tip "可以直接使用 `rustc main.rs` 进行编译" + +得到的结果如下: + +```bash +$ for ((i = 0; i < 16; i++)); do ./main; done +Result: 9595 +Result: 8838 +Result: 8315 +Result: 7602 +Result: 9120 +Result: 8485 +Result: 8831 +Result: 8717 +Result: 8812 +Result: 8955 +Result: 9266 +Result: 8168 +Result: 9159 +Result: 10000 +Result: 9664 +Result: 10000 +``` + +可以看到,每次运行的结果都可能不一样,这是因为 `COUNTER += 1` 操作并不是原子的,它包含了读取、修改和写入三个步骤,而在多线程环境下,这三个步骤之间可能会被其他线程(通过操作系统的时钟中断或其他方式)打断,反汇编上述代码,可以看到 `COUNTER += 1` 的实际操作: + +```nasm +mov rax, qword [obj.main::COUNTER::hfb966cd5c23908b7] # read COUNTER to rax +add rax, 1 # rax += 1 +# ... overflow check by rustc ... +mov qword [obj.main::COUNTER::hfb966cd5c23908b7], rax # write rax to COUNTER +``` + +考虑如下的执行顺序(实际执行的时钟中断会慢得多,所以上述代码使用循环来凸显这一问题): + +```nasm +# Thread 1 +mov rax, qword [obj.main::COUNTER::hfb966cd5c23908b7] +add rax, 1 + +# !!! Context Switch !!! + +# Thread 2 +mov rax, qword [obj.main::COUNTER::hfb966cd5c23908b7] + +# !!! Context Switch !!! + +# Thread 1 +mov qword [obj.main::COUNTER::hfb966cd5c23908b7], rax + +# !!! Context Switch !!! + +# Thread 2 +add rax, 1 +mov qword [obj.main::COUNTER::hfb966cd5c23908b7], rax +``` + +在这样的执行顺序下,`COUNTER` 的值会比预期少,几个线程可能会同时读取到相同的值,然后同时写入相同的值,这样的行为就会导致 `+=` 的语意被破坏。 + +上面这种访问共享资源的代码片段被称为**临界区**,为了保证临界区的正确性,需要确保**每次只有一个线程可以进入临界区**,也即保证这部分指令序列是**互斥**的。 + +### 原子指令 + +一般而言,为了解决并发任务带来的问题,需要通过指令集中的原子操作来保证数据的一致性。在 Rust 中,这类原子指令被封装在 `core::sync::atomic` 模块中,作为架构无关的原子操作来提供并发安全性。 + +以 `AtomicUsize` 为例,它提供了一系列的原子操作,如 `fetch_add`、`fetch_update`、`compare_exchange` 等,这些操作都是原子的,不会被其他线程打断,对于之前的例子: + +```rust +static COUNTER: AtomicUsize = AtomicUsize::new(0); +COUNTER.fetch_add(1, Ordering::SeqCst); +``` + +其中 `Ordering` 用户控制内存顺序,在单核情况下,`Ordering` 的选择并不会影响程序的行为,可以简单了解,并尝试回答思考题 4 的内容。 + +在编译器优化后将会被编译为: + +```nasm +lock inc qword [obj.main::COUNTER::h2889e4585a2a2d30] +``` + +这就是一句原子的 `inc` 指令,中断或任务切换都不会打断这个指令的执行,从而保证了 `COUNTER` 的一致性。 + +在了解了原子指令的基本概念后,可以利用它来为用户态程序提供两种简单的同步操作:自旋锁 `SpinLock` 和信号量 `Semaphore`。其中自旋锁的实现并不需要内核态的支持,而信号量则会涉及到进程调度等操作,需要内核态的支持。 + +正因如此,在进行内核编写的过程中遇到的 `Mutex` 和 `RwLock` 等用于保障内核态数据一致性的锁机制**均是基于自旋锁实现的**,_你可能在之前的实验中遇到过系统因为自旋忙等待导致的异常情况_。 + +#### 自旋锁 + +自旋锁 `SpinLock` 是一种简单的锁机制,它通过不断地检查锁的状态来实现线程的阻塞,直到获取到锁为止。 + +在 `pkg/lib/src/sync.rs` 中,关注 `SpinLock` 的实现: + +```rust +pub struct SpinLock { + bolt: AtomicBool, +} + +impl SpinLock { + pub const fn new() -> Self { + Self { + bolt: AtomicBool::new(false), + } + } + + pub fn acquire(&mut self) { + // FIXME: acquire the lock, spin if the lock is not available + } + + pub fn release(&mut self) { + // FIXME: release the lock + } +} + +// Why? Check reflection question 5 +unsafe impl Sync for SpinLock {} +``` + +在实现 `acquire` 和 `release` 时,你需要使用 `AtomicBool` 的原子操作来保证锁的正确性: + +- `load` 函数用于读取当前值。 +- `store` 函数用于设置新值。 +- `compare_exchange` 函数用户原子得进行比较-交换,也即比较当前值是否为目标值,如果是则将其设置为新值,否则返回当前值。 + +在进行循环等待时,可以使用 `core::hint::spin_loop` 提高性能,在 x86_64 架构中,它实际上会编译为 `pause` 指令。 + +#### 信号量 + +得利于 Rust 良好的底层封装,自旋锁的实现非常简单。但是也存在一定的问题: + +- 忙等待:自旋锁会一直占用 CPU 时间,直到获取到锁为止,这会导致 CPU 利用率的下降。 +- 饥饿:如果一个线程一直占用锁,其他线程可能会一直无法获取到锁。 +- 死锁:如果两个线程互相等待对方占有的锁,就会导致死锁。 + +信号量 `Semaphore` 是一种更为复杂的同步机制,它可以用于控制对共享资源的访问,也可以用于控制对临界区的访问。通过与进程调度相关的操作,信号量还可以用于控制进程的执行顺序、提高 CPU 利用率等。 + +信号量需要实现四种操作: + +- `new`:根据所给出的 `key` 创建一个新的信号量。 +- `remove`:根据所给出的 `key` 删除一个已经存在的信号量。 +- `siganl`:也叫做 `V` 操作,也可以被 `release/up/verhogen` 表示,它用于释放一个资源,使得等待的进程可以继续执行。 +- `wait`:也叫做 `P` 操作,也可以被 `acquire/down/proberen` 表示,它用于获取一个资源,如果资源不可用,则进程将会被阻塞。 + +为了实现与内核的交互,信号量的操作将被实现为一个系统调用,它将使用到三个系统调用参数: + +```rust +// op: u8, key: u32, val: usize -> ret: any +Syscall::Sem => sys_sem(&args, context), +``` + +其中 `op` 为操作码,`key` 为信号量的键值,`val` 为信号量的值,`ret` 为返回值。根据先前的约定,`op` 被放置在 `rdi` 寄存器中,`key` 和 `val` 分别被放置在 `rsi` 和 `rdx` 寄存器中,可以通过 `args.arg0`、`args.arg1` 和 `args.arg2` 来进行访问。 + +信号量相关内容在 `pkg/kernel/src/proc/sync.rs` 中进行实现: + +“资源” 被抽象为一个 `usize` 整数,它**并不需要使用 `AtomicUsize`**,为了存储等待的进程,需要在此整数外额外使用一个 `Vec` 来存储等待的进程。它们二者将会被一个自旋锁实现的互斥锁(在内核中直接使用 `spin::Mutex`)保护。 + +```rust +pub struct Semaphore { + count: usize, + wait_queue: VecDeque, +} +``` + +信号量操作的结果使用 `SemaphoreResult` 表示: + +```rust +pub enum SemaphoreResult { + Ok, + NotExist, + Block(ProcessId), + WakeUp(ProcessId), +} +``` + +- `Ok`:表示操作成功,且无需进行阻塞或唤醒。 +- `NotExist`:表示信号量不存在。 +- `Block(ProcessId)`:表示操作需要阻塞线程,一般是当前进程。 +- `WakeUp(ProcessId)`:表示操作需要唤醒线程。 + +为了实现信号量的 KV 存储,使用 `SemaphoreSet` 定义信号量集合的操作: + +```rust +pub struct SemaphoreSet { + sems: BTreeMap>, +} +``` + +并在 `ProcessData` 中添加为线程共享资源: + +```rust +pub struct ProcessData { + // ... + pub(super) semaphores: Arc>, + // ... +} +``` + +!!! note "关于这里的一堆锁……" + + 在本实验实现的单核处理器下,`Semaphore` 的实现似乎并不需要内部的 `Mutex` 进行保护,只需要外部的 `RwLock` 进行保护即可。 + + 但在多核处理器下,`Semaphore` 的实现可能会涉及到多个核心的并发访问,因此需要使用 `Mutex` 来提供更细粒度的锁保护。在进行添加、删除操作时,对 `RwLock` 使用 `write` 获取写锁,而在进行 `signal`、`wait` 操作时,对 `RwLock` 使用 `read` 来获取更好的性能和控制。 + + 综上考量,这里就保留了 `Mutex` 的使用。 + +由于信号量会阻塞进程,所以需要在系统调用的处理中按照信号量的返回值进行相关进程操作,一个代码示例如下: + +```rust +pub fn sem_wait(key: u32, context: &mut ProcessContext) { + x86_64::instructions::interrupts::without_interrupts(|| { + let manager = get_process_manager(); + let pid = processor::current_pid(); + let ret = manager.current().write().sem_wait(key, pid); + match ret { + SemaphoreResult::Ok => context.set_rax(0), + SemaphoreResult::NotExist => context.set_rax(1), + SemaphoreResult::Block(pid) => { + // FIXME: save, block it, then switch to next + // maybe use `save_current` and `switch_next` + } + _ => unreachable!(), + } + }) +} +``` + +请参考实验代码给出的相关注释内容,完成信号量的实现、不同操作的系统调用服务实现,最后完善作为系统调用的 `sys_sem`: + +```rust +pub fn sys_sem(args: &SyscallArgs, context: &mut ProcessContext) { + match args.arg0 { + 0 => context.set_rax(new_sem(args.arg1 as u32, args.arg2)), + 1 => context.set_rax(remove_sem(args.arg1 as u32)), + 2 => sem_siganl(args.arg1 as u32, context), + 3 => sem_wait(args.arg1 as u32, context), + _ => context.set_rax(usize::MAX), + } +} +``` + +??? tip "记得完善用户侧 `pkg/lib/src/sync.rs` 中对信号量的操作" + + 参考别的用户态函数,如 `pkg/lib/src/io.rs` 的构建。 + + 使用 `op` 来分配信号量的用户态函数。 + +### 测试任务 + +在实现了 `SpinLock` 和 `Semaphore` 的基础上,你需要完成如下的用户程序任务来测试你的实现: + +#### 多线程计数器 + +在所给代码的 `pkg/app/counter` 中实现了一个多线程计数器,多个线程对一个共享的计数器进行累加操作,最终输出计数器的值。 + +为了提供足够大的可能性来触发竞态条件,该程序使用了一些手段来刻意构造一个临界区,这部分代码不应被修改。 + +你需要通过上述**两种方式**,分别保护该临界区,使得计数器的值最终为 `800`。 + +!!! note "尝试修改代码,使用**两组线程**分别测试 `SpinLock` 和 `Semaphore`" + + 一个参考代码行为如下: + + ```rust + fn main() -> isize { + let pid = sys_fork(); + + if pid == 0 { + test_semaphore(); + } else { + test_spin(); + sys_wait_pid(pid); + } + + 0 + } + ``` + + 你可以在 `test_spin` 和 `test_semaphore` 中分别继续 `fork` 更多的进程用来实际测试。 + +!!! tip "想想这些锁机制该如何声明才能被正确利用?" + +#### 消息队列 + +创建一个用户程序 `pkg/app/mq`,结合使用信号量,实现一个消息队列: + +- 父进程使用 fork 创建额外 16 个进程,其中一半为生产者,一半为消费者。 + +- 生产者不断地向消息队列中写入消息,消费者不断地从消息队列中读取消息。 + +- 每个线程处理的消息总量共 10 条。 + + 即生产者会产生 10 个消息,每个消费者只消费 10 个消息。 + +- 在每个线程生产或消费的时候,输出相关的信息。 + +- 在生产者和消费者完成上述操作后,使用 `sys_exit(0)` 直接退出。 + +- 最终使用父进程等待全部的子进程退出后,输出消息队列的消息数量。 + +- 在父进程创建完成 16 个进程后,使用 `sys_stat` 输出当前的全部进程的信息。 + +你需要保证最终消息队列中的消息数量为 0,你可以开启内核更加详细的日志,并使用输出的相关信息尝试证明队列的正常工作: + +- 在从队列取出消息时,消息为空吗? +- 在向队列写入消息时,队列是否满了? +- 在队列为空时,消费者是否被阻塞? +- 在队列满时,生产者是否被阻塞? + +#### 哲学家的晚饭 + +假设有 5 个哲学家,他们的生活只是思考和吃饭。这些哲学家共用一个圆桌,每位都有一把椅子。在桌子中央有一碗米饭,在桌子上放着 5 根筷子。 + +当一位哲学家思考时,他与其他同事不交流。时而,他会感到饥饿,并试图拿起与他相近的两根筷子(筷子在他和他的左或右邻居之间)。 + +一个哲学家一次只能拿起一根筷子。显然,他不能从其他哲学家手里拿走筷子。当一个饥饿的哲学家同时拥有两根筷子时,他就能吃。在吃完后,他会放下两根筷子,并开始思考。 + +创建一个用户程序 `pkg/app/dinner`,使用课上学到的知识,实现并解决哲学家就餐问题: + +- 创建一个程序,模拟五个哲学家的行为。 + +- 每个哲学家都是一个独立的线程,可以同时进行思考和就餐。 + +- 使用互斥锁来保护每个筷子,确保同一时间只有一个哲学家可以拿起一根筷子。 + +- 使用等待操作调整哲学家的思考和就餐时间,以增加并发性和实际性。 + + - 如果你实现了 `sys_time` 系统调用(Lab 4),可以使用它来构造 `sleep` 操作。 + - 如果你并没有实现它,可以参考多线程计数器中的 `delay` 函数进行实现。 + +- 当哲学家成功就餐时,输出相关信息,如哲学家编号、就餐时间等。 + +- 向程序中引入一些随机性,例如在尝试拿筷子时引入一定的延迟,模拟竞争条件和资源争用。 + +- 可以设置等待时间或循环次数,以确保程序能够运行足够长的时间,并尝试观察到不同的情况,如死锁和饥饿。 + +??? tip "在用户态中引入伪随机数" + + Rust 提供了一些伪随机数生成器,你可以使用 `rand` 库来引入一些随机性,以模拟不同的情况。 + + ```toml + [dependencies] + rand = { version = "0.8", default-features = false } + rand_chacha = { version = "0.3", default-features = false } + ``` + + 在无标准库的环境下,你需要为伪随机数生成器提供种子。 + + 如果你实现了 `sys_time` 系统调用,这会是一个很方便的种子。 + + 如果你没有实现,不妨试试使用 `sys_getpid` 或者 `fork` 顺序等数据作为种子来生成随机数。 + + 以 `ChaCha20Rng` 伪随机数生成器为例,使用相关方法获取随机数: + + ```rust + use rand::prelude::*; + use rand_chacha::ChaCha20Rng; + + fn main() { + // ... + let time = lib::sys_time(); + let mut rng = ChaCha20Rng::seed_from_u64(time.timestamp() as u64); + println!("Random number: {}", rng.gen::()); + // ... + } + ``` + + 相关文档请查阅:[The Rust Rand Book](https://rust-random.github.io/book/) + +通过观察程序的输出和行为,请尝试构造并截图记录以下现象: + +- 某些哲学家能够成功就餐,即同时拿到左右两侧的筷子。 +- 尝试构造死锁情况,即所有哲学家都无法同时拿到他们需要的筷子。 +- 尝试构造饥饿情况,即某些哲学家无法获得足够的机会就餐。 + +尝试解决上述可能存在的问题,并介绍你的解决思路。 + +??? tip "可能的解决思路……" + + 分布式系统中,常见的解决思路是引入一个**“服务生”**来协调哲学家的就餐。 + + 这个服务生会**控制筷子的分配**,从而**避免死锁和饥饿的情况**。 + ## 思考题 1. 在 Lab 2 中设计输入缓冲区时,如果不使用无锁队列实现,而选择使用 `Mutex` 对一个同步队列进行保护,在编写相关函数时需要注意什么问题?考虑在进行 `pop` 操作过程中遇到串口输入中断的情形,尝试描述遇到问题的场景,并提出解决方案。 + +2. 在进行 `fork` 的复制内存的过程中,系统的当前页表、进程页表、子进程页表、内核页表等之间的关系是怎样的?在进行内存复制时,需要注意哪些问题? + +3. 为什么在实验的实现中,`fork` 系统调用必须在任何 Rust 内存分配(堆内存分配)之前进行?如果在堆内存分配之后进行 `fork`,会有什么问题? + +4. 进行原子操作时候的 `Ordering` 参数是什么?此处 Rust 声明的内容与 [C++20 规范](https://en.cppreference.com/w/cpp/atomic/memory_order) 中的一致,尝试搜索并简单了解相关内容,简单介绍该枚举的每个值对应于什么含义。 + +5. 在实现 `SpinLock` 的时候,为什么需要实现 `Sync` trait?类似的 `Send` trait 又是什么含义? + +6. `core::hint::spin_loop` 使用的 `pause` 指令和 Lab 4 中的 `x86_64::instructions::hlt` 指令有什么区别?这里为什么不能使用 `hlt` 指令? + +## 加分项 + +1. 🤔 参考信号量相关系统调用的实现,尝试修改 `waitpid` 系统调用,在进程等待另一个进程退出时进行阻塞,并在目标进程退出后携带返回值唤醒进程。 + +2. 🤔 尝试实现如下用户程序任务,完成用户程序 `fish`: + + - 创建三个子进程,让它们分别能输出且只能输出 `>`,`<` 和 `_`。 + - 使用学到的方法对这些子进程进行同步,使得打印出的序列总是 `<><_` 和 `><>_` 的组合。 + + 在完成这一任务的基础上,其他细节可以自行决定如何实现,包括输出长度等。 + +3. 🤔 尝试和前文不同的其他方法解决哲学家就餐问题,并验证你的方法能够正确解决它,简要介绍你的方法,并给出程序代码和测试结果。 diff --git a/docs/wiki/elf.md b/docs/wiki/elf.md index 91b5b5f..8ecb1c1 100644 --- a/docs/wiki/elf.md +++ b/docs/wiki/elf.md @@ -271,7 +271,7 @@ typedef struct elf64_phdr { 08 .init_array .fini_array .data.rel.ro .dynamic .got ``` -## 在编译链接的过程中控制 ELF 的结构 +## 控制 ELF 的结构 以下的程序会把 `a` 和 `function()` 放入对应的 section 中: @@ -298,7 +298,7 @@ SECTIONS 使用以下命令编译 ```bash -gcc main.c -c main.o && ld main.o -T ./script.ld -o main +gcc main.c -c -o main.o && ld main.o -T ./script.ld -o main ``` 观察结果,你也可以使用 `readelf`,这里使用 `gdb`插件 `gef`的`vmmap`命令来观察,也可以直接观察 `/proc/pid/` diff --git a/docs/wiki/linux.md b/docs/wiki/linux.md index 07dc69a..6c9d913 100644 --- a/docs/wiki/linux.md +++ b/docs/wiki/linux.md @@ -26,9 +26,26 @@ wsl --install -d Ubuntu 关于其他的配置,可以在网上找到大量的参考资料,请自行搜索阅读,或寻求 LLM 的帮助。 -### 使用 VMware Workstation +### 使用其他虚拟机软件 -参考 [VMware Workstation 安装 Ubuntu 22.04 LTS](https://zhuanlan.zhihu.com/p/569274366) 教程。 +如果你不想使用 WSL2,也可以使用其他虚拟机软件,如 VMware Workstation、VirtualBox 等,安装 Ubuntu 22.04,相关安装教程请自行搜索。 + +!!! warning "使用须知" + + 请注意,你需要自行处理如下问题,以达到与 WSL 2 类似的能力: + + - 与 Windows 之间的剪贴板共享(需要安装 VMware Tools 等辅助工具和 Guest 侧驱动) + - 与 Windows 之间的文件共享 (需要配置共享文件夹,或者使用网络共享协议) + + 如果有需要在 Windows 上使用 SSH 连接到虚拟机,你需要在虚拟机中安装 SSH 服务,并配置网络连接。 + + **以上内容都需要你具有一定的 Windows 和 Linux 系统的使用经验,如果你不确定自己是否能够完成这些操作,请使用 WSL 2。** + +### 使用实体机 + +如果你已经拥有了一台 Linux 服务器或者台式机,笔者相信你的折腾能力。 + +你可以使用任何你喜欢的发行版,但请注意内核版本、QEMU 版本都不应低于实验的参考标准。 ## 安装项目开发环境 @@ -63,11 +80,13 @@ wsl --install -d Ubuntu source "$HOME/.cargo/env" ``` + !!! tip "如果遇到了网络问题,请参考 [rsproxy.cn](https://rsproxy.cn/) 进行配置。" + 在安装完成后,请使用如下命令,确保你的相关软件包**不低于**如下标准: ```bash $ rustc --version -rustc 1.77.0-nightly (11f32b73e 2024-01-31) +rustc 1.76.0 (07dca489a 2024-02-04) $ qemu-system-x86_64 --version QEMU emulator version 6.2.0 (Debian 1:6.2+dfsg-2ubuntu6.15) diff --git a/docs/wiki/windows.md b/docs/wiki/windows.md index 35ffd31..16751e5 100644 --- a/docs/wiki/windows.md +++ b/docs/wiki/windows.md @@ -1,5 +1,29 @@ # Windows 环境配置 +!!! warning "关于环境选择" + + **如非特殊需要,强烈建议使用 WSL 2 在 Windows 上进行开发。** + + **借助于 VSCode 和 Remote WSL 插件,可以实现更好的编写代码、编译、调试体验。** + + **WSL 2 的安装和配置请参考 [WSL 2 官方文档](https://docs.microsoft.com/zh-cn/windows/wsl/install)。** + + **TL;DR** + + ```bash + wsl --install -d Ubuntu + ``` + + --- + + **如果你选择使用 WSL 2,可以跳过此文档的内容,转至 [Linux 环境配置](./linux.md) 进行配置。** + + **如果你继续选择使用 Windows 直接进行实验,请确保:** + + - **你具有良好的 Windows 折腾能力,了解环境变量配置、Windows 目录权限等。** + - **通读下列文档,不要做一步看一步。** + - **你能够正确理解下列文档中所描述的步骤的含义和目的。** + !!! tip "关于 Windows 10" 本文主要面向 Windows 11 用户,在 Windows 10 上你可能需要补全一些额外的步骤,如: @@ -29,7 +53,7 @@ rust 提供了两种 windows 上的工具链:`msvc` 和 `gnu`,详细信息 在安装 Visual Studio 时,需要选择如下组件: - `MSVC v143 - VS 2022 C++ x64/x86 build tools (latest)` - - `Windows 11 SDK (10.0.22621.0)` + - `Windows 11 SDK` `msvc` 工具链可以提供更好的 Windows 应用兼容性,也是 Windows 上开发 rust 应用推荐的工具链。 diff --git a/src/0x02/pkg/kernel/src/memory/gdt.rs b/src/0x02/pkg/kernel/src/memory/gdt.rs index a615655..1b0a91e 100644 --- a/src/0x02/pkg/kernel/src/memory/gdt.rs +++ b/src/0x02/pkg/kernel/src/memory/gdt.rs @@ -15,6 +15,8 @@ lazy_static! { // initialize the TSS with the static buffers // will be allocated on the bss section when the kernel is load + // + // DO NOT MODIFY THE FOLLOWING CODE tss.privilege_stack_table[0] = { const STACK_SIZE: usize = IST_SIZES[0]; static mut STACK: [u8; STACK_SIZE] = [0; STACK_SIZE]; diff --git a/src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs b/src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs index 18b2711..0f3a8db 100644 --- a/src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs +++ b/src/0x04/pkg/kernel/src/interrupt/syscall/mod.rs @@ -1,8 +1,10 @@ use crate::{memory::gdt, proc::*}; use alloc::format; -use syscall_def::Syscall; use x86_64::structures::idt::{InterruptDescriptorTable, InterruptStackFrame}; +// NOTE: import `ysos_syscall` package as `syscall_def` in Cargo.toml +use syscall_def::Syscall; + mod service; use super::consts; @@ -45,6 +47,9 @@ pub fn dispatcher(context: &mut ProcessContext) { // fd: arg0 as u8, buf: &[u8] (ptr: arg1 as *const u8, len: arg2) Syscall::Write => { /* FIXME: write to fd & return length */}, + // None -> pid: u16 + Syscall::GetPid => { /* FIXME: get current pid */ }, + // path: &str (ptr: arg0 as *const u8, len: arg1) -> pid: u16 Syscall::Spawn => { /* FIXME: spawn process from name */}, // ret: arg0 as isize diff --git a/src/0x04/pkg/lib/src/lib.rs b/src/0x04/pkg/lib/src/lib.rs index 95a4304..dc7e36a 100644 --- a/src/0x04/pkg/lib/src/lib.rs +++ b/src/0x04/pkg/lib/src/lib.rs @@ -21,7 +21,6 @@ use core::fmt::*; pub use alloc::*; pub use io::*; pub use syscall::*; -pub use utils::*; #[macro_export] macro_rules! print { diff --git a/src/0x04/pkg/lib/src/syscall.rs b/src/0x04/pkg/lib/src/syscall.rs index 0b40d86..ebab5b5 100644 --- a/src/0x04/pkg/lib/src/syscall.rs +++ b/src/0x04/pkg/lib/src/syscall.rs @@ -63,6 +63,11 @@ pub fn sys_spawn(path: &str) -> u16 { syscall!(Syscall::Spawn, path.as_ptr() as u64, path.len() as u64) as u16 } +#[inline(always)] +pub fn sys_get_pid() -> u16 { + syscall!(Syscall::GetPid) as u16 +} + #[inline(always)] pub fn sys_exit(code: isize) -> ! { syscall!(Syscall::Exit, code as u64); diff --git a/src/0x04/pkg/syscall/src/lib.rs b/src/0x04/pkg/syscall/src/lib.rs index d6741a7..5b17202 100644 --- a/src/0x04/pkg/syscall/src/lib.rs +++ b/src/0x04/pkg/syscall/src/lib.rs @@ -10,6 +10,8 @@ pub enum Syscall { Read = 0, Write = 1, + GetPid = 39, + Spawn = 59, Exit = 60, WaitPid = 61, diff --git a/src/0x05/pkg/app/counter/Cargo.toml b/src/0x05/pkg/app/counter/Cargo.toml new file mode 100644 index 0000000..495db03 --- /dev/null +++ b/src/0x05/pkg/app/counter/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ysos_counter" +version = "0.1.0" +edition = "2021" + +[dependencies] +lib = { path="../../lib", package="yslib"} diff --git a/src/0x05/pkg/app/counter/src/main.rs b/src/0x05/pkg/app/counter/src/main.rs new file mode 100644 index 0000000..6049405 --- /dev/null +++ b/src/0x05/pkg/app/counter/src/main.rs @@ -0,0 +1,68 @@ +#![no_std] +#![no_main] + +use lib::*; + +extern crate lib; + +const THREAD_COUNT: usize = 8; +static mut COUNTER: isize = 0; + +fn main() -> isize { + let mut pids = [0u16; THREAD_COUNT]; + + for i in 0..THREAD_COUNT { + let pid = sys_fork(); + if pid == 0 { + do_counter_inc(); + sys_exit(0); + } else { + pids[i] = pid; // only parent knows child's pid + } + } + + let cpid = sys_get_pid(); + println!("process #{} holds threads: {:?}", cpid, &pids); + sys_stat(); + + for i in 0..THREAD_COUNT { + println!("#{} waiting for #{}...", cpid, pids[i]); + sys_wait_pid(pids[i]); + } + + println!("COUNTER result: {}", unsafe { COUNTER }); + + 0 +} + +fn do_counter_inc() { + for _ in 0..100 { + // FIXME: protect the critical section + inc_counter(); + } +} + +/// Increment the counter +/// +/// this function simulate a critical section by delay +/// DO NOT MODIFY THIS FUNCTION +fn inc_counter() { + unsafe { + delay(); + let mut val = COUNTER; + delay(); + val += 1; + delay(); + COUNTER = val; + } +} + +#[inline(never)] +#[no_mangle] +fn delay() { + for _ in 0..0x100 { + core::hint::spin_loop(); + } +} + +entry!(main); diff --git a/src/0x05/pkg/kernel/src/proc/sync.rs b/src/0x05/pkg/kernel/src/proc/sync.rs new file mode 100644 index 0000000..a03c464 --- /dev/null +++ b/src/0x05/pkg/kernel/src/proc/sync.rs @@ -0,0 +1,104 @@ +use super::ProcessId; +use alloc::collections::*; +use spin::Mutex; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub struct SemaphoreId(u32); + +impl SemaphoreId { + pub fn new(key: u32) -> Self { + Self(key) + } +} + +/// Mutex is required for Semaphore +#[derive(Debug, Clone)] +pub struct Semaphore { + count: usize, + wait_queue: VecDeque, +} + +/// Semaphore result +#[derive(Debug)] +pub enum SemaphoreResult { + Ok, + NotExist, + Block(ProcessId), + WakeUp(ProcessId), +} + +impl Semaphore { + /// Create a new semaphore + pub fn new(value: usize) -> Self { + Self { + count: value, + wait_queue: VecDeque::new(), + } + } + + /// Wait the semaphore (acquire/down/proberen) + /// + /// if the count is 0, then push the process into the wait queue + /// else decrease the count and return Ok + pub fn wait(&mut self, pid: ProcessId) -> SemaphoreResult { + // FIXME: if the count is 0, then push pid into the wait queue + // return Block(pid) + // FIXME: else decrease the count and return Ok + } + + /// Signal the semaphore (release/up/verhogen) + /// + /// if the wait queue is not empty, then pop a process from the wait queue + /// else increase the count + pub fn signal(&mut self) -> SemaphoreResult { + // FIXME: if the wait queue is not empty + // pop a process from the wait queue + // return WakeUp(pid) + // FIXME: else increase the count and return Ok + } +} + +#[derive(Debug, Default)] +pub struct SemaphoreSet { + sems: BTreeMap>, +} + +impl SemaphoreSet { + pub fn insert(&mut self, key: u32, value: usize) -> bool { + trace!("Sem Insert: <{:#x}>{}", key, value); + + // FIXME: insert a new semaphore into the sems + // use `insert(/* ... */).is_none()` + } + + pub fn remove(&mut self, key: u32) -> bool { + trace!("Sem Remove: <{:#x}>", key); + + // FIXME: remove the semaphore from the sems + // use `remove(/* ... */).is_some()` + } + + /// Wait the semaphore (acquire/down/proberen) + pub fn wait(&self, key: u32, pid: ProcessId) -> SemaphoreResult { + let sid = SemaphoreId::new(key); + + // FIXME: try get the semaphore from the sems + // then do it's operation + // FIXME: return NotExist if the semaphore is not exist + } + + /// Signal the semaphore (release/up/verhogen) + pub fn signal(&self, key: u32) -> SemaphoreResult { + let sid = SemaphoreId::new(key); + + // FIXME: try get the semaphore from the sems + // then do it's operation + // FIXME: return NotExist if the semaphore is not exist + } +} + +impl core::fmt::Display for Semaphore { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Semaphore({}) {:?}", self.count, self.wait_queue) + } +} diff --git a/src/0x05/pkg/lib/src/sync.rs b/src/0x05/pkg/lib/src/sync.rs new file mode 100644 index 0000000..b3a5b9b --- /dev/null +++ b/src/0x05/pkg/lib/src/sync.rs @@ -0,0 +1,50 @@ +use core::{ + hint::spin_loop, + sync::atomic::{AtomicBool, Ordering}, +}; + +use crate::*; + +pub struct SpinLock { + bolt: AtomicBool, +} + +impl SpinLock { + pub const fn new() -> Self { + Self { + bolt: AtomicBool::new(false), + } + } + + pub fn acquire(&self) { + // FIXME: acquire the lock, spin if the lock is not available + } + + pub fn release(&self) { + // FIXME: release the lock + } +} + +unsafe impl Sync for SpinLock {} // Why? Check reflection question 5 + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct Semaphore { + /* FIXME: record the sem key */ +} + +impl Semaphore { + pub const fn new(key: u32) -> Self { + Semaphore { key } + } + + // FIXME: inline functions with syscall +} + +unsafe impl Sync for Semaphore {} + +#[macro_export] +macro_rules! semaphore_array { + [$($x:expr),+ $(,)?] => { + [ $($crate::Semaphore::new($x),)* ] + } +}