0%

解决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

概念

关键是在于这个虚拟化层,我们用虚拟化层来实现对硬件抽象层的虚拟化.

虚拟化的效率高是因为Hypervisor的体系结构环境是相同的.

这里的II型是KVM这种.1.5型可能更多的是

物理设备只有一个CPU的时候用多任务假装是vcpu.

有点像虚拟内存<->物理内存.用的也是页表的方式来建立对应关系.

vcpu的方式是一样的实现vDev.其实就是怎么把一个设备的划分成很多小的粒度,然后再分配资源给它.

最简Hypervisor执行流程:

  1. 加载Guest OS内核Image到新建地址空间。
  2. 准备虚拟机环境,设置特殊上下文。
  3. 结合特殊上下文和指令sret切换到V模式,即VM-ENTRY。
  4. OS内核只有一条指令,调用sbi-call的关机操作。
  5. 在虚拟机中,sbi-call超出V模式权限,导致VM-EXIT退出虚拟机,切换回Hypervisor。
  6. Hypervisor响应VM-EXIT的函数检查退出原因和参数,进行处理,由于是请求关机,清理虚拟机后,退出。

这里这个V模式一定要注意,之前我们只涉及了US模式.

S变成了HS.
HS是关键,作为联通真实世界和虚拟世界的通道.体系结构设计了双向变迁机制.
后边实现的很多东西都来自于HS这个特权级的寄存器.

H扩展后,S模式发送明显变化:原有s[xxx]寄存器组作用不变,新增hs[xxx]和vs[xxx]
hs[xxx]寄存器组的作用:面向Guest进行路径控制,例如异常/中断委托等
vs[xxx]寄存器组的作用:直接操纵Guest域中的VS,为其准备或设置状态

意思好像是要用HS里的H去假冒VS?

根据spv的不同决定sret会进入虚拟化还是进入user.

这里还多一个spvp,指示HS对V模式下地址空间是否有操作权限,1表示有权限操作,0无权限.

总结下来感觉虚拟机更像是一个进程,但是它又比进程多很多东西,需要运行自己的一套寄存器.

实验

运行之后是触发了一个IllegalInstructiontrap.

所以确实是会报错的.

这里要注意是QEMU的版本问题,如果是默认的6.x会导致无法正常进入vmexit_handler.
我这里安装的QEMU9.1.0版本,可以参见我自己的博客[rCore学习笔记 03]配置rCore开发环境 - winddevil - 博客园,下载的时候把版本改成9.1.0即可.

这里知道li指令访问了a7寄存器违反了不能访问csr的规定.

由于li的长度为4,我们做出如下修改tf.sepc += 4;:

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
// modules/axhal/src/arch/riscv/trap.rs
#[no_mangle]
fn riscv_trap_handler(tf: &mut TrapFrame, from_user: bool) {
let scause: scause::Scause = scause::read();
match scause.cause() {
#[cfg(feature = "uspace")]
Trap::Exception(E::UserEnvCall) => {
tf.regs.a0 = crate::trap::handle_syscall(tf, tf.regs.a7) as usize;
tf.sepc += 4;
}
Trap::Exception(E::LoadPageFault) => handle_page_fault(tf, MappingFlags::READ, from_user),
Trap::Exception(E::StorePageFault) => handle_page_fault(tf, MappingFlags::WRITE, from_user),
Trap::Exception(E::InstructionPageFault) => {
handle_page_fault(tf, MappingFlags::EXECUTE, from_user)
}
Trap::Exception(E::Breakpoint) => handle_breakpoint(&mut tf.sepc),
Trap::Interrupt(_) => {
handle_trap!(IRQ, scause.bits());
}
Trap::Exception(E::IllegalInstruction) => {
tf.sepc += 4;
info!("Illegal Instruction");
}
_ => {
panic!(
"Unhandled trap {:?} @ {:#x}:\n{:#x?}",
scause.cause(),
tf.sepc,
tf
);
}
}
}

课后作业

注意触发缺页异常之后要调整sepc的值,防止再回去再触发缺页异常然后无限循环了.

因为未知原因会卡住,这题的解题思路应该和h_1_0是一样的.

qemu 模拟 igb设备

Make方式

在linux系统下可使用make方式运行,需修改arceos/scripts/make目录下qemu.mk文件,更改 Qemu 模拟网卡为 igb。

qemu_args-$(NET)的参数,由-device virtio-net-$(vdev-suffix),netdev=net0修改为-device igb,netdev=net0

在arceos目录下运行make命令:

1
make A=examples/httpclient PLATFORM=aarch64-qemu-virt LOG=debug SMP=2 NET=y NET_DEV=user run

最终生成的运行命令:

1
qemu-system-aarch64 -m 128M -smp 2 -cpu cortex-a72 -machine virt -kernel examples/httpclient/httpclient_aarch64-qemu-virt.bin -device virtio-net-pci,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5555-:5555,hostfwd=udp::5555-:5555 -nographic

这里其实我们可以自己进行一个理解,先从qemu的命令开始:

  1. -m 设置分配的内存
  2. -smp 设置对称多处理(SMP)配置,这里指定的是创建一个具有两个 CPU 核心的虚拟机
  3. -cpu 指定要模拟的 CPU 类型,在这里是 Cortex-A72
  4. -machine 选择平台,这里使用 virt 类型的机器模型,这是一个通用的,不与任何特定硬件绑定的虚拟平台
  5. -kernel 指定内核映像文件,这个文件是在虚拟机启动时加载的程序或操作系统内核
  6. -device 添加一个 PCI 设备到虚拟机中,这个设备是一个VirtIO 网络适配器,并且它连接到了 IDnet0 的网络后端
  7. -netdev 定义了一个用户模式网络后端,其 IDnet0。同时设置了主机端口转发规则,将主机的 TCPUDP5555 端口转发到虚拟机的相同端口
  8. -nographic 禁用图形输出

那么我们可以得到如下的问题.

如何编译得来.bin文件?

看log的时候很不仔细,其实在log里是有的:

1
cargo -C examples/httpclient build -Z unstable-options --target aarch64-unknown-none-softfloat --target-dir /home/winddevil/workspace/arceos/target --release  --features "axstd/log-level-debug axstd/smp"

然后我们直接运行这个编译过程是发现会报错的,这是什么原因呢?输出log是:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
warning: `/home/winddevil/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/winddevil/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/winddevil/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
warning: `/home/winddevil/.cargo/config` is deprecated in favor of `config.toml`
note: if you need to support cargo 1.38 or earlier, you can symlink `config` to `config.toml`
Compiling log v0.4.21
Compiling cfg-if v1.0.0
Compiling tock-registers v0.8.1
Compiling bitflags v2.6.0
Compiling axerrno v0.1.0
Compiling byteorder v1.4.3
Compiling const-default v1.0.0
Compiling memory_addr v0.3.1
Compiling bit_field v0.10.2
Compiling percpu v0.1.3
Compiling lock_api v0.4.10
Compiling lazyinit v0.2.1
Compiling axconfig v0.1.0 (/home/winddevil/workspace/arceos/modules/axconfig)
Compiling int_ratio v0.1.0
Compiling static_assertions v1.1.0
Compiling linkme v0.3.27
Compiling scopeguard v1.2.0
Compiling handler_table v0.1.1
Compiling kernel_guard v0.1.1
Compiling axdriver_base v0.1.0 (https://github.com/arceos-org/axdriver_crates.git?tag=v0.1.0#78686a7e)
Compiling aarch64-cpu v9.4.0
Compiling rlsf v0.2.1
Compiling dw_apb_uart v0.1.0
Compiling arm_gicv2 v0.1.0
Compiling arm_pl011 v0.1.0
Compiling bitmap-allocator v0.1.0
Compiling heapless v0.7.16
Compiling kspin v0.1.0
Compiling zerocopy v0.7.35
Compiling hash32 v0.2.1
Compiling stable_deref_trait v1.2.0
Compiling smoltcp v0.10.0 (https://github.com/rcore-os/smoltcp.git?rev=2ade274#2ade2747)
Compiling axdriver v0.1.0 (/home/winddevil/workspace/arceos/modules/axdriver)
Compiling num-traits v0.2.16
Compiling managed v0.8.0
Compiling axlog v0.1.0 (/home/winddevil/workspace/arceos/modules/axlog)
Compiling bitflags v1.3.2
Compiling axio v0.1.0
Compiling spin v0.9.8
Compiling allocator v0.1.0 (https://github.com/arceos-org/allocator.git?tag=v0.1.0#16496d88)
Compiling axdriver_net v0.1.0 (https://github.com/arceos-org/axdriver_crates.git?tag=v0.1.0#78686a7e)
Compiling axalloc v0.1.0 (/home/winddevil/workspace/arceos/modules/axalloc)
Compiling virtio-drivers v0.7.4
Compiling axhal v0.1.0 (/home/winddevil/workspace/arceos/modules/axhal)
Compiling chrono v0.4.38
Compiling page_table_entry v0.4.0
Compiling axdriver_virtio v0.1.0 (https://github.com/arceos-org/axdriver_crates.git?tag=v0.1.0#78686a7e)
Compiling axdriver_pci v0.1.0 (https://github.com/arceos-org/axdriver_crates.git?tag=v0.1.0#78686a7e)
Compiling page_table_multiarch v0.4.0
error[E0425]: cannot find function `init_boot_page_table` in module `crate::platform::mem`
--> modules/axhal/src/platform/aarch64_common/boot.rs:100:27
|
100 | crate::platform::mem::init_boot_page_table(addr_of_mut!(BOOT_PT_L0), addr_of_mut!(BOOT_PT_L1));
| ^^^^^^^^^^^^^^^^^^^^ not found in `crate::platform::mem`

error[E0425]: cannot find value `rust_entry` in module `crate::platform`
--> modules/axhal/src/platform/aarch64_common/boot.rs:139:38
|
139 | entry = sym crate::platform::rust_entry,
| ^^^^^^^^^^ not found in `crate::platform`

error[E0425]: cannot find value `rust_entry_secondary` in module `crate::platform`
--> modules/axhal/src/platform/aarch64_common/boot.rs:170:38
|
170 | entry = sym crate::platform::rust_entry_secondary,
| ^^^^^^^^^^^^^^^^^^^^ not found in `crate::platform`

error[E0425]: cannot find value `PSCI_METHOD` in crate `axconfig`
--> modules/axhal/src/platform/aarch64_common/psci.rs:82:31
|
82 | let ret = match axconfig::PSCI_METHOD {
| ^^^^^^^^^^^ not found in `axconfig`

error[E0425]: cannot find value `PSCI_METHOD` in crate `axconfig`
--> modules/axhal/src/platform/aarch64_common/psci.rs:85:58
|
85 | _ => panic!("Unknown PSCI method: {}", axconfig::PSCI_METHOD),
| ^^^^^^^^^^^ not found in `axconfig`

error[E0425]: cannot find value `UART_PADDR` in crate `axconfig`
--> modules/axhal/src/platform/aarch64_common/pl011.rs:9:43
|
9 | const UART_BASE: PhysAddr = pa!(axconfig::UART_PADDR);
| ^^^^^^^^^^ not found in `axconfig`

For more information about this error, try `rustc --explain E0425`.
error: could not compile `axhal` (lib) due to 6 previous errors
warning: build failed, waiting for other jobs to finish...

这里有个非常重要的细节,我们报告的错都是modules/axhal/src/platform/aarch64_common这个文件夹里的.

那么我们发现这个paltform不对啊,它怎么不给我编译aarch64-qemu-virt呢?

这里要提到一个我们经常会使用到的东西,即查看make过程的V=1,在make指令后边加上这句,就可以看到make的过程

然后我们看到:

1
2
3
4
5
6
7
APP: "examples/httpclient"
APP_TYPE: "rust"
FEATURES: ""
arceos features: "axstd/log-level-debug"
lib features: "axstd/smp"
app features: ""
RUSTFLAGS: "-C link-arg=-T/home/winddevil/workspace/arceos/target/aarch64-unknown-none-softfloat/release/linker_aarch64-qemu-virt.lds -C link-arg=-no-pie -C link-arg=-znostart-stop-gc"

这里是来自scripts/make/build.mk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ifneq ($(filter $(MAKECMDGOALS),doc doc_check_missing),)  # run `cargo doc`
$(if $(V), $(info RUSTDOCFLAGS: "$(RUSTDOCFLAGS)"))
export RUSTDOCFLAGS
else ifeq ($(filter $(MAKECMDGOALS),clippy unittest unittest_no_fail_fast),) # not run `cargo test` or `cargo clippy`
ifneq ($(V),)
$(info APP: "$(APP)")
$(info APP_TYPE: "$(APP_TYPE)")
$(info FEATURES: "$(FEATURES)")
$(info arceos features: "$(AX_FEAT)")
$(info lib features: "$(LIB_FEAT)")
$(info app features: "$(APP_FEAT)")
endif
ifeq ($(APP_TYPE), c)
$(if $(V), $(info CFLAGS: "$(CFLAGS)") $(info LDFLAGS: "$(LDFLAGS)"))
else
$(if $(V), $(info RUSTFLAGS: "$(RUSTFLAGS)"))
export RUSTFLAGS
endif
endif

这里后来发现两个问题,一个是虽然输出了几个关键的环境变量还是没有输出全部的环境变量还是会导致不能编译

这里介绍一个关键字export:

1
2
3
4
5
6
7
export [-fnp][变量名称]=[变量设置值]

参数说明:

-f  代表[变量名称]中为函数名称。
-n  删除指定的变量。变量实际上并未删除,只是不会输出到后续指令的执行环境中。
-p  列出所有的shell赋予程序的环境变量。

上述是通过export的环境变量.

那么其中有一个比较可疑的,就是RUSTFLAGS,在这里:

自定义参数的空格分隔列表,用来传递给 Cargo 执行的所有编译器调用。与cargo rustc不同,这对于传递一个标志 全部的 编译实例是有用的。

经过多次尝试,设置这两个环境变量即可编译:

1
2
3
export RUSTFLAGS="-C link-arg=-T/home/winddevil/workspace/arceos/target/aarch64-unknown-none-softfloat/release/linker_aarch64-qemu-virt.lds -C link-arg=-no-pie -C link-arg=-znostart-stop-gc"
export AX_PLATFORM="aarch64-qemu-virt"
cargo -C examples/httpclient build -Z unstable-options --target aarch64-unknown-none-softfloat --target-dir /home/winddevil/workspace/arceos/target --release --features "axstd/log-level-debug axstd/smp"

这说明了编译流程里是有设置很多环境变量的.

编译过程很好理解:

但是仍然没有办法解决在cargo build的时候到底有哪些环境变量被export了.

我们搜索export,其实也不多:

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
// Makefile

export AX_ARCH=$(ARCH)
export AX_PLATFORM=$(PLATFORM_NAME)
export AX_SMP=$(SMP)
export AX_MODE=$(MODE)
export AX_LOG=$(LOG)
export AX_TARGET=$(TARGET)
export AX_IP=$(IP)
export AX_GW=$(GW)

// scripts/make/build.mk

ifneq ($(filter $(MAKECMDGOALS),doc doc_check_missing),) # run `cargo doc`
$(if $(V), $(info RUSTDOCFLAGS: "$(RUSTDOCFLAGS)"))
export RUSTDOCFLAGS
else ifeq ($(filter $(MAKECMDGOALS),clippy unittest unittest_no_fail_fast),) # not run `cargo test` or `cargo clippy`
ifneq ($(V),)
$(info APP: "$(APP)")
$(info APP_TYPE: "$(APP_TYPE)")
$(info FEATURES: "$(FEATURES)")
$(info arceos features: "$(AX_FEAT)")
$(info lib features: "$(LIB_FEAT)")
$(info app features: "$(APP_FEAT)")
endif
ifeq ($(APP_TYPE), c)
$(if $(V), $(info CFLAGS: "$(CFLAGS)") $(info LDFLAGS: "$(LDFLAGS)"))
else
$(if $(V), $(info RUSTFLAGS: "$(RUSTFLAGS)"))
export RUSTFLAGS
endif
endif

我们需要看看各个包在编译的时候,build.rs是怎么运行的,会加入什么环境变量进去.

依赖图:

这里边最重要的就是config.rs,它是由axconfig进行编译的时候build.rs生成的.

build.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
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
use std::io::{Result, Write};
use std::path::{Path, PathBuf};
use toml_edit::{Decor, DocumentMut, Item, Table, Value};

fn resolve_config_path(platform: Option<&str>) -> Result<PathBuf> {
let mut root_dir = PathBuf::from(std::env!("CARGO_MANIFEST_DIR"));
root_dir.extend(["..", ".."]);
let config_dir = root_dir.join("platforms");

let builtin_platforms = std::fs::read_dir(&config_dir)?
.filter_map(|e| {
e.unwrap()
.file_name()
.to_str()?
.strip_suffix(".toml")
.map(String::from)
})
.collect::<Vec<_>>();

let path = match platform {
None | Some("") => "defconfig.toml".into(),
Some(plat) if builtin_platforms.contains(&plat.to_string()) => {
config_dir.join(format!("{plat}.toml"))
}
Some(plat) => {
let path = PathBuf::from(&plat);
if path.is_absolute() {
path
} else {
root_dir.join(plat)
}
}
};

Ok(path)
}

fn get_comments<'a>(config: &'a Table, key: &str) -> Option<&'a str> {
config
.key(key)
.and_then(|k| k.leaf_decor().prefix())
.and_then(|s| s.as_str())
.map(|s| s.trim())
}

fn add_config(config: &mut Table, key: &str, item: Item, comments: Option<&str>) {
config.insert(key, item);
if let Some(comm) = comments {
if let Some(mut dst) = config.key_mut(key) {
*dst.leaf_decor_mut() = Decor::new(comm, "");
}
}
}

fn load_config_toml(config_path: &Path) -> Result<Table> {
let config_content = std::fs::read_to_string(config_path)?;
let toml = config_content
.parse::<DocumentMut>()
.expect("failed to parse config file")
.as_table()
.clone();
Ok(toml)
}

fn gen_config_rs(config_path: &Path) -> Result<Vec<u8>> {
fn is_num(s: &str) -> bool {
let s = s.replace('_', "");
if s.parse::<usize>().is_ok() {
true
} else if let Some(s) = s.strip_prefix("0x") {
usize::from_str_radix(s, 16).is_ok()
} else {
false
}
}

// Load TOML config file
let mut config = if config_path == Path::new("defconfig.toml") {
load_config_toml(config_path)?
} else {
// Set default values for missing items
let defconfig = load_config_toml(Path::new("defconfig.toml"))?;
let mut config = load_config_toml(config_path)?;

for (key, item) in defconfig.iter() {
if !config.contains_key(key) {
add_config(
&mut config,
key,
item.clone(),
get_comments(&defconfig, key),
);
}
}
config
};

add_config(
&mut config,
"smp",
toml_edit::value(std::env::var("AX_SMP").unwrap_or("1".into())),
Some("# Number of CPUs"),
);

// Generate config.rs
let mut output = Vec::new();
writeln!(
output,
"// Platform constants and parameters for {}.",
config["platform"].as_str().unwrap(),
)?;
writeln!(output, "// Generated by build.rs, DO NOT edit!\n")?;

for (key, item) in config.iter() {
let var_name = key.to_uppercase().replace('-', "_");
if let Item::Value(value) = item {
let comments = get_comments(&config, key)
.unwrap_or_default()
.replace('#', "///");
match value {
Value::String(s) => {
writeln!(output, "{comments}")?;
let s = s.value();
if is_num(s) {
writeln!(output, "pub const {var_name}: usize = {s};")?;
} else {
writeln!(output, "pub const {var_name}: &str = \"{s}\";")?;
}
}
Value::Array(regions) => {
if key != "mmio-regions" && key != "virtio-mmio-regions" && key != "pci-ranges"
{
continue;
}
writeln!(output, "{comments}")?;
writeln!(output, "pub const {var_name}: &[(usize, usize)] = &[")?;
for r in regions.iter() {
let r = r.as_array().unwrap();
writeln!(
output,
" ({}, {}),",
r.get(0).unwrap().as_str().unwrap(),
r.get(1).unwrap().as_str().unwrap()
)?;
}
writeln!(output, "];")?;
}
_ => {}
}
}
}

Ok(output)
}

fn main() -> Result<()> {
let platform = option_env!("AX_PLATFORM");
let config_path = resolve_config_path(platform)?;

println!("Reading config file: {:?}", config_path);
let config_rs = gen_config_rs(&config_path)?;

let out_dir = std::env::var("OUT_DIR").unwrap();
let out_path = Path::new(&out_dir).join("config.rs");
println!("Generating config file: {}", out_path.display());
std::fs::write(out_path, config_rs)?;

println!("cargo:rerun-if-changed={}", config_path.display());
println!("cargo:rerun-if-env-changed=AX_PLATFORM");
println!("cargo:rerun-if-env-changed=AX_SMP");
Ok(())
}

我们通过在编译过程中加入-vv查看更详细的编译过程,即:

1
2
3
export RUSTFLAGS="-C link-arg=-T/home/winddevil/workspace/arceos/target/aarch64-unknown-none-softfloat/release/linker_aarch64-qemu-virt.lds -C link-arg=-no-pie -C link-arg=-znostart-stop-gc"
export AX_PLATFORM="aarch64-qemu-virt"
cargo -C examples/httpclient build -Z unstable-options --target aarch64-unknown-none-softfloat --target-dir /home/winddevil/workspace/arceos/target --release --features "axstd/log-level-debug axstd/smp" -vv

可以看到一个好的信息,

1
[axconfig 0.1.0] Generating config file: /home/winddevil/workspace/arceos/target/release/build/axconfig-f271638000f4f11a/out/config.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
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
// Platform constants and parameters for aarch64-qemu-virt.
// Generated by build.rs, DO NOT edit!

/// Architecture identifier.
pub const ARCH: &str = "aarch64";
/// Platform identifier.
pub const PLATFORM: &str = "aarch64-qemu-virt";
/// Platform family.
pub const FAMILY: &str = "aarch64-qemu-virt";
/// Base address of the whole physical memory.
pub const PHYS_MEMORY_BASE: usize = 0x4000_0000;
/// Size of the whole physical memory.
pub const PHYS_MEMORY_SIZE: usize = 0x800_0000;
/// Base physical address of the kernel image.
pub const KERNEL_BASE_PADDR: usize = 0x4008_0000;
/// Base virtual address of the kernel image.
pub const KERNEL_BASE_VADDR: usize = 0xffff_0000_4008_0000;
/// Linear mapping offset, for quick conversions between physical and virtual
/// addresses.
pub const PHYS_VIRT_OFFSET: usize = 0xffff_0000_0000_0000;
/// Offset of bus address and phys address. some boards, the bus address is
/// different from the physical address.
pub const PHYS_BUS_OFFSET: usize = 0;
/// Kernel address space base.
pub const KERNEL_ASPACE_BASE: usize = 0xffff_0000_0000_0000;
/// Kernel address space size.
pub const KERNEL_ASPACE_SIZE: usize = 0x0000_ffff_ffff_f000;
/// MMIO regions with format (`base_paddr`, `size`).
pub const MMIO_REGIONS: &[(usize, usize)] = &[
(0x0900_0000, 0x1000),
(0x0910_0000, 0x1000),
(0x0800_0000, 0x2_0000),
(0x0a00_0000, 0x4000),
(0x1000_0000, 0x2eff_0000),
(0x40_1000_0000, 0x1000_0000),
];
/// VirtIO MMIO regions with format (`base_paddr`, `size`).
pub const VIRTIO_MMIO_REGIONS: &[(usize, usize)] = &[
(0x0a00_0000, 0x200),
(0x0a00_0200, 0x200),
(0x0a00_0400, 0x200),
(0x0a00_0600, 0x200),
(0x0a00_0800, 0x200),
(0x0a00_0a00, 0x200),
(0x0a00_0c00, 0x200),
(0x0a00_0e00, 0x200),
(0x0a00_1000, 0x200),
(0x0a00_1200, 0x200),
(0x0a00_1400, 0x200),
(0x0a00_1600, 0x200),
(0x0a00_1800, 0x200),
(0x0a00_1a00, 0x200),
(0x0a00_1c00, 0x200),
(0x0a00_1e00, 0x200),
(0x0a00_3000, 0x200),
(0x0a00_2200, 0x200),
(0x0a00_2400, 0x200),
(0x0a00_2600, 0x200),
(0x0a00_2800, 0x200),
(0x0a00_2a00, 0x200),
(0x0a00_2c00, 0x200),
(0x0a00_2e00, 0x200),
(0x0a00_3000, 0x200),
(0x0a00_3200, 0x200),
(0x0a00_3400, 0x200),
(0x0a00_3600, 0x200),
(0x0a00_3800, 0x200),
(0x0a00_3a00, 0x200),
(0x0a00_3c00, 0x200),
(0x0a00_3e00, 0x200),
];
/// Base physical address of the PCIe ECAM space.
pub const PCI_ECAM_BASE: usize = 0x40_1000_0000;
/// End PCI bus number (`bus-range` property in device tree).
pub const PCI_BUS_END: usize = 0xff;
/// PCI device memory ranges (`ranges` property in device tree).
pub const PCI_RANGES: &[(usize, usize)] = &[
(0x3ef_f0000, 0x1_0000),
(0x1000_0000, 0x2eff_0000),
(0x80_0000_0000, 0x80_0000_0000),
];
/// UART Address
pub const UART_PADDR: usize = 0x0900_0000;

pub const UART_IRQ: usize = 1;
/// GICC Address
pub const GICC_PADDR: usize = 0x0801_0000;

pub const GICD_PADDR: usize = 0x0800_0000;
/// PSCI
pub const PSCI_METHOD: &str = "hvc";
/// pl031@9010000 {
/// clock-names = "apb_pclk";
/// clocks = <0x8000>;
/// interrupts = <0x00 0x02 0x04>;
/// reg = <0x00 0x9010000 0x00 0x1000>;
/// compatible = "arm,pl031\0arm,primecell";
/// };
/// RTC (PL031) Address
pub const RTC_PADDR: usize = 0x901_0000;
/// Timer interrupt frequency in Hz.
pub const TIMER_FREQUENCY: usize = 0;
/// Stack size of each task.
pub const TASK_STACK_SIZE: usize = 0x40000;
/// Number of timer ticks per second (Hz). A timer tick may contain several timer
/// interrupts.
pub const TICKS_PER_SEC: usize = 100;
/// Number of CPUs
pub const SMP: usize = 1;

由于modules/axconfig/src/lib.rs里有:

1
2
3
4
#[rustfmt::skip]
mod config {
include!(concat!(env!("OUT_DIR"), "/config.rs"));
}

那么我们可以看到一些使用了axconfig::xx的引用的来源.

透传到hostnet0网络后端和5555端口是怎么被访问到的?

解析modules/axdriver/src/ixgbe.rs

log

这里是log:

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
78
79
80
81
82
83
84
85
86
87
qemu-system-aarch64 -m 128M -smp 2 -cpu cortex-a72 -machine virt -kernel examples/httpclient/httpclient_aarch64-qemu-virt.bin -device virtio-net-pci,netdev=net0 -netdev user,id=net0,hostfwd=tcp::5555-:5555,hostfwd=udp::5555-:5555 -nographic

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 = aarch64
platform = aarch64-qemu-virt
target = aarch64-unknown-none-softfloat
smp = 2
build_mode = release
log_level = debug

[ 0.005105 axruntime:130] Logging is enabled.
[ 0.006216 axruntime:131] Primary CPU 0 started, dtb = 0x44000000.
[ 0.007098 axruntime:133] Found physcial memory regions:
[ 0.007682 axruntime:135] [PA:0x40080000, PA:0x400a6000) .text (READ | EXECUTE | RESERVED)
[ 0.008824 axruntime:135] [PA:0x400a6000, PA:0x400af000) .rodata (READ | RESERVED)
[ 0.009895 axruntime:135] [PA:0x400af000, PA:0x400b3000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
[ 0.011083 axruntime:135] [PA:0x400b3000, PA:0x40133000) boot stack (READ | WRITE | RESERVED)
[ 0.011895 axruntime:135] [PA:0x40133000, PA:0x40159000) .bss (READ | WRITE | RESERVED)
[ 0.012720 axruntime:135] [PA:0x40159000, PA:0x48000000) free memory (READ | WRITE | FREE)
[ 0.013704 axruntime:135] [PA:0x9000000, PA:0x9001000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 0.014160 axruntime:135] [PA:0x9100000, PA:0x9101000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 0.015061 axruntime:135] [PA:0x8000000, PA:0x8020000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 0.016082 axruntime:135] [PA:0xa000000, PA:0xa004000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 0.017048 axruntime:135] [PA:0x10000000, PA:0x3eff0000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 0.017742 axruntime:135] [PA:0x4010000000, PA:0x4020000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 0.018484 axruntime:208] Initialize global memory allocator...
[ 0.019127 axruntime:209] use TLSF allocator.
[ 0.019775 axalloc:212] initialize global allocator at: [0xffff000040159000, 0xffff000048000000)
[ 0.020798 axmm:60] Initialize virtual memory management...
[ 0.027013 axmm:63] kernel address space init OK: AddrSpace {
va_range: VA:0xffff000000000000..VA:0xfffffffffffff000,
page_table_root: PA:0x40161000,
}
[ 0.028517 axruntime:150] Initialize platform devices...
[ 0.028923 axdriver:152] Initialize device drivers...
[ 0.029312 axdriver:153] device model: static
[ 0.029799 axdriver::bus::pci:97] PCI 00:00.0: 1b36:0008 (class 06.00, rev 00) Standard
[ 0.031156 axdriver::bus::pci:97] PCI 00:01.0: 1af4:1000 (class 02.00, rev 00) Standard
[ 0.031683 axdriver::bus::pci:54] BAR 1: MEM [0x10000000, 0x10001000)
[ 0.032182 axdriver::bus::pci:54] BAR 4: MEM [0x10004000, 0x10008000) 64bit pref
[ 0.034618 virtio_drivers::transport:78] Device features: Features(CTRL_GUEST_OFFLOADS | MAC | MRG_RXBUF | STATUS | CTRL_VQ | CTRL_RX | CTRL_VLAN | CTRL_RX_EXTRA | GUEST_ANNOUNCE | CTL_MAC_ADDR | RING_INDIRECT_DESC | RING_EVENT_IDX | VERSION_1)
[ 0.036068 virtio_drivers::device::net::dev_raw:30] negotiated_features Features(MAC | STATUS | RING_INDIRECT_DESC | RING_EVENT_IDX)
[ 0.036781 virtio_drivers::device::net::dev_raw:37] Got MAC=[52, 54, 00, 12, 34, 56], status=Status(LINK_UP)
[ 0.038039 axalloc:118] expand heap memory: [0xffff0000403a5000, 0xffff0000403e5000)
[ 0.039351 axdriver::bus::pci:104] registered a new Net device at 00:01.0: "virtio-net"
[ 0.050505 axdriver:160] number of NICs: 1
[ 0.050868 axdriver:163] NIC 0: "virtio-net"
[ 0.051241 axnet:43] Initialize network subsystem...
[ 0.051457 axnet:46] use NIC 0: "virtio-net"
[ 0.051956 axalloc:118] expand heap memory: [0xffff0000403e5000, 0xffff0000404e5000)
[ 0.052391 axalloc:118] expand heap memory: [0xffff0000404e5000, 0xffff0000406e5000)
[ 0.053174 axnet::smoltcp_impl:332] created net interface "eth0":
[ 0.053471 axnet::smoltcp_impl:333] ether: 52-54-00-12-34-56
[ 0.054021 axnet::smoltcp_impl:334] ip: 10.0.2.15/24
[ 0.054368 axnet::smoltcp_impl:335] gateway: 10.0.2.2
[ 0.054664 axruntime::mp:19] starting CPU 1...
[ 0.054937 axhal::platform::aarch64_common::psci:113] Starting CPU 1 ON ...
[ 0.055480 axruntime:186] Primary CPU 0 init OK.
[ 0.056642 axruntime::mp:36] Secondary CPU 1 started.
[ 0.056872 axruntime::mp:46] Secondary CPU 1 init OK.
Hello, simple http client!
dest: 49.12.234.183:80 (49.12.234.183:80)
[ 0.058237 0 axnet::smoltcp_impl:100] socket #0: created
[ 0.059213 0 smoltcp::iface::interface:1473] address 10.0.2.2 not in neighbor cache, sending ARP request
[ 0.386031 0 axnet::smoltcp_impl::tcp:430] TCP socket #0: connected to 49.12.234.183:80
HTTP/1.1 200 OK
Server: nginx
Date: Sat, 07 Dec 2024 07:03:37 GMT
Content-Type: text/plain
Content-Length: 14
Connection: keep-alive
Access-Control-Allow-Origin: *
Cache-Control: no-cache, no-store, must-revalidate

211.83.106.222
[ 0.675625 0 axnet::smoltcp_impl::tcp:247] TCP socket #0: shutting down
[ 0.676139 0 axnet::smoltcp_impl:128] socket #0: destroyed
[ 0.676366 0 axruntime:199] main task exited: exit_code=0
[ 0.676586 0 axhal::platform::aarch64_common::psci:96] Shutting down...

技术交流会议内容

技术文档

igb-driver/doc/tack.md at main · qclic/igb-driver

会议内容

主要看一下设备树和BAR空间的概念。

4.5节讲了怎么进行初始化。

怎么完成这个初始化呢?操作寄存器

说明reset成功

初始化流程

关中断

在发出全局重置后,也需要禁用中断

IMC 寄存器


全局Reset并且进行常规配置

CTRL寄存器

RCTL 寄存器

TCTL寄存器

RXPBS寄存器

TXPBS寄存器

设置PHY和链接

CTRL_EXT

Copper PHY Link设置

以太网——PHY、MAC和 MII基础知识 - 知乎

自动协商

链路的解析流控制行为要放到CTRL.TFCECTRL.RFCE里.

在自动协商结束后,MAC从PHY中识别”链接指示”之前需要设置CTRL.SLU.

MAC速度解决

  1. 根据PHY指示的速度来进行解决
    1. 直接读取PHY寄存器,详见下面一节MDIO的读取
    2. 通过STATUS.SPEED 寄存器来读取PHY的SPD_IND寄存器
  2. 软件要求MAC尝试从PHY到MAC RX_CLK自动检测PHY速度,然后相应地编程MAC速度
  3. MAC通过使用PHY的内部PHY-到MAC速度指示(SPD_IND),基于PHY指示自动检测和设置MAC的链路速度
强制设置MAC速度

设置CTRL.frcspdCTRL.speed

这里有一点非常重要,就是在设置一个位的时候,假如我们不知道其原始状态,我们应该先将其清零,然后再设置

使用PHY的指示值

设置CTRL.ASDECTRL.FRCSPD

MAC双工解决

设置CTRL.frcdplxCTRL.FD

使用PHY寄存器

暂且跳过,这部分是高级功能,直接通过MDIO接口操作PHY设备

MDIO接口

  1. 物理接口
  2. 特殊协议-可以跨连接运行
  3. 一组内部的可寻址寄存器

内部和外扩的接口由:

  1. 数据线MDIO
  2. 时钟线MDC

通过访问MAC的寄存器可以访问这两种接口.

MDC是MDIO的时钟线,这个时钟信号不一定一直要有,只要在有数据的时候有没数据的时候就冻结就行了.最大工作频率为2.5MHz.

MDIO是一种传送PHY和MAC之间命令的双向数据信号,因此MAC可以通过一些指令来读取和写PHY管理寄存器.

直接通过访问MDIC寄存器即可访问这个接口.

MDIC寄存器

The PHY address for MDIO accesses is 00001b.

MAC/PHY 链接设置

这个有4种方法,按照情况

自动设置

只需要设置CTRL.FRCDPLXCTRL.FRCSPD0b.

启动PHY的自动协商

接收寄存器初始化

初始化多播表


然后这个是11位也就是[11:0],

那么[11:5]决定指向哪个寄存器,刚好128个寄存器嘛.

那么[4:0]决定是指向哪个位,刚好32个位嘛.

接收功能

主要是要学一下rings.

Rx队列分配

接收到的包分为三个阶段:

  1. L2 Filters 用于确保包已经收到
  2. Pool Select 用于虚拟化环境,定义Rx包的目标虚拟端口(称之为)一个数据包可以与任意数量的端口/池相关联
  3. Queue Select 这一步Rx包已经成功通过过滤器,并且和一个或者多个接收描述符队列相连接

这是一个类开关结构.

这是一种网络资源虚拟化,把网卡的接口也就是最上边的,虚拟化成很多有粒度的资源

接收数据包的目的地

  • 虚拟化
  • RSS
  • L2以太网过滤器
  • L3/L4 5-元组过滤器
  • TCP SYN筛选器

通常,包接收包括识别线上包的存在、执行地址过滤、将包存储在接收数据FIFO中、将数据传输到主机存储器中的16个接收队列中的一个,以及更新接收描述符的状态。

队列分配.

虚拟化

在虚拟化环境中,DMA资源被多个软件共享.

通过在DMA中分配 接收描述符队列(receive descriptor queues) 来完成虚拟化.

队列分配到虚拟分区是按集合完成的,每个集合都有相同数量的队列,那么这个队列组合叫做.

虚拟化会为每个接收到的数据包分配一个或者多个池索引.

包的分配是根据池索引和一些其它的约束来分配到池中的.

RSS

RSS通过将数据包分配到不同的描述符队列中,在多个处理器核心之间分配数据包处理.

RSS为每个接收到的数据包分配一个RSS索引.

包的分配是根据RSS索引和一些其它的约束(比如上述提到的池索引)来分配到池中的.

L2以太网过滤器

这些过滤器根据其L2以太网类型识别数据包,并将它们分配给接收队列.

L3/L4 5-元组过滤器

识别出指定的L3/L4流,或者L3/L4流的组合.

每个过滤器由一个5个元组组成(协议、源和目标IP地址、源和目标TCP/ UDP端口).

TCP SYN筛选器

把有SYN标志的TCP包专门放到一个独立的队列里监视.

非虚拟化中的队列

队列设置寄存器

每一个队列会有一套配置寄存器,用于控制队列操作。

  • RDBAL 和 RDBAH — 接收描述符基址
  • RDLEN — 接收描述符长度
  • RDH — 接收描述符头
  • RDT — 接收描述符尾
  • RXDCTL — 接收描述符控制
  • RXCTL — 接收 DCA 控制

DCA在网络硬件接口和网络驱动中通常指的是“Direct Cache Access”,即直接缓存访问。DCA是一种硬件技术,旨在通过减少数据在内存和处理器之间的传输延迟来提高网络数据包处理的效率。具体来说,DCA可以将网络数据直接写入处理器的缓存,从而提高数据处理的速度和效率。

一共有16个队列.

定义描述符队列功能的CSR被复制了8个副本给虚拟功能(VF,Virtual Function)索引,以实现虚拟化.

每一套被复制的寄存器对应了一组队列,这些队列的VF index相同.

注意是CSR被复制了8份,而不是队列被复制了8份.

这个虚拟化不是按照队列来划分的,不是说一个队列虚拟出几个队列.而是每个功能都认为自己拥有整个网卡,所以假设你的网卡支持8个虚拟功能(VF),那么每个描述符队列的控制和状态寄存器会被复制8份,每份对应一个VF index(从0到7)。
(Gen by GPT-4o)

SRRCTL(Split and Replication Receive Control) 寄存器用于控制接收数据包的拆分和复制功能。这些功能在处理网络数据包时可以提高灵活性和性能。
主要功能包括:

  1. 数据包拆分(Split)
    • 可以将接收到的网络数据包拆分成多个部分,以便于在不同的内存区域中存储。这样可以提高数据包处理的效率,特别是在数据包非常大的情况下。
    • 通过拆分数据包,能够更高效地利用缓存和内存,从而减少处理延迟。
  2. 数据包复制(Replication)
    • 允许将接收到的数据包复制到多个队列中。这样可以支持多播和广播数据包的高效处理。
    • 数据包复制有助于在多个处理器核心之间分配接收的网络流量,从而提高处理吞吐量。
      SRRCTL寄存器中的各个字段可以配置这些功能的具体行为,例如启用或禁用拆分和复制、设置拆分的阈值等。

PSRTYPE(Packet Split Receive Type) 寄存器用于指定数据包拆分的类型和模式。它定义了如何将接收到的数据包拆分成不同的部分。
主要功能包括:

  1. 拆分模式选择
    • PSRTYPE寄存器允许选择不同的拆分模式,例如基于数据包头部的拆分、基于数据包长度的拆分等。
    • 不同的拆分模式适用于不同的应用场景,例如高效处理TCP/IP头部、优化内存使用等。
  2. 拆分类型配置
    • 可以配置拆分的具体类型,例如将数据包拆分成固定大小的块、将头部和数据部分分开存储等。
    • 通过配置拆分类型,可以更好地优化数据包的处理和存储,提高整体性能。
      PSRTYPE寄存器中的字段用于详细配置这些拆分模式和类型,确保数据包能够按照预期方式进行拆分和处理。

有了上述功能,那么每个虚拟化功能(VF)就可以按照自己的喜好来处理每个数据包,就好像每个数据包都专门发给他一样,实际上是拆分复制处理过的数据包.

通过L2 Filter 来确保包已经收到

L2 Filter被翻译为二级数据包过滤.

MAC地址过滤
单播过滤器
多播过滤器
VLAN 过滤

可管理性过滤

五元组过滤器

#todo

RSS接收侧缩放

RSS是一种将接收到的数据包分发到多个描述符队列中的机制。然后,软件将每个队列分配给不同的处理器,在多个处理器之间共享数据包处理的负载。

VLAN过滤器

#todo

接收数据存储

Host Buffer

其大小可以通过RCTL.BSIZE设置packatbuffer的大小,会影响所有的接收队列.

也可也通过每个队列SRRCTL[n].BSIZEPACKET来设置.在设置为0的时候是采用RCTL.BSIZE 的默认设置.

在使用advanced descriptor的时候需要通过设置SRRCTL.BSIZEHEADER来决定headerbuffer的大小.

片上Rx Buffer

82576包含一个64 KBytes的包缓冲区,可用于存储包,直到它们被转发到主机。
此外,为了支持第7.10.3节所述的本地包的转发,提供了20kbyt的交换机缓冲区。此缓冲区作为所有本地流量的接收缓冲区。

片上descriptor Buffer

82576为每个接收队列包含一个32个描述符缓存,用于减少包处理的延迟,并通过突发中获取和写回描述符来优化PCIe带宽的使用。

传统接收描述符格式

SRRCTL[n],DESCTYPE = 000b

这里ixgbe暂时跳过了,我们也暂时跳过,可以根据Advanced Descriptor来进行后续的补充工作

接收描述符Ring结构

移植到ixgbe代码

寻找igb的id

我们用的是82576,在ethernet-linux-igb,也就是linuxigb驱动项目里的src/e1000_hw.h文件夹里可以查出.

E1000_DEV_ID_82576 0x10C9

解决宏定义问题

ixgbe-driversrc/constants.rs文件里存在大量的宏,是用于规定一些需要的.

我们把ethernet-linux-igb中的src/e1000_hw.hE1000_DEV_ID_82576等宏改为rust形式,并且重新命名.

例如把E1000_DEV_ID_82576的名字改为IGB_DEV_ID_82576.

读取MAC

这里有一点语焉不详的地方,就是RAL0RAH0是从EEPROM加载出来的数据,那么这里有我们需要的数据.

TIPS

git的换行符问题

有时候是从windows clone下来的,所以在wsl里是linux方式读取,因此刚刚clone下来就是”被更改的状态”.

1
git config --global core.autocrlf true

下一步

第一阶段练习总结

作为一名Cpper,我之前对Rust的了解可以说是毫无了解,这次受到朋友的邀请,第一次接触Rust,刚开始以为就是像C一样的过程语言。然而,当我真正开始深入学习和使用Rust时,我发现它远比我预想的要有趣和富有挑战性。特别是在进行Rustlings练习的过程中,我收获颇丰,也深刻体会到了Rust语言的强大和魅力。

Rustlings是一个设计精良的Rust语言练习项目,通过一系列由简到难的练习题,帮助开发者逐步掌握Rust的语法和特性。在练习的过程中,我遇到了许多看似简单但实则深奥的问题,这些问题让我不断思考和探索,也让我对Rust有了更深入的理解。

通过Rustlings的练习,我只能感叹面向编译器编程的魅力。

此外,Rustlings的练习还让我认识到了自己在编程思维方面的不足总的来说,Rustlings练习是我学习Rust过程中的一个重要环节。它不仅让我掌握了Rust的基本语法和特性,还锻炼了我的编程思维和解决问题的能力。我相信,在未来的学习和工作中,我会继续利用Rustlings等资源来深化对Rust的理解和应用,并不断提升自己的编程水平。同时,我也期待在未来的项目中能够充分发挥Rust的优势,写出更加健壮、高效的程序。

stage3总结

u1.0

很简单,定位到 axstd 里修改 println 的输出,前后加上转义字符就好了

u2.0

找到 axstd 里,把 collection 替换成自己的 collection 包就可以,在自己的 collection 包实现 HashMap 结构体,只需要简单实现接口,覆盖测试用例即可。另外自己在替换 axstd 的 collection 的时候,遇到的问题是模块的导出关系没弄清导致的刚开始测试用例找不到我的代码.

u3.0

做的过程中,先按照自己的想法做了一下,发现编译不过哈哈,然后看群里消息,学到了 const generic 这个知识点。然后顺便看到了 blogOS 对于 bump allocator 的介绍,就拿过来参考了一下,这个 bump allocator 确实简单的不可思议。 最后写完之后还是不过,打了日志才发现是自己初始化漏了 b_pos 和 p_pos 的初始化.

h1.0 & h2.0

实验内容还是很简单的,但是虚拟化这块之前没有了解过,但是比较感兴趣。中间一周把宏内核实验翘掉读了虚拟化原理那本书。在听老师讲解过程中感觉,对一个不熟悉的东西,尽量简化,然后做出一个最小 work 的样板,然后不断迭代添加复杂功能,是非常好的学习方式。

stage4总结

stage 4 算是很扎实的一个阶段。这个阶段从头开始学习了我原来不怎么熟悉的异步运行时。整体上来说算是比较系统的学习了如何构建一个异步运行时,同时也阅读了不少tokio这种生产环境级别的异步运行时代码,受益良多。
本阶段还学习了一些关于 uring io 的知识,同时也顺便比较系统的梳理了 linux io 这块的知识点。通过每周一次的交流活动中能比较有效的调整自己的学习方向,同时也能补充很多有用的信息。很多同学水平很高,在交流过程中深感差距,还需要不断学习。

stage4 工作进度

本周工作(第一周):

阅读以下两个链接中文档,比较系统的了解了 rust 的异步原理

####下周安排(第二周):

  • 阅读 tokio 源码,学习生产环境中的异步运行时实现

本周工作(第二周):

由于本项目最终要实现一个基于 uring io 的异步运行时,于是决定从 io 的角度切入 tokio 的源码阅读。在阅读过程中发现 tokio 的文件 io 部分都是转发给某个独立线程,是基于阻塞式的操作。为了对比文件 io ,还阅读了部分 tcp 相关的源码,证实了的确网络 io 是使用了基于 epoll 的 mio 来做管理。而且本周对 uring io 有一个粗略地了解,目前看来 uring io 在文件 io 方面可能优势会更明显。 网络 io 这块相较于 epoll 优势没那么大,那么接下来第三周可能要优先实现基于 uring io 的文件 io 异步运行时的相关工作。

此外本周还阅读了以下资料

下周安排(第三周):

编写一个简易的基于 uring io 的文件 io 的异步运行时。

本周工作(第三周):

本周首先调研了一下在 smol 中实现基于 uring io 的可能性。首先 smol 社区中已经有一个 PR
async-io PR:Integrate io_uring in the Reactor
● 简要介绍了实现思路,
● 作者说要等等 polling Expose raw handles for the Poller 这个 issuing合并.
大概粗略浏览了一下,但是由于对 uring io 不太熟悉用法,还没有看懂,暂时搁置。

然后继续阅读了 uring io 的相关资料,包括

最后是实现我自己的uring io异步运行时
我的async-fs-uring,这里实现了一个建议的基于 reactor 模式异步运行时,并实现了基于uring io的文件读。主要思想就是把 io 委托给 reactor 去提交,然后 reactor 不断轮询,如果有 io 完成了,就返回给对应的异步任务。实现过程中比较困难的点就是buf 管理,需要保证 buf 在异步读过程中一直有效。我这里做法是直接把 buf 的所有权移交给 UringReadFuture.这只是一个权宜之计,因为我这里实现的比较简单,在异步读进行过程中 UringReadFuture不会被 drop 掉。实际上后来也阅读了 tokio-uring 的相关设计文档,也了解到了一些更合理的设计方案,但是还没有时间来实现。

未来计划:通过实现一个建议的基于 uring io 的异步运行时让我对 uring io 有了基本的了解。后续可能会进一步了解生产环境级的基于 uring io 的异步运行时的实现以及与传统阻塞式和epoll结合的异步运行时的实现差异

一阶段学习体会

早就听说过大名鼎鼎的 rustlings, 这次借着 oscamp 的机会来做一做。做的过程中发现 oscamp 的 rustlings 似乎比官方的 rustlings 加了很多私货,难度大了不少,
但是也的确把编写 很多有用的 rust 特性 和 os 过程中会用得到的一些 rust 的 feature 都涵盖了。在做的过程中,也学习到了很多以前自己没有注意或者使用过的 rust 特性。
更重要的是,通过一些题目的学习,能摆脱原来写 c++ 的思维,写出一些更 rust 风格的代码,比如返回值多用 Option 和 Result,更多的使用 match 和 if let, 而不是用原来 c++/c 风格的错误判断,
以及更多的用迭代器等方法。

一阶段学完之后,尤其是经历了大量和编译器的斗智斗勇之后,我对 rust 的理解加深很多,也对 rust 带来的相比 c++ 的很多优势有了更深的体会。相信 rust 和 c++ 的学习会互相促进。

二阶段学习体会

本科期间零零散散一致有学过 rust 和 rcore 的一些东西,由于各种各样的事情没能坚持下来。
读硕士之后刚好又系统学习了一下 rust ,正巧有同学来约我参加 oscamp , 于是就来参加。

如今再做 rcore 感觉要比本科期间轻松了很多。在游刃有余之后,能从 rcore 中汲取的知识也就更多了。
文件系统的组织方式、并发的实现和管理等,这些本科期间令我头晕转向的知识点,现在都能比较轻松地理解。也能从中窥探出一个真实生产环境下的操作系统是如何设计的。

本次实验中,前四个实验都比较轻松,思路清晰地拿下了。最让我头晕转向的是第五个实验,也就是死锁检测的实现。
刚开始被银行家算法迷惑,感觉很难实现。后来又做了尝试做了环路检测来检测死锁。但是做的过程中发现环路检测可能比较难处理信号量。于是又回过头来思考银行家算法。
结合前面尝试做环路检测的过程,觉得只要实现一套类似银行家算法的检测机制就好了。于是把代码全部推翻,从头来过很快就拿下了。

这一次 rcore 学习体验不错,希望接下来的三阶段能学到一些有意思、更贴近实际场景的东西。

2024秋冬季OS2EDU第三阶段总结报告

概述

在2024秋冬季开源操作系统训练营的第三阶段中,我深入研究并实现了一个高效的内存分配器,该分配器结合了Slab分配器和TLSF(Two-Level Segregated Fit)分配器的优势。本报告将详细分析每个链接中的源码,并探讨我实现的新调度器的优势。

Slab分配器

源码分析

Slab分配器的核心思想是将内存分割成固定大小的块,并将这些块组织成一个或多个列表,以便于快速分配和释放。每个Slab结构体维护了一个特定大小的块列表。

1
2
3
4
pub struct Slab<const BLK_SIZE: usize> {
free_block_list: FreeBlockList<BLK_SIZE>,
total_blocks: usize,
}

Slab结构体包含一个free_block_list,这是一个双向链表,用于存储可用的块,以及total_blocks,记录总块数。Slab的实现提供了newgrowallocatedeallocate等方法,用于初始化、扩展、分配和释放内存块。

优势分析

  • 固定大小的块:由于每个Slab只管理固定大小的块,因此分配和释放操作可以非常快速。
  • 缓存局部性Slab分配器倾向于将最近释放的块重新分配给请求,这有助于提高缓存局部性。
  • 减少外部碎片:通过固定大小的块分配,Slab分配器可以减少外部碎片。

TLSF分配器

源码分析

TLSF分配器实现了一种动态内存分配算法,它通过维护多个空闲块列表来优化内存分配。TLSF分配器的关键特点是其二级索引结构,它允许快速找到合适的空闲块。

1
2
3
4
5
pub struct TlsfByteAllocator {
inner: Tlsf<'static, u32, u32, 16, 2>,
total_bytes: usize,
used_bytes: usize,
}

TlsfByteAllocator结构体封装了rlsf::Tlsf,并提供了initadd_memoryallocdealloc等方法,用于初始化、添加内存、分配和释放内存。

优势分析

  • 高内存利用率:TLSF通过低粒度(SLLEN为2)的索引结构,可以更有效地利用内存,减少内部碎片。
  • 快速分配和释放:TLSF的二级索引结构允许快速定位空闲块,从而加快分配和释放速度。

新调度器实现

设计理念

在我的新调度器设计中,我结合了SlabTLSF的优点,将较小的块分配任务交给Slab分配器,而将较大的块分配任务交给TLSF分配器。这种设计允许我复用已经分配的指定大小的块,从而提高内存分配的效率。

优势分析

  • 灵活性:新调度器可以根据请求的大小灵活选择使用SlabTLSF,这使得它可以适应不同的内存分配需求。
  • 内存利用率:通过将较大的块分配给TLSF,我可以利用其高内存利用率的优势,同时Slab分配器可以快速处理小规模的分配请求。
  • 性能优化:新调度器通过复用已经分配的块,减少了分配和释放操作的开销,提高了整体性能。

学习感想

通过参与2024秋冬季开源操作系统训练营,我对内存管理有了更深入的理解。实现SlabTLSF分配器的过程让我体会到了操作系统中内存管理的复杂性和重要性。我学到了如何设计和实现高效的内存分配策略,这对于我未来的职业生涯和学术研究都是非常宝贵的经验。此外,我也认识到了团队合作的重要性,因为在实现过程中,我与队友们进行了深入的讨论和协作,共同解决了遇到的技术难题。总的来说,这次训练营不仅提升了我的技术能力,也锻炼了我的团队合作能力。

结论

通过实现基于SlabTLSF的内存分配器,我成功地提高了内存分配的效率和内存利用率。新调度器的设计不仅灵活,而且能够根据请求的大小选择合适的分配器,这在操作系统的内存管理中是非常重要的。我的实现展示了如何在保证性能的同时,优化内存使用,这对于任何需要高效内存管理的系统都是至关重要的。

异步通信框架: evering

evering 是受 io_uring 启发的的异步通信框架,作为本文实现异步 IPC 的基石,它

  • 🦀 基于 Rust 异步模型
  • 🐧 基于共享内存
  • 📡 提供双向 SPSC 消息队列
  • 🎬 提供灵活的资源管理机制

evering 使用两个单生产者单消费者(SPSC)并发队列进行通信,

1
2
3
4
5
6
7
8
9
10
       Client                                     Server
| |
.------->| |<---------.
| | Request Queue | |
| (Send Request) -->-(3)->-(2)->-(1)->-- (Receive Response) |
| | | |
| | | |
| (Receive Response) --<-(1)-<-(2)-<-(3)-<-- (Send Request) |
| | Response Queue | |
'--------' '----------'

受限于 SPSC 通信,目前 evering 只能遵循 thread-per-core 模型,不同于 tokio、async-std 等使用 work-stealing 的运行时.在基于 thread-per-core 的异步模型中,每个线程有一个局部的运行时,不同线程之间保持最小的通信,甚至完全独立.而基于 work-stealing 的异步模型中,任何异步任务都可能由于运行时的调度而被“偷”到其他线程上运行.二者相比有以下异同:

  • 在 thread-per-core 模型中,异步任务不必担心同步问题,即在 Rust 中不需要使用诸如 impl Send + Future 的类型,这可以大大简化异步编码体验.而在 work-stealing 模型中,几乎所有的异步任务都需要注意线程之间的同步,这样难免会带来一些额外的开销,也使得异步编码更为繁琐^1
  • 正如 work-stealing 这名称所示,在此类模型中,运行时会根据程序实时状态对不同线程上的异步任务进行负载均衡,使得每个线程都不会陷入长时间的忙碌或空间.而对于 thread-per-core 模型,由于异步任务不能在多线程之间共享,当处理某个 CPU 密集型任务时,很可能因为长时间阻塞而导致该线程上的其他任务迟迟得不到处理,最终出现较高的响应延迟.因此 thread-per-core 不适合 I/O 密集混合 CPU 密集的场景.

实验性异步 IPC 模型: evering-ipc

evering-ipc 在 evering 的基础上,同时利用 Linux 的共享内存机制实现了异步 IPC.具体而言,要通信的两个进程使用以下所示的共享内存区进行同步:

1
2
3
4
5
6
7
.-----------------------------------------------------------------------------.
| | | | |
| [1] uring offsets | [2] allocator | [3] uring buffers | [4] free memory ... |
| ^ | | | ^ |
'-|-------------------------------------------------------------------------|-'
'-- start of the shared memory (page aligned) |
end of the shared memory --'
  • [1] 是整个共享内存区的起始,通过 mmap(2) 映射到地址空间中,因此是页对齐的.
  • [1] 中包含 evering 的两个通信队列的偏移量,基于这个偏移量配合 evering 提供的构造队列的接口,可以确保两个进程看到的状态是相同的.
  • [2] 中包含一个单线程的内存分配器,这个分配器通常由客户端使用.
  • [3] 是通信队列的实际所在位置.
  • [4] 是其余全部的空闲内存,这些内存由 [2] 中的分配器管理.

在 evering-ipc 中,uring 仅用于传递静态类型且通常仅有十几到几十个字节的消息.而对于更大的以及编译期间无法确定大小的数据,它利用共享的空闲内存来传递.具体而言,

  1. 客户端获取内存分配器,从中分配合适的内存块,并写入请求体作为待传递数据
  2. 客户端将该内存块指针以及其他信息构造成请求头作为消息写入请求队列.
  3. 服务端接收并解析请求,得到请求头请求体
  4. 服务端处理请求,将处理结果构造响应头作为消息写入响应队列.
  5. 客户端收到响应,检查结果并执行后续流程.

当服务端同样也需要传响应体作为数据时,有多种思路可以实现:

  1. 将分配器加锁使它能同时在服务端和客户端使用.对于请求频率特别高的场景,锁可能会降低整体性能.
  2. 增加一个分配器供服务端使用.对于空闲内存分配比例的设定可能不容易权衡.
  3. 结合前两个思路,使用两个分配器,但空闲内存加锁,根据程序运行状况动态调整分配给两方的空闲内存.
  4. 空闲内存完全由客户端管理,在请求时,客户端分配合适大小的内存块供服务端写入响应体.实际中,客户端可能需要预请求一次来确定响应体的大小.

evering-ipc 目前采用第 4 种思路.此外,不难发现,在 evering-ipc 中,数据都是通过指针进行传递的.而在程序运行时,共享内存通常被映射到不同的地址,因此实际传递的是数据块在共享内存中的偏移量.同时,共享内存区的起始地址都是页对齐的,这保证了所有基于相对偏移量得到的地址,即使在两个进程间不同,对齐也是一致的,从而满足 Rust 对不同类型内存布局的要求.

多种 IPC 方案的性能测试: ipc-benchmark

ipc-benchmark 针对多种 IPC 方案进行了关于通信延迟的性能测试,这些方案包括:

  • 基于 evering + memfd 的 IPC
  • 基于 shmipc + memfd 的 IPC
  • 基于 tokio + epoll + UDS 的 IPC
  • 基于 tokio + io_uring + UDS 的 IPC
  • 基于 monoio + io_uring + UDS 的 IPC

其中,

  • memfd 是 create_memfd(2) 创建的匿名内存.
  • UDS 是指 Unix Domain Socket.
  • shmipc 是基于共享内存的高性能 IPC 框架.
  • tokio 是基于 epoll(7) 和 work-stealing 模型的异步 I/O 运行时.
  • tokio-uring 为 tokio 提供了基于 io_uring 的异步 I/O 实现.
  • monoio 是基于 io_uring 和 thread-per-core 模型的异步 I/O 运行时.

对于每个框架我们采用如下测试方案:

  1. 启动两个线程作为客户端和服务端.
  2. 客户端写入 i32 的请求头以及大小为 buf_size 的请求体.
  3. 服务端接收并校验请求头和请求体,随后同样写入 i32 的响应头和 buf_size 大小的响应体.
  4. 客户端接收并校验响应头和响应体.

其中,buf_size 以字节为单位,由 4B 逐渐增长到 4M.

结果对比

详细的性能测试的结果见 https://github.com/loichyan/openoscamp-2025s/tree/ipc-benchmark,下面我们将对该结果进行简略的分析.

此图对比了完整的测试结果,其中 $x$ 轴是上述的 buf_size,而 $y$ 轴则是完成一次测试所需的平均时间.可以看到,随着数据大小的增长 evering 和 shmipc 明显优于其他三者,并且相对于表现最差的 tokio_epoll,二者分别有接近 50% 和 30% 的性能提升.

此图对比了前五个测试的结果,此时数据并不算大,都在通常范围之内.这里能发现相对于另外三者,evering 和 shmipc 都有超过 80% 的性能提升.

此图对比了中间五轮测试的结果,此时数据大小开始逐渐出现大幅度的增长.可以看到,除了 evering 和 shmipc 外的三者针对大块数据的传输并无明显差异.

此图对比了最后五轮测试的结果,此时数据大小已接近极端情况.这里能观察到与第一个对比图同样的结果.

测试结论

单从性能的角度来看,对于上述五种 IPC 方案,evering > shmipc >> monoio > tokio_uring > tokio_epoll

对比前两者,shmipc 支持 MPSC(多生产者多消费者)的通信方式,而本测试中仅使用 SPSC 的模型进行测试,因此无法发挥其完整的优势.另外,对共享内存处理的方式不同也可能导致了一些性能差异.而对于另外三者,由于使用 UDS 需要将数据在用户空间和内核空间来回拷贝,在面对大块数据时,这将大大降低整体性能.而对于极小的数据块,又由于系统调用等带来的开销,最终需要接近 10 倍的额外时间来完成测试.这一点可以在火焰图^2中体现:

evering (buf_size=4B) tokio-epoll (buf_size=4B)

此图中展示了在 4B 数据下,性能测试主体函数中各子过程的占比.其中,蓝色高亮部分是校验数据过程,用作参照.不难发现,evering 中主要时间都消耗在传递消息所需的多线程同步上了.而在 tokio-epoll 中则是多个与内核交互的函数调用占用主要时间.在后几轮测试中,当数据变得非常大时,这些消耗则变得无关紧要,此时的性能热点是数据传递引起的内存拷贝.下面的火焰图可以佐证:

evering (buf_size=4M) tokio-epoll (buf_size=4M)

此图与上面两个图相同,不过这里的数据大小是 4M.很明显,当数据非常大时,evering 中绝大部分时间用来初始化需要传递的数据,但传递的过程几乎不占用太多时间.而 tokio-epoll 中的情况更加复杂,除了拷贝数据以外,还花费了相当一大部分时间执行内存分配,这些内存用于放置从内核空间传递来的数据.

至于后三者的性能差别,我们猜测主要是由于:

  1. 设计架构不同,monoio 是单线程的 thread-per-core 模型,因此与我们的测试相性更好.
  2. 基于 io_uring 实现的异步 I/O 相对于基于 epoll 的,理论上会花费更少的时间在与内核交互上.并且测试中我们利用 io_uring_register(2) 做了进一步的优化,减少了传递小数据时内核处理的开销.

未完成的任务

让 evering 支持 MPSC

正如开头所述,evering 目前只实现了 SPSC 队列,受限于此,不能很好的处理复杂的应用场景.而实现 MPSC 队列之后,客户端就能更好的与 tokio 等运行时协同工作.要实现这个目标,主要是对 evering 的队列结构 RawUring 和操作驱动结构 Driver 进行多线程改造,并且尽可能要实现无锁的数据结构.对于 RawUring 的改造可以参考现有的并发队列实现.而 Driver 底层依赖于 slab::Slab 结构体,因此改造难度稍高.不过,目前 evering 已经支持使用固定大小的 Slab,基于这一点可以大幅降低多线程化的难度.

基于 evering 实现系统调用

evering-ipc 只适用于用户进程之间的通信,而本项目最初的目标是实现用户进程和内核的通信.对于这个目标,除了处理共享内存以外,可能还会遇到页表隔离的问题,以及用户进程意外退出后,对应内核异步任务的清理问题.更长远来看,当实现异步系统调用之后,那么可以将用户进程转彻底换成类似 Rust 中 Future 的无栈协程,从而将开销进一步降低.此外,迁移到异步系统调用时,不可避免的会破坏与现有同步代码的兼容性.因此,实现基于有栈绿色线程的异步系统调用也是一个值得尝试的目标.