首页/文章列表/文章详情

[rCore学习笔记 022]多道程序与分时任务

编程知识1832024-08-06评论

写在前面

本随笔是非常菜的菜鸡写的。如有问题请及时提出。

可以联系:1160712160@qq.com

GitHhub:https://github.com/WindDevil(目前啥也没有

思考

上一节我们也提到了关于多道程序的放置和加载问题的事情.对比上一章的加载,我们需要把所有的APP全部都加载到内存中.

在这一节的描述中,官方文档提出了:
但我们也会了解到,每个应用程序需要知道自己运行时在内存中的不同位置,这对应用程序的编写带来了一定的麻烦。而且操作系统也要知道每个应用程序运行时的位置,不能任意移动应用程序所在的内存空间,即不能在运行时根据内存空间的动态空闲情况,把应用程序调整到合适的空闲空间中。

这里其实我脑子里是非常难受的,就是关于这个调整到合适的空闲空间中 , 因为上一章的程序也没有这个功能,我感觉是后续的内容可能会涉及到对于 碎片空间的利用.

多道程序的放置

回想我们上一章的时候让我们惊叹的link_app.S和对应的build.rs脚本,我们可以猜想到大概也是要通过build.rs来修改每个APP的链接地址.

可是build.py已经忘记了,唉,不知道这个记忆力需要学到啥时候才能学完.

回顾link_app.S,可以看到,实际上在.data段保存了所有的APP:

 .align 3 .section .data .global _num_app_num_app: .quad 7 .quad app_0_start .quad app_1_start .quad app_2_start .quad app_3_start .quad app_4_start .quad app_5_start .quad app_6_start .quad app_6_end .section .data .global app_0_start .global app_0_endapp_0_start: .incbin"../user/target/riscv64gc-unknown-none-elf/release/00hello_world.bin"app_0_end: .section .data .global app_1_start .global app_1_endapp_1_start: .incbin"../user/target/riscv64gc-unknown-none-elf/release/01store_fault.bin"app_1_end: .section .data .global app_2_start .global app_2_endapp_2_start: .incbin"../user/target/riscv64gc-unknown-none-elf/release/02power.bin"app_2_end: .section .data .global app_3_start .global app_3_endapp_3_start: .incbin"../user/target/riscv64gc-unknown-none-elf/release/03priv_inst.bin"app_3_end: .section .data .global app_4_start .global app_4_endapp_4_start: .incbin"../user/target/riscv64gc-unknown-none-elf/release/04priv_csr.bin"app_4_end: .section .data .global app_5_start .global app_5_endapp_5_start: .incbin"../user/target/riscv64gc-unknown-none-elf/release/test1_write0.bin"app_5_end: .section .data .global app_6_start .global app_6_endapp_6_start: .incbin"../user/target/riscv64gc-unknown-none-elf/release/test1_write1.bin"app_6_end:

这时候脑子里浮现出一个想法,那么这难道不算全部都加载到内存里了吗?

很显然不是,只是链接在了.data段.

查看user下的link.ld,你可以看到所有的APP的起始地址都是0x80400000:

OUTPUT_ARCH(riscv)ENTRY(_start)BASE_ADDRESS = 0x80400000;SECTIONS{ . = BASE_ADDRESS; .text : { *(.text.entry) *(.text .text.*) } .rodata : { *(.rodata .rodata.*) *(.srodata .srodata.*) } .data : { *(.data .data.*) *(.sdata .sdata.*) } .bss : { start_bss = .; *(.bss .bss.*) *(.sbss .sbss.*) end_bss = .; } /DISCARD/ : { *(.eh_frame) *(.debug*) }}

所以如果想要所有的APP都能够加载在一起,那么需要修改的是user下的link.ld.

为什么要这么做,官方文档做出了描述:
之所以要有这么苛刻的条件,是因为目前的操作系统内核的能力还是比较弱的,对应用程序通用性的支持也不够(比如不支持加载应用到内存中的任意地址运行),这也进一步导致了应用程序编程上不够方便和通用(应用需要指定自己运行的内存地址)。事实上,目前应用程序的编址方式是基于绝对位置的,并没做到与位置无关,内核也没有提供相应的地址重定位机制。

因此,通过在user下写一个build.py来对每一个APP生成一个链接文件,(所以还是python好用吗):

 # user/build.py import os base_address = 0x80400000 step = 0x20000 linker = 'src/linker.ld' app_id = 0 apps = os.listdir('src/bin') apps.sort() for app in apps: app = app[:app.find('.')] lines = [] lines_before = [] with open(linker, 'r') as f: for line in f.readlines(): lines_before.append(line) line = line.replace(hex(base_address), hex(base_address+step*app_id)) lines.append(line) with open(linker, 'w+') as f: f.writelines(lines) os.system('cargo build --bin %s --release' % app) print('[build.py] application %s start with address %s' %(app, hex(base_address+step*app_id))) with open(linker, 'w+') as f: f.writelines(lines_before) app_id = app_id + 1

这个文件是对link.ld里的0x80400000进行修改,每一个步长为0x20000,修改好了之后就开始使用cargo build --bin单独构建对应APP.

这时候就体现了我的想当然,上一部分的学习中,我们学到build.rs会在执行cargo run之前被调用,这时候我们就盲目地认为build.py也会被调用.

实际上不是这样的,我们需要在make build的过程中调用它,因此需要修改user/Makefile.

增加:

APPS := $(wildcard $(APP_DIR)/*.rs)...elf: $(APPS)    @python3 build.py...

这里会有一些我看不太懂的地方,我们询问通义千问:

  1. 使用$(APPS)是检查这些文件有没有更新
  2. 使用@是指静默运行指令

但是我们会发现当前AI的局限性,他们是懂得,我总感觉还少点什么少点提纲挈领的东西.

于是我们可以查询Makefile教程和示例指南 (foofun.cn).

Makefile语法:

Makefile由一组 rules 组成。 rule通常如下所示:

targets: prerequisitescommandcommandcommand
  • targets (目标) 是文件名,用空格分隔。 通常,每个rule只有一个。
  • commands (命令) 是通常用于创建目标的一系列步骤。 这些 需要以制表符 开头,不可以是空格。
  • prerequisites(先决条件) 也是文件名,用空格分隔。 在运行目标的命令之前,这些文件需要存在。 这些也称为 dependencies(依赖项)

可以看到,这一句基本语法,比我们凭借想象和经验的理解要好上很多倍.这个$(APPS)我们把它归类为prerequisites,自然就可以理解makefile在工作时会尝试检查文件的存在.

同样我们可以知道使用$()是引用变量,使用$(fn, arguments)是调用函数,这个不要搞不清楚,具体的还是看Makefile教程和示例指南 (foofun.cn).

这里有两个TIPS:

  1. 搜索的时候增加filetype:pdf在寻找成体系的理论性的东西的时候很好用
  2. 搜索的时候用英文+cookbook的方式往往能够找到很好的工程手册

这就说明了开源世界的重要性,做完rCore,我想我们应该去贡献一下开源世界.

官方的文件还添加了:

...clean:@cargo clean.PHONY: elf binary build clean

clean的具体实现不再赘述,而.PHONY的意思是伪目标(phony targets),用于列出那些并非真实文件的目标,而是代表某种操作的标签.

声明了伪目标,make的过程中就不会去寻找这些文件存在与否,但是本身makefile有很强大的解析功能,因此大部分情况不声明.PHONY也是没关系的.

多道应用程序的加载

思考上一章中应用程序的加载是通过结构体AppManagerload_app方法来实现.

unsafe fn load_app(&self, app_id: usize) {if app_id >= self.num_app {println!("All applications completed!");//panic!("Shutdown machine!");shutdown(false);}println!("[kernel] Loading app_{}", app_id);// clear app areacore::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);// Memory fence about fetching the instruction memory// It is guaranteed that a subsequent instruction fetch must// observes all previous writes to the instruction memory.// Therefore, fence.i must be executed after we have loaded// the code of the next app into the instruction memory.// See also: riscv non-priv spec chapter 3, 'Zifencei' extension.asm!("fence.i");}

可以看到实际上是在.data段把APP直接拷贝到内存之中.

但是本章是没这个环节的,是把应用程序一股脑加载到内存中.

这里脑子里冒出来一个问题,为什么不直接就地运行APP(指直接把sp寄存器指向链接到的位置).这里忽略了在.data段的APP是不能写入的.

那么对于已经分别设置为不同的BASE_ADDRESS的APP,我们要想办法把他们从.data中加载到内存中.

替代上一节的batch.rs,我们创建os/src/loader.rs,里边有load_appsget_base_i以及``:

 // os/src/loader.rspub fn load_apps() { extern"C" { fn _num_app(); } let num_app_ptr = _num_app as usize as *const usize; let num_app = get_num_app(); let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) }; // load apps for i in 0..num_app { let base_i = get_base_i(i); // clear region (base_i..base_i + APP_SIZE_LIMIT).for_each(|addr| unsafe { (addr as *mut u8).write_volatile(0) }); // load app from data section to memory let src = unsafe { core::slice::from_raw_parts( app_start[i] as *const u8, app_start[i + 1] - app_start[i] ) }; let dst = unsafe { core::slice::from_raw_parts_mut(base_i as *mut u8, src.len()) }; dst.copy_from_slice(src); } unsafe { asm!("fence.i"); }}fn get_base_i(app_id: usize) -> usize { APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT}pub fn get_num_app() -> usize { extern"C" { fn _num_app(); } unsafe { (_num_app as usize as *const usize).read_volatile() }}

可以看到在load_apps中,首先使用get_base_i计算当前的APP的偏置地址,然后使用和上一章相同的方法,把APP的内容加载进去.而get_num_app则负责直接获取APP的数量.

同样地,我们即使使用的是多道程序放置及加载的程序,那么我们仍然需要内核栈用户栈.

另外,在官方的实现中,使用了一个config.rs用来储存用户层APP的各项配置.

//! Constants used in rCorepub const USER_STACK_SIZE: usize = 4096 * 2;pub const KERNEL_STACK_SIZE: usize = 4096 * 2;pub const MAX_APP_NUM: usize = 4;pub const APP_BASE_ADDRESS: usize = 0x80400000;pub const APP_SIZE_LIMIT: usize = 0x20000;

因为程序之间的数据是不能共享的,而且也为了防止出现上下文错误,因此需要给每一个APP设置一套用户栈内核栈:

#[repr(align(4096))]#[derive(Copy, Clone)]struct KernelStack { data: [u8; KERNEL_STACK_SIZE],}#[repr(align(4096))]#[derive(Copy, Clone)]struct UserStack { data: [u8; USER_STACK_SIZE],}static KERNEL_STACK: [KernelStack; MAX_APP_NUM] = [KernelStack { data: [0; KERNEL_STACK_SIZE],}; MAX_APP_NUM];static USER_STACK: [UserStack; MAX_APP_NUM] = [UserStack { data: [0; USER_STACK_SIZE],}; MAX_APP_NUM];impl KernelStack { fn get_sp(&self) -> usize { self.data.as_ptr() as usize + KERNEL_STACK_SIZE } pub fn push_context(&self, trap_cx: TrapContext) -> usize { let trap_cx_ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext; unsafe { *trap_cx_ptr = trap_cx; } trap_cx_ptr as usize }}impl UserStack { fn get_sp(&self) -> usize { self.data.as_ptr() as usize + USER_STACK_SIZE }}

同时,因为目前所有的APP都已经加载,因此不需要保存每个APP在未加载时候的位置,因此对AppManager进行裁剪,只保留当前APP和APP总数的功能,同时在lazy_static里边使用get_num_app简化操作:

struct AppManager { num_app: usize, current_app: usize,}impl AppManager { pub fn get_current_app(&self) -> usize { self.current_app } pub fn move_to_next_app(&mut self) { self.current_app += 1; }}lazy_static! { static ref APP_MANAGER: UPSafeCell<AppManager> = unsafe { UPSafeCell::new({ let num_app = get_num_app(); AppManager { num_app, current_app: 0, } }) };}

同样地,我们也需要定制一个上下文,使用__restore利用这个上下文恢复(实际上可以理解为配置上下文)用户态.

这时候脑子里的流出就不是单纯的spsscratch用户态内核态互换了,而是__restore把第一个参数a0里的函数入口entry送入了sp,然后又通过后续一系列操作把以这个sp为基准的sscratch也配置进去.这样就实现了多个APP上下文的切换.

这里截取一小段__restore:

...mv sp, a0ld t0, 32*8(sp)ld t1, 33*8(sp)ld t2, 2*8(sp)csrw sstatus, t0csrw sepc, t1csrw sscratch, t2...

那么怎么制定这个上下文呢,我们可以想到TrapContext结构体的两个组成部分一个是用户栈的位置一个是APP入口位置,这里偷取官方的代码,

pub fn init_app_cx(app_id: usize) -> usize { KERNEL_STACK[app_id].push_context(TrapContext::app_init_context( get_base_i(app_id), USER_STACK[app_id].get_sp(), ))}

然后改造上一章写得run_next_app即可,这里的关键点在于1. 去掉加载APP的环节 2. 因为去掉加载APP的环节,因此需要在切换而不是在加载的时候判断APP是不是运行结束:

pub fn run_next_app() -> ! { let mut app_manager = APP_MANAGER.exclusive_access(); let current_app = app_manager.get_current_app(); if current_app >= app_manager.num_app-1 { println!("All applications completed!"); shutdown(false); } 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(init_app_cx(current_app)); } panic!("Unreachable in batch::run_current_app!");}

随后需要在代码里解决一些依赖问题,

  1. main.rs里增加pub mod loader
  2. batch::run_next_app换成loader::run_next_app
  3. main函数中把batch的初始化和运行修改为loader::load_apps();loader::run_next_app();

尝试运行

根据评论区的经验,我建议大家先执行一下clean:

cd usermake cleanmake buildcd ../osmake run

运行结果:

[rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0.______ __ __ _______.___________. _______..______ __| _ \ | | | | / | | / || _ \ | || |_) | | | | | | (----`---| |----`| (----`| |_) || || / | | | | \ \ | | \ \ | _ < | || |\ \----.| `--' |.----) | | | .----) | | |_) || || _| `._____| \______/ |_______/ |__| |_______/ |______/ |__|[rustsbi] Implementation : RustSBI-QEMU Version 0.2.0-alpha.2[rustsbi] Platform Name : riscv-virtio,qemu[rustsbi] Platform SMP : 1[rustsbi] Platform Memory : 0x80000000..0x88000000[rustsbi] Boot HART : 0[rustsbi] Device Tree Region : 0x87000000..0x87000f02[rustsbi] Firmware Address : 0x80000000[rustsbi] Supervisor Address : 0x80200000[rustsbi] pmp01: 0x00000000..0x80000000 (-wr)[rustsbi] pmp02: 0x80000000..0x80200000 (---)[rustsbi] pmp03: 0x80200000..0x88000000 (xwr)[rustsbi] pmp04: 0x88000000..0x00000000 (-wr)[kernel] Hello, world![kernel] trap init endHello, world![kernel] Application exited with code 0Into Test store_fault, we will insert an invalid store operation...Kernel should kill this application![kernel] PageFault in application, kernel killed it.3^10000=5079(MOD 10007)3^20000=8202(MOD 10007)3^30000=8824(MOD 10007)3^40000=5750(MOD 10007)3^50000=3824(MOD 10007)3^60000=8516(MOD 10007)3^70000=2510(MOD 10007)3^80000=9379(MOD 10007)3^90000=2621(MOD 10007)3^100000=2749(MOD 10007)Test power OK![kernel] Application exited with code 0Try to execute privileged instruction in U ModeKernel should kill this application![kernel] IllegalInstruction in application, kernel killed it.All applications completed!
神弓

博客园

这个人很懒...

用户评论 (0)

发表评论

captcha