0%

总结

第二阶段的强度慢慢上来了,由于平时要上班和前期收不到短信验证码的问题,不能跟上每周的直播课,主要通过学习tutorial文档和课后看录频和课件的方式学习。本阶段对内存管理,进程管理和文件系统, 线程进行了系统的学习。特别是内存映射和文件系统章节,学习到很多。同时,随着代码量增大,学习的同时画UML图对理解代码和整个框架也很有帮助。

Rust补完计划

src:rust官网rust官文rust官仓crates.iorust-wiki卡狗圣经

​ Rust可看作一个在语法层面(编译时)具有严格检查和限制的C语言上位。且扩展了面向对象的便捷方法绑定。编译和运行方式类似于C/C++,可以rustc xxx.rs编译,./xxx运行。有约定的项目目录格式,可使用Cargo配置toml进行包管理、编译、运行、测试等等。包资源网站为CratesIO,见src↑。不支持运算符重载,支持多态。其中语句为:表达式+;,语句的值是()

​ 为了安全,几乎所有的方法/变量/属性都是私有的,除非使用pub进行显式公用声明。

​ 说到底,编程语言就是人类用来快速生成机器码以便于执行的模板引擎,有的语法层面(编译时/解释时)有强约束,有的仅仅是把特定字符串替换成另外的字符串或二进制,属于弱约束或者无约束。所有你在编程语言所看到的抽象,在机器码的层面本来就是一场幻月楼阁。比如你在编程语言层面,继承多态面向对象权限生命周期搞的花里胡哨的,但是在机器码看来,就仅仅是把PC变一下,或者某数据/指针变一下而已,你所关心的语法层面,语义特性,都是高层编译时/解释时的语法约束。这些约束让你写正确的高级语法的同时,最重要的是保证执行的结果符合预期。所以学底层的,一定要层层解耦,梳理层层抽象!

快速开始

安装:

1
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

推荐开发环境:

VSCode + rust-analyzer,VIM,RustRover

见面礼:

1
2
// main.rs
fn main() { println!("hello world!") }
1
rustc main.rs -o main && ./main

使用包管理器:

1
2
3
4
5
6
cargo new my_project_name	# 生成项目目录结构,包括`toml`、`lock`、`src`、`test`等等
cargo build # 编译后生成文件在`target/debug/项目名`下生成可执行文件,可选--release生成优化版本
cargo run # 仅run也会build
cargo clean # 清除target下编译后的文件
cargo check # 仅检查语法是否出错
# 安装新的包,在Cargo.toml的依赖下配置:`包名=版本`即可

包管理器代理:vim ~/.cargo/config.toml

1
2
3
4
5
[source.crates-io]
replace-with = 'ustc'

[source.ustc]
registry = "sparse+https://mirrors.ustc.edu.cn/crates.io-index/"

初次接触

语法语义一览表:

标识符:^[a-Z0-9_][a-Z0-9_$]* 其中,命名习惯为:CONST_VALStructNameImplNamemethod_nameval_name

注释符:// 单行注释/* 多行注释 *//// 文档注释,支持MD语法以及文档测试以及自动生成文档

运算符:+ - * / % += -= *= /= %= ! ~|& ^ [] ; , >> << == != < <= > >= && ||

变量声明:const CONST_VAL: i32 = 123; static STATIC_VAL: u32 = 321; let emm = 233; let mut var = 0;

类型别名:type word = u64

类型转换:var as type_name type_name::from(var)

分支:if 条件必须是布尔值 { ... } else { ... } match obj { case xx => { ... }, _ => { ... } }

循环:loop { ... } for i in 0..10 while n < 233break continue

支持循环标签来跳出指定的循环:tag1: loop { tag2: while true { break: tag1 } }

函数格式:fn add(a: i32, b: i32) -> i32 { a + b } 默认返回值是最后一条表达式的值,等同于:return a+b;

匿名函数:|a: i32, b: i32| -> i32 { a + b } 如果只有一条执行语句,可以省略大括号

类和对象:采用结构体struct(存数据)和特质trait(类似于抽象interface存方法)抽象,数据方法分离的思想

方法多态:方法和数据的关系是多对多,支持采用数据/特质签名来访问匿去的数据/方法:TraitA::fun1(&obj)

基本类型:i8 u8 i16 u16 … 有无符号+位数,str,bool,f64 … 同整型

类型变体:&i32 - 不可变引用,&mut - 可变引用,*const - 不可变裸指针,*mut - 可变裸指针

容器类型:[1, 2, 3] - Array - 定长同类型,(1, "heke", 1228) - Tuple - 定长不可变

数据容器:struct Person { age: u8; name: &str; } struct Bag (i32, i32, u8)

枚举类型:enum State { StateA, StateB, State233=233, ,PA(ch) ... } 详见特殊部分与模式匹配

其他容器:VecDequeQueueBTreeSetBTreeMap

导入管理:mod package_emm; mod mode_name { fn emm() { ... } } use mode_name::emm;

异步支持:asyncawaitstd::thread,以及异步的通道和异步智能指针

快速迁移(将采用Py作为对比语言):


1
2
def emm(a: int, b: int) -> float:
return float(a) + b
1
2
3
4
pub fn emm(a: i32, b: i32) -> f64 {
// 运算块的值是最后一句表达式的值,所以不显示写return也可以,语句的值是()表示空!
a as f64 + b // 或写为:`return a as f64 + b` 或 `return f64::from(a) + b;`
}

1
2
3
def emm(op) -> (int, str, int):
op
return 1, "heke", 1228
1
2
3
4
pub fn emm(op) -> (i32, &str, i32) {  // op会在编译时自动根据所调用时的类型生成对应类型标签的多态方法
op;
1, "heke".as_str(), 1228
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A:
def __init__(self, a: int, b: int, c: int):
self.a = a
self.b = b
self.c = c

def method233(self):
self.a += self.b
return None

if __name__ == '__main__':
obj = A(1,2,3)
obj.method233()
print(f"a: {obj.a}, b: {obj.b}")
print("c: ", obj.c)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct A {
pub a: i32,
pub b: i32,
pub c: i32,
}
impl A {
pub fn method233(&self) {
self.a += self.b;
return None;
}
}
fn main() {
let mut obj = A {a: 1, b: 2, c: 3};
obj.method233();
println!("{}", format!("a: {}, b: {}", obj.a, obj.b));
println!("c: {}", obj.c);
}

1
2
3
# method.py
def func():
pass
1
2
3
4
5
6
7
8
9
# __init__.py
from method import * # '0

from method import func # 语法多余,但是为了对比语法'1
from .method import func # 语法多余,但是为了对比语法'2
__all__ = ['func'] # 'export

func()
method.func()
1
2
3
4
// method.rs
pub fn func() {
;
}
1
2
3
4
5
6
7
8
9
10
11
// lib.rs / mod.rs
mod method; // '0
use method::*; // '0

pub use method::func; // pub use使得导入此包的也可以被别的包从此包导入'1 'export
pub use crate::method::func; // crate表示从包的根目录进行相对路径索引'2 'export

// 如果有main,则在main内:
func();
method::func();
crate::method::func();
Read more »

前导

本博客作为开源操作系统训练营2025S的1、2阶段学习记录,简单总结了在这两个阶段的学习和coding。留作纪念,也希望能够帮助到大家。

Read more »

2025年春夏开源操作系统训练营三阶段总结

第一阶段总结

第一次写rust,被所有权干烂了,不停和编译器搏斗+不停拷问AI终于磕磕绊绊写完了,以前习惯于C++的引用和指针乱飘了,写rust确实比C和C++安全,因为有编译器的所有权机制保障。

第二阶段总结

因为以前写过NJUPA(极其推荐),所以对上下文切换有一点概念。rcore-os的上下文切换采用了xv6类似的方式,在内核内部做控制流切换来切换执行的用户程序。对于操作系统中常用的锁,rcore采用了单核中的使用rust所有权机制来实现,上锁的时候将所有权转交给函数,尝试二次获取的时候就会因为所有权引发rust错误,避免了内核的死锁的情况。cargo的编译链接功能确实比GNU那一套方便,rust的属性语法支持也比C++要好很多。

地址空间和页表中极多的用来表示地址组成的结构体,充分体现了rust面向对象的特性和into的语法,增加了代码的可读性,以前用C系的写也就使用各种宏。rcore用了额外的BTreeMap来支持快速查询页表。作业题的实现也大多是调用现有的函数就差不多可以了。

到文件系统这里比较新鲜,因为以前没有怎么接触过,文件系统的实现层层嵌套,层层抽象,一个功能实现往往要修改多层,理解起来比较困难。

并发的作业是用Coffman提出的方法做死锁检测,开始的时候理解成了银行家算法,读作业提示的时候也没有读懂,卡了很长时间,在群u的提示下才通过。后来在上操作系统课的时候翻到书上才知道这是Coffman的死锁检测算法(书上的描述其实和作业里写得差不多)。

第三阶段

第三阶段换了个内核模式叫unikernel,不太习惯这样的组件化操作系统,还是习惯以前的传统宏内核结构。

print_with_color很简单,理解一下arceos提供的axstd模块,为内核应用提供了运行时环境。

support_hash_map在理解了rust可以自定义堆分配器后,唯一要解决的就是rust中如何得到一个hashable值的hash值,然后做简单的映射即可,测试不需要特别追求效率。

alt_alloc主要是实现一下需要的接口,了解如何向arceos添加自己的分配器,bump allocator可以说是最简单的分配器了。

ramfs_rename要把cargo里的包换成arceos下面的(不然文件系统的包永远是网上拉下来的),找到对traitVfsOpsVfsNodeOps的实现之后,搞清楚记录文件的数据结构,实现rename就可以了。

sys_map同样搞清楚task_ext提供的接口就可以直接调库了,第一次实现忘记输入地址是NULL的话要由内核自己找地址,遂发现了一个叫做find_free_area的妙妙工具。

simple_hv最简单的一次,同样的由操作系统代为执行一些指令的手法可以用来帮用户程序加载一些未对齐的数据(如果处理器不支持)。

第二阶段总结:操作系统学习的感悟与理解

经过这一阶段对操作系统核心内容的系统学习和实践,我对操作系统的本质、关键机制以及与 Rust 结合的优势有了更深刻的理解。以下是我的主要体会和总结:


一、操作系统的本质与核心任务

操作系统是管理硬件资源、为上层应用提供抽象和隔离的基础软件。它的核心任务包括:

  • 进程与线程管理:通过进程(资源分配单位)和线程(调度单位)实现多任务并发,保障系统的响应性和资源利用率。PCB(进程控制块)和 TCB(线程控制块)是操作系统调度和管理的核心数据结构。
  • 内存管理:通过虚拟内存、分页机制和权限控制,实现进程隔离、内存高效分配与回收。页表、TLB、懒分配、写时复制等机制极大提升了系统的安全性和性能。
  • 进程调度:采用多种调度算法(如时间片轮转、优先级、MLFQ等)实现公平与高效的 CPU 分配。上下文切换和调度策略直接影响系统吞吐和响应速度。
  • 系统调用接口:为用户程序提供受控访问硬件和内核资源的通道,实现用户态与内核态的安全切换。
  • 硬件抽象与性能优化:通过缓存、TLB、ASID 等机制优化访问速度,利用中断和异常机制实现高效的事件响应。

Read more »

之前对操作系统有一定理论基础,rcore 和 arceos 项目对我最大的挑战主要包括:

  1. risc-v 体系结构的知识,尤其是特权架构。这对理解 trap、context_switch、地址空间相关的代码极其重要。
  2. arceos 项目的组织构建。最底层是 axhal,抽象了硬件架构和运行平台,往上是各个 module 例如 axtask 等,再向上是 axapi 乃至 ulib。这种组件化的设计思想充分利用的 rust 语言的优势,极大方便构建。

unikernel 架构是没有特权级切换的,应用程序也运行在 s 态。刚开始没有仔细理解 ppt,给我造成了挺大的困扰。

hashmap 的实验我并没有自己手写代码,而是直接引入了 hashbrown 库。但手撕一下代码应该能更加锻炼能力。

此外,hypervisor 给我带来了挺大的困难,参考其他同学的经验我才得以通过。

Chapter 1

Execution Environment

Execution Environment

The execution environment is defined by the Target Triplet, which specifies the platform, CPU architecture, and library required for the build. For example: x86_64-unknown-linux-gnu.

Components of the Target Triplet:

  • Platform: The specific operating system or runtime environment.
  • CPU Architecture: The underlying hardware architecture (e.g., x86_64, ARM).
  • Library: The standard library or runtime support required.

If the target platform contains no std or any support syscall, such platform called bare-metal, Rust contains a core lib independent of any platform support.

If we change .cargo/config s.t.:

1
2
3
# os/.cargo/config
[build]
target = "riscv64gc-unknown-none-elf"

it called cross compile because the running platform is different form execution platform.

No Std and No Main

The basic functionality provided by std and start semantic is panic_handler and main entry.

To toggle it off with:

1
2
#![no_std]
#![no_main]

RISCV

As for riscv, thing will be tough in here, we need to complete our own entry point, exit, and basic functionality like print/println.

First, we need to define linker and entry for stack allocation.

Linker:

1
2
3
4
5
6
7
# os/src/linker.ld
OUTPUT_ARCH(riscv)
ENTRY(_start) # entry point
BASE_ADDRESS = 0x80200000; # base addr for entry

SECTIONS
...

Stack Space:

1
2
3
4
5
6
7
8
9
10
11
12
13
# os/src/entry.asm
.section .text.entry
.globl _start
_start:
la sp, boot_stack_top
call rust_main # call rust_main function as entry

.section .bss.stack
.globl boot_stack_lower_bound
boot_stack_lower_bound:
.space 4096 * 16
.globl boot_stack_top
boot_stack_top:

For riscv, we need to call RustSBI(a underlying specification for rust in riscv).

After implement sbi_call, we could construct put_char:

1
2
3
4
5
6
7
8
9
const SBI_CONSOLE_PUTCHAR: usize = 1;

fn sbi_call(...) -> usize {
...
}

pub fn console_putchar(c:usize) {
sbi_call(SBI_CONSOLE_PUTCHAR,c,0,0)
}

With a formal interface for write:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Stdout;

impl Write for Stdout {
fn write_str(&mut self, s: &str) -> fmt::Result {
for c in s.chars() {
console_putchar(c as usize);
}
Ok(())
}
}

pub fn print(args: fmt::Arguments) {
Stdout.write_fmt(args).unwrap();
}

Now we construct basic functionality in println, you could also handle panic_handler and others…

Chapter 3

Introduction

We need to place multiple app to multiple memory address to run app in cycle. Rather run once and clear for next.

First We want to place each app to each isolated addr, due to our kernel restriction, we need to load it with build.py.


Task

Task: a workflow process

Define every time slice of Task as Task Slice

Define the switch between app as Task switching

We need to store Task Context

Design:
switch

We will store these register in ctx:

1
2
3
4
5
6
7
// os/src/task/context.rs

pub struct TaskContext {
ra: usize,
sp: usize,
s: [usize; 12],
}
1
2
3
4
5
6
7
8
9
10
11
12
.altmacro
.macro SAVE_SN n
sd s\n, (\n+2)*8(a0)
.endm
.macro LOAD_SN n
ld s\n, (\n+2)*8(a1)
.endm
.section .text
.globl __switch
__switch:
...
# logic by store previous context and take out the new one to current register

Expose to Rust

1
2
3
4
5
6
7
8
9
10
11
12
// os/src/task/switch.rs

global_asm!(include_str!("switch.S"));

use super::TaskContext;

extern "C" {
pub fn __switch(
current_task_cx_ptr: *mut TaskContext,
next_task_cx_ptr: *const TaskContext
);
}

We will design TaskManager:

  • Store each App state array and current running app.
  • each state store TaskContext and TaskState for running or exited etc…
  • Init and ready by store the __restore ctx to TaskContext
  • Run for switch cx if needed.
1
2
3
4
5
6
7
8
9
10
let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
drop(inner);
// before this, we should drop local variables that must be dropped manually
unsafe {
__switch(
current_task_cx_ptr,
next_task_cx_ptr,
);
}rust

Dispatch Design

Collaboration

Manually design interface yield for App to use

1
2
3
4
5
6
7
pub fn sys_yield() -> isize {
syscall(SYSCALL_YIELD, [0, 0, 0])
}

// user/src/lib.rs

pub fn yield_() -> isize { sys_yield() }

But it can be inefficient for some case that app already done its work but reluctant to exit.

Preemptive

We will design interrupt clock in a fixed time bound to force switch between app.

  • Set timer design and get time
  • Set timer for trigger
  • enable timer and handle the interrupt cause of Timer in ecall

You should know this as a pre-knowledge:

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
const SBI_SET_TIMER: usize = 0;

pub fn set_timer(timer: usize) {
sbi_call(SBI_SET_TIMER, timer, 0, 0);
}

// os/src/timer.rs

use crate::config::CLOCK_FREQ;
const TICKS_PER_SEC: usize = 100;

pub fn set_next_trigger() {
set_timer(get_time() + CLOCK_FREQ / TICKS_PER_SEC);
}

// os/src/trap/mod.rs

match scause.cause() {
Trap::Interrupt(Interrupt::SupervisorTimer) => {
set_next_trigger();
suspend_current_and_run_next();
}
}

// enable supervisor clock
// os/src/trap/mod.rs

use riscv::register::sie;

pub fn enable_timer_interrupt() {
unsafe { sie::set_stimer(); }
}

Chapter 2

Introduction

Introduction

It corresponds to riscv:

  • Privilege for S(guaranteed by Supervisor Execution Environment of RustSBI)
  • User for U(constructed in current chapter as Application Execution Environment)

Reason:

  • Safety(Prevent app from accessing kernel)
  • Recoverable

Workflow:

  • Start application and user-mode context
  • Trap(Called by system level) to handle system
    • Goes wrong! Kill it!
    • Finish! Next!
  • Restore to user-mode context

riscv designs following CSR(Control and Status Register) to handle this:

CSR

CSR

Begin Trap:

  • sstatus: SPP seg to the current level of CPU.
  • sepc: next addr after Trap finished.
  • scause/stval: Trap cause and additional info.
  • stvec: storage of entry addr of Trap

stvec is a 64-bit CSR, with:

  • MODE(Direct/Vectored) [1:0](read from right to left): 2-bits
  • BASE [63:2]: 62-bits

finally, it will return by instruction sret which will change level and jump by sepc.

Construct Trap

Design:

  • General register will be shared by U-level and S-level.
  • Maintain a reasonable state of CSR.
  • Separate workflow of U-level and S-level by stack

Construct:

  • build KernelStack and UserStack for separation
  • in KernelStack, we store TrapContext in it, by asm and rust to control dispatch and handle, then store the code to stvec as the entry of Trap.
  • restore register for UserStack by push a new context refer to UserStack.

build stack and push context:

1
2
3
4
5
6
7
8
9
10
11
12
13
// stack struct ...

// buttom to top
fn get_sp(&self) -> usize {
self.data.as_ptr() as usize + KERNEL_STACK_SIZE
}
pub fn push_context(&self, cx: TrapContext) -> &'static mut TrapContext {
let cx_ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
unsafe {
*cx_ptr = cx;
}
unsafe { cx_ptr.as_mut().unwrap() }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// os/src/trap/context.rs

#[repr(C)]
pub struct TrapContext {
pub x: [usize; 32], // General register
pub sstatus: Sstatus,
pub sepc: usize,
}
/// set stack pointer to x_2 reg (sp)
pub fn set_sp(&mut self, sp: usize) {
self.x[2] = sp;
}
/// init app context
pub fn app_init_context(entry: usize, sp: usize) -> Self {
let mut sstatus = sstatus::read(); // CSR sstatus
sstatus.set_spp(SPP::User); //previous privilege mode: user mode
let mut cx = Self {
x: [0; 32],
sstatus,
sepc: entry, // entry point of app
};
cx.set_sp(sp); // app's user stack pointer
cx // return initial Trap Context of app
}

We will design __alltrap and __restore for operation by asm and part of rust:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.altmacro
.macro SAVE_GP n
sd x\n, \n*8(sp)
.endm
.macro LOAD_GP n
ld x\n, \n*8(sp)
.endm
.align_2
__alltraps:
...
# set input argument of trap_handler(cx: &mut TrapContext)
mv a0, sp # sp is point to TrapContext in kernel stack
call trap_handler # (&mut TrapContext)

--restore:
...

To handle Trap context, we will use riscv lib:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// os/Cargo.toml

[dependencies]
riscv = { git = "https://github.com/rcore-os/riscv", features = ["inline-asm"] }

// os/src/trap/mod.rs
global_asm!(include_str!("trap.S"));

pub fn init() {
extern "C" { fn __alltraps(); }
// write to stvec
unsafe {
stvec::write(__alltraps as usize, TrapMode::Direct);
}
}

#[no_mangle]
pub fn trap_handler(cx: &mut TrapContext) -> &mut TrapContext {
...
}

restore operation:

1
2
3
4
5
6
7
extern "C" { fn __restore(cx_addr: usize); }
unsafe {
__restore(KERNEL_STACK.push_context(
TrapContext::app_init_context(APP_BASE_ADDRESS, USER_STACK.get_sp())
// This context store the ptr to UserStack for restoration
) as *const _ as usize);
}

Construct User App

  • Link app binary to kernel with specify memory layout
  • Read the layout, use AppManager to maintain and store
  • Load app from memory layout, copy consecutively to APP_BASE_ADDRESS(Currently we have no ability to dynamically read address)
  • AppManager will run each app
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# os/src/link_app.S

.align 3
.section .data
.global _num_app # read from the ptr
_num_app:
.quad 5
.quad app_0_start
.quad app_1_start
.quad app_2_start
.quad app_3_start
.quad app_4_start
.quad app_4_end`

...

Design it!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// os/src/batch.rs

struct AppManager {
num_app: usize,
current_app: usize,
app_start: [usize; MAX_APP_NUM + 1],
}

// part of read in static init of AppManager
let num_app_ptr = _num_app as usize as *const usize;
let num_app = num_app_ptr.read_volatile();
let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
let app_start_raw: &[usize] = core::slice::from_raw_parts(
num_app_ptr.add(1), num_app + 1
);
app_start[..=num_app].copy_from_slice(app_start_raw);

Load App:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// part of code of copying to kernel
asm!("fence.i");
// clear app area
core::slice::from_raw_parts_mut(
APP_BASE_ADDRESS as *mut u8,
APP_SIZE_LIMIT
).fill(0);
let app_src = core::slice::from_raw_parts(
self.app_start[app_id] as *const u8,
self.app_start[app_id + 1] - self.app_start[app_id]
);
let app_dst = core::slice::from_raw_parts_mut(
APP_BASE_ADDRESS as *mut u8,
app_src.len()
);
app_dst.copy_from_slice(app_src);

Run each app!

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
// os/src/batch.rs

pub fn run_next_app() -> ! {
let mut app_manager = APP_MANAGER.exclusive_access();
let current_app = app_manager.get_current_app();
unsafe {
app_manager.load_app(current_app);
}
app_manager.move_to_next_app();
drop(app_manager);
// before this we have to drop local variables related to resources manually
// and release the resources
extern "C" { fn __restore(cx_addr: usize); }
unsafe {
__restore(KERNEL_STACK.push_context(
TrapContext::app_init_context(APP_BASE_ADDRESS, USER_STACK.get_sp())
) as *const _ as usize);
}
panic!("Unreachable in batch::run_current_app!");
}

// main logic:
// os/src/main.rs

...above code
// load entry for trap
trap::init()
// load app
batch::run_next_app()