Skip to content

Commit fe3cb73

Browse files
Merge branch 'comp-and-judge' into final
2 parents 54fac12 + cf158c3 commit fe3cb73

File tree

1 file changed

+60
-228
lines changed

1 file changed

+60
-228
lines changed

doc/task.md

Lines changed: 60 additions & 228 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
# 任务管理
22

3-
Eonix内核的任务管理系统采用了模块化的分层设计,将任务调度的运行时层(`Task`与POSIX规范中的线程与进程资源抽象分离,构建了一个灵活、高效的多任务管理框架。这种分层设计使各模块职责更加清晰,同时提供了较高的灵活性,能够支持不同类型的运行时与调度策略。
3+
在决赛阶段,我们对初赛时未完全完成的任务管理模块进行了完善。我们的运行时现在以无栈协程为打底,同时可以兼顾无栈协程以及有栈协程,做到了 "Pay for and only for what you need, and nothing else" 的零成本抽象思想。Eonix内核的任务管理系统采用了模块化的分层设计,将任务调度的运行时层(`Task`、有栈与无栈的区分(是否使用 `stackful` 进行包装)以及POSIX规范中的线程与进程资源抽象分离,构建了一个灵活、高效的多任务管理框架。这种分层设计使各模块职责更加清晰,同时提供了较高的灵活性,能够支持不同类型的运行时与调度策略。
44

55
## 核心设计概述
66

77
任务管理是内核调度和进程间通信的基础,其核心组件分为两个层次:
88

99
### 运行时层
10-
- **任务(Task)**:调度的基本单位,代表一个可调度的执行单元
11-
- **执行器(Executor)**:负责具体任务的执行,管理任务的生命周期
12-
- **调度器(Scheduler)**:管理所有任务,负责任务的调度和切换
13-
- **执行上下文(ExecutionContext)**:保存任务执行状态,支持上下文切换
10+
- **任务(Task)**:调度的基本单位,代表一个无栈打底(我们可以在其上实现有栈的任务,具体见下),可调度的执行单元
11+
- **运行时(Runtime)**:管理所有任务,负责任务的调度和切换
1412
- **就绪队列(ReadyQueue)**:管理就绪的任务,供调度器使用
1513

1614
### POSIX资源抽象层
@@ -21,105 +19,84 @@ Eonix内核的任务管理系统采用了模块化的分层设计,将任务调
2119

2220
## 任务模型
2321

24-
任务是Eonix内核调度的基本单位,由`Task`结构体表示,每个任务包含独立的执行上下文、状态信息和执行器。任务支持协作式调度,通过 `park``unpark` 方法进行任务的暂停和恢复。
22+
### 无栈
2523

26-
### 任务的基本结构
27-
28-
每个任务由`Task`结构体表示,包含以下关键组件:
29-
```rust
30-
pub struct Task {
31-
pub id: TaskId, // 唯一标识符
32-
pub(crate) on_rq: AtomicBool, // 是否在就绪队列中
33-
pub(crate) unparked: AtomicBool, // 是否被唤醒
34-
pub(crate) cpu: AtomicU32, // 亲和性CPU
35-
pub(crate) state: TaskState, // 任务状态
36-
pub(crate) execution_context: ExecutionContext, // 执行上下文
37-
executor: AtomicUniqueRefCell<Option<Pin<Box<dyn Executor>>>>, // 执行器
38-
link_task_list: RBTreeAtomicLink, // 全局任务列表链接
39-
}
40-
```
24+
任务是Eonix内核调度的基本单位,由`Task`结构体表示,每个任务包含状态信息和对应的 Future 对象。每一个任务天生就是无栈的,
25+
所以我们的运行时在实现的时候可以非常简单与纯粹,不需要考虑到复杂的同步、抢占等问题,每个 CPU 只需要一个栈,一个 Runtime
26+
进行任务的调度。如果你不需要跑有栈的任务,这就是你全部需要的资源开销。因此这些任务非常轻量化,例如我们可以实现 call_rcu
27+
来将 RCU 对象的释放推迟。这个函数可以直接将这个对象扔到一个 async 闭包中,等待 RCU 读者都释放后再执行。
4128

42-
### 任务的生命周期
43-
44-
任务创建通过 `Task::new` 方法实现,接受一个实现了 `Run` trait 的可运行对象和一个栈类型。任务创建后会获得一个 `TaskHandle`,可用于控制任务和获取任务结果。
45-
46-
任务的状态变化遵循以下流程:
47-
1. 初始状态为 `RUNNING`,表示任务可以执行
48-
2. 当任务调用 `park` 方法时,状态变为 `PARKING`
49-
3. 如果任务没有被唤醒,则进入调度器
50-
4. 调度器可能将任务状态变为 `PARKED`,表示任务已被挂起
51-
5. 当任务被 `unpark` 唤醒时,状态返回 `RUNNING`,任务重新可被调度
52-
53-
任务执行通过 `run` 方法进行,当执行完成后返回 `ExecuteStatus::Finished` 状态。
54-
55-
## 执行器和调度器
56-
57-
### 执行器
58-
59-
执行器是负责具体任务执行的组件,由 `Executor` trait 定义:
29+
无栈任务的创建也非常简单,只需要在 `Runtime` 对象上调用 `spawn` 方法,传入你要跑的 `Future` 即可。
6030

6131
```rust
62-
pub trait Executor: Send {
63-
fn progress(&self) -> ExecuteStatus;
64-
}
65-
```
66-
67-
执行器通过 `progress` 方法推进任务的执行,并管理任务的输出和完成状态。`ExecutorBuilder` 提供了灵活的任务和栈配置方式,支持不同类型任务的创建。
68-
69-
### 调度器
70-
71-
调度器是任务管理的核心,负责任务的调度决策。调度器通过全局单例 `Scheduler` 实现,主要功能包括:
32+
async foo() {
33+
let join_handle = RUNTIME.spawn(async {
34+
println_info!("We are in the task!");
35+
sleep(3).await;
7236

73-
1. **任务切换**:通过 `schedule` 方法选择下一个要执行的任务,并进行上下文切换
74-
2. **任务激活**:通过 `activate` 方法将任务添加到就绪队列中,使其可被调度
75-
3. **任务跟踪**:通过全局任务列表 `TASKS` 和每CPU变量 `CURRENT_TASK` 跟踪所有任务和当前执行的任务
37+
42
38+
});
7639

77-
### 就绪队列
78-
79-
就绪队列是调度器的重要组成部分,使用 `ReadyQueue` 结构体管理每个CPU上就绪的任务:
80-
81-
```rust
82-
pub struct ReadyQueue {
83-
queue: SpinIrq<VecDeque<Arc<Task>>>,
40+
assert_eq!(join_handle.await, 42);
8441
}
8542
```
8643

87-
就绪队列支持任务的入队、出队和调度操作,确保任务能够高效地被选择执行。每个CPU核心维护一个独立的就绪队列,通过 `cpu_rq``local_rq` 函数访问。
44+
无栈任务遵守协作式多任务的规范,所有人在需要等待时使用 `await` 让出控制权,并且保存 `Waker` 以便准备好时唤醒。如果你
45+
需要做一些计算密集型的任务,你可以将这些任务拆分到专门的有栈的、按时间片抢占的任务中,或者是使用有栈的 Worker 来运行
46+
你的任务。我们将在下方介绍。
8847

89-
## 上下文管理
48+
### 有栈
9049

91-
执行上下文是任务调度和切换的关键,通过 `ExecutionContext` 结构体表示:
50+
有栈任务是在无栈任务的基础上,将对应的 `Future` 对象通过 `stackful` 这个函数进行包装后得到的 `Future`。它在
51+
`Runtime` 看来,以及调度时与正常的无栈任务没有 **任何** 的区别。在内部,我们会为每一个有栈的任务分配一个栈,并将这个
52+
`Future` 对象保存到栈上。我们使用 `TrapContext``captured_trap_return` 这个功能来实现有栈的任务,捕获时钟
53+
中断来实现时间片的抢占。如果想将一个计算密集型的任务转变成有栈、可抢占的任务,或者是将一个现有的,很难 async 化的任务
54+
适配到我们的内核中,我们只需要创建一个正常的 `Future`,然后将其包在 `stackful` 的里面......
9255

9356
```rust
94-
#[derive(Debug)]
95-
pub struct ExecutionContext(UnsafeCell<TaskContext>);
57+
RUNTIME.spawn(stackful(async { // 这里使用 stackful 函数将 Future 包装成有栈的任务
58+
for generation in 0..2147483647 {
59+
let result = some_computation_heavy_work(generation);
60+
println_info!("Generation {}: Result = {}", generation, result);
61+
}
62+
}));
9663
```
9764

98-
上下文包含任务的寄存器状态,如程序计数器(IP)、栈指针(SP)和中断状态等。TaskContext 具体结构根据不同架构有不同的实现,但都提供了统一的接口。
99-
100-
### 上下文切换
65+
我们对于上面的抽象得到的好处非常明显:
10166

102-
上下文切换通过以下方式实现:
67+
如果我们想要一个轻量级的任务,我们可以使用无栈协程,保证不要在非 await 的地方进行阻塞调用,我们就可以获益于 Rust 的无
68+
栈协程带来的高性能,以及方便的使用方法。由于每个任务在切换的过程其实只是 `poll` 函数的调用,而任务内存的分配我们通过
69+
高性能的 Slab 和 Buddy 分配器,做到了极低的开销。我们没有为我们不需要的功能买单(有栈任务的栈资源,以及上下文切换的
70+
开销),同时对于我们需要的部分,我们也将开销做到了最小。
10371

104-
1. **`switch_to`方法**:在两个上下文间切换,保存当前状态并加载目标状态
105-
```rust
106-
pub fn switch_to(&self, to: &Self) {
107-
let Self(from_ctx) = self;
108-
let Self(to_ctx) = to;
109-
unsafe {
110-
TaskContext::switch(&mut *from_ctx.get(), &mut *to_ctx.get());
111-
}
112-
}
113-
```
72+
如果我们确实需要抢占,或者是需要更复杂的调度策略,我们可以使用有栈的任务。但是我们的抽象设计仍然保证了我们不会比传统的
73+
任务调度方案更劣,甚至会更好:
74+
1. 有栈任务的上下文切换开销与传统线程相同。
75+
2. 有栈任务与传统线程相同,需要栈空间。但是我们将 Future 对象也相应地放到了栈上,从而实现了更高效的内存使用。
76+
3. 我们的调度策略仍然是基于协作式多任务的,不会因为使用有栈任务而引入额外的抢占开销。
77+
4. 我们在实现过程当中将有栈任务的资源放到了 Future 对象中,从而保证了当任务结束时,资源一定会得到释放。减少了设计的复杂度。
11478

115-
2. **`switch_noreturn`方法**:实现不返回的上下文切换,用于任务退出场景
116-
3. **`call1`方法**:支持带参数的函数调用,常用于任务初始化
79+
更具体地了解我们的实现,可以参考 crates/eonix_runtime/src/scheduler.rs、 crates/eonix_runtime/src/task.rs
80+
和src/kernel/task.rs。
11781

118-
每个CPU还维护一个局部调度器上下文 `LOCAL_SCHEDULER_CONTEXT`,用于在任务挂起时返回到调度器:
82+
### 任务的基本结构
11983

84+
每个任务由`Task`结构体表示,包含以下关键组件:
12085
```rust
121-
#[eonix_percpu::define_percpu]
122-
static LOCAL_SCHEDULER_CONTEXT: ExecutionContext = ExecutionContext::new();
86+
pub struct Task {
87+
/// 任务的唯一标识符
88+
pub id: TaskId,
89+
/// 任务所归属的 CPU
90+
pub(crate) cpu: AtomicU32,
91+
/// 状态
92+
pub(crate) state: TaskState,
93+
/// 任务的 Executor,一个类型擦除的 Future
94+
executor: AtomicUniqueRefCell<Executor>,
95+
/// 在全局任务链表中的 Link
96+
link_task_list: RBTreeAtomicLink,
97+
/// 在就绪队列中的 Link
98+
link_ready_queue: LinkedListAtomicLink,
99+
}
123100
```
124101

125102
## 信号管理
@@ -323,54 +300,6 @@ pub struct Process {
323300
- 运行时层可以独立演化,而不影响POSIX语义的实现
324301
- 更容易支持不同类型的线程模型,如内核线程、用户线程等
325302

326-
## 异步支持与阻塞操作
327-
328-
Eonix任务系统充分利用Rust语言的异步编程模型,提供了高效的异步执行和阻塞操作支持。
329-
330-
### 异步Future执行
331-
332-
Task实现了`block_on`方法,可以在当前任务上阻塞执行一个`Future`对象:
333-
334-
```rust
335-
pub fn block_on<F>(future: F) -> F::Output
336-
where
337-
F: Future,
338-
{
339-
let waker = Waker::from(Task::current().clone());
340-
let mut context = Context::from_waker(&waker);
341-
let mut future = pin!(future);
342-
343-
loop {
344-
if let Poll::Ready(output) = future.as_mut().poll(&mut context) {
345-
break output;
346-
}
347-
Task::park();
348-
}
349-
}
350-
```
351-
352-
### 任务等待与唤醒机制
353-
354-
Task实现了`Wake` trait,使其可以作为异步唤醒器使用:
355-
356-
```rust
357-
impl Wake for Task {
358-
fn wake(self: Arc<Self>) {
359-
self.wake_by_ref();
360-
}
361-
362-
fn wake_by_ref(self: &Arc<Self>) {
363-
self.unpark();
364-
}
365-
}
366-
```
367-
368-
`JoinHandle`提供了等待任务完成并获取结果的方法,通过`join`方法阻塞当前任务直到目标任务完成并返回其结果。
369-
370-
当任务需要等待某些事件时,可以调用`Task::park`方法暂停自己,并在事件发生时通过`unpark`方法唤醒。这种机制结合异步编程模型,为Eonix提供了高效的I/O和事件处理能力。
371-
372-
## 关键特性与技术亮点
373-
374303
### 抢占式调度
375304

376305
Eonix内核实现了基于时间片的抢占式调度机制。在时钟中断处理中,系统会调用 `should_reschedule()` 函数检查当前任务的时间片是否已经用尽:
@@ -423,13 +352,6 @@ Eonix内核采用了创新的分层设计来管理任务:
423352
2. **灵活性**:用户态进程可以使用不同的运行时实现,如基于Task的标准运行时或无栈协程的运行时
424353
3. **可靠性**:模块间接口明确,降低了错误发生的可能性
425354

426-
### 多核支持
427-
428-
Eonix内核原生支持多核处理器,通过对称多处理(SMP)架构实现:
429-
- 每个CPU核心有独立的调度器上下文(`LOCAL_SCHEDULER_CONTEXT`
430-
- 每个CPU有独立的就绪队列(`ReadyQueue`
431-
- 全局任务列表(`TASKS`)协调各个CPU之间的任务分配
432-
433355
### POSIX兼容性
434356

435357
Eonix内核的任务管理模块支持POSIX标准中的核心概念:
@@ -442,12 +364,6 @@ Eonix内核的任务管理模块支持POSIX标准中的核心概念:
442364

443365
虽然Eonix内核已经实现了完善的任务管理系统,但以下几个方面仍有优化空间:
444366

445-
### 运行时与POSIX层接口优化
446-
447-
当前的分层设计虽然解耦了不同层次的职责,但有些接口可能存在冗余或不一致,需要进一步精简和统一。主要包括:
448-
- Thread与Task之间的转换接口可以更加优化
449-
- 信号处理机制在两层之间的交互可以进一步简化
450-
451367
### 多核负载均衡
452368

453369
改进跨CPU的任务迁移和负载均衡机制,特别是在CPU核心数量较多时,提高整体系统效率:
@@ -467,87 +383,3 @@ Eonix内核的任务管理模块支持POSIX标准中的核心概念:
467383
│ └── [tid]/ # 线程信息
468384
└── sched # 调度器统计信息
469385
```
470-
471-
### 无栈协程支持
472-
473-
我们希望在内核中加入无栈协程的支持,以提供更高效的异步编程模型。无栈协程可以减少上下文切换的开销,并允许更灵活的任务调度。
474-
475-
#### 无栈协程的优势
476-
477-
无栈协程(Stackless Coroutine)相比传统的有栈协程(如Eonix当前的Task模型)具有以下显著优势:
478-
479-
1. **内存效率**:无栈协程不需要为每个协程分配独立的栈空间,大幅降低内存占用,特别适合高并发场景
480-
2. **切换开销低**:无需保存和恢复完整的栈和寄存器上下文,切换开销远低于有栈协程和线程
481-
3. **更好的编译时优化**:编译器可以对无栈协程进行更彻底的优化,包括内联和状态机优化
482-
4. **更易于调试**:无栈协程的状态更加明确,更容易追踪和调试
483-
484-
#### 实现方案
485-
486-
在Eonix内核中实现无栈协程支持,我们计划采用以下方案:
487-
488-
1. **基于Rust的生成器特性**:利用Rust语言对异步编程和生成器的原生支持,通过`async`/`await`语法构建无栈协程
489-
```rust
490-
// 无栈协程示例
491-
async fn example_coroutine() -> Result<(), Error> {
492-
// 异步操作,不会阻塞内核
493-
let data = read_disk_async().await?;
494-
process_data(data).await?;
495-
Ok(())
496-
}
497-
```
498-
499-
2. **Future执行器优化**:开发专门的内核Future执行器,高效管理和调度异步任务
500-
```rust
501-
pub struct KernelExecutor {
502-
task_queue: LinkedList<Pin<Box<dyn Future<Output = ()>>>>, // 使用一个侵入式链表
503-
// 其他字段...
504-
}
505-
506-
impl KernelExecutor {
507-
pub fn spawn<F>(&mut self, future: F)
508-
where F: Future<Output = ()> + 'static {
509-
self.task_queue.push_back(Box::pin(future));
510-
}
511-
512-
pub fn run(&mut self) {
513-
// 执行调度逻辑...
514-
}
515-
}
516-
```
517-
518-
3. **与现有任务模型集成**:保持与现有Task模型的兼容性,允许两种模式共存
519-
- 通过适配层使无栈协程可以在当前的调度器上运行
520-
- 允许现有代码逐步迁移到无栈协程模型
521-
522-
#### 与现有任务管理系统的集成
523-
524-
Eonix的分层设计为集成无栈协程提供了良好基础。我们计划:
525-
526-
1. 在运行时层(`eonix_runtime`)增加对无栈协程的支持:
527-
- 实现`Future`特性的执行器
528-
- 提供与协程状态相关的唤醒与调度接口
529-
530-
2. 在POSIX抽象层保持现有的线程、进程抽象不变,但内部实现可利用无栈协程:
531-
- 系统调用可以返回`Future`而非直接阻塞
532-
- I/O操作可以异步化,提高并发性能
533-
534-
3. 提供统一的API适配层,使异步代码和同步代码可以无缝协作:
535-
```rust
536-
// 在同步环境中执行异步代码
537-
fn sync_operation() {
538-
runtime::block_on(async_operation())
539-
}
540-
541-
// 在异步环境中执行同步代码
542-
async fn run_blocking<F, R>(f: F) -> R
543-
where F: FnOnce() -> R + Send + 'static,
544-
R: Send + 'static {
545-
// 在单独的线程中执行阻塞操作
546-
}
547-
```
548-
549-
#### 实现挑战与解决方案
550-
551-
由于Eonix内核中有大量现有的阻塞API,迁移到无栈协程模型主要面临需要将所有阻塞操作改造为异步模式,需要大量的接口重构工作的挑战。Eonix 经过我们的改造,其架构已经具备集成无栈协程的能力。我们的分层设计使运行时层和资源抽象层解耦,为引入无栈协程奠定了基础。但由于需要对所有阻塞相关接口进行修改,这是一项工作量巨大的任务,我们暂时没有完全实现。我们只要写一个简单的无栈协程调度器,将 Future 对象放入其中即可。这样即可实现比如真正的 IOWorker (而不是目前基于Task的状态)。
552-
553-
我们计划在未来的版本中逐步引入无栈协程支持,首先从非关键路径开始,如目前已经几乎完全完成的设备驱动,以及部分完成的文件系统操作,然后逐步扩展到核心子系统,最终实现全面的异步编程支持,以进一步提升系统的性能、并发能力和资源利用率。

0 commit comments

Comments
 (0)