0%

总说

在训练营的第四阶段,几乎一直在读东西,一边看一边学asynchronous和Rust,之前几乎没有了解和使用过异步方面的东西。
学习效果不是很好,结营后还得多读多写这一块的代码,继续学习。

细说

第一周:

第二周:

第三周:

  • 读了io_uring的手册,顺带了解了一下使用环形队列和内存映射,无锁环提高性能的原理
  • 读了一下smol源码,主要是futures-lite和async-executor部分
  • 尝试使用iou封装的接口实现简单的异步文件读写,但是实现的waker有问题,没有成功

总结

最后这一阶段,鄙人主要围绕rust的异步编程async/await及经典运行时库tokio展开阅读研究。但并没有深入理解其中原理及实现,更多的是比较浅显地了解,像tokio的有些模块并没有看,还又比较多的疑问在里面。如果后续有时间的话,希望能继续阅读并深入。代码实现上,仅在rcore上实现了一个极为简单的用户态运行时,跑了几个异步任务,有点协程的意思了(。但是并没有实现waker机制,后续考虑进一步实现完善

协程

有栈协程

函数运行在调用栈上,把函数作为一个协程,那么协程的上下文就是这个函数及其嵌套函数的栈帧存储的值,以及此时寄存器存储的值。如果我们调度协程,也就是保存当前正在运行的协程上下文,然后恢复下个将要运行的协程的上下文。这样我们就轻松的完成了协程调度。并且因为保存的上下文和普通函数执行的上下文是一样的,所以有栈协程可以在任意嵌套函数中挂起(无栈协程不行)。

有栈协程的优点在易用性上,通常只需要调用对应的方法,就可以切换上下文挂起协程。在有栈协程调度时,需要频繁的切换上下文,开销较大。单从实现上看,有栈协程更接近于内核级线程,都需要为每个线程保存单独的上下文(寄存器、栈等),区别在于有栈协程的调度由应用程序自行实现,对内核是透明的,而内核级线程的调度由系统内核完成,是抢占式的。

无栈协程

相比于有栈协程直接切换栈帧的思路,无栈协程在不改变函数调用栈的情况下,采用类似生成器的思路实现了上下文切换。通过编译器将生成器改写为对应的迭代器类型(内部实现是一个状态机)。

而无栈协程需要在编译器将代码编译为对应的状态机代码,挂起的位置在编译器确定。无栈协程的优点在性能上,不需要保存单独的上下文,内存占用低,切换成本低,性能高。缺点是需要编译器提供语义支持,无栈协程的实现是通过编译器对语法糖做支持,rust的aysnc\await就是语法糖,编译器将带有这些关键字的方法编译为生成器,以及对应的类型作为状态机。

只有状态机的支持才能进行协程调度,例如Rust中的tokio,基于Future的用户态线程,根据poll方法获取Future状态,它不可以在任意嵌套函数中挂起(同步代码未实现状态机)。

tokio

我主要从tokio::main宏出发,一步步分析其中调用关系。主要围绕runtime库的build及Runtime结构体的方法及函数

使用#[tokio::main]宏生成的代码

1
2
3
4
5
6
7
8
9
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
// ...
})
}

结构体Builder用作运行时runtime配置,通过new_current_thread()使用单线程运行时,new_multi_thread()使用多线程运行时,并会返回Builder实例

new_multi_thread

1
2
3
4
5
6
7
8
9
/// Returns a new builder with the multi thread scheduler selected.
///
/// Configuration methods can be chained on the return value.
#[cfg(feature = "rt-multi-thread")]
#[cfg_attr(docsrs, doc(cfg(feature = "rt-multi-thread")))]
pub fn new_multi_thread() -> Builder {
// The number `61` is fairly arbitrary. I believe this value was copied from golang.
Builder::new(Kind::MultiThread, 61)
}

调用new方法返回一个KindMultiThreadBuilder,对于定时器和I/O事件释放CPU之前需要61ticks

enable_all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/// Enables both I/O and time drivers.
///
/// Doing this is a shorthand for calling `enable_io` and `enable_time`
/// individually. If additional components are added to Tokio in the future,
/// `enable_all` will include these future components.
pub fn enable_all(&mut self) -> &mut Self {
#[cfg(any(
feature = "net",
all(unix, feature = "process"),
all(unix, feature = "signal")
))]
self.enable_io();
#[cfg(feature = "time")]
self.enable_time();

self
}

操作成员变量,使能运行时I/O和定时器

build

1
2
3
4
5
6
7
8
9
10
11
12
/// Creates the configured `Runtime`.
///
/// The returned `Runtime` instance is ready to spawn tasks.
pub fn build(&mut self) -> io::Result<Runtime> {
match &self.kind {
Kind::CurrentThread => self.build_current_thread_runtime(),
#[cfg(feature = "rt-multi-thread")]
Kind::MultiThread => self.build_threaded_runtime(),
#[cfg(all(tokio_unstable, feature = "rt-multi-thread"))]
Kind::MultiThreadAlt => self.build_alt_threaded_runtime(),
}
}

运行时创建的核心步骤,创建已经做配置的runtime,并返回Runtime实例准备创建异步任务

1
2
3
4
5
6
7
8
9
10
pub struct Runtime {
/// Task scheduler
scheduler: Scheduler,

/// Handle to runtime, also contains driver handles
handle: Handle,

/// Blocking pool handle, used to signal shutdown
blocking_pool: BlockingPool,
}

scheduler:异步任务调度器

handle:运行时句柄

blocking_pool:阻塞池句柄,用于发出关闭信号

build -> build_threaded_runtime

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
fn build_threaded_runtime(&mut self) -> io::Result<Runtime> {
use crate::loom::sys::num_cpus;
use crate::runtime::{Config, runtime::Scheduler};
use crate::runtime::scheduler::{self, MultiThread};

let core_threads = self.worker_threads.unwrap_or_else(num_cpus);

let (driver, driver_handle) = driver::Driver::new(self.get_cfg(core_threads))?;

// Create the blocking pool
let blocking_pool =
blocking::create_blocking_pool(self, self.max_blocking_threads + core_threads);
let blocking_spawner = blocking_pool.spawner().clone();

// Generate a rng seed for this runtime.
let seed_generator_1 = self.seed_generator.next_generator();
let seed_generator_2 = self.seed_generator.next_generator();

let (scheduler, handle, launch) = MultiThread::new(
core_threads,
driver,
driver_handle,
blocking_spawner,
seed_generator_2,
Config {
before_park: self.before_park.clone(),
after_unpark: self.after_unpark.clone(),
before_spawn: self.before_spawn.clone(),
after_termination: self.after_termination.clone(),
global_queue_interval: self.global_queue_interval,
event_interval: self.event_interval,
local_queue_capacity: self.local_queue_capacity,
#[cfg(tokio_unstable)]
unhandled_panic: self.unhandled_panic.clone(),
disable_lifo_slot: self.disable_lifo_slot,
seed_generator: seed_generator_1,
metrics_poll_count_histogram: self.metrics_poll_count_histogram_builder(),
},
);

let handle = Handle { inner: scheduler::Handle::MultiThread(handle) };

// Spawn the thread pool workers
let _enter = handle.enter();
launch.launch();

Ok(Runtime::from_parts(Scheduler::MultiThread(scheduler), handle, blocking_pool))
}
  1. 引入

num_cpus:用于获取系统的 CPU 核心数量。

Config:运行时的配置项。

MultiThread:多线程调度器,负责在多个线程间调度异步任务。

  1. 确定核心线程数
1
let core_threads = self.worker_threads.unwrap_or_else(num_cpus);
  • self.worker_threads:用户在Builder配置的核心线程数。如果未指定,使用系统的 CPU 核心数作为默认值(num_cpus())。
  • core_threads 是最终确定的核心线程数,表示运行时的调度线程数量。
  1. 初始化驱动
1
let (driver, driver_handle) = driver::Driver::new(self.get_cfg(core_threads))?;
  • driver:负责管理底层 IO 和定时器的核心组件。
  • driver_handle:运行时与驱动交互的句柄,用于调度异步任务和管理定时器。
  • get_cfg(core_threads):返回结构体driver::Cfg,包含驱动的配置项,包含workers等配置。
  1. 创建阻塞任务池
1
2
let blocking_pool = blocking::create_blocking_pool(self, self.max_blocking_threads + core_threads);
let blocking_spawner = blocking_pool.spawner().clone();
  • 阻塞任务池:用于执行需要阻塞运行的任务(例如文件 IO 或计算密集型任务),避免阻塞异步任务调度线程。

    • 线程数 = 用户指定的 max_blocking_threads + 核心线程数。
  • blocking_spawner:用于将任务提交到阻塞任务池的对象。

  1. 生成随机数种子
1
2
let seed_generator_1 = self.seed_generator.next_generator();
let seed_generator_2 = self.seed_generator.next_generator();
  • Tokio 的调度器可能需要随机化任务的分配,例如在多线程调度器中均衡负载。
  • seed_generator_1seed_generator_2 是生成的两个独立随机数种子,分别用于不同的组件。
  1. 初始化调度器
1
2
3
4
5
6
7
8
let (scheduler, handle, launch) = MultiThread::new(
core_threads,
driver,
driver_handle,
blocking_spawner,
seed_generator_2,
Config { ... },
);
  • 调用 MultiThread::new 创建一个多线程调度器,返回:

    • scheduler:核心调度器,管理线程间任务的分配和调度。
    • handle:调度器的句柄,用于外部与调度器交互。
    • launch:启动调度器工作线程的接口。
  • 传递的参数:

    • core_threads:核心线程数。

    • driverdriver_handle:用于与 IO 和定时器交互的驱动组件。

    • blocking_spawner:用于执行阻塞任务的接口。

    • seed_generator_2:随机数种子,用于内部调度逻辑。

    • Config:调度器的配置,包括以下内容:

      • before_parkafter_unpark:线程挂起与唤醒时的回调函数。
      • before_spawnafter_termination:任务生成和结束时的回调函数。
    • global_queue_intervalevent_interval:全局队列检查和事件处理的时间间隔。

      • local_queue_capacity:本地任务队列容量。
    • 其他运行时的定制项。

  1. 创建运行时句柄
1
let handle = Handle { inner: scheduler::Handle::MultiThread(handle) };
  • Handle 是对调度器的抽象封装,用于外部访问运行时和调度器的功能。
  1. 启动工作线程
1
2
let _enter = handle.enter();
launch.launch();
  • handle.enter():将当前线程设置为调度器的上下文,用于初始化调度环境。
  • launch.launch():启动调度器的所有工作线程,使其开始运行。
  1. 返回运行时
1
Ok(Runtime::from_parts(Scheduler::MultiThread(scheduler), handle, blocking_pool))
  • 创建并返回完整的Runtime实例,包括:
    • Scheduler::MultiThread(scheduler):多线程调度器。
    • handle:运行时句柄。
    • blocking_pool:阻塞任务池。

driver::Driver::new方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub(crate) fn new(cfg: Cfg) -> io::Result<(Self, Handle)> {
let (io_stack, io_handle, signal_handle) = create_io_stack(cfg.enable_io, cfg.nevents)?;

let clock = create_clock(cfg.enable_pause_time, cfg.start_paused);

let (time_driver, time_handle) =
create_time_driver(cfg.enable_time, io_stack, &clock, cfg.workers);

Ok((
Self { inner: time_driver },
Handle {
io: io_handle,
signal: signal_handle,
time: time_handle,
clock,
},
))
}
  1. 创建 IO 栈
1
let (io_stack, io_handle, signal_handle) = create_io_stack(cfg.enable_io, cfg.nevents)?;
  • 调用 create_io_stack 创建 IO 栈(底层 IO 相关的资源),返回:

    • io_stack:IO 栈的核心组件,用于管理底层 IO 操作。
    • io_handle:用于和 IO 栈交互的句柄。
    • signal_handle:用于处理信号的句柄(如处理 Ctrl+C)。
  • 参数解释:

    • cfg.enable_io:如果为 true,启用 IO 支持;否则,不创建 IO 栈。
    • cfg.nevents:指定事件队列的大小,用于多路复用器。
  1. 创建时钟
1
let clock = create_clock(cfg.enable_pause_time, cfg.start_paused);
  • 调用 create_clock 创建时钟组件,用于时间相关功能的管理,返回一个 Clock 对象。
  • 参数解释:
    • cfg.enable_pause_time:是否支持暂停时间。
    • cfg.start_paused:是否让时钟在启动时进入暂停状态。

时钟的主要用途:

  • 为定时器、延迟任务提供精确的时间点。
  • 支持测试或调试时暂停时间的功能。
  1. 创建时间驱动器
1
2
let (time_driver, time_handle) =
create_time_driver(cfg.enable_time, io_stack, &clock, cfg.workers);
  • 调用 create_time_driver

    创建时间驱动器,返回:

    • time_driver:管理时间相关任务的核心组件(例如定时器、延迟任务)。
    • time_handle:与时间驱动器交互的句柄。
  • 参数解释:

    • cfg.enable_time:是否启用时间功能。
    • io_stack:IO 栈,与时间驱动器共享底层的事件循环。
    • &clock:前面创建的时钟,提供时间相关支持。
    • cfg.workers:用于分配时间任务的工作线程数。

时间驱动器的主要功能:

  • 实现异步延迟(如 tokio::time::sleep)。
  • 管理基于时间的任务调度。
  1. 返回结果
1
2
3
4
5
6
7
8
9
Ok((
Self { inner: time_driver },
Handle {
io: io_handle,
signal: signal_handle,
time: time_handle,
clock,
},
))
  • 返回一个包含驱动器和其句柄的元组:
    • Self { inner: time_driver }
      • 驱动器的核心部分是时间驱动器,封装到 Self 结构体中。
    • Handle
      • 包含多个句柄,用于驱动器与外部的交互:
        • io_handle:处理 IO 事件的句柄。
        • signal_handle:处理信号的句柄。
        • time_handle:管理时间任务的句柄。
        • clock:提供时间支持的时钟实例。

tokio启动

通过launch.launch();启动所有的 worker 线程:

1
2
3
4
5
pub(crate) fn launch(mut self) {
for worker in self.0.drain(..) {
runtime::spawn_blocking(move || run(worker));
}
}

runtime::spawn_blocking 调用时, || run(worker) 匿名函数会被传进去,这其实就是 worker 线程要执行的逻辑。

如下,匿名函数会被包装为 BlockingTask,并被放在 blocking thread 的 run queue 中,这样当它运行时就会执行这个匿名函数。因为这时没有足够的线程,就会初始化一个新的 OS 线程(如果有 idle 的线程,就会通过 condvar 通知),并开始执行 blocking 线程的逻辑。每个 worker 都占用一个 blocking 线程,并在 blocking 线程中运行直到最后。

1
2
3
4
5
6
7
8
9
10
11
// runtime::spawn_blocking:
let (task, _handle) = task::joinable(BlockingTask::new(func));

let mut shared = self.inner.shared.lock();
shared.queue.push_back(task);

let mut builder = thread::Builder::new(); // Create OS thread
// run worker thread
builder.spawn(move || {
rt.blocking_spawner.inner.run(id);
})

作为常年开发网络程序的我,对异步、并发操作却仅仅停留在使用层面,这对我来说是一件长期的困扰。于是在第四阶段,我毫不犹豫地选择了“基于协程异步机制的OS”方向。在这个方向上,我学到了很多新的知识,也遇到了很多新的问题。

第一周

第一周的任务主要是了解 Rust 中异步的基本概念,通过阅读资料,我了解到了 Rust 中的 asyncawait 关键字背后的原理,并且了解到了如何实现有栈/无栈协程。过去我只了解过有栈协程,而无栈协程对我来说是一个全新的概念,其 LLVM Generator 的实现机制让我感到十分精妙。在这一周中,我主要是通过阅读资料和代码来了解这些概念,对于这些概念的理解还不是很深入。

第二周

第二周主要的工作是阅读 Tokio 代码,我阅读了 Tokio 的 netsignalsync 模块,我将部分代码的分析记录在了共享文档中,这些代码的阅读让我对异步编程有了更深入的理解。

第三周

第三周通过阅读 epollio-uring 等相关资料,我了解到了异步 IO 的原理。通过阅读 async-iopolling 的代码,我了解了 epoll 在 Rust 异步运行时中的运用,而通过阅读 monoiotokio-uring 的代码,我了解了 io-uring 在 Rust 异步运行时中的运用,这些代码的阅读让我对异步 IO 有了更深入的理解。io-uring 的机制起初让我感到困惑,但是在领悟到 io-uring 事实上是一个异步的系统调用框架而不是 epoll 这样的文件描述符复用机制后,我便茅塞顿开。

在阅读了大量相关的资料后,我也着手开始实现一个简单的异步运行时,通过对 mini-rust-runtime 的学习和修改,我将其中使用 polling 实现的基于 epoll 的异步运行时改为了基于 io-uring 的异步运行时,并初步支持了文件的异步读写和 TCP 连接。因为使用 io-uring 必须从系统调用层面进行编程,抽象层次很低,在查阅了大量资料后我才勉强完成了这个十分粗糙的实现。

总结

毫不夸张的说,这短短的三周我学到了近年来对我来说最有价值的知识,使我对异步编程的理解有了质的飞跃,使我未来能更好地开发出高性能的网络程序。在学习的过程中,我也遇到了很多问题,但是通过查阅资料和请教老师,我都得到了解决。这次训练营对我来说是一次非常有意义的经历,我也希望未来能有机会继续参加这样的活动。

第一周:

有栈协程与无栈协程

协程这块的概念可能比较混乱,不太能说得清,但是计科毕竟大多数情况下也不是深究定义的,所以纤程、绿色线程、协程大体上是可以混为一谈的,以下我们统称为协程。
尽管名称可能不同,但它们都可以被划分为两大类,一类是有栈(stackful)协程,一类是无栈(stackless)协程。
此处的有栈和无栈指的不是指协程在运行时是否需要栈,(毕竟不能回到“远古时代”的面条式编程)对于大多数语言来说,一个函数调用另一个函数,总是存在调用栈的;而是指协程是否可以在其任意嵌套函数中被挂起。有栈协程是可以的,而无栈协程则不可以。

有栈协程

实现一个协程的关键点在于如何保存、恢复和切换上下文。
在有栈协程中,我们将函数作为协程,保存上下文也就是保存从这个函数及其嵌套函数的栈帧存储的值。恢复上下文则是将这些值重新写回对应的栈帧和寄存器当中。切换上下文也就是保存当前的上下文,恢复下一个要执行的函数的上下文。有栈协程就是这么地朴素,也是我这种刚听说协程的萌新最易于理解的一种协程实现。

无栈协程

相比于无栈协程直接切换栈帧的思路,无栈协程则没有改变调用栈。而是使用了生成器(一种特殊的迭代器,能够在函数的执行过程中保存状态,并在需要时恢复执行)。这种特性可以用来模拟协程切换的行为,从而实现上下文切换。

无栈协程就是把代码转换成状态机,我们可以将 Future 视为一种协程。

rust的 Future 是通过状态机的形式来实现异步操作的。每个 Future 都是一个状态机,表示一个可能尚未完成的计算。
它通过轮询(polling)的方式推进计算过程,直到完成。
poll 方法会被异步运行时反复调用,当 Future 返回 Poll::Pending 时表示还没准备好,当返回 Poll::Ready 时表示完成。
所以,对future的运算实际上也就可以视为是在执行协程。

它不依赖操作系统或运行时的栈切换,而是通过将状态信息嵌入到 Future 的数据结构中。这样可以在编译时生成高效的代码来管理异步操作。

rust的协程

rust在古早版本(1.0之前)曾经有过一个有栈协程的方案——绿色线程。但是由于不符合零成本抽象的思想被移除了。此外,对于rust而言,绿色线程需要尽可能地减小预分配的堆栈大小,进而降低内存上的开销,毕竟需要比操作系统的线程更加轻量级,否则为什么不直接使用操作系统的线程呢?
之后,rust使用了无栈协程的方案,虽然增加了开发上的复杂度,但是良好地解决了并发问题。

其他

阅读了:

https://os.phil-opp.com/async-await/

两百行实现绿色线程:
https://zhuanlan.zhihu.com/p/100058478

第二周:

基本上就读读tokio和smol的源码,但是说实话,没太看明白,但是smol确实会更简单易懂一点。剩下的就是在读io_uring

io_uring机制

io_uring的设计主要围绕着环形缓冲区实现,SQ和CQ都是环形的,并且大小都是2的n次方(位运算奇技淫巧 index % size == index & (size - 1),当且仅当size为2的n次方时成立)。并且由于 UInt 的回环,所以可以直接tail - head,这种设计的一个优势是可以利用环的完整大小,而无需额外管理“环已满”标志。

应用程序与内核通过io_uring交互的过程大致如下:

应用程序通过提交队列SQ将SQE(IO请求)提交给内核,内核操作完成之后通过完成队列CQ将CQE(完成的IO请求)写回给应用程序,应用程序自行处理已完成的事件。

SQ 和 CQ 是内核与应用程序共享的内存区域,这样子,我们避免了频繁的用户态和内核态之间的切换,并且支持批量地提交请求,也减少了系统调用的次数,提高了性能。此外,由于 io_uring 可以直接访问用户态提供的缓冲区,避免不必要的内存拷贝操作。

其他

看了io_uring的一些资料:

https://kernel.dk/io_uring.pdf

https://zhuanlan.zhihu.com/p/361955546

稍微看了看:

https://tony612.github.io/tokio-internals/01.html

第三周:

实现运行时

大体上是参考了 async-rt-bookmaglev 这两个写出来的

仓库在my_runtime

其他

读了点与异步协程运行时以及IO_Uring有关的一些资料

一个实验性质的运行时: https://github.com/ringbahn/maglev

训练营内的一位同学的博客:https://zhuanlan.zhihu.com/p/12850908116

rust与io_uring: https://zhuanlan.zhihu.com/p/346219893

无栈协程: https://mthli.xyz/coroutines-in-c/

第四阶段选择了基于协程的操作系统,但并没有选择引入协程到操作系统的方向,不过也有相关思考,这段时间主要学习了rust的协程以及异步运行时的功能以及如何设计

协程只是可以挂起和恢复的函数

函数只有2个行为:调用和返回,函数返回后,栈上所拥有的状态会被全部销毁,协程则可以挂起,协程挂起时可以保留协程上下文,恢复则恢复协程的上下文,协程的上下文取决于协程内的局部变量等,反正是比线程上下文小,当协程像函数一样返回,协程也要被销毁

Cpp 协程和Rust协程的对比

cpp和rust同为无栈协程,但设计不同

  • 对于协程的调用者

cpp的做法是协程的调用者会得到来自promise_type结构体内get_return_object方法返回的对象,这里一般通过form_promise构造协程句柄coroutine_handle,调用者可以通过协程句柄resume协程和destroy协程,cpp的协程不是lazy的,这点和rust不一样,相当于拿到future后直接开始poll,不过cpp不需要waker,cpp的handle不需要程序员提供函数也不需要提供指针,已经构造好了,而rust需要rawwaker,需要一个函数表定义行为,再传入任务对象的指针,让waker能够操作任务对象,自然而然就能调度这个任务了,cpp只需要管理好何时resume,但rust是loop poll,程序员管理何时把future重新加入被poll的队列

rust则是每一个async函数都会自动生成并返回一个future对象,通过future trait可以看到主要是通过poll来执行协程内代码以及状态切换,这里贴一下tokio教学文档里的例子

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
33
34
35
impl Future for MainFuture {
type Output = ();

fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>)
-> Poll<()>
{
use MainFuture::*;

loop {
match *self {
State0 => {
let when = Instant::now() +
Duration::from_millis(10);
let future = Delay { when };
*self = State1(future);
}
State1(ref mut my_future) => {
match Pin::new(my_future).poll(cx) {
Poll::Ready(out) => {
assert_eq!(out, "done");
*self = Terminated;
return Poll::Ready(());
}
Poll::Pending => {
return Poll::Pending;
}
}
}
Terminated => {
panic!("future polled after completion")
}
}
}
}
}

rust中.await会变成对其future的poll,而waker则需要在对最外层future的poll时构造进context作为形参,通过poll的结果决定是否挂起,但是rust协程没有恢复这个操作,rust的协程是通过waker把任务重新调度回来再poll

cpp使用Awaiter来控制协程,符合直觉的操作,没有rust那么绕,resume就是直接重新进入协程

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
enum State {
Start,
YieldValue,
FinalSuspend,
Done
};

struct CoroutineStateMachine {
State current_state = Start;
int a = 1, b = 1; //

// promise_type的引用,协程的promise接口
promise_type& promise;

void resume() {
try {
switch (current_state) {
case Start:
// 执行 initial_suspend
if (promise.initial_suspend()) {
current_state = YieldValue;
return; // 挂起
}
// 进入协程主体
[[fallthrough]];

case YieldValue:
while (a < 1000000) {
// co_yield a
promise.yield_value(a);
current_state = YieldValue;
std::tie(a, b) = std::make_tuple(b, a + b);
return; // 挂起
}
// co_return
promise.return_void();
current_state = FinalSuspend;
[[fallthrough]];

case FinalSuspend:
// 执行 final_suspend
if (promise.final_suspend()) {
current_state = Done;
return; // 挂起
}
// 结束
[[fallthrough]];

case Done:
return; // 协程结束
}
} catch (...) {
// 异常处理
if (!promise.initial_await_resume_called()) {
promise.unhandled_exception();
}
}
}
};
  • 内存布局

cpp是全都开堆上,而rust的future可自由选择在栈上还是堆上,对于自引用的结构体,当其被协程捕获作为协程的局部变量时,不允许转移所有权,我们使用Pin来进行保障,因为引用所代表的地址已经被标记为无效

异步运行时设计

协程是用户态的任务调度机制,而线程是内核的任务机制,做的事和内核一样,不断执行不同任务,不过是协作式调度,我们需要手动挂起来让其他任务运行,设想单线程环境下的任务调度,我们需要把任务存储在一个集合中,一个接一个运行协程,协程挂起时就再放入集合中。初步想法是这样的,但我们又不在内核态,完全可以把任务交给内核,而不是协程一挂起就重新放回集合,当集合为空时我们的线程就可以休息(多线程环境下其他线程使协程重新加入集合并把休眠的线程唤醒[把阻塞任务丢给了专门的线程])或是主动检查异步任务是否准备完成(阻塞调用不在用户态[需要内核的异步支持])

这样我们的线程可以把阻塞都丢给其他线程或是内核执行,而本身只需要处理更多的任务,提高并发量

和线程一样,我们也需要一个调用在协程里创建一个协程,我们需要spawn,有时候我们需要等待另一个协程的结果(仍然不会阻塞,因为外层的协程也挂起了),我们要为此添加JoinHandle,如果我们持有一个线程池来执行任务,spawn出的协程就需要一个面对线程池调度的waker,当资源准备好时加入线程池所用的任务队列,但当我们对其JoinHandle进行.await的话我们需要把当前协程的waker和其默认的waker进行替换,因为我们需要这个原本自由的协程在我们等待他的协程上恢复而不是在线程池中恢复后,任务的返回值不被关心,等待自然drop,使用线程池我们可以把同步调用异步化,让阻塞在单独的线程上运行,如果使用io_uring的话就更轻松了

io_uring

io_uirng是真正的异步io,通过他我们可以实现上述的第二种方案,当异步任务都处理完了我们的线程就检查完成队列,然后重新加入集合中,进行poll,此时资源已经准备好,不会造成任何阻塞

对协程引入操作系统的想法

考虑多核情况下,每个核心运行一个协程执行器,对于Mutex或信号量如果资源申请失败那么直接把任务挂起,把waker和任务指针记录下来,当资源被释放时,自动寻找等待队列的第一个元素,通过waker提供的函数表操作任务指针,使其重新加入对应核心的任务执行队列中。协程在内核态给我感觉是和序列生成器一般,能在一些部分减少时间片的浪费,提高任务处理的效率

x86 架构 hypervisor SeaBIOS 引导与 Linux 启动实现

1. seabios 工作流程

1
2
3
4
(1) POST( Power On Self Test):上电自检,BIOS 对计算机硬件(CPU、主板、内存等)的检测。
(2) POST 之后的初始化与启动相关硬件(磁盘、键盘控制器等)。
(3) 为 OS 创建一些参数,如 ACPI、E820 表等。
(4) 选择引导设备,从设备中加载 BootLoader,进而启动操作系统。

2. qemu 加载seabios过程

1
2
3
(1) qemu加载 seabios 在地址的 4G 最顶端的 LOW_MMIO 区,以及低 1M 区域各有一份。
(2) cpu 的第一条取指地址为 0xFFFFFFF0,该地址指向贴近 4G 的 BIOS 的最后 16 个字节,这也是 BIOS 的第一条指令。
(3) BIOS 最后 16 个字节处,是一个长跳转指令,目的就是换到低 1M 段空间去执行 entry_post ( ORG 0xe05b )

3. kbuild 使用方法

1
2
3
4
5
6
7
8
参考: https://github.com/Starry-OS/Starry

# To download the tool
$ cargo install kbuild
$ mkdir crates
$ kbuild patch add axstarry
$ kbuild patch remove axstarry
$ kbuild patch list

4. seabios 编译方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cat > .config << EOF
# for qemu machine types 2.0 + newer
CONFIG_QEMU=y
CONFIG_ROM_SIZE=256
CONFIG_ATA_DMA=n

CONFIG_XEN=n

CONFIG_DEBUG_LEVEL=9
CONFIG_DEBUG_SERIAL=y
EOF
echo "CONFIG_DEBUG_LEVEL=9" >> .config

make PYTHON=python3 oldnoconfig
make

5. seabios 反汇编

1
2
3
4
objdump -D -b binary -m i8086 bios.bin
objdump -D -b binary -m i8086 romlayout.o

-M intel : 指定intel格式

6. kvm 中所有 port IO

所谓端口Port IO, x86上使用in out指令进行访问, 和内存的地址空间完全隔离.(ARM上没有PIO) Guest以Linux为例: cat /proc/ioports查看当前OS的所有的ioports :

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
33
34
35
36
37
38
  0000-0cf7 : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-0060 : keyboard
0064-0064 : keyboard
0070-0077 : rtc0
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
03c0-03df : vga+
03f8-03ff : serial
0510-051b : QEMU0002:00
0510-051b : fw_cfg_io
0600-067f : 0000:00:1f.0
0600-0603 : ACPI PM1a_EVT_BLK
0604-0605 : ACPI PM1a_CNT_BLK
0608-060b : ACPI PM_TMR
0620-062f : ACPI GPE0_BLK
0630-0633 : iTCO_wdt.0.auto
0630-0633 : iTCO_wdt
0660-067f : iTCO_wdt.0.auto
0660-067f : iTCO_wdt
0700-073f : 0000:00:1f.3
0700-073f : i801_smbus
0cf8-0cff : PCI conf1
0d00-ffff : PCI Bus 0000:00
1000-1fff : PCI Bus 0000:01
2000-2fff : PCI Bus 0000:02
3000-3fff : PCI Bus 0000:03
4000-4fff : PCI Bus 0000:04
5000-5fff : PCI Bus 0000:05
6000-6fff : PCI Bus 0000:06
7000-7fff : PCI Bus 0000:07
c040-c05f : 0000:00:1f.2
c040-c05f : ahci

7. 项目实现总结

项目刚开始, 我把seabios当作 kernel,写了个简单的 bios 来引导 seabios ,seabios成功运行

1
2
3
4
5
6
7
8
9
10
11
12
.section .text
.code16
.global entry16
entry16:
cli
cld

xor ax, ax
mov ds, ax
mov es, ax

ljmp 0xf000, 0xe05b

后面通过学习vmcs的使用方法,增加了CS寄存器的设置后,seabios 可以自启动成功。

1
2
3
4
VmcsGuestNW::RIP.write(entry.as_usize() & 0xffff)?;
VmcsGuest16::CS_SELECTOR.write(((entry.as_usize() >> 4) & 0xf000) as u16)?;
// On Intel requires 'base' to be 'selector * 16' in real mode.
VmcsGuestNW::CS_BASE.write(entry.as_usize() & 0xf0000)?;

对应的linux.tmol修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cpu_num = 1
phys_cpu_sets = [1]
entry_point = 0xf_e05b
bios_path = "bios-256k.bin"
bios_load_addr = 0xc_0000
kernel_path = "arceos-x86_64.bin"
kernel_load_addr = 0x100_0000
# ramdisk_path = ""
# ramdisk_load_addr = 0
# disk_path = "disk.img"
# Memory regions with format (`base_paddr`, `size`, `flags`).
memory_regions = [
[0x0000_0000, 0x1_0000, 0x13], # IO Port 64K 0b10011
[0x0001_0000, 0x400_0000, 0x7], # Low RAM 64M 0b111
[0xfec0_0000, 0x1000, 0x17], # IO APIC 4K 0b10111
[0xfee0_0000, 0x1000, 0x17], # Local APIC 4K 0b10111
[0xfed0_0000, 0x1000, 0x17], # HPET 4K 0b10111
]
# Emu_devices
# Name Base-Ipa Ipa_len Alloc-Irq Emu-Type EmuConfig
emu_devices = [
]

Seabios 加载内核流程,seabios加载内核是通过 fw_cfg 的 file 接口,读取 multiboo.bin 当作 rom 来加载的,这个 multiboo.bin是linux内核封装过的带有 0x55aa 标记的可以引导的 rom,seabios读取到 rom后,加载到内存中然后执行。整理需要实现内容如下(“对号” 为截至此笔记已完成的):

    1. seabios第一条指令地址为: 0xf000:0xe05b, 支持设置primary vcpu第一条指令地址 entry_point.
1
2
1. 目前实模式下还不支设置超过0xffff的地址
2. 考虑设置代码段 CS 寄存器
    1. 设置虚拟化需要截获的io端口

      1
      有些端口需要进行截获, 否则会透传到宿主机, 获取宿主机的信息, 例如pci信息, 内存大小信息等
    1. dma 实现支持

      1
      很多数据的传输需要通过 dma 传输
    1. 实现fw_cfg设备模拟
- [x] fw_cfg 实现 pio, 设备地址 [0x510, 0x511]
1
告诉seabios, 虚拟化环境为 “QEMU”
- [ ] fw_cfg 实现 dma, 设备地址 [0x514]
1
用于传输数据, 例如内核data数据等
    1. 实现rtc设备模拟, 设备地址 [0x70, 0x71]
1
在虚拟化环境中, seabios 通过 rtc 几个保留的寄存器获取内存大小信息
    1. multiboot 实现

      1
      seabios通过内核启动是通过multiboot协议启动的, 需要将内核文件进行重新封装
  • 其他 …

修改链接如下:

https://github.com/hbuxiaofei/arceos-umhv/tree/support-seabios

https://github.com/hbuxiaofei/axvcpu/tree/support-seabios

https://github.com/hbuxiaofei/x86_vcpu/tree/support-seabios

https://github.com/hbuxiaofei/axvm/tree/support-seabios

https://github.com/hbuxiaofei/axdevice/tree/support-seabios

运行日志:

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
[  0.307806 0:2 axvm::vm:230] Booting VM[1]
[ 0.308076 0:2 arceos_vmm::vmm:40] VM[1] boot success
[ 0.308390 0:2 axtask::run_queue:393] task block: Task(2, "main")
[ 0.308757 0:3 axtask::task:471] task drop: Task(4, "")
[ 0.309079 0:3 axtask::run_queue:393] task block: Task(3, "gc")
[ 0.309436 0:5 arceos_vmm::vmm::vcpus:240] VM[1] Vcpu[0] waiting for running
[ 0.309852 0:5 arceos_vmm::vmm::vcpus:243] VM[1] Vcpu[0] running...
[ 0.310227 0:5 x86_vcpu::vmx::vcpu:118] VmxVcpu bind to current processor vmcs @ PA:0x5b2000
[ 0.310751 0:5 axvm::vm:258] >>>>> exit_reason IoWrite {
port: 0x70,
width: Byte,
data: 0x8f,
}

[ 0.311332 0:5 axvm::vm:289] IoWrite: 0x70 Byte 0x8f
[ 0.311646 0:5 axdevice::device:96] emu: GPA:0x70..GPA:0x72 handler write port:GPA:0x70 width:1 val:0x8f
[ 0.312180 0:5 axdevice::rtc:54] Rtc select 0xf

[ 0.312482 0:5 axvm::vm:258] >>>>> exit_reason IoRead {
port: 0x71,
width: Byte,
}

[ 0.312984 0:5 axvm::vm:278] IoRead: 0x71 Byte
[ 0.313268 0:5 axdevice::device:79] emu: GPA:0x70..GPA:0x72 handler read port:GPA:0x71 width:1
[ 0.313758 0:5 axdevice::rtc:81] Rtc read addr: GPA:0x71 GPA:0x71

[ 0.314130 0:5 axdevice::rtc:62] Rtc get index: 0xf

[ 0.314454 0:5 axvm::vm:258] >>>>> exit_reason Nothing

[ 0.314787 0:5 x86_vcpu::vmx::vcpu:131] VmxVcpu unbind from current processor vmcs @ PA:0x5b2000
[ 0.315285 0:5 x86_vcpu::vmx::vcpu:118] VmxVcpu bind to current processor vmcs @ PA:0x5b2000
SeaBIOS (version 1.16.0-20241104_115553-centos83-dev)
BUILD: gcc: (GCC) 8.5.0 20210514 (Red Hat 8.5.0-4) binutils: version 2.30-108.el8_5.1
enabling shadow ram
[ 0.316631 0:5 axvm::vm:258] >>>>> exit_reason IoWrite {
port: 0xcf8,
width: Dword,
data: 0x80000000,
}

[ 0.317243 0:5 axvm::vm:289] IoWrite: 0xcf8 Dword 0x80000000
[ 0.317586 0:5 axdevice::device:96] emu: GPA:0xcf8..GPA:0xd00 handler write port:GPA:0xcf8 width:4 val:0x80000000
[ 0.318159 0:5 axdevice::pci:210] >>> axdevice pci write GPA:0xcf8 0x80000000...

[ 0.318592 0:5 axdevice::pci:87] >>> set address 0x0 : device:0x0 : 0x0 : 0x0

[ 0.319020 0:5 axvm::vm:258] >>>>> exit_reason IoRead {
port: 0xcfc,
width: Word,
}
read QEMU_CFG_SIGNATURE 85(U)
Found QEMU fw_cfg
>>> qemu_cfg_read_entry start ...
>>> cfg read qemu_cfg_read over
>>> qemu_cfg_read_entry over ...
QEMU fw_cfg: 956659(0xe98f3) 0x2
QEMU fw_cfg DMA interface supported
>>> qemu_early_e820 call qemu_cfg_read_entry, port:0x19
>>> qemu_cfg_read_entry start ...
>>> cfg read qemu_cfg_dma_transfer 0x6f80 4
>>> dma outl: 0x518 0x806f000000000000
[ 0.508528 0:5 axvm::vm:258] >>>>> exit_reason IoWrite {
port: 0x518,
width: Dword,
data: 0x206f0000,
}

[ 0.509263 0:5 axvm::vm:289] IoWrite: 0x518 Dword 0x206f0000
[ 0.509671 0:5 axdevice::device:96] emu: GPA:0x510..GPA:0x522 handler write port:GPA:0x518 width:4 val:0x206f0000
[ 0.510356 0:5 axdevice::fwcfg:238] >>> do_write GPA:0x518 4 0x206f0000

[ 0.510837 0:5 axdevice::fwcfg:226] dma_write: GPA:0x518 0x206f0000

>>> dma outl over: 0x518 0x806f000000000000
QEMU: Terminated

参考文档:

SeaBIOS实现简单分析

浅度剖析 SeaBIOS 之 QEMU 初始化

<<Qemu/kvm源码解析与应用>> - 李强

三、四阶段学习总结

ArceOS及Starry项目

ArceOS

ArceOS是一个组件化内核,我的理解就是把传统内核中的那些模块都变成一个一个的组件,如果你需要fs相关的功能,你就把这个包弄进来,编译进来,然后就可以在里面直接用他的功能。这样给使用这个内核的人一种灵活性,同时每个模块相对独立,也让不同的模块之间的耦合较松,维护进来也很方便。

另一个好处就是我可以在这个项目的基础上做出我自己的扩展,比如说第四阶段的项目Starry。或者是如果我只需要用到其中的一些功能,例如我在一些嵌入式设备上使用,那额外的功能就可能完全没有用,还会有额外的开销,我就可以把我不需要的功能全都干掉。

具体来说主要看了的一些模块:

  • axhal管理了和平台相关的那些东西,这样可以把平台相关的代码(尽量)全都压缩到一个模块里,只暴露出一个接口就可以了,避免漏得到处都是
  • axmm实现的是虚拟内存管理相关的功能,提供了一些有关的抽象。里面用了MemorySet这个东西,把具体的实现给分开了(到Backend里),扩展性也比较好。
  • axtask实现了任务管理的功能,提供了很好用的接口,每一个任务可以被很容易地创建。这个项目里用来实现用户进程也很方便。
  • axfs实现的是文件系统相关的功能。
  • axns实现的是类似于Linuxnamespace的功能,可以让一些不同的应用有自己独占的资源。

等等

Starry

Starry是在ArceOS基础上做的一个宏内核的项目。我的理解大概就是,使用ArceOS提供的这些模块的基本功能,在这个基础之上做出我们常见的宏内核的样子,并且可以和我们现在常用的Linux等内核实现兼容,这样有很多的好处。

比如其中之一就是,用传统的方式来实现一个宏内核,很有可能会得到一个很乱很庞大的代码树。但是ArceOS他每个模块都分开来,本身各个模块的分工都是很清晰的,我们只需要用这些已有的功能,把我们需要的东西给组装起来,然后再加上额外的一些东西就可以了。这样我们写起来也很快乐,得到的代码也比较好理解。

我做的事

一句话说一下的话,就是在这个项目框架的基础上实现了一些系统调用。

任务管理

ArceOS的任务管理基本上是保持了简单的风格,有了大多数我们所必需的任务管理的内容,同时也把类似于Linux中的进程组啊,会话啊这些内容给拿掉,保持了简洁。他有一个TaskExt的设计,就是如果我们需要什么额外的功能,我们可以在这个位置加上我们所需要的field,这样就实现了扩展性。

就比如在这个项目里,我们对其扩展,加上了pid的字段。ArceOS中的任务已经有一个编号了,但是我们希望将对用户空间可见的部分和ArceOS中这些实现相关的部分给分开来。所以我们再添加了一个额外的pid,专门用于用户空间进程的管理,以后如果我们实现了进程组,会话等,可以再在这些加上pgidsid等,或者可以加上侵入式链表来把这些Task给串起来等。

然后还加了用于实现brk的变量,记录当前进程的break的位置。另外,用户空间程序的地址空间和Context也放在这里。因为每一个用户态程序都需要自己的(也有可能和其他人共用)虚拟地址空间,所以把这些放在这个位置,而不需要去修改原本ArceOS的任务部分。

AxNamespace

其中还用到的一个部分就是AxNamespace,但是这个用的觉得有一些问题。这个东西本来的作用大概就是我可以虚拟出几个空间,在这些空间里的进程都有自己的一些资源。在AxNamespace的描述里说,可以实现每进程专属的资源。他的实现是类似于 Thread Local,我有一个全局的数据,然后我可以选择,对于每一个任务,我在访问的时候要不要全都用这个全局的数据,还是每个任务都有自己的数据。

实际使用中发现,他在初始化每个任务自己的数据的时候,是直接把全局的这个数据给Copy过来。也就是说,如果我想把东西放到这个里面,那我需要这个东西都是Copy的。但是CURRENT_DIR这样的东西,他用的是AxResource,或者也叫Arc<Mutex<T>>,那把这个东西直接复制其实应该是会造成一些问题。其一就是他根本没有实现希望有的复制的语义。第二个就是这样实际会造成内存泄漏,因为这样相当于是造出了一个没有引用计数的Arc。如果我要是用一个Copy的东西呢?我觉得这样也比较麻烦。。。或者说这个实现在某种意义上有点像TLS。所以说这个AxNamespace的使用是我感觉有些疑惑的一个点。

实现的系统调用

实现了clone,但是具体的flags没有处理,只按照fork的语义+返回并且用给定的栈。这个位置应该加上Copy on Write的支持,在MemorySetBackend里加上这个,给每个页一个标记,然后在处理Page Fault的时候如果这个标记有了,就把这个页给复制一份,然后取消共享(虽然因为时间的关系没有实现)。我觉得Backend这个设计很好,在一些库里可以看到。这个设计让我们可以很方便地扩展三方库给的一些功能,同时不破坏这个库本身的代码和结构。

内存管理相关

现在的内存管理部分还是非常的简单,没有CoW,也没有懒分配,这个是我希望可以进一步完善的部分。axmm还有其他的一些库提供了很好的抽象,我觉得在这些基础上完成这个部分应该会相对比较轻松。

文件系统相关

实现了opencloseread还有dup等。这些都比较简单,因为ArceOS本身已经大致提供了这些功能的函数了,我们只需要把这些功能给封装一下。

在做这个部分的时候,想到了一个玩法。因为Starry本身就是一个ArceOS的应用,那我们可不可以在一个ArceOS的基座上同时跑两个Starry呢?现在这样实现,他们的进程之间就是分开的(都在TaskExt中有各自管理的pid),并且我们可以将他们在文件系统中的根目录给分开,比如说ArceOS这边的/mounta是其中一个的根,/mountb是另外一个的根。这样有些类似于是容器了,但是可能能提供更好的隔离性?

碎碎念

期末周杀我,这次项目中没做很多的事,还是感觉很遗憾吧。但是看了ArceOS的架构以后,感觉有很多的收获,了解到了一些新的想法。希望有时间的时候,可以进一步完善这个项目。参加这次训练营非常开心。

阶段一-Rustlings

这个阶段主要是需要进行rust相关的学习.

其实早就有接触过rust,但是语法,尤其是所有权和生命周期这块,一直还比较生疏.

有关于所有权相关的内容主要是看了这个视频:如何一步一步推导出Rust所有权、借用规则、引用类型以及秒懂生命周期标注_哔哩哔哩_bilibili

其中关于rust作为无gc语言,大部分内存都是保存在栈上的,只有少部分是保存在栈里边.

‘a指出返回的引用与输入参数x、y之间有关联.

这个非常非常重要.

阶段二-OS内核实验

ch0-2

可以参见我之前复现rCore-Tutorial-Book-v3 3.6.0-alpha.1 文档的笔记,被公开在博客园上.

[winddevil的笔记 - 博客园

这次学习主要是从ch3~ch8的学习,重点换做了解决通过测例,和一些自己的问题.

ch3

主要是讲的一个任务切换的流程,有了任务切换之后又通过定时器中断实现了抢占式调度.

替代文本|800

ch4

这一章主要解决的是一个虚拟地址转换为物理地址的过程,说是虚拟地址,我原来以为真的改变了地址,实际上每一次调用资源都还是使用了物理地址的,利用地址空间对所有的需要访问具体物理地址的对象进行操作.

替代文本|800

ch5

这一章同样是承接了上一章的知识,讲的主要是一个进程的概念,加入了很多新的结构体,后边我应该会有时间的时候更新一下图片.

进程中最巧妙的就是使用了fork这个复制一个任务的操作,有了进程,那么就可以实现编程的简洁性,倭只需要编写一个小任务,然后再进行组合,而不是调用fn,然后自己设计各种分支结构.

有了进程,相当于把调度的工作委托给了os.

ch6

在上一章的基础上,引入了块和文件系统.

这一部分的知识学的非常的不牢靠.

但是让我印象深刻的地方是,这一章基本实现了我对之前学习的时候发现windows是可以直接”点开”应用这个操作的好奇.

那么应用保存在哪里,为什么我用U盘拷贝了还是可以继续运行.

之前学习单片机的时候很少想到我可以通过什么东西对”可执行”的东西进行操作.

通过二进制文件进行加载然后运行的操作属实惊艳到了我.

ch7

这个和ch4更加相关.之前运行rtos的时候总是想着,那么这个变量可以直接以全局变量的方式进行传输为什么我要使用各种比如信号量比如邮箱的方式,现在就一目了然了.

因为地址空间的不同所以进程之间的通信需要通过管道,也就是需要经由操作系统这一层.

ch8

这一部分让我想起了之前进行单片机编程的时候的临界段保护操作,那时候是通过非常暴力的方法关掉了所有的中断以保证这次读取不会出现问题.

或者使用原子操作保证中断无法打断单一时钟下的操作.

这里并没有和硬件和中断打交道,而是选用了三种方式,加锁\条件变量\信号量的方式.

使用银行家算法进行了调度,算法不难,但是调用本身很麻烦,需要在每一次加锁的时候对题中的变量进行操作.并且每一次上锁的时候都需要detect,那么对上锁的程序也必须进行改造.

本次体验

越到后边越忙,如果有幸进入下一个阶段一定不能好高骛远多线程操作,一定要留足时间给自己.

详细编写过程

附在ArceOS igb网卡驱动编写上

体会

前边很多时间用在流控、过滤器上边。由于igb-drive和ixgbe-drive的抽象化不一样。尤其是对ring和dma内存的结构的抽象。

比如把ring初始化的时候的操作,需要适配Tx和Rx。看文档的时间花了很久,具体怎么抽象化反而做的很差。

最后igb-drive卡在Descriptor构成之后想要写入Tail发送,但是想不出怎么进行发送上边。

很难解决具体的问题。

后来看到群友有人魔改ixgbe-dirve,得到成功。进而参考学习,自己也可以通过修改本地的ixgbe驱动来实现成功的httpserver。

并且看到有的人参考的驱动是旧版本的linux驱动,而我参考的是新版本的驱动,这就体现了我策略性的问题。

后续

不管这次结果如何,仍然想要参加下一期的学习,并且尝试自己完成igb驱动的编写,并且放到自己的blog中去。

解决rust-analyzer的问题

创建:.vscode/settings.json:

1
2
3
{
    "rust-analyzer.cargo.features": ["axstd"]
}

这里一定要注意对rust-analyzer进行Restart Server,这个操作也可以见我的这篇博客.

直接用vscode打开wsl环境下的工程

直接在wslcmd下输入code ..

这里可能会显示在安装VSCode Server,不是在安装VSCode本身,而是在安装一个服务,安装好自己就帮你打开了.

组件化内核

不同的内核要求就有不同的设计.

Linux是宏内核就照着宏内核设计.

那么组件化内核就照着组件化内核设计.

复杂的内核=很多小的内核合起来的

宏内核和Hypervisor与Unikernel没有明显的界限,因为这实际上是一个设计模式.

  • 协作性
  • 定位容易,维护难度低
  • 开发效率高

用组件化内核替代原本的宏内核.但是这个内核是组件化开发的,最终组成一个总的内核.

UniKernel

有一个非常不同的地方应用和内核都在内核态并且在同一个地址空间里,互相是可见的.

RTOS似乎就是一个UniKernel.

那么打包完是同一个image.

那么宏内核是内核是一个image应用是另一个image,用syscall来打通.

因为是同一特权级,所以安全性不太行.

那么如果UniKernel需要保证安全性,那么就需要Hypervisor来帮你解决这个问题.

不同的组件可以用axruntime来组织.

裸机->逻辑程序

裸机->unikernel->OS程序

用OS去实现RTOS.

用裸机+虚拟化实现类似于docker的功能.

实验支撑的开发

每一节课是一个针对每个需求的内核.

那么新的内核是从旧的内核组建而成的.

宏内核和Unikernel的区别是加了用户特权级和用户的地址空间.看起来是增加了两个组件形成的,实际上到了很远的一条路.

让Unikernel实现Hypervisor,这样就可以实现虚拟化.从Host升级到Guest.

宏内核的东西比较复杂

实验环境的建立

使用WSL2+Ubuntu 22.04.2 LTS环境.

安装 WSL | Microsoft Learn

Windows Terminal - Windows官方下载 | 微软应用商店 | Microsoft Store

框架

引导过程

是通过axhal.

实际上使用的是_start这个指针.

通过一系列的asm操作来进行完成页表和函数调用和MMU的启动的支持.

日志级别控制与features

使用Cargo.toml来控制features.

使用环境变量:

  1. 具体环境变量
  2. 使用通用环境变量

三个部分汇集起来到axfeat

课后练习-支持带颜色的打印输出

[print_with_color]: 支持带颜色的打印输出。

要求:

  1. 修改一个组件的实现
  2. 执行make run A=exercises/print_with_color

这一点非常重要

预期:字符串输出带颜色。(具体颜色不做要求)
提示:在不同层次的组件上修改,影响的输出范围不同。
例如,修改axstd可能只影响println!的输出;修改axhal则可能一并影响ArceOS启动信息的颜色。

通过修改APP层实现

修改exercises\print_with_color\src\main.rs:

1
2
3
4
... ...
fn main() {
println!("\x1b[31m[WithColor]: Hello, Arceos!\x1b[0m");
}

分支名称:print_with_color_app

通过修改ulib:axstd来实现

ulib\axstd\src\macros.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[macro_export]
macro_rules! print {
($($arg:tt)*) => {
$crate::io::__print_impl(format_args!("\x1b[31m{}\x1b[0m", format_args!($($arg)*)));
}
}

/// Prints to the standard output, with a newline.
#[macro_export]
macro_rules! println {
() => { $crate::print!("\n") };
($($arg:tt)*) => {
$crate::io::__print_impl(format_args!("\x1b[31m{}\n\x1b[0m", format_args!($($arg)*)));
}
}

分支名称:print_with_color_axstd

通过修改axhal:write_bytes来实现

修改modules\axhal\src\lib.rs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
... ...
pub mod console {
pub use super::platform::console::*;

/// Write a slice of bytes to the console.
pub fn write_bytes(bytes: &[u8]) {
let color_begin = "\x1b[31m";
let color_end = "\x1b[0m";
for c in color_begin.bytes() {
putchar(c);
}
for c in bytes {
putchar(*c);
}
for c in color_end.bytes() {
putchar(c);
}
}
}
... ...

分支名称:print_with_color_axhal

通过修改axlog:ax_println来实现(不了)

可以看到:modules\axruntime\src\lib.rs里调用了这个宏,

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
... ...
pub extern "C" fn rust_main(cpu_id: usize, dtb: usize) -> ! {
ax_println!("{}", LOGO);
ax_println!(
"\
arch = {}\n\
platform = {}\n\
target = {}\n\
smp = {}\n\
build_mode = {}\n\
log_level = {}\n\
",
option_env!("AX_ARCH").unwrap_or(""),
option_env!("AX_PLATFORM").unwrap_or(""),
option_env!("AX_TARGET").unwrap_or(""),
option_env!("AX_SMP").unwrap_or(""),
option_env!("AX_MODE").unwrap_or(""),
option_env!("AX_LOG").unwrap_or(""),
);
#[cfg(feature = "rtc")]
ax_println!(
"Boot at {}\n",
chrono::DateTime::from_timestamp_nanos(axhal::time::wall_time_nanos() as _),
);
... ...
}

并且这个宏的位置在modules\axlog\src\lib.rs,我们修改它:

1
2
3
4
5
6
7
8
... ...
macro_rules! ax_println {
() => { $crate::ax_print!("\n") };
($($arg:tt)*) => {
$crate::__print_impl(format_args!("\x1b[31m{}\x1b[0m\n", format_args!($($arg)*)));
}
}
... ...

这里只能使得如下部分变成红色,而不能满足题意:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
       d8888                            .d88888b.   .d8888b.
d88888 d88P" "Y88b d88P Y88b
d88P888 888 888 Y88b.
d88P 888 888d888 .d8888b .d88b. 888 888 "Y888b.
d88P 888 888P" d88P" d8P Y8b 888 888 "Y88b.
d88P 888 888 888 88888888 888 888 "888
d8888888888 888 Y88b. Y8b. Y88b. .d88P Y88b d88P
d88P 888 888 "Y8888P "Y8888 "Y88888P" "Y8888P"

arch = riscv64
platform = riscv64-qemu-virt
target = riscv64gc-unknown-none-elf
smp = 1
build_mode = release
log_level = warn

分支名称:print_with_color_axlog

问题和疑问

  1. make是怎么编译的?
  2. axruntime是怎么加载的编译好的APP并且运行的?
  3. sbi用的什么?

axstd支持Collections

具体可以看这里的介绍:集合类型 - Rust语言圣经(Rust Course)

最开始我学Rust的时候没有内化这个概念.

实际上集合类型就是VectorHashMap以及String这样的类型.

其实到这里我们自己就可以总结出了:

类型长度可变->指针地址和大小变量存在栈上,具体内容存在堆上->需要堆的支持->需要动态内存分配器.

那么实际上要支持Collections就是要支持一个动态内存分配器.

rCore中对动态内存分配器的描述:[rCore学习笔记 028] Rust 中的动态内存分配 - winddevil - 博客园

rCore中引用的动态内存分配器:[rCore学习笔记 029] 动态内存分配器实现-以buddy_system_allocator源码为例 - winddevil - 博客园

这里注意这张图,在Unikernel中,内存管理器也是和APP放在一起的,主要的思想还是APP和Kernel在同一特权级.

alloc实现接口

这个是需要实现两个功能:

  1. 实现byteAllocatorpageAllocatir.
  2. byteAllocator只需要实现rust自带的一个Trait#[global_alloctor]即可
  3. pageAllocator的实现方式是用了一个全局变量来实现相应的功能的

这里的源代码在modules/axalloc/src/lib.rs这里看.

这里是实现了一个GlobalAllocator类,然后实例化一个

  1. 实现GlobalAllocTrait给它,有allocdealloc这两个方法,即可实现内存分配.用#[global_alloctor]标注这个实例即可.
  2. GlobalAllocator实现了alloc_pagesdealloc_pages,通过直接获取这个实例

这里实现的时候traitfnstruct本身的fn重合,这是允许的.

调用时,MyTrait::do_something(&struct)struct.do_something两种调用形式是调用的不同的方法.

实现方法:

  1. 最开始的时候页分配器是先分配一部分内存给字节分配器的
  2. 先找字节分配器分配,任何如果不够了找页分配器追加

在当前为了给HashMap提供分配器的是core库里allocator里自带的TlsfByteAllocator作为字节分配器,BitmapPageAllocator作为页分配器.

那么为了实现自己的字节分配器就需要要给它实现两个Trait,即BaseAllocatorByteAllocator.

这个下一节可能会讲到.

课后练习-支持HashMap类型

这里要搞懂关于库的文件夹的互相依赖关系.

为什么core里没HashMap?

因为HashMap需要的随机数生成器涉及到体系结构.

那么这里的random()是来自于axhal就非常的合理了.

有关于Cargo.toml

两个Cargo.toml:

  1. .\Cargo.toml
  2. ulib\axstd\Cargo.toml

使用cargo tree -p axstd -e features可以很好地看到features结构.

有关于结构问题

这里注意一点,在ulib/axstd/src/lib.rs里已经引用了collections,

1
pub use alloc::{boxed, collections, format, string, vec};

而在exercises/support_hashmap/src/main.rs里引用的是:

1
use std::collections::HashMap;

rust这个项目里发现,library\alloccollection里并没有HashMap,HashMap是在library\stdcollection里.

所以其实我们是要把这两个collection的内容合并.

直接使用hashbrown

创建ulib/axstd/src/collections/mod.rs:

1
2
3
4
// 合并alloc::collections里的内容
pub use alloc::collections::*;
// 引用hashbrown
pub use hashbrown::HashMap;

ulib/axstd/Cargo.toml加入hashbrown:

1
2
3
4
5
... ...
[dependencies]
... ...
hashbrown = "0.15.1"
... ...

完全仿写std

自己实现以hashbrown::HashMapbaseHashMap.

通过题中给出的random实现RandomState.

想在axstd中调用axhal/random,需要在ulib/axstd/Cargo.toml里加入:

1
2
[dependencies]
axhal = { workspace = true }

#TODO

拉链法实现

#TODO