0%

2024秋冬季训练营第四阶段总结-戴志强

在训练营中做了什么

rust的异步机制学习

在 Rust 的异步编程中,asyncawait 是不可忽略的核心概念。

当一个函数被标记为 async 时,它会被转换为一个返回 Future 的函数。实际上,async 函数并不是立即执行的,而是生成了一个 Future,表示一个可能尚未完成的异步计算。类似地,async 代码块的行为与 async 函数本质相同,都可以生成一个 Future

调用 async 函数后,你需要使用 await 来获取其结果:

1
some_func().await;

await的作用

await 的核心任务是对 Future 进行轮询(poll)。当调用 await 时,程序会检查 Future 是否已完成。如果尚未完成,当前任务会被暂停,并将控制权交还给执行器(executor)。此时,CPU 资源可以被其他任务使用。随后,执行器会在适当的时机再次轮询该 Future,直到其完成。


异步任务的顺序

考虑以下代码:

1
2
func1().await;
func2().await;

如果 func1 中存在一个耗时的操作(例如 2 秒的延迟),在 await 首次轮询时,由于任务未完成,执行器会暂停该任务并继续寻找其他可以执行的任务,例如 func2。这样就实现了异步调度。

然而,如果你在异步代码中混合了同步操作,比如:

1
2
func1().await;
println!("1");

这种情况下,func1 的任务未完成时,程序会暂停整个任务链,而不会直接跳到同步代码。也就是说,println!("1") 的执行必须等到 func1 完成后才能继续。这表明异步任务的执行顺序可以调整,但同步代码的执行仍然是严格按顺序进行的。


async/await的底层原理

asyncawait 的实现涉及编译器的转换。在编译时,所有 async 函数会被转换为状态机。这种状态机的实现方式类似于无栈协程。每个 Future 都维护着一个状态,记录其当前的执行进度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
enum PollResult {
Ready,
Pending,
}
enum State {
StateA,
StateB,
StateC,
}
struct Coroutine {
state: State,
}
impl Coroutine {
pub fn poll(&mut self) -> PollResult {
match self.state {
State::StateA => {
/* Do something */
self.state = State::StateB;
return PollResult::Pending;
}
State::StateB => {
/* Do something */
self.state = State::StateC;
return PollResult::Pending;
}
State::StateC => {
/* Do something */
return PollResult::Ready;
}
}
}
}

上述代码中的 poll 函数即为协程的具体运行函数,不难看出其是一个状态机模型,上层每次调用 poll 时都可能改变其状态,从而推进其运行;总的来说,无栈协程的调度是通过函数返回然后调用另一个函数实现的,而不是像有栈协程那样直接原地更改栈指针。

此外,编译器会为异步函数生成一个表,类似于 C++ 的虚函数表(vtable)。每当一个任务暂停时,执行器会通过这个表找到下一个可以执行的任务,并对其进行轮询。这种机制确保了异步任务之间的高效调度和协作。

通过这些特性,Rust 实现了高性能的异步编程,同时避免了传统回调方式的复杂性,保证了代码的可读性和可靠性。

Phoenix 异步内核学习

在这个内核中,异步任务的执行流是通过 Rust 的 asyncawait 特性来实现的。以下是执行流的一个简要过程。

任务的核心调度

首先,从内核的主循环开始:

1
2
3
loop {
executor::run_until_idle();
}

这里的 run_until_idle 是任务调度的入口,其核心逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
pub fn run_until_idle() -> usize {
let mut n = 0;
loop {
if let Some(task) = TASK_QUEUE.fetch() {
task.run();
n += 1;
} else {
break;
}
}
n
}

它从任务队列中提取任务并运行,直到队列为空。这里的任务可以是一个用户线程,也可以是其他异步任务。


线程的定义

内核将线程与进程分开设计。线程的定义如下:

1
2
3
4
5
6
7
8
pub struct Thread {
tid: Arc<TidHandle>,
mailbox: Mailbox,
pub sig_trampoline: SignalTrampoline,
pub process: Arc<Process>,
pub sig_queue: SpinNoIrqLock<SigQueue>,
pub inner: UnsafeCell<ThreadInner>,
}

虽然内部资源管理方式与传统内核略有不同,但核心思想是一致的:通过 Thread 表示一个具体的任务实体。

在内核中,当加载一个 ELF 文件并创建线程时,会生成一个 Thread 实例。随后,这些线程会被包装为异步任务进行管理。


线程生命周期的实现

线程的执行过程通常包括以下几步:

  1. 加载用户态寄存器(ld regs)。
  2. 返回用户态执行(sret)。
  3. 用户态发生中断时,陷入内核处理(trap)。
  4. 继续返回用户态,循环往复,直到线程终止。

在一些传统内核中,这个循环过程可能直接用汇编实现(通常写在 .S 文件中)。但在该内核中,将线程生命周期抽象为 Rust 异步函数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
pub async fn threadloop(thread: Arc<Thread>) {
thread.set_waker(async_utils::take_waker().await);
loop {
trap::user_trap::trap_return();
trap::user_trap::trap_handler().await;

if thread.is_zombie() {
break;
}
}
handle_exit(&thread);
}

这个设计的关键在于使用 async 将线程的整个生命周期建模为一个 Future,使得线程的运行和中断处理都可以以异步方式进行,而无需依赖汇编实现。这种方式可以充分利用 Rust 异步编程的特性。


任务的 Future 封装

内核将每个线程包装为一个 Future,具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub struct UserTaskFuture<F: Future + Send + 'static> {
task_ctx: Box<LocalContext>,
task_future: F,
}

impl<F: Future + Send + 'static> Future for UserTaskFuture<F> {
type Output = F::Output;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = unsafe { self.get_unchecked_mut() };
let hart = processor::local_hart();
hart.push_task(&mut this.task_ctx);

let ret = unsafe { Pin::new_unchecked(&mut this.task_future).poll(cx) };
hart.pop_task(&mut this.task_ctx);

ret
}
}

其中,task_ctx 包含 CPU 上下文和线程指针,负责管理任务的运行状态。

在初始化时,线程会被封装成 Future,并推入调度队列:

1
2
3
4
5
6
pub fn spawn_thread(thread: Arc<Thread>) {
let future = UserTaskFuture::new(thread.clone(), threadloop(thread));
let (runnable, task) = executor::spawn(future);
runnable.schedule();
task.detach();
}

此处的 threadloop 被作为一个 Future 传递给 UserTaskFuture::new,从而将线程的整个生命周期管理交由异步调度器。

异步的实现

我尝试对 rCore 内核进行异步化改造,利用 Rust 提供的 asyncawait 实现异步编程。参考了 Phoenix 的实现思路,将进出用户态等流程封装为一个协程。由于这是我第一次接触操作系统相关知识,在重构内核时,需要通过大量的调试来有机地理解和掌握内核的整体工作流程。然而,受限于期末周和其他大作业的时间压力,目前仅完成了将进出用户态等过程迁移到协程中的初步工作,仍有许多细节尚未完善。