
OS 简单教程九
唤醒应用
第一个应用都是内核运行的,下面就来看看怎么加载并运行应用
unsafe fn load_app(&mut self) {
// clear app area
let app_src = core::slice::from_raw_parts(
self.app_start[0] as *const u8,
self.app_start[1] - self.app_start[0],
);
// 入口点是从 elf 加载的
let (memory_set, user_sp, entry_point) = MemorySet::from_elf(&app_src);
self.token = memory_set.token();
// 通过查表, 从虚拟页获得实际的物理页
self.trap_cx_ppn = memory_set
.translate(VirtAddr::from(TRAP_CONTEXT).into())
.unwrap()
.ppn();
// 因为目前还是单任务, 暂没有 task 和 TaskContext, 只有 trap
// 内核栈单纯用于内核的函数调用
// map a kernel-stack in kernel space
let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(0);
KERNEL_SPACE.lock().insert_framed_area(
kernel_stack_bottom.into(),
kernel_stack_top.into(),
MapPermission::R | MapPermission::W,
);
// 获取 trap context 的指针并赋值
let trap_cx = self.trap_cx_ppn.get_mut();
*trap_cx = TrapContext::app_init_context(
entry_point,
user_sp,
KERNEL_SPACE.lock().token(),
kernel_stack_top,
trap_handler as usize,
);
}
}
pub fn run() -> ! {
// 加载 app 到虚拟地址
unsafe {
APP_MANAGER.inner.borrow_mut().load_app();
}
// 调用 restore 启动 app
trap_return();
}
- load_app 从内核的数据段加载应用的 elf
- 因为 TrapContext 在每个应用固定的虚拟地址,所以通过手动查映射可以查到
- 初始化 TrapContext ,通过 elf 中的 entry_point 设置 sepc 也就是 trap 后执行的第一行代码
- 跳到 restore 的地址,执行汇编
打印
铺垫了那么多,要在用户态打印就必须 trap 到内核态,要 trap 就必须满足
- 首先要保存寄存器到 trap context ,不同的是它在用户地址空间,因为只有一个 sscratch 做中转、用来保存 sp 了,没有中转 token(保存地址空间页表的地址)的寄存器
- 还要把地址空间切换到内核,因为需要的数据结构都在内核态,所以内核栈也在内核 先来满足 2 ,我们建立内核和应用地址空间
建立内核地址空间
pub fn new_kernel() -> Self {
let mut memory_set = Self::new_bare();
// map trampoline
memory_set.map_trampoline();
// map kernel sections
println!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
println!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
println!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
println!(
".bss [{:#x}, {:#x})",
sbss_with_stack as usize, ebss as usize
);
println!("mapping .text section");
memory_set.push(
MapArea::new(
(stext as usize).into(),
(etext as usize).into(),
MapType::Identical,
MapPermission::R | MapPermission::X,
),
None,
);
println!("mapping .rodata section");
memory_set.push(
MapArea::new(
(srodata as usize).into(),
(erodata as usize).into(),
MapType::Identical,
MapPermission::R,
),
None,
);
println!("mapping .data section");
memory_set.push(
MapArea::new(
(sdata as usize).into(),
(edata as usize).into(),
MapType::Identical,
MapPermission::R | MapPermission::W,
),
None,
);
println!("mapping .bss section");
memory_set.push(
MapArea::new(
(sbss_with_stack as usize).into(),
(ebss as usize).into(),
MapType::Identical,
MapPermission::R | MapPermission::W,
),
None,
);
println!("mapping physical memory");
memory_set.push(
MapArea::new(
(ekernel as usize).into(),
MEMORY_END.into(),
MapType::Identical,
MapPermission::R | MapPermission::W,
),
None,
);
for pair in MMIO {
memory_set.push(
MapArea::new(
(*pair).0.into(),
((*pair).0 + (*pair).1).into(),
MapType::Identical,
MapPermission::R | MapPermission::W,
),
None,
);
}
memory_set
}
把内核各个数据段用恒等映射对应起来,只是没有映射内核栈,因为内核栈一个应用一个,在映射应用的时候映射。最后一部分是映射 MMIO 的地址,会在编写驱动的时候用上。
建立应用地址空间
pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize) {
let mut memory_set = Self::new_bare();
// map trampoline
memory_set.map_trampoline();
// map program headers of elf, with U flag
let elf = xmas_elf::ElfFile::new(elf_data).unwrap();
let elf_header = elf.header;
let magic = elf_header.pt1.magic;
assert_eq!(magic, [0x7f, 0x45, 0x4c, 0x46], "invalid elf!");
let ph_count = elf_header.pt2.ph_count();
let mut max_end_vpn = VirtPageNum(0);
for i in 0..ph_count {
let ph = elf.program_header(i).unwrap();
if ph.get_type().unwrap() == xmas_elf::program::Type::Load {
let start_va: VirtAddr = (ph.virtual_addr() as usize).into();
let end_va: VirtAddr = ((ph.virtual_addr() + ph.mem_size()) as usize).into();
let mut map_perm = MapPermission::U;
let ph_flags = ph.flags();
if ph_flags.is_read() {
map_perm |= MapPermission::R;
}
if ph_flags.is_write() {
map_perm |= MapPermission::W;
}
if ph_flags.is_execute() {
map_perm |= MapPermission::X;
}
let map_area = MapArea::new(start_va, end_va, MapType::Framed, map_perm);
max_end_vpn = map_area.vpn_range.get_end();
memory_set.push(
map_area,
Some(&elf.input[ph.offset() as usize..(ph.offset() + ph.file_size()) as usize]),
);
}
}
// map user stack with U flags
let max_end_va: VirtAddr = max_end_vpn.into();
let mut user_stack_bottom: usize = max_end_va.into();
// guard page
user_stack_bottom += PAGE_SIZE;
let user_stack_top = user_stack_bottom + USER_STACK_SIZE;
memory_set.push(
MapArea::new(
user_stack_bottom.into(),
user_stack_top.into(),
MapType::Framed,
MapPermission::R | MapPermission::W | MapPermission::U,
),
None,
);
// map TrapContext
memory_set.push(
MapArea::new(
TRAP_CONTEXT.into(),
TRAMPOLINE.into(),
MapType::Framed,
MapPermission::R | MapPermission::W,
),
None,
);
(
memory_set,
user_stack_top,
elf.header.pt2.entry_point() as usize,
)
}
- 解析 elf ,根据 elf 映射应用的各个数据段,
- 映射用户栈
- 映射 TrapContext
Trap
再满足 1 ,TrapContext 在应用地址空间的跳板后面,在汇编里保存寄存器到 TrapContext 前面介绍过。
跳板
跳板是干嘛的?其实 trap_handler 就保存在其中,它的权限也没有 U ,应用是不能访问的。
fn map_trampoline(&mut self) {
self.page_table.map(
VirtAddr::from(TRAMPOLINE).into(),
PhysAddr::from(strampoline as usize).into(),
PTEFlags::R | PTEFlags::X,
);
}
跳板在内核和应用中都有,且虚拟地址相同,保证在 trap 的时候切换地址空间前后都能访问到 trap_handler ,也就是相同虚拟地址映射到相同的物理地址。注意,map_trampoline 没有使用 MapArea::new ,隐含是恒等映射,不需要分配物理内存,只要在 trap.asm 中指定 .section .text.trampoline 就可以,会把 trap.asm 的数据放在跳板中。
改造系统调用–字符输出
因为打印的数据结构在应用地址空间,而 trap 执行在内核地址空间。用应用地址空间的 token 通过查表的方式获取要打印的数据结构。
use crate::{mmu::translated_byte_buffer, task::current_user_token};
const FD_STDOUT: usize = 1;
pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
match fd {
FD_STDOUT => {
// 改为使用虚拟内存
let buffers = translated_byte_buffer(current_user_token(), buf, len);
for buffer in buffers {
print!("{}", core::str::from_utf8(buffer).unwrap());
}
len as isize
}
_ => {
panic!("Unsupported fd in sys_write!");
}
}
}