0%

学习内容

介绍地址空间和页表相关的内容.任务调度下次课说.

  1. 如何在一个主任务的基础上启用一个子任务,让他完成一系列的工作
  2. 启用一个单任务的基础上能够开两个任务,然后完成两个任务之间的通信

makefile的原理

调用是这样实现的:

首先是Makefile里的:

1
run: build justrun

它需要有build和justrun这两个虚拟文件.

再去看build:

1
build: $(OUT_DIR) $(OUT_BIN)

需要的是OUT_DIR和OUT_BIN这两个实体文件.

创建它们两个的文件在scripts/make/build.mk:

1
2
3
4
5
$(OUT_DIR):
$(call run_cmd,mkdir,-p $@)

$(OUT_BIN): _cargo_build $(OUT_ELF)
$(call run_cmd,$(OBJCOPY),$(OUT_ELF) --strip-all -O binary $@)

这里调用的run_cmd在scripts/make/utils.mk:

1
2
3
4
5
6
7
8
9
10
11
GREEN_C := \033[92;1m
CYAN_C := \033[96;1m
YELLOW_C := \033[93;1m
GRAY_C := \033[90m
WHITE_C := \033[37m
END_C := \033[0m

define run_cmd
@printf '$(WHITE_C)$(1)$(END_C) $(GRAY_C)$(2)$(END_C)\n'
@$(1) $(2)
endef

这里$(1)和$(2)表示接受的是两个参数.

这个是两个操作,

  1. 通过颜色参数把要执行的命令输出出来(第一行)
  2. 第二行相当于执行接受的两个参数
  3. $@是代表这个虚拟文件本身

其实这一套操作下来就是创建这个OUT_DIR这个名字的文件夹.

在Makefile中:

1
2
3
4
5
6
A ?= tour/u_1_0
APP ?= $(A)
... ...

# Paths
OUT_DIR ?= $(APP)

这时候把目光转回OUT_BIN.它在scripts/make/build.mk中:

1
2
$(OUT_BIN): _cargo_build $(OUT_ELF)
$(call run_cmd,$(OBJCOPY),$(OUT_ELF) --strip-all -O binary $@)

那么它的构建需要虚拟文件_cargo_build和实体文件OUT_ELF.

那么_cargo_build的功能也是先进行输出,随后调用cargo_build:

1
2
3
4
5
6
7
8
_cargo_build:
@printf " $(GREEN_C)Building$(END_C) App: $(APP_NAME), Arch: $(ARCH), Platform: $(PLATFORM_NAME), App type: $(APP_TYPE)\n"
ifeq ($(APP_TYPE), rust)
$(call cargo_build,$(APP),$(AX_FEAT) $(LIB_FEAT) $(APP_FEAT))
@cp $(rust_elf) $(OUT_ELF)
else ifeq ($(APP_TYPE), c)
$(call cargo_build,ulib/axlibc,$(AX_FEAT) $(LIB_FEAT))
endif

那么cargo_build在scripts/make/cargo.mk里:

1
2
3
define cargo_build
$(call run_cmd,cargo -C $(1) build,$(build_args) --features "$(strip $(2))")
endef

由于我们知道run_cmd是什么套路了,因此这边就是执行cargo来构建一个elf文件.

回到OUT_BIN这边,得到两个所需文件之后,通过OBJCOPY ?= rust-objcopy --binary-architecture=$(ARCH)(在Makefile)中定义,把elf多余的信息头去掉,只留下可执行二进制文件:

1
2
$(OUT_BIN): _cargo_build $(OUT_ELF)
$(call run_cmd,$(OBJCOPY),$(OUT_ELF) --strip-all -O binary $@)

最后回到justrun,它调用了run_qemu(在scripts/make/qemu.mk),

1
2
3
4
define run_qemu
@printf " $(CYAN_C)Running$(END_C) on qemu...\n"
$(call run_cmd,$(QEMU),$(qemu_args-y))
endef

这里调用了QEMU,是依赖于ARCH ?= riscv64(在Makefile):

1
QEMU := qemu-system-$(ARCH)

这里调用了qemu_args-y,同样是依赖于ARCH的这里不赘述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
qemu_args-x86_64 := \
-machine q35 \
-kernel $(OUT_ELF)

qemu_args-riscv64 := \
-machine virt \
-bios default \
-kernel $(OUT_BIN)

qemu_args-aarch64 := \
-cpu cortex-a72 \
-machine virt \
-kernel $(OUT_BIN)

qemu_args-y := -m 128M -smp $(SMP) $(qemu_args-$(ARCH))

ReadPFlash

本节目标:

  1. 引入页表管理组件,通过地址空间重映射,支持设备MMIO
  2. 地址空间概念,重映射的意义,页表机制

希望能从PFlash把应用的数据加载进来,以为运行后边的程序做基础.

实验没有paging时的情况

正常运行

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
OpenSBI v0.9
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|

Platform Name : riscv-virtio,qemu
Platform Features : timer,mfdeleg
Platform HART Count : 1
Firmware Base : 0x80000000
Firmware Size : 100 KB
Runtime SBI Version : 0.2

Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000087000000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes

Boot HART ID : 0
Boot HART Domain : root
Boot HART ISA : rv64imafdcsu
Boot HART Features : scounteren,mcounteren,time
Boot HART PMP Count : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count : 0
Boot HART MHPM Count : 0
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109

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

Try to access dev region [0xFFFFFFC022000000], got 0x646C6670
Got pflash magic: pfld

pfld在哪?

在scripts/make/utils.mk中,在pflash中写入了pfld:

1
2
3
4
5
6
7
8
9
10
define mk_pflash
@RUSTFLAGS="" cargo build -p origin --target riscv64gc-unknown-none-elf --release
@rust-objcopy --binary-architecture=riscv64 --strip-all -O binary ./target/riscv64gc-unknown-none-elf/release/origin /tmp/origin.bin
@printf "pfld\00\00\00\01" > /tmp/prefix.bin
@printf "%08x" `stat -c "%s" /tmp/origin.bin` | xxd -r -ps > /tmp/size.bin
@cat /tmp/prefix.bin /tmp/size.bin > /tmp/head.bin
@dd if=/dev/zero of=./$(1) bs=1M count=32
@dd if=/tmp/head.bin of=./$(1) conv=notrunc
@dd if=/tmp/origin.bin of=./$(1) seek=16 obs=1 conv=notrunc
endef

那么这个在哪里调用呢?答案是没有调用.

我们是直接pull下来的,如果调用make pflash_img就会重新生成它.

在scripts/make/qemu.mk里:

1
2
3
4
5
6
7
8
9
10
qemu_args-y := -m 128M -smp $(SMP) $(qemu_args-$(ARCH))

qemu_args-$(PFLASH) += \
-drive if=pflash,file=$(CURDIR)/$(PFLASH_IMG),format=raw,unit=1
... ...

define run_qemu
@printf " $(CYAN_C)Running$(END_C) on qemu...\n"
$(call run_cmd,$(QEMU),$(qemu_args-y))
endef

而在Makefile里PFLASH被指定为y.这样就会在运行qemu的时候加上pflash.img这个文件.

没有指定paging的情况

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#![cfg_attr(feature = "axstd", no_std)]
#![cfg_attr(feature = "axstd", no_main)]

#[macro_use]
#[cfg(feature = "axstd")]
extern crate axstd as std;

use core::{mem, str};
use std::os::arceos::modules::axhal::mem::phys_to_virt;

/// Physical address for pflash#1
const PFLASH_START: usize = 0x2200_0000;

#[cfg_attr(feature = "axstd", no_mangle)]
fn main() {
// Makesure that we can access pflash region.
let va = phys_to_virt(PFLASH_START.into()).as_usize();
let ptr = va as *const u32;
unsafe {
println!("Try to access dev region [{:#X}], got {:#X}", va, *ptr);
let magic = mem::transmute::<u32, [u8; 4]>(*ptr);
println!("Got pflash magic: {}", str::from_utf8(&magic).unwrap());
}
}

这里需要看下一节关于PFlash的部分,因为这时候需要访问外设,在没有remap的时候是1G的恒等映射,外设没有映射到地址空间中,因此报错.

一开始只映射了物理空间的0x8000_0000到0xC000_0000.

这里访问的物理地址是0x2200_0000,本来就不属于刚刚提到的物理空间,因此通过恒等映射平移也得不到恒等映射之后的虚拟地址.

产生的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
OpenSBI v0.9
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|

Platform Name : riscv-virtio,qemu
Platform Features : timer,mfdeleg
Platform HART Count : 1
Firmware Base : 0x80000000
Firmware Size : 100 KB
Runtime SBI Version : 0.2

Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000087000000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes

Boot HART ID : 0
Boot HART Domain : root
Boot HART ISA : rv64imafdcsu
Boot HART Features : scounteren,mcounteren,time
Boot HART PMP Count : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count : 0
Boot HART MHPM Count : 0
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109

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

Try to access dev region [0xFFFFFFC022000000], got [ 2.002842 0 axruntime::lang_items:5] panicked at modules/axhal/src/arch/riscv/trap.rs:55:13:
Unhandled trap Exception(LoadFault) @ 0xffffffc080203fa6:
TrapFrame {
regs: GeneralRegisters {
ra: 0xffffffc08020376c,
sp: 0xffffffc0802499f0,
gp: 0x0,
tp: 0x0,
t0: 0x3f,
t1: 0x23,
t2: 0x5,
s0: 0xffffffc080249a80,
s1: 0xffffffc080249c60,
a0: 0xffffffc022000000,
a1: 0xffffffc080249a80,
a2: 0x0,
a3: 0xffffffc080203f9e,
a4: 0x2,
a5: 0xffffffc080202b76,
a6: 0xa,
a7: 0x1,
s2: 0x2,
s3: 0xffffffc080249bc0,
s4: 0xffffffc080249bf0,
s5: 0x38,
s6: 0xffffffc080205098,
s7: 0x2,
s8: 0x1,
s9: 0x24000,
s10: 0x2,
s11: 0xffffffc08026e000,
t3: 0x23,
t4: 0x3a,
t5: 0x5000,
t6: 0x55555555,
},
sepc: 0xffffffc080203fa6,
sstatus: 0x8000000000006100,
}

分支名称:tour_u_3_0_no_paging

MAP和REMAP

ArceOS Unikernel包括两阶段地址空间映射,
Boot阶段默认开启1G空间的恒等映射;
如果需要支持设备MMIO区间,通过指定一个feature - “paging”来实现重映射。

上一节说了启动之后需要remap,这样才可以实现重映射.

那么就需要打开paging.

初始化的线性页表

其实是创建了两个映射,相当于拿前一个恒等映射做了跳板,因为要求开启MMU之后仍然可以以原来的物理地址正常访问.

#TODO

后续创建多级页表

#TODO

PFlash

是一个模拟闪存磁盘.QEMU启动的时候会自动从内存中加载内容到固定的MMIO区域.

读操作是不需要驱动的,但是写是需要驱动的.

目前我们只需要读,只要加载成功即可.

物理地址空间

外设被映射到一个物理地址空间里边.

注意:linker_riscv64-qemu-virt.lds,段布局都是听的它的.

分页

类似于rCore的三级页表,我们实验中用的也是SV39.

分页阶段1-恒等映射+偏移

我们希望sbi和kernel都保存在高地址空间.

  1. 开局的时候把虚拟空间内的地址和物理空间内的地址完全对应上.
  2. 给指针寄存器pc栈寄存器sp加偏移,这里的偏移是0xffff_ffc0_0000_0000.

sbi还是放在0x8000_0000,kernel还是放在0x8020_0000.

那么在这个情况下其实已经是物理地址了,就是一个线性偏移的操作实现虚拟地址和物理地址的映射.

如果不需要访问物理设备,现在就可以完成了.

分页阶段2-重建映射

重映射的时候干脆在虚拟地址空间里把sbi去掉,因为不应该继续访问sbi了.

不同的数据段的权限不一样,比如READ,WRITE,EXECUTE不一样.比如代码段就只能读和运行不能写.在重建的时候就不需要给它这些权限.

这样设备的地址空间也可以被映射进来,权限粒度也更细.

这里似乎仍然是线性映射.

多任务

需求:启用多任务,开一个子任务,让子任务替主任务完成一些具体工作,然后回到主任务.

并发是多个任务同时在等待使用CPU而不是同时运行.并行是真的需要同时运行.

调度的一个很好的描述:一个是保存现场,一个是任务无感知.

实验多任务

make run A=tour/u_4_0

任务代码解析:

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
#![cfg_attr(feature = "axstd", no_std)]
#![cfg_attr(feature = "axstd", no_main)]

#[macro_use]
#[cfg(feature = "axstd")]
extern crate axstd as std;

use core::{mem, str};
use std::thread;
use std::os::arceos::modules::axhal::mem::phys_to_virt;

/// Physical address for pflash#1
const PFLASH_START: usize = 0x2200_0000;

#[cfg_attr(feature = "axstd", no_mangle)]
fn main() {
println!("Multi-task is starting ...");

let worker = thread::spawn(move || {
println!("Spawned-thread ...");

// Makesure that we can access pflash region.
let va = phys_to_virt(PFLASH_START.into()).as_usize();
let ptr = va as *const u32;
let magic = unsafe {
mem::transmute::<u32, [u8; 4]>(*ptr)
};
if let Ok(s) = str::from_utf8(&magic) {
println!("Got pflash magic: {s}");
0
} else {
-1
}
});

let ret = worker.join();
// Makesure that worker has finished its work.
assert_eq!(ret, Ok(0));

println!("Multi-task OK!");
}

和上一节的内容一样,同样是访问了我们预导入的pflash.img的前几个字符pfld.

只不过用了spawn的方法生成,并且用join的方法等待.

任务的数据结构

有一个关键点在于task_ext,是任务的拓展属性,是面向宏内核和Hypervisor的关键.

通用调度框架

分层,并且实现同样的接口,这样就可以自己决定是什么样的调度机制.

系统默认内置任务

  • GC: 除main之外的任务(线程)退出后,由gc负责回收清理。
  • IDLE: 当其它所有任务都阻塞时,执行它。对某些arch,wait_for_irqs对应非忙等指令.比如等待中断什么的,而不是忙等,如果发生新的”event“(自己取的名)那么就会马上响应.

MsgQueue

本节目标:

  1. 任务的切换机制,协作式调度算法
  2. 同步的方式,Mutex的机制

运行实验

执行make run A=tour/u_5_0:

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
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
OpenSBI v0.9
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|

Platform Name : riscv-virtio,qemu
Platform Features : timer,mfdeleg
Platform HART Count : 1
Firmware Base : 0x80000000
Firmware Size : 100 KB
Runtime SBI Version : 0.2

Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000087000000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes

Boot HART ID : 0
Boot HART Domain : root
Boot HART ISA : rv64imafdcsu
Boot HART Features : scounteren,mcounteren,time
Boot HART PMP Count : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count : 0
Boot HART MHPM Count : 0
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109

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

Multi-task is starting ...
Wait for workers to exit ...
worker1 ...
worker1 [0]
worker2 ...
worker2 [0]
worker2: nothing to do!
worker1 [1]
worker2 [1]
worker2: nothing to do!
worker1 [2]
worker2 [2]
worker2: nothing to do!
worker1 [3]
worker2 [3]
worker2: nothing to do!
worker1 [4]
worker2 [4]
worker2: nothing to do!
worker1 [5]
worker2 [5]
worker2: nothing to do!
worker1 [6]
worker2 [6]
worker2: nothing to do!
worker1 [7]
worker2 [7]
worker2: nothing to do!
worker1 [8]
worker2 [8]
worker2: nothing to do!
worker1 [9]
worker2 [9]
worker2: nothing to do!
worker1 [10]
worker2 [10]
worker2: nothing to do!
worker1 [11]
worker2 [11]
worker2: nothing to do!
worker1 [12]
worker2 [12]
worker2: nothing to do!
worker1 [13]
worker2 [13]
worker2: nothing to do!
worker1 [14]
worker2 [14]
worker2: nothing to do!
worker1 [15]
worker2 [15]
worker2: nothing to do!
worker1 [16]
worker2 [16]
worker2: nothing to do!
worker1 [17]
worker2 [17]
worker2: nothing to do!
worker1 [18]
worker2 [18]
worker2: nothing to do!
worker1 [19]
worker2 [19]
worker2: nothing to do!
worker1 [20]
worker2 [20]
worker2: nothing to do!
worker1 [21]
worker2 [21]
worker2: nothing to do!
worker1 [22]
worker2 [22]
worker2: nothing to do!
worker1 [23]
worker2 [23]
worker2: nothing to do!
worker1 [24]
worker2 [24]
worker2: nothing to do!
worker1 [25]
worker2 [25]
worker2: nothing to do!
worker1 [26]
worker2 [26]
worker2: nothing to do!
worker1 [27]
worker2 [27]
worker2: nothing to do!
worker1 [28]
worker2 [28]
worker2: nothing to do!
worker1 [29]
worker2 [29]
worker2: nothing to do!
worker1 [30]
worker2 [30]
worker2: nothing to do!
worker1 [31]
worker2 [31]
worker2: nothing to do!
worker1 [32]
worker2 [32]
worker2: nothing to do!
worker1 [33]
worker2 [33]
worker2: nothing to do!
worker1 [34]
worker2 [34]
worker2: nothing to do!
worker1 [35]
worker2 [35]
worker2: nothing to do!
worker1 [36]
worker2 [36]
worker2: nothing to do!
worker1 [37]
worker2 [37]
worker2: nothing to do!
worker1 [38]
worker2 [38]
worker2: nothing to do!
worker1 [39]
worker2 [39]
worker2: nothing to do!
worker1 [40]
worker2 [40]
worker2: nothing to do!
worker1 [41]
worker2 [41]
worker2: nothing to do!
worker1 [42]
worker2 [42]
worker2: nothing to do!
worker1 [43]
worker2 [43]
worker2: nothing to do!
worker1 [44]
worker2 [44]
worker2: nothing to do!
worker1 [45]
worker2 [45]
worker2: nothing to do!
worker1 [46]
worker2 [46]
worker2: nothing to do!
worker1 [47]
worker2 [47]
worker2: nothing to do!
worker1 [48]
worker2 [48]
worker2: nothing to do!
worker1 [49]
worker2 [49]
worker2: nothing to do!
worker1 [50]
worker2 [50]
worker2: nothing to do!
worker1 [51]
worker2 [51]
worker2: nothing to do!
worker1 [52]
worker2 [52]
worker2: nothing to do!
worker1 [53]
worker2 [53]
worker2: nothing to do!
worker1 [54]
worker2 [54]
worker2: nothing to do!
worker1 [55]
worker2 [55]
worker2: nothing to do!
worker1 [56]
worker2 [56]
worker2: nothing to do!
worker1 [57]
worker2 [57]
worker2: nothing to do!
worker1 [58]
worker2 [58]
worker2: nothing to do!
worker1 [59]
worker2 [59]
worker2: nothing to do!
worker1 [60]
worker2 [60]
worker2: nothing to do!
worker1 [61]
worker2 [61]
worker2: nothing to do!
worker1 [62]
worker2 [62]
worker2: nothing to do!
worker1 [63]
worker2 [63]
worker2: nothing to do!
worker1 [64]
worker2 [64]
worker2 ok!
worker1 ok!
Multi-task OK!

分析代码,worker1是尝试获取Arc中的这个双端队列,然后尝试在队列的最后放东西.

由于是协作式调度,worker1每次放入的之后都会yield,因此worker2就会接手,然后尝试把队列里所有的内容都打出来,如果队列为空就报告worker2: nothing to do!,然后再由worker1接手CPU.

协作式调度

与rCore不同,现在使用的是List而不是一个数组,原理上就不设置任务的个数了.

互斥锁和自旋锁

自旋锁可能是我们脑子中的那个锁,每次访问资源需要访问这个锁,如果没办法访问那你这个任务要处理这种情况.

互斥锁则是自己加了一个等待队列,如果有任务在等待这个资源,那么这个任务被加入等待队列之后不会参与调度,这样就节省了很多任务切换时的资源.

bump内存分配算法

#TODO

挑战性作业

initialize global allocator at: [0xffffffc08026f000, 0xffffffc088000000)

5376*2

align是8的是有关于Vec的内存的分配

131,665,920

1048576

524288+8000

#TODO

概念

  1. 虚拟时钟中断支持;虚拟机外设的支持。
  2. 虚拟机中运行宏内核和通过透传pflash加载应用。

实验

H_3_0和H_4_0还没有上传做不了基础实验.

课后作业

可以尝试跑rcore-ch1群里有大佬已经实现了.

实验1

这部分实验是上部分的作业暂时略过.

LinuxApp

实验命令:

1
2
3
make payload
./update_disk.sh payload/hello_c/hello
make run A=tour/m_3_0 BLK=y

这里看payload/hello_c/Makefile,可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
TARGET := hello

CC := riscv64-linux-musl-gcc
STRIP := riscv64-linux-musl-strip

all: $(TARGET)

%: %.c
$(CC) -static $< -o $@
$(STRIP) $@

clean:
@rm -rf ./$(TARGET)

可以看到我们使用的编译器和信息移除工具都是指定的版本是linux.

这张图有些害人匪浅了,

这个图是linux应用的用户栈.

但是我们从实用的角度来看,应用主函数的原型:

1
int main(int argc, char *argv[], char *enp[]);

我们只需要在栈里边按顺序保存:

  1. argc
  2. arg_ptr
  3. env_ptr
  4. auxv

即可,只要argc的值是对的,arg_ptr和env_ptr指向的实例是对的即可.

这里有一个疑问:到底谁是对的?

在kernel-elf-parser里的src/user_stack.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
//! Initialize the user stack for the application
//!
//! The structure of the user stack is described in the following figure:
//! position content size (bytes) + comment
//! ------------------------------------------------------------------------
//! stack pointer -> [ argc = number of args ] 8
//! [ argv[0] (pointer) ] 8 (program name)
//! [ argv[1] (pointer) ] 8
//! [ argv[..] (pointer) ] 8 * x
//! [ argv[n - 1] (pointer) ] 8
//! [ argv[n] (pointer) ] 8 (= NULL)
//! [ envp[0] (pointer) ] 8
//! [ envp[1] (pointer) ] 8
//! [ envp[..] (pointer) ] 8
//! [ envp[term] (pointer) ] 8 (= NULL)
//! [ auxv[0] (Elf32_auxv_t) ] 16
//! [ auxv[1] (Elf32_auxv_t) ] 16
//! [ auxv[..] (Elf32_auxv_t) ] 16
//! [ auxv[term] (Elf32_auxv_t) ] 16 (= AT_NULL vector)
//! [ padding ] 0 - 16
//! [ argument ASCIIZ strings ] >= 0
//! [ environment ASCIIZ str. ] >= 0
//!
//! (0xbffffff8) [ end marker ] 8 (= NULL)
//!
//! (0xc0000000) < bottom of stack > 0 (virtual)
//!
//! More details can be found in the link: <https://articles.manugarg.com/aboutelfauxiliaryvectors.html>

形成的栈:

|300

运行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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
OpenSBI v0.9
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|

Platform Name : riscv-virtio,qemu
Platform Features : timer,mfdeleg
Platform HART Count : 1
Firmware Base : 0x80000000
Firmware Size : 100 KB
Runtime SBI Version : 0.2

Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000087000000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes

Boot HART ID : 0
Boot HART Domain : root
Boot HART ISA : rv64imafdcsu
Boot HART Features : scounteren,mcounteren,time
Boot HART PMP Count : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count : 0
Boot HART MHPM Count : 0
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109

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 = info

[ 1.746356 0 axruntime:130] Logging is enabled.
[ 1.856119 0 axruntime:131] Primary CPU 0 started, dtb = 0x87000000.
[ 1.905723 0 axruntime:133] Found physcial memory regions:
[ 1.962960 0 axruntime:135] [PA:0x80200000, PA:0x80232000) .text (READ | EXECUTE | RESERVED)
[ 2.026512 0 axruntime:135] [PA:0x80232000, PA:0x80241000) .rodata (READ | RESERVED)
[ 2.073912 0 axruntime:135] [PA:0x80241000, PA:0x80244000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
[ 2.124278 0 axruntime:135] [PA:0x80244000, PA:0x80284000) boot stack (READ | WRITE | RESERVED)
[ 2.168556 0 axruntime:135] [PA:0x80284000, PA:0x802ad000) .bss (READ | WRITE | RESERVED)
[ 2.212764 0 axruntime:135] [PA:0x802ad000, PA:0x88000000) free memory (READ | WRITE | FREE)
[ 2.261680 0 axruntime:135] [PA:0x101000, PA:0x102000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 2.305544 0 axruntime:135] [PA:0xc000000, PA:0xc210000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 2.349843 0 axruntime:135] [PA:0x10000000, PA:0x10001000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 2.394978 0 axruntime:135] [PA:0x10001000, PA:0x10009000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 2.440055 0 axruntime:135] [PA:0x22000000, PA:0x24000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 2.485718 0 axruntime:135] [PA:0x30000000, PA:0x40000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 2.530990 0 axruntime:135] [PA:0x40000000, PA:0x80000000) mmio (READ | WRITE | DEVICE | RESERVED)
[ 2.583846 0 axruntime:208] Initialize global memory allocator...
[ 2.621634 0 axruntime:209] use TLSF allocator.
[ 2.816195 0 axmm:81] Initialize virtual memory management...
[ 3.188863 0 axruntime:150] Initialize platform devices...
[ 3.249907 0 axtask::api:68] Initialize scheduling...
[ 3.436552 0 axtask::api:74] use Completely Fair scheduler.
[ 3.474966 0 axdriver:152] Initialize device drivers...
[ 3.510394 0 axdriver:153] device model: static
[ 3.664938 0 virtio_drivers::device::blk:59] config: 0xffffffc040006000
[ 3.721121 0 virtio_drivers::device::blk:64] found a block device of size 65536KB
[ 3.787426 0 axdriver::bus::pci:104] registered a new Block device at 00:01.0: "virtio-blk"
[ 21.285217 0 axfs:41] Initialize filesystems...
[ 21.329601 0 axfs:44] use block device 0: "virtio-blk"
[ 22.099152 0 fatfs::dir:139] Is a directory
[ 22.277106 0 fatfs::dir:139] Is a directory
[ 22.556181 0 fatfs::dir:139] Is a directory
[ 22.683443 0 fatfs::dir:139] Is a directory
[ 22.770783 0 axruntime:176] Initialize interrupt handlers...
[ 22.932112 0 axruntime:186] Primary CPU 0 init OK.
[ 23.210370 0:2 m_3_0::loader:58] e_entry: 0x50E
phdr: offset: 0x0=>0x0 size: 0x17CC=>0x17CC
VA:0x0 - VA:0x2000
phdr: offset: 0x1E70=>0x2E70 size: 0x338=>0x9A8
VA:0x2000 - VA:0x4000
entry: 0x50e
Mapping user stack: VA:0x3fffff0000 -> VA:0x4000000000
New user address space: AddrSpace {
va_range: VA:0x0..VA:0x4000000000,
page_table_root: PA:0x8064e000,
}
[ 23.946790 0:4 m_3_0::task:56] Enter user space: entry=0x50e, ustack=0x3fffffffc0, kstack=VA:0xffffffc0806a7010
handle_syscall [96] ...
handle_syscall [29] ...
Unimplemented syscall: SYS_IOCTL
handle_syscall [66] ...
Hello, UserApp!
handle_syscall [66] ...

handle_syscall [94] ...
[SYS_EXIT_GROUP]: system is exiting ..
monolithic kernel exit [Some(0)] normally!
[ 24.504671 0:2 axhal::platform::riscv64_qemu_virt::misc:3] Shutting down...

可以看到运行过程中还调用了:SYS_IOCTL和SYS_SET_TID_ADDRESS两个系统调用.

这是因为:”示例m_3_0基于musl工具链以静态方式编译,工具链为应用附加的部分也会调用syscall。”

就是添加的这个_start和_exit的系统调用.

set_tid_address会设置clear_child_tid的值,在进程创建和释放的时候会用到.
set_tid_address在父线程创建一个子线程的时候会把自己的tid写到这个address的区域里.
clear_child_tid在释放自己线程或者锁和其它资源的时候,会把返回的值里写入到address里.

ioctl是用来设置对外输出终端属性的.
现在用的是sbi的putchar,因此可以直接跳过.

对于不同的体系结构,系统调用号不同。示例是基于riscv64的系统调用号规范。

最后总结就是我们设置好合理的syscall,把系统调用号设置好,那么就可以实现一定程度上的兼容.

像这个APP只需要提供syscall的兼容层就行了.

其余的兼容层根据APP不同也需要实现.

对Linux常用文件系统的支持

arceOS是通过axfs_ramfs对procfs和sysfs提供兼容.通过axfs_devfs提供devfs的兼容.
目前用ramfs进行兼容是一个临时的方法.
也就是使用内存文件系统.访问的时候相当于访问了一个基于内存的节点,里边有一些基于内存的数据,这些数据是其它子系统填充过来的数据.
正常的Linux是你访问这个proc之类的文件的时候实际上是调用了一个回调函数去获取系统状态.

实现mmap系统调用

实现方法:

  1. 通过sys_read方法读取到文件里的内容.
  2. 读取当前的任务的user space.
  3. 寻找空闲的映射空间的虚拟地址
  4. 构造flag.
  5. 创建一块frame,并且把虚拟地址映射到frame.
  6. 把文件内容拷贝到内存中去
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
#[allow(unused_variables)]
fn sys_mmap(
addr: *mut usize,
length: usize,
prot: i32,
flags: i32,
fd: i32,
_offset: isize,
) -> isize {
const MAX_MMAP_SIZE: usize = 64;
let mut buf: [u8; 64] = [0u8;MAX_MMAP_SIZE];
unsafe {
let buf_ptr = &mut buf as *mut _ as *mut c_void;
sys_read(fd, buf_ptr, length+_offset as usize);
}
let mut buf = &buf[_offset as usize..length+_offset as usize];

let binding = current();
let mut uspace = &mut binding.task_ext().aspace.lock();

let free_va = if addr.is_null() {
uspace.find_free_area(
(addr as usize).into(),
length,
VirtAddrRange::new(
uspace.base(),
uspace.end()))
.unwrap_or_else(|| panic!("No free area for mmap"))
}else{
(addr as usize).into()
};

// 把prot转换成MappingFlags
let mut flags = MappingFlags::from(MmapProt::from_bits_truncate(prot));
flags.set(MappingFlags::USER, true);

uspace.map_alloc(
free_va,
PAGE_SIZE_4K,
flags,
true)
.unwrap();
let (paddr, _, _) = uspace
.page_table()
.query(free_va)
.unwrap_or_else(|_| panic!("Mapping failed for segment"));
unsafe {
core::ptr::copy_nonoverlapping(
buf.as_ptr(),
phys_to_virt(paddr).as_mut_ptr(),
PAGE_SIZE_4K,
);
}
free_va.as_usize() as isize
}

这里flags的处理还是很不到位,需要后续增加.

内容

虚拟机运行的实验内核是第一周的u_3_0:从pflash设备读出数据,验证开头部分。

有两种处理方式:

  1. 模拟模式 - 为虚拟机模拟一个pflash,以file1为后备文件。当Guest读该设备时,提供file1文件的内容。
  2. 透传模式 - 直接把宿主物理机(即qemu)的pflash透传给虚拟机。

优劣势:模拟模式可为不同虚拟机提供不同的pflash内容,但效率低;透传模式效率高,但是捆绑了设备。

实验

课后作业

回顾与展望

之前就是做了一系列的实验建立了unikernel的框架.

通过unikernel的形式通过增加一些组件来跨过这个边界,来实现一个宏内核.

从Unikernel到宏内核

通过跨越模式边界,弄一个简单的系统调用的操作.

增加用户特权级和特权级上下文切换是变成宏内核的关键.

实验1

rust-analyzer不能正常解析代码的原因,需要在.vscode/settings.json里加入"rust-analyzer.cargo.target": "riscv64gc-unknown-none-elf"

实验命令行:

1
2
3
make payload
./update_disk.sh ./payload/origin/origin
make run A=tour/m_1_0 BLK=y

如果不能执行payload说明代码版本太老了,需要先git fetch origin然后再git merge origin到当前的分支

这里注意如果make payload报错Error,那么一定是因为没有配置好musl的环境变量,注意看一下~/.bashrc,记得更新完~/.bashrc要进行狠狠的source ~/.bashrc

对于./update_disk.sh ./payload/origin/origin的操作对于我这种没操作过的人来说是非常神奇的操作.这一步实际上是把disk.img挂载在linux的文件系统里,然后在直接用linux的指令直接往里边拷贝应用文件的数据.

然后make run A=tour/m_1_0 BLK=y就和上一节课的实验一样了.

跑出来的结果是:

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
OpenSBI v0.9
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|

Platform Name : riscv-virtio,qemu
Platform Features : timer,mfdeleg
Platform HART Count : 1
Firmware Base : 0x80000000
Firmware Size : 100 KB
Runtime SBI Version : 0.2

Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000087000000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes

Boot HART ID : 0
Boot HART Domain : root
Boot HART ISA : rv64imafdcsu
Boot HART Features : scounteren,mcounteren,time
Boot HART PMP Count : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count : 0
Boot HART MHPM Count : 0
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109

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

[ 21.794824 0 fatfs::dir:139] Is a directory
[ 22.065035 0 fatfs::dir:139] Is a directory
[ 22.359963 0 fatfs::dir:139] Is a directory
[ 22.490439 0 fatfs::dir:139] Is a directory
app: /sbin/origin
paddr: PA:0x80642000
Mapping user stack: VA:0x3fffff0000 -> VA:0x4000000000
New user address space: AddrSpace {
va_range: VA:0x0..VA:0x4000000000,
page_table_root: PA:0x80641000,
}
Enter user space: entry=0x1000, ustack=0x4000000000, kstack=VA:0xffffffc080697010
handle_syscall ...
[SYS_EXIT]: process is exiting ..
monolithic kernel exit [Some(0)] normally!

让我们看一下orgin的app内容:

1
2
3
4
5
6
7
8
9
10
#[no_mangle]
unsafe extern "C" fn _start() -> ! {
core::arch::asm!(
"addi sp, sp, -4",
"sw a0, (sp)",
"li a7, 93",
"ecall",
options(noreturn)
)
}

很容易懂的,就是调用了第93号syscall.

课后练习

主要是要理解AddrSpace在map_alloc的时候的populating选项.

根据在rCore中学到的经验,去查看源码,我们的结构是这样的.

|800

就是在创建MemoryArea的时候要传入一个泛型Backend.

应该就是和这边页的懒加载有关的内容.

调用到最后调用的是modules/axmm/src/backend/alloc.rs这个文件里的map_alloc,因为层层抽象,这里各个参数都还原成了最开始tour/m_1_0/src/main.rs里的变量名称.

|800

然后关键代码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if populate {
// allocate all possible physical frames for populated mapping.
for addr in PageIter4K::new(start, start + size).unwrap() {
if let Some(frame) = alloc_frame(true) {
if let Ok(tlb) = pt.map(addr, frame, PageSize::Size4K, flags) {
tlb.ignore(); // TLB flush on map is unnecessary, as there are no outdated mappings.
} else {
return false;
}
}
}
true
} else {
// Map to a empty entry for on-demand mapping.
let flags = MappingFlags::empty();
pt.map_region(start, |_| 0.into(), size, flags, false, false)
.map(|tlb| tlb.ignore())
.is_ok()
}

这里假如我们的poplulate是选定的true,那么就会立即根据4k一个大小的frame进行内存申请,然后把这个虚拟地址和刚刚申请到的frame在page_table中映射起来.

但是如果我们选定populate为false,那么直接把虚拟地址和0这个错误的物理地址映射起来.

那么这时候实际上就需要我们在访问到这个物理地址的时候,再进行物理页申请.

那么在访问到这个地址的时候会发生缺页异常.

这时候我们运行一下应用:

1
2
3
make payload
./update_disk.sh payload/origin/origin
make run A=tour/m_1_0/ BLK=y

这是对应的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
97
98
99
100
101
102
103
104
105
106
107
OpenSBI v0.9
____ _____ ____ _____
/ __ \ / ____| _ \_ _|
| | | |_ __ ___ _ __ | (___ | |_) || |
| | | | '_ \ / _ \ '_ \ \___ \| _ < | |
| |__| | |_) | __/ | | |____) | |_) || |_
\____/| .__/ \___|_| |_|_____/|____/_____|
| |
|_|

Platform Name : riscv-virtio,qemu
Platform Features : timer,mfdeleg
Platform HART Count : 1
Firmware Base : 0x80000000
Firmware Size : 100 KB
Runtime SBI Version : 0.2

Domain0 Name : root
Domain0 Boot HART : 0
Domain0 HARTs : 0*
Domain0 Region00 : 0x0000000080000000-0x000000008001ffff ()
Domain0 Region01 : 0x0000000000000000-0xffffffffffffffff (R,W,X)
Domain0 Next Address : 0x0000000080200000
Domain0 Next Arg1 : 0x0000000087000000
Domain0 Next Mode : S-mode
Domain0 SysReset : yes

Boot HART ID : 0
Boot HART Domain : root
Boot HART ISA : rv64imafdcsu
Boot HART Features : scounteren,mcounteren,time
Boot HART PMP Count : 16
Boot HART PMP Granularity : 4
Boot HART PMP Address Bits: 54
Boot HART MHPM Count : 0
Boot HART MHPM Count : 0
Boot HART MIDELEG : 0x0000000000000222
Boot HART MEDELEG : 0x000000000000b109

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

[ 21.690418 0 fatfs::dir:139] Is a directory
[ 21.963457 0 fatfs::dir:139] Is a directory
[ 22.252957 0 fatfs::dir:139] Is a directory
[ 22.383790 0 fatfs::dir:139] Is a directory
app: /sbin/origin
paddr: PA:0x80642000
Mapping user stack: VA:0x3fffff0000 -> VA:0x4000000000
New user address space: AddrSpace {
va_range: VA:0x0..VA:0x4000000000,
page_table_root: PA:0x80641000,
}
Enter user space: entry=0x1000, ustack=0x4000000000, kstack=VA:0xffffffc080687010
[ 23.235085 0:4 axhal::arch::riscv::trap:24] No registered handler for trap PAGE_FAULT
[ 23.319751 0:4 axruntime::lang_items:5] panicked at modules/axhal/src/arch/riscv/trap.rs:25:9:
Unhandled User Page Fault @ 0x1002, fault_vaddr=VA:0x3ffffffffc (WRITE | USER):
TrapFrame {
regs: GeneralRegisters {
ra: 0x0,
sp: 0x3ffffffffc,
gp: 0x0,
tp: 0x0,
t0: 0x0,
t1: 0x0,
t2: 0x0,
s0: 0x0,
s1: 0x0,
a0: 0x0,
a1: 0x0,
a2: 0x0,
a3: 0x0,
a4: 0x0,
a5: 0x0,
a6: 0x0,
a7: 0x0,
s2: 0x0,
s3: 0x0,
s4: 0x0,
s5: 0x0,
s6: 0x0,
s7: 0x0,
s8: 0x0,
s9: 0x0,
s10: 0x0,
s11: 0x0,
t3: 0x0,
t4: 0x0,
t5: 0x0,
t6: 0x0,
},
sepc: 0x1002,
sstatus: 0x40020,
}

实现方法tour/m_2_0里的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[register_trap_handler(PAGE_FAULT)]
fn handle_page_fault(vaddr: VirtAddr, access_flags: MappingFlags, is_user: bool) -> bool {
if is_user {
if !axtask::current()
.task_ext()
.aspace
.lock()
.handle_page_fault(vaddr, access_flags)
{
ax_println!("{}: segmentation fault, exit!", axtask::current().id_name());
axtask::exit(-1);
} else {
ax_println!("{}: handle page fault OK!", axtask::current().id_name());
}
true
} else {
false
}
}

这里主要是调用了aspace也即当前任务地址空间中处理缺页故障的方法.

就像我们之前在上一节分析到的Backend的map方法一样,还是调用了Backend的remap方法.

就是当即分配一个frame,然后把当前出问题的va虚拟地址重新映射到frame.

组件化内核的心得和实践经验

应用场景多样化->多种内核场景的出现

Unikernel->安全性换高效性->为一个APP适配一个内核

宏内核就是典型的Linux之类的操作系统

微内核主要是安全->用形式化证明安全性->反复切换用户态以至于很慢

虚拟机管理程序->hypervisor->多个内核每个内核认为自己独享了整个设备

关注点在于组件化场景下的异构内核的快速实现.理解概念和优势.

不同的需求对应了不同的内核->使用不同的组件实现不同的内核

使用宏内核+hypervisor的架构也可以实现这个功能,但是会产生性能瓶颈.

利用对unikernel的几个部件的连接方式的修改,加一个宏内核插件,这样就可以变成宏内核.

通过对unikernel对于hypervisor插件的调用,就可以变成hypervisor的系统.

其实上边论述的是优势所在.

BACKBONE层的重要性:把共性放在下层内容.
TASK的拓展:把任务看成是内核资源的集合.

未来工作:扩展泛型化——同时引入不同类型扩展 -> 甚至能到异构内核

第四阶段参加了项目四基于协程异步机制的 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】最小化异步操作系统

前提

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

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 生命周期,同时需要 Send 和 Sync 实现跨线程共享,虽然现在没用到,但是 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 和一个 waker,target_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 入口,task1 和 task2 是两个异步任务,task1 先打印 start task 1,然后延迟 200ms,再打印 end task 1。task2 也是类似,只是延迟时间更长。运行时先执行 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体系结构知识,总结这三个月,收获非常大,在这里也希望开源操作系统训练营可以越办越好,更多的人可以在这里学到知识,收获成长。

总结

在该阶段中,我完成了前期的练习,着重学习了ArceOS宏内核模式下是如何支持Linux应用的,并期望基于此,实现劫持ecall以在Unikernel形态下支持Linux应用。虽然这个方案最终感觉不太可行,但在这个过程中,我还是学到了很多系统相关的知识,收获颇丰,期待下一次的训练营!

一些建议

这个项目阶段个人感觉时间还是太短了,且靠近学期末。如果可以把项目阶段放到假期,或者把前期训练阶段放到假期、把项目阶段放到学期开始,可能大家的参与度都会更高一些。

总结报告详情

个人博客网页:https://yumu20030130.github.io/
行文匆忙,多有不严谨之处,后续会在个人博客中完善。

  1. Unikernel内核模式下支持Linux多应用的前期准备:https://yumu20030130.github.io/2024/12/21/Unikernel%E5%86%85%E6%A0%B8%E6%A8%A1%E5%BC%8F%E4%B8%8B%E6%94%AF%E6%8C%81Linux%E5%A4%9A%E5%BA%94%E7%94%A8%E7%9A%84%E5%89%8D%E6%9C%9F%E5%87%86%E5%A4%87/

  2. ArceOS宏内核模式基本应用支持:https://yumu20030130.github.io/2024/11/30/%E6%9C%80%E7%AE%80%E5%AE%8F%E5%86%85%E6%A0%B8%E6%A8%A1%E5%BC%8F%E5%86%85%E6%A0%B8%E6%9E%84%E5%BB%BA/

  3. ArceOS宏内核模式如何支持Linux应用的运行:https://yumu20030130.github.io/2024/12/20/ArceOS%E5%AE%8F%E5%86%85%E6%A0%B8%E6%A8%A1%E5%BC%8F%E5%A6%82%E4%BD%95%E6%94%AF%E6%8C%81Linux%E5%BA%94%E7%94%A8%E7%9A%84%E8%BF%90%E8%A1%8C/

  1. Unikernel模式尝试劫持ecall以支持Linux应用运行:https://yumu20030130.github.io/2024/12/20/Unikernel%E6%A8%A1%E5%BC%8F%E5%B0%9D%E8%AF%95%E5%8A%AB%E6%8C%81ecall%E4%BB%A5%E6%94%AF%E6%8C%81Linux%E5%BA%94%E7%94%A8%E8%BF%90%E8%A1%8C/

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

学习内容

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

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

学习收获

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

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

下一步计划

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