0%

作为常年开发网络程序的我,对异步、并发操作却仅仅停留在使用层面,这对我来说是一件长期的困扰。于是在第四阶段,我毫不犹豫地选择了“基于协程异步机制的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的架构以后,感觉有很多的收获,了解到了一些新的想法。希望有时间的时候,可以进一步完善这个项目。参加这次训练营非常开心。

第四阶段参加了项目四基于协程异步机制的 OS,主要学习了协程异步的基本原理,阅读了 Tokio 源码,还了解了 io_uring。最终实现了简单的异步任务调度的操作系统,实现了异步任务调度功能,已经用于模拟异步延迟的 delay 函数,下一阶段还要继续实现异步的 I/O。

第四阶段还旁听了项目一题目一 Unikernel 支持 LinuxApp 第一周的学习,并参与项目实验,切身体验到了单内核应用开发所遇到的问题和难点。第二周是关于实现 Linux 应用支持的,因为本身对 Linux 应用了解就较少,也没精力和时间投入学习,只能战略性先放弃了。

我觉得经过两个项目的学习,我已经有了一个构建单内核异步操作系统的想法:内核像库一样提供,开发者只需选择需要使用的 future,然后像构建普通应用一样构建单内核操作系统,可以主要应用于嵌入式系统。接下来就是尝试实现这个框架,OS 库实现内存分配,异步任务调度等功能,使开发者调用库即可构建支持 Http Server 的单内核 OS,并运行到真机上。

同时,我也输出了两篇笔记,内容如下:

【Async OS】协程基本概念

协程的目的

协程的目的在于解决并发问题。常规并发是借助操作系统提供的线程来实现,其过程包含生成线程,通过系统调用执行 I/O 操作,并且在 I/O 执行期间会阻塞相应线程直至操作完成。在此过程中,存在两大显著问题:

  1. 用户态和内核态切换成本颇高。每次切换都涉及到系统资源的开销以及一定的时间消耗,这在频繁进行切换的场景下,会对整体性能产生较大影响。
  2. 操作系统线程需要预分配堆栈。每个线程都要提前分配好相应的堆栈空间来存储运行时的数据,当要实现大规模并发时,大量的线程就意味着需要大量内存来维持这些堆栈,内存资源占用较大。

协程解决并发问题的方式

协程主要通过以下两种方式来解决上述并发问题:

  1. 实现用户态的线程,也就是协程本身。协程运行在用户态,避免了频繁进出内核态带来的高昂切换成本,使得执行流程相对更为高效、轻便。
  2. 采用无栈协程的方式,实现不保存堆栈。这就避免了像操作系统线程那样,为每个任务都预留大量堆栈空间,从而节省内存开销。

线程、绿色线程与协程

协程的概念相对模糊,它本质上是一种可以暂停后再恢复执行的函数。不过其暂停机制存在歧义,可分为显式的通过语法函数实现(对应协作式调度)以及隐式地由运行时执行(对应抢占式调度)这两种情况。像知名的 Golang 使用的是堆栈式的抢占式调度方案,在 Rust 术语里,将这种类似操作系统线程的、堆栈式的抢占式调度方案定义为 “绿色线程” 或 “虚拟线程”。从本质上看,它们除了是在用户态实现之外,和操作系统线程并无根本差异。而严格意义上的协程,理应是无栈的协作式调度。

协作式调度与抢占式调度

协作式调度的特点是,任务若不主动让出执行权(yield),就会持续执行下去。与之相反,抢占式调度则是任务随时可能被切换出去。现代操作系统出于避免恶意程序长时间占用 CPU 的考量,大多采用抢占式调度方式。然而,抢占式调度存在明显缺点,由于任务随时可能被切换,所以必须保存任务的堆栈,如此一来,当任务再次被切回时,才能恢复到切换出去时的状态。这就导致在大规模并发场景下,需要耗费大量内存来保存众多任务的堆栈。

有栈协程与无栈协程

有栈协程就是上述提到的在抢占式调度场景下,需要保存任务堆栈的协程类型。那么无栈协程是如何实现的呢?在协作式调度中,因为任务不会被外部强制切出,所以可以在主动让出执行权(yield)时,仅保存必要的状态信息,无需像有栈协程那样完整保存计算过程中的数据。更进一步来说,甚至可以直接利用状态机来实现,从而彻底摆脱对堆栈保存的依赖。

Rust 的协程情况

早期 Rust 曾有过一个堆栈式协程的方案,但在 1.0 版本发布前被移除了。对于 Rust 而言,绿色线程需要解决的关键问题是怎样减小预分配堆栈的大小,进而降低内存开销,毕竟若不能比操作系统线程更节省内存,那使用操作系统线程就好了,没必要再另辟蹊径。
其中 Golang 采用的一种方法是堆栈复制,即先分配一个较小的堆栈,待其达到上限时,再将数据转移至更大的堆栈。但这种方式会引发两个问题:一方面,需要跟踪并更新原先指向堆栈的指针,这一过程本质上和垃圾回收器类似,只是将释放内存变成了移动内存;另一方面,内存复制操作会带来额外的性能开销。而 Golang 本身就有垃圾回收器且能接受额外的性能开销,所以在某些方面可以应对这种方式带来的问题。但对于注重性能和内存管理效率的 Rust 来说,这两点都是难以接受的,因此 Rust 最终选择使用无栈式的协程方案,其实现原理是将代码编译成状态机,虽然这种方式相对较难理解,但社区中已有不少优秀文章对此进行了清晰讲解,例如 blog-os 的 async-await 章节。以下是编译成状态机后的大致伪代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
enum ExampleStateMachine {
Start(StartState),
WaitingOnFooTxt(WaitingOnFooTxtState),
WaitingOnBarTxt(WaitingOnBarTxtState),
End(EndState),
}

impl Future for ExampleStateMachine {
type Output = String; // return type of `example`

fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
loop {
match self { // TODO: handle pinning
ExampleStateMachine::Start(state) => {…}
ExampleStateMachine::WaitingOnFooTxt(state) => {…}
ExampleStateMachine::WaitingOnBarTxt(state) => {…}
ExampleStateMachine::End(state) => {…}
}
}
}
}

总结

协程主要是用于解决并发问题,而非性能问题。

Golang 中的协程属于 “有栈协程”,与操作系统中基于堆栈式抢占式调度的线程本质相同,在 Rust 中被称作 “绿色线程”。而 Rust 实现的协程是 “无栈协程”,采用无堆栈的协作式调度方案,其核心原理是将代码编译成状态机。

网上常见对于 async Rust 的批判,认为其提升不了多少性能,却需要投入大量资源进行开发,还增加了开发复杂度,甚至会导致 “函数着色” 问题。实际上,这些批判有一定道理,但需要明确的是协程本身旨在解决并发问题,而非聚焦于性能提升。

【Async OS】最小化异步操作系统

前提

需要了解操作系统基础知识,实现简单的操作系统内核框架,并且要实现堆内存分配,因为必须要使用 BoxPin,还要实现打印功能用于测试,还需要实现时间获取,用于模拟异步延迟。

Async in Rust

Rust 提供了 Future trait 用于实现异步操作,其结构定义如下:

1
2
3
4
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}

poll 方法接受两个参数,Pin<&mut Self> 其实和 &mut Self 类似,只是需要 Pin 来固定内存地址,cx 参数是用于传入一个唤醒器,在异步任务完成时可以通过唤醒器发出信号。poll 方法返回一个 Poll 枚举:

1
2
3
4
pub enum Poll<T> {
Ready(T),
Pending,
}

大致工作原理其实很简单,调用异步函数的 poll 方法,如果返回的是 Pending 表示值还不可用,CPU 可以先去执行其他任务,稍候再试。返回 Ready 则表示任务已完成,可以接着执行往下的程序。

运行时

知道基本原理后,基于异步来构建操作系统的思路就很清晰了,即遇到 Pending 就切换到另一个任务,直到所有任务都完成。

先创建一个 Runtime 结构体,包含 tasks 队列,用于存储需要执行的任务。spawn 方法用于将任务添加到队列,run 方法用于执行异步任务,其逻辑是先取出队列中一个任务,通过 loop 不断尝试执行异步任务,如果任务 Pending,则先去执行另一个任务,以此实现非阻塞,直到队列任务全部为空。

poll 方法需要传入 Waker 参数,用于在异步任务完成后发出信号,因为目前是 loop 盲等的机制,并没有实现真正的唤醒,所以先采用一个虚假唤醒器。

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
pub struct Runtime {
tasks: VecDeque<Task>,
}

impl Runtime {
pub fn new() -> Self {
Runtime {
tasks: VecDeque::new(),
}
}

pub fn spawn(&mut self, future: impl Future<Output = ()> + Send + Sync + 'static) {
self.tasks.push_back(Task::new(future))
}

pub fn run(&mut self) {
while let Some(mut task) = self.tasks.pop_front() {
let waker = dummy_waker();
let mut context = Context::from_waker(&waker);
loop {
match task.poll(&mut context) {
Poll::Ready(val) => break val,
Poll::Pending => {
self.tasks.push_back(task);
break;
}
};
}
}
}
}

任务

任务的结构也很简单,含有一个内部 future,在 poll 时执行 future 的 poll。内部 future 定义很长但是不复杂,基于的 future 结构是 Future<Output = ()>,然后需要一个 'static 生命周期,同时需要 SendSync 实现跨线程共享,虽然现在没用到,但是 Rust 编译器可不同意你不写。dyn 声明动态类型也是必须要,同时还需要使用 Box 包裹来使编译器确定闭包大小,Pin 用于固定内存位置不可以动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Task {
future: Pin<Box<dyn Future<Output = ()> + Send + Sync + 'static>>,
}

impl Task {
pub fn new(future: impl Future<Output = ()> + Send + Sync + 'static) -> Task {
Task {
future: Box::pin(future),
}
}

fn poll(&mut self, cx: &mut Context) -> Poll<()> {
self.future.as_mut().poll(cx)
}
}

虚假唤醒器

无需了解过多,真正实现唤醒器的时候才需深入了解。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn dummy_waker() -> Waker {
unsafe { Waker::from_raw(dummy_raw_waker()) }
}

fn dummy_raw_waker() -> RawWaker {
fn no_op(_: *const ()) {}
fn clone(_: *const ()) -> RawWaker {
dummy_raw_waker()
}

let vtable = &RawWakerVTable::new(clone, no_op, no_op, no_op);
RawWaker::new(0 as *const (), vtable)
}

Delay

基础框架实现完后,还需要实现一个延迟任务,用于模拟耗时操作。我打算实现一个 delay 方法模拟 sleep,以测试任务 sleep 的时候运行时会切换到下一个任务。代码很简单,DelayFuture 结构体包含一个 target_time 和一个 wakertarget_time 表示延迟到什么时候,waker 用于在延迟完成后发出信号。poll 方法中判断当前时间是否大于 target_time,如果大于则返回 Ready,否则返回 Pending

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
pub async fn delay(ms: usize) {
DelayFuture::new(ms).await;
}

struct DelayFuture {
target_time: usize,
waker: Option<Waker>,
}

impl DelayFuture {
fn new(ms: usize) -> Self {
DelayFuture {
target_time: get_time_ms() + ms,
waker: None,
}
}
}

impl Future for DelayFuture {
type Output = ();

fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if get_time_ms() >= self.target_time {
Poll::Ready(())
} else {
Poll::Pending
}
}
}

测试

rust_main 操作系统的 rust 入口,task1task2 是两个异步任务,task1 先打印 start task 1,然后延迟 200ms,再打印 end task 1task2 也是类似,只是延迟时间更长。运行时先执行 task1,然后切换到 task2,再切换到 task1,最后切换到 task2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[no_mangle]
fn rust_main() {
let mut rt = Runtime::new();
rt.spawn(task1());
rt.spawn(task2());
rt.run();
}

async fn task1() {
println!("start task 1");
delay(200).await;
println!("end task 1");
}

async fn task2() {
println!("start task 2");
delay(500).await;
println!("end task 2");
}

打印结果:

1
2
3
4
start task 1
start task 2
end task 1
end task 2

总结

Rust 的异步编程还是很方便的,但是目前的实现还是很粗糙,比如没有实现真正的唤醒器,也没有实现真正的异步操作,只是模拟了异步延迟。下一步是实现真正的唤醒器,然后尝试实现异步的 I/O。

arceos宏内核扩展

我选择的方向是arceos宏内核扩展,主要工作是在新的宏内核扩展starry-next上,补全系统调用。我希望通过这个过程能更好的理解宏内核的内部的结构,和组件化操作系统的抽象解耦思想,也希望自己的代码能够更好的实现宏内核扩展和基座的解耦,虽然现在还是以先实现功能为前提,但是先完成后完美,之后逐步完善功能,并且抽离耦合的部分,让系统调用的实现更加优雅。

实现成果

目前已经完成绝大部分系统调用的实现,最花费时间的是任务clone的相关系统调用,将arceos的多任务切换(时钟中断抢占机制),地址空间(与rcore的地址空间排布不同,复制内核的地址空间)等相关的知识进行研究之后,才将clone的简单功能fork实现,之后关于任务相关的系统调用就很顺利了。

接下来,对arceos的文件系统进行研究,研究了线程间资源独立的新机制namespace,使用了一个很好的方法,实现了unikernel资源全局共享,使用宏内核扩展后,资源线程间独立或共享的机制。实现文件系统相关的api主要也就是调用axfs提供的api进行实现,需要解决的是axfs不支持open目录,但是openat这个系统调用需要打开目录,所以需要解决这个问题。

感悟

通过四阶段的学习,学习了组件化操作系统实现宏内核的思想和方法,经历过这三个月的学习,从不会rust和操作系统的小白,慢慢一步一步做,从学习rust,到完成rcore,再到学习arceos,逐步学习操作系统多任务、地址空间、文件系统等等,更加理解了riscv特权级结构,sv39页表结构等risv体系结构知识,总结这三个月,收获非常大,在这里也希望开源操作系统训练营可以越办越好,更多的人可以在这里学到知识,收获成长。

2024秋冬开源操作系统训练营第四阶段总结-张宇驰

学习内容

这个阶段,我选择了unikernel方向一的任务,为arceos实现linux的app移植,其中我进行了如下方向的探索:

  1. 跟安同学讨论了基于对Ecall进行截断的思路,并自己尝试截断Ecall并导向自己提供的函数,在最小幅度的情况下实现。最后也是其他东西都有了,但是没有做明白怎么截断。
  2. 同样是在群里,和安同学讨论在SBI中进行转发的设想,不过后来没琢磨明白怎么处理这些新的调用号而困惑,最后也仅仅停留在理论层面上。
  3. 最终的选择:自己实现Mocklibc,验证了用自己的函数替换musl中的系统调用的思路,也就是获得abitable,然后在对应的地方直接进行跳转。交流会的时候看到了另一个同学针对动态链接的实现思路,尝试修改代码后发现支持动态链接还是好做的,但是由于时间原因在交这份报告之前还没有跑起来。

学习收获

这个任务对我来说属实难度比较大,总结一下有两点没有做好:

  1. 时间管理。在学校安排中,我们进行了为期两周的实习,其实空余时间挺多的,但是自己没有利用起来(也跟中间还有个考试有关),然后往后做的时间少而且累。
  2. 探索能力。在面对二、三周的复杂项目时,我没有很好的确定到自己的方向,在一定程度上延误了时间。(不过就是探索了很多有意思的东西了)

下一步计划

往后顺着PPT上的东西继续探索吧,我不是特别在意一定要在训练营之中去做什么事儿。出于对这个项目的兴趣,我打算保持跟这里的交流,往后继续探索,验证一下自己的想法。然后看看寒假有没有机会去实习,吧这个项目完善完善,而且家里也没人,假期回家的意义也不大。

一、验证说明

在这个仓库中 https://github.com/ghostdragonzero/arceos_test 我直接修改了ixgbe的pid vid匹配参数来匹配igb网卡,并且通过修改axdriver到引用我自己的仓库。以此来实现替换ixgbe的目的。
拉取后可以直接执行make A=examples/httpserver PLATFORM=aarch64-qemu-virt LOG=debug SMP=2 NET=y FEATURES=driver-ixgbe NET_DEV=user run

这个仓库 https://github.com/ghostdragonzero/igb-driver/tree/my_driver
是我在share_test框架下实现的驱动完成了 link_up rx_init tx_init 初始化流程也是我在这个框架下实现然后搬到ixgbe框架里在arceos内运行没问题的
但是这里实现的memry是使用的arceos中hal的实现 ring结构是直接使用的ixgbe的ring

二、学习的感受

这是我第一次成功完成这个训练营,之前参加了很多次都在各种阶段就没有坚持下去,这次能成功完成是相当开心的。

并且这次的驱动开发给我感受最深的就是,硬件(虽然是模拟的外设)的逻辑是很固定的出现了预期之外的行为,99%是自己写的存在错误
记录我认为在我开发的时候我查看时间最久的问题:

  1. 注意有效数据位
    在操作MDIC寄存器的时候,我使用的先读取在写入的方法,但是我读取的是u32,它的数据位是低16位。我把高位的标志位也保留到了下次要写的数据里面。导致一直出错,后面是使用断点调试的方法才对比出来不同再进行了改正。
  2. 注意不要定义类似的常量
    还是在操作MCIC寄存器的时候 在判断标志位时候我错误的使用了MDIC_READ作为READY导致我一直读取成功但是写失败,这我反复的追查了好久,但是这也是我自己的不细心。
  3. 注意set_flag和set_reg的区别
    这是在最后的检查的时候发现的,因为是在ixgbe的基础上使用的所以一些寄存器不仅仅是地址变了 一些功能也分配到了不同的寄存器,而导致有些适用set_reg有些则应该是set_flag
    这一点要尤其注意因为很多时候就是一些标志位的不同就会有不一样的地方

三、学习的总结

这次学习给我最大的收获就是真的从datasheet来一步步的开发一个驱动的经验,并且在开发的过程中一步一步的调试,通过设置断点来看寄存器的值。这个是我最喜欢的一个功能,因为我在平时驱动的开发时候最难受的就是修改需要编译版本才能做验证,找不到一个快速验证的办法导致问题进展缓慢。如果能有多一点的可以这样来验证的控制就好了。

再来说一下,这次学习中我的不足,实际上我算是让这个网卡能用起来了 但是我还是不理解他更上层的东西,不清楚他的ring是怎样收发信息的,不明白他这个网络是怎么跑通的,群友说的报文又是什么。体现在开发过程中就是,出现问题我只能再次去查看linux的igb驱动都配置了哪些寄存器,ixgbe有什么流程但是对于这些流程的意义我不明白,只是datasheet上写了,或者看到其他驱动做了于是我也就去做试试看。

感觉还需要继续学习一下网络相关的基本概念才能真的完成这个网卡驱动

四、关于ixgbe与igb的思考

因为我是直接鸠占鹊巢的方式在arceos里成功用起来igb,所以我认为既然能够这样用起来 是不是侧面说明对于ixgb_hal和ixgbe_nic的抽象就是一个能够在igb上(或者同一E100系列的网卡)复用的结构。后续是不是可以直接通过条件编译或者直接在网卡驱动的匹配处来区分出来,这样可以减少重复的代码结构,并且我认为上层本来也不关心下层实际的控制器。

碎碎念

因为之前在忙别的事情,所以晚了一周进组,写代码更是只剩两天时间,所以对代码基本是能跑就行的态度。

不过也有好处,因为晚开始所以资料会比一开始弄多点,开始写的时候就基本能 link up 了。

找参考资料

最重要的参考资料自然是 Intel 82576 手册。不过如果有代码参考肯定是更好的。

ArceOS 自己项目里面就有一个 ixgbe 的驱动,虽然是不同型号的网卡,但是部分逻辑可以参考,而且是 Rust 编写,很好理解。

其次就是 igb 官方驱动,在 github 上可以找到,参考资料里也给了链接。

我简单瞟了两眼,里面感觉到这估计在 linux kernel 源码里也有一份,一搜果然有。

正好我机器上有一份之前用来学习的 linux 源码,配置一下正好可以看看。

Linux src

把 CONFIG_IGB 开起来,编译一下再 gen_compile_commands.py 生成下 compile_commands.json 就可以愉快的跳转了。

驱动初始化代码在 __igb_open 里,感觉把这玩意实现了应该就可以了。

为了方便实现,我直接跳过了 Flow Control 的部分,感觉应该不会有太大问题。

参考里一直到初始化 link 和 phy 的部分都挺好懂,但是到初始化 rx 和 tx 的时候就开始有点艰难了。

ethernet-intel-igb v1.0.1

于是我又转头看 github 上 igb 驱动的代码,不过我特意切换到了 v1.0.1 的 tag 上,一般越早期的代码越简单越好懂。

果然这里的初始化很简单,搞了几个寄存器就完了。

不过 v1.0.1 用的地址都是 alias 地址,我还是自己查了查手册,把正确的地址定义到常量上搞完的。

开启中断的方式在代码里也挺简单的。

不过让我觉得意外的是,老师给的关中断的方式和 igb C 驱动的方式并不一致,我最终决定参考 C 驱动的方式来关闭中断和开启中断。

Ring

到了 Ring 部分就困难了,我直接放弃了自己写,正好群里有人提到 ixgbe 改改就能用,我就决定直接把我写完的初始化部分放到 ixgbe 里头跑。

把 ixgbe fork 下来,加一层 axdriver_net 的实现,把类型名重置一下,再在 ArceOS 的 Cargo.toml 里头改下依赖名字,把 ixgbe.rs 里的 Hal 实现复制一份到 driver.rs 里,一通操作下来先让程序可以编译。

把原来的东西直接改写进去就能跑 httpclient 了,非常的快乐。

但是跑 http server 时会遇到 curl 不通的情况。原因是没有在 RCTL 里加入 BAM flag, 加上就好了。

我自己 debug 的时候发现现象是收不到包,所以中途加过 BAM flag, 但是因为图省事写成 0x8000 的形式,没有单独弄一个 const value 去存。

结果当时 typo 打成了 0x800 或者 0x80000, 导致 bug 还在,后来 revert 的时候就删掉了。

我跑 http client 的时候发现实际上是可以正常收包的,所以怀疑是 host 机器不能直接发包到 qemu 里,于是在 http server 源码里开了个 http client 来 request, 结果就 OK 了。

当时不明所以,不过群里问了下发现正确做法是加 BAM flag, 于是就可以直接在 host 机器上连接到 server 了。