实现批处理操作系统¶
将应用程序链接到内核¶
在本章中,我们要把应用程序的二进制镜像文件作为数据段链接到内核里, 内核需要知道应用程序的数量和它们的位置。
在 os/src/main.rs
中能够找到这样一行:
core::arch::global_asm!(include_str!("link_app.S"));
这里我们引入了一段汇编代码 link_app.S
,它是在 make run
构建操作系统时自动生成的,里面的内容大致如下:
1# os/src/link_app.S
2
3 .align 3
4 .section .data
5 .global _num_app
6_num_app:
7 .quad 3
8 .quad app_0_start
9 .quad app_1_start
10 .quad app_2_start
11 .quad app_2_end
12
13 .section .data
14 .global app_0_start
15 .global app_0_end
16app_0_start:
17 .incbin "../user/target/riscv64gc-unknown-none-elf/release/hello_world.bin"
18app_0_end:
19
20 .section .data
21 .global app_1_start
22 .global app_1_end
23app_1_start:
24 .incbin "../user/target/riscv64gc-unknown-none-elf/release/bad_address.bin"
25app_1_end:
26
27 .section .data
28 .global app_2_start
29 .global app_2_end
30app_2_start:
31 .incbin "../user/target/riscv64gc-unknown-none-elf/release/power.bin"
32app_2_end:
第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像,
并且各自有一对全局符号 app_*_start, app_*_end
指示它们的开始和结束位置。
而第 3 行开始的另一个数据段相当于一个 64 位整数数组。
数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用程序的起始地址,
最后一个元素放置最后一个应用程序的结束位置。这样数组中相邻两个元素记录了每个应用程序的始末位置,
这个数组所在的位置由全局符号 _num_app
所指示。
这个文件是在 cargo build
时,由脚本 os/build.rs
控制生成的。
找到并加载应用程序二进制码¶
我们在 os
的 batch
子模块中实现一个应用管理器 AppManager
,结构体定义如下:
struct AppManager {
num_app: usize,
current_app: usize,
app_start: [usize; MAX_APP_NUM + 1],
}
初始化 AppManager
的全局实例:
lazy_static! {
static ref APP_MANAGER: UPSafeCell<AppManager> = unsafe {
UPSafeCell::new({
extern "C" {
fn _num_app();
}
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);
AppManager {
num_app,
current_app: 0,
app_start,
}
})
};
}
初始化的逻辑很简单,就是找到 link_app.S
中提供的符号 _num_app
,并从这里开始解析出应用数量以及各个应用的开头地址。
用容器 UPSafeCell
包裹 AppManager
是为了防止全局对象 APP_MANAGER
被重复获取。
注解
UPSafeCell
实现在 sync
模块中,调用 exclusive_access
方法能获取其内部对象的可变引用,
如果程序运行中同时存在多个这样的引用,会触发 already borrowed: BorrowMutError
。
UPSafeCell
既提供了内部可变性,又在单核情境下防止了内部对象被重复借用,我们将在后文中多次见到它。
这里使用了外部库 lazy_static
提供的 lazy_static!
宏。
lazy_static!
宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置初始值,
但是有些全局变量的初始化依赖于运行期间才能得到的数据。
如这里我们借助 lazy_static!
声明了一个 AppManager
结构的名为 APP_MANAGER
的全局实例,
只有在它第一次被使用到的时候才会进行实际的初始化工作。
AppManager
的方法中, print_app_info/get_current_app/move_to_next_app
都相当简单直接,需要说明的是 load_app
:
1unsafe fn load_app(&self, app_id: usize) {
2 if app_id >= self.num_app {
3 panic!("All applications completed!");
4 }
5 info!("[kernel] Loading app_{}", app_id);
6 // clear icache
7 core::arch::asm!("fence.i");
8 // clear app area
9 core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, APP_SIZE_LIMIT).fill(0);
10 let app_src = core::slice::from_raw_parts(
11 self.app_start[app_id] as *const u8,
12 self.app_start[app_id + 1] - self.app_start[app_id],
13 );
14 let app_dst = core::slice::from_raw_parts_mut(APP_BASE_ADDRESS as *mut u8, app_src.len());
15 app_dst.copy_from_slice(app_src);
16}
这个方法负责将参数 app_id
对应的应用程序的二进制镜像加载到物理内存以 0x80400000
起始的位置,
这个位置是批处理操作系统和应用程序之间约定的常数地址。
我们将从这里开始的一块内存清空,然后找到待加载应用二进制镜像的位置,并将它复制到正确的位置。
清空内存前,我们插入了一条奇怪的汇编指令 fence.i
,它是用来清理 i-cache 的。
我们知道, 缓存又分成 数据缓存 (d-cache) 和 指令缓存 (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。
通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。
但在这里,我们会修改会被 CPU 取指的内存区域,使得 i-cache 中含有与内存不一致的内容,
必须使用 fence.i
指令手动清空 i-cache ,让里面所有的内容全部失效,
才能够保证程序执行正确性。
警告
模拟器与真机的不同之处
在 Qemu 模拟器上,即使不加刷新 i-cache 的指令,大概率也能正常运行,但在物理计算机上不是这样。
batch
子模块对外暴露出如下接口:
init
:调用print_app_info
的时第一次用到了全局变量APP_MANAGER
,它在这时完成初始化;run_next_app
:批处理操作系统的核心操作,即加载并运行下一个应用程序。 批处理操作系统完成初始化,或者应用程序运行结束/出错后会调用该函数。下节再介绍其具体实现。