找回密码
 立即注册
首页 业界区 安全 [rCore学习笔记 022]多道程序与分时任务

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

嗦或 3 天前
写在前面

本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
GitHhub:https://github.com/WindDevil (目前啥也没有
思考

上一节我们也提到了关于多道程序的放置和加载问题的事情.对比上一章的加载,我们需要把所有的APP全部都加载到内存中.
在这一节的描述中,官方文档提出了:
但我们也会了解到,每个应用程序需要知道自己运行时在内存中的不同位置,这对应用程序的编写带来了一定的麻烦。而且操作系统也要知道每个应用程序运行时的位置,不能 任意移动应用程序所在的内存空间 ,即不能在运行时根据内存空间的动态空闲情况,把应用程序 调整到合适的空闲空间 中。
这里其实我脑子里是非常难受的,就是关于这个 调整到合适的空闲空间中 , 因为上一章的程序也没有这个功能,我感觉是后续的内容可能会涉及到对于 碎片空间 的利用.
多道程序的放置

回想我们上一章的时候让我们惊叹的link_app.S和对应的build.rs脚本,我们可以猜想到大概也是要通过build.rs来修改每个APP的链接地址.
可是build.py已经忘记了,唉,不知道这个记忆力需要学到啥时候才能学完.
回顾link_app.S,可以看到,实际上在.data段保存了所有的APP:
  1.     .align 3
  2.     .section .data
  3.     .global _num_app
  4. _num_app:
  5.     .quad 7
  6.     .quad app_0_start
  7.     .quad app_1_start
  8.     .quad app_2_start
  9.     .quad app_3_start
  10.     .quad app_4_start
  11.     .quad app_5_start
  12.     .quad app_6_start
  13.     .quad app_6_end
  14.     .section .data
  15.     .global app_0_start
  16.     .global app_0_end
  17. app_0_start:
  18.     .incbin "../user/target/riscv64gc-unknown-none-elf/release/00hello_world.bin"
  19. app_0_end:
  20.     .section .data
  21.     .global app_1_start
  22.     .global app_1_end
  23. app_1_start:
  24.     .incbin "../user/target/riscv64gc-unknown-none-elf/release/01store_fault.bin"
  25. app_1_end:
  26.     .section .data
  27.     .global app_2_start
  28.     .global app_2_end
  29. app_2_start:
  30.     .incbin "../user/target/riscv64gc-unknown-none-elf/release/02power.bin"
  31. app_2_end:
  32.     .section .data
  33.     .global app_3_start
  34.     .global app_3_end
  35. app_3_start:
  36.     .incbin "../user/target/riscv64gc-unknown-none-elf/release/03priv_inst.bin"
  37. app_3_end:
  38.     .section .data
  39.     .global app_4_start
  40.     .global app_4_end
  41. app_4_start:
  42.     .incbin "../user/target/riscv64gc-unknown-none-elf/release/04priv_csr.bin"
  43. app_4_end:
  44.     .section .data
  45.     .global app_5_start
  46.     .global app_5_end
  47. app_5_start:
  48.     .incbin "../user/target/riscv64gc-unknown-none-elf/release/test1_write0.bin"
  49. app_5_end:
  50.     .section .data
  51.     .global app_6_start
  52.     .global app_6_end
  53. app_6_start:
  54.     .incbin "../user/target/riscv64gc-unknown-none-elf/release/test1_write1.bin"
  55. app_6_end:
复制代码
这时候脑子里浮现出一个想法,那么这难道不算全部都加载到内存里了吗?
很显然不是,只是链接在了.data段.
查看user下的link.ld,你可以看到所有的APP的起始地址都是0x80400000:
  1. OUTPUT_ARCH(riscv)
  2. ENTRY(_start)
  3. BASE_ADDRESS = 0x80400000;
  4. SECTIONS
  5. {
  6.     . = BASE_ADDRESS;
  7.     .text : {
  8.         *(.text.entry)
  9.         *(.text .text.*)
  10.     }
  11.     .rodata : {
  12.         *(.rodata .rodata.*)
  13.         *(.srodata .srodata.*)
  14.     }
  15.     .data : {
  16.         *(.data .data.*)
  17.         *(.sdata .sdata.*)
  18.     }
  19.     .bss : {
  20.         start_bss = .;
  21.         *(.bss .bss.*)
  22.         *(.sbss .sbss.*)
  23.         end_bss = .;
  24.     }
  25.     /DISCARD/ : {
  26.         *(.eh_frame)
  27.         *(.debug*)
  28.     }
  29. }
复制代码
所以如果想要所有的APP都能够加载在一起,那么需要修改的是user下的link.ld.
为什么要这么做,官方文档做出了描述:
之所以要有这么苛刻的条件,是因为目前的操作系统内核的能力还是比较弱的,对应用程序通用性的支持也不够(比如不支持加载应用到内存中的任意地址运行),这也进一步导致了应用程序编程上不够方便和通用(应用需要指定自己运行的内存地址)。事实上,目前应用程序的编址方式是基于绝对位置的,并没做到与位置无关,内核也没有提供相应的地址重定位机制。
因此,通过在user下写一个build.py来对每一个APP生成一个链接文件,(所以还是python好用吗):
  1. # user/build.py
  2. import os
  3. base_address = 0x80400000
  4. step = 0x20000
  5. linker = 'src/linker.ld'
  6. app_id = 0
  7. apps = os.listdir('src/bin')
  8. apps.sort()
  9. for app in apps:
  10.      app = app[:app.find('.')]
  11.      lines = []
  12.      lines_before = []
  13.      with open(linker, 'r') as f:
  14.          for line in f.readlines():
  15.              lines_before.append(line)
  16.              line = line.replace(hex(base_address), hex(base_address+step*app_id))
  17.              lines.append(line)
  18.      with open(linker, 'w+') as f:
  19.          f.writelines(lines)
  20.      os.system('cargo build --bin %s --release' % app)
  21.      print('[build.py] application %s start with address %s' %(app, hex(base_address+step*app_id)))
  22.      with open(linker, 'w+') as f:
  23.          f.writelines(lines_before)
  24.      app_id = app_id + 1
复制代码
这个文件是对link.ld里的0x80400000进行修改,每一个步长为0x20000,修改好了之后就开始使用cargo build --bin来 单独 构建对应APP.
这时候就体现了我的想当然,上一部分的学习中,我们学到build.rs会在执行cargo run之前被调用,这时候我们就盲目地认为build.py也会被调用.
实际上不是这样的,我们需要在make build的过程中调用它,因此需要修改user/Makefile.
增加:
  1. APPS := $(wildcard $(APP_DIR)/*.rs)
  2. ...
  3. elf: $(APPS)
  4.     @python3 build.py
  5. ...
复制代码
这里会有一些我看不太懂的地方,我们询问通义千问:

  • 使用$(APPS)是检查这些文件有没有更新
  • 使用@是指静默运行指令
但是我们会发现当前AI的局限性,他们是懂得,我总感觉还少点什么少点提纲挈领的东西.
于是我们可以查询Makefile教程和示例指南 (foofun.cn).
Makefile语法:
Makefile由一组 rules 组成。 rule通常如下所示:
  1. targets: prerequisites
  2.         command
  3.         command
  4.         command
复制代码

  • targets (目标) 是文件名,用空格分隔。 通常,每个rule只有一个。
  • commands (命令) 是通常用于创建目标的一系列步骤。 这些 需要以制表符 开头,不可以是空格。
  • prerequisites(先决条件) 也是文件名,用空格分隔。 在运行目标的命令之前,这些文件需要存在。 这些也称为 dependencies(依赖项)
可以看到,这一句基本语法,比我们凭借想象和经验的理解要好上很多倍.这个$(APPS)我们把它归类为prerequisites,自然就可以理解makefile在工作时会尝试检查文件的存在.
同样我们可以知道使用$()是引用变量,使用$(fn, arguments)是调用函数,这个不要搞不清楚,具体的还是看Makefile教程和示例指南 (foofun.cn).
这里有两个TIPS:

  • 搜索的时候增加filetype:pdf在寻找成体系的理论性的东西的时候很好用
  • 搜索的时候用英文+cookbook的方式往往能够找到很好的工程手册
这就说明了开源世界的重要性,做完rCore,我想我们应该去贡献一下开源世界.
官方的文件还添加了:
  1. ...
  2. clean:
  3.         @cargo clean
  4. .PHONY: elf binary build clean
复制代码
clean的具体实现不再赘述,而.PHONY的意思是 伪目标(phony targets) ,用于列出那些并非真实文件的目标,而是代表某种操作的标签.
声明了伪目标,make的过程中就不会去寻找这些文件存在与否,但是本身makefile有很强大的解析功能,因此 大部分情况不声明.PHONY也是没关系的 .
多道应用程序的加载

思考上一章中应用程序的加载是通过结构体AppManager的load_app方法来实现.
  1. unsafe fn load_app(&self, app_id: usize) {
  2.         if app_id >= self.num_app {
  3.                 println!("All applications completed!");
  4.                 //panic!("Shutdown machine!");
  5.                 shutdown(false);
  6.         }
  7.         println!("[kernel] Loading app_{}", app_id);
  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.         // Memory fence about fetching the instruction memory
  17.         // It is guaranteed that a subsequent instruction fetch must
  18.         // observes all previous writes to the instruction memory.
  19.         // Therefore, fence.i must be executed after we have loaded
  20.         // the code of the next app into the instruction memory.
  21.         // See also: riscv non-priv spec chapter 3, 'Zifencei' extension.
  22.         asm!("fence.i");
  23. }
复制代码
可以看到实际上是在.data段把APP直接拷贝到内存之中.
但是本章是没这个环节的,是把应用程序一股脑加载到内存中.
这里脑子里冒出来一个问题,为什么不直接就地运行APP(指直接把sp寄存器指向链接到的位置).这里忽略了在.data段的APP是不能 写入 的.
那么对于已经分别设置为不同的BASE_ADDRESS的APP,我们要想办法把他们从.data中加载到内存中.
替代上一节的batch.rs,我们创建os/src/loader.rs,里边有load_apps和get_base_i以及``:
  1. // os/src/loader.rs
  2. pub fn load_apps() {
  3. extern "C" { fn _num_app(); }
  4. let num_app_ptr = _num_app as usize as *const usize;
  5. let num_app = get_num_app();
  6. let app_start = unsafe {
  7.          core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1)
  8. };
  9. // load apps
  10. for i in 0..num_app {
  11.          let base_i = get_base_i(i);
  12.          // clear region
  13.          (base_i..base_i + APP_SIZE_LIMIT).for_each(|addr| unsafe {
  14.                  (addr as *mut u8).write_volatile(0)
  15.          });
  16.          // load app from data section to memory
  17.          let src = unsafe {
  18.                  core::slice::from_raw_parts(
  19.                          app_start[i] as *const u8,
  20.                          app_start[i + 1] - app_start[i]
  21.                  )
  22.          };
  23.          let dst = unsafe {
  24.                  core::slice::from_raw_parts_mut(base_i as *mut u8, src.len())
  25.          };
  26.          dst.copy_from_slice(src);
  27. }
  28. unsafe {
  29.          asm!("fence.i");
  30. }
  31. }
  32. fn get_base_i(app_id: usize) -> usize {
  33. APP_BASE_ADDRESS + app_id * APP_SIZE_LIMIT
  34. }
  35. pub fn get_num_app() -> usize {
  36.     extern "C" {
  37.         fn _num_app();
  38.     }
  39.     unsafe { (_num_app as usize as *const usize).read_volatile() }
  40. }
复制代码
可以看到在load_apps中,首先使用get_base_i计算当前的APP的偏置地址,然后使用和上一章相同的方法,把APP的内容加载进去.而get_num_app则负责直接获取APP的数量.
同样地,我们即使使用的是多道程序放置及加载的程序,那么我们仍然需要 内核栈用户栈 .
另外,在官方的实现中,使用了一个config.rs用来储存 用户层APP 的各项配置.
  1. //! Constants used in rCore
  2. pub const USER_STACK_SIZE: usize = 4096 * 2;
  3. pub const KERNEL_STACK_SIZE: usize = 4096 * 2;
  4. pub const MAX_APP_NUM: usize = 4;
  5. pub const APP_BASE_ADDRESS: usize = 0x80400000;
  6. pub const APP_SIZE_LIMIT: usize = 0x20000;
复制代码
因为程序之间的数据是不能共享的,而且也为了防止出现上下文错误,因此需要给每一个APP设置一套 用户栈内核栈 :
  1. #[repr(align(4096))]
  2. #[derive(Copy, Clone)]
  3. struct KernelStack {
  4.     data: [u8; KERNEL_STACK_SIZE],
  5. }
  6. #[repr(align(4096))]
  7. #[derive(Copy, Clone)]
  8. struct UserStack {
  9.     data: [u8; USER_STACK_SIZE],
  10. }
  11. static KERNEL_STACK: [KernelStack; MAX_APP_NUM] = [KernelStack {
  12.     data: [0; KERNEL_STACK_SIZE],
  13. }; MAX_APP_NUM];
  14. static USER_STACK: [UserStack; MAX_APP_NUM] = [UserStack {
  15.     data: [0; USER_STACK_SIZE],
  16. }; MAX_APP_NUM];
  17. impl KernelStack {
  18.     fn get_sp(&self) -> usize {
  19.         self.data.as_ptr() as usize + KERNEL_STACK_SIZE
  20.     }
  21.     pub fn push_context(&self, trap_cx: TrapContext) -> usize {
  22.         let trap_cx_ptr = (self.get_sp() - core::mem::size_of::<TrapContext>()) as *mut TrapContext;
  23.         unsafe {
  24.             *trap_cx_ptr = trap_cx;
  25.         }
  26.         trap_cx_ptr as usize
  27.     }
  28. }
  29. impl UserStack {
  30.     fn get_sp(&self) -> usize {
  31.         self.data.as_ptr() as usize + USER_STACK_SIZE
  32.     }
  33. }
复制代码
同时,因为目前所有的APP都已经加载,因此不需要保存每个APP在未加载时候的位置,因此对AppManager进行裁剪,只保留当前APP和APP总数的功能,同时在lazy_static里边使用get_num_app简化操作:
  1. struct AppManager {
  2.     num_app: usize,
  3.     current_app: usize,
  4. }
  5. impl AppManager {
  6.     pub fn get_current_app(&self) -> usize {
  7.         self.current_app
  8.     }
  9.     pub fn move_to_next_app(&mut self) {
  10.         self.current_app += 1;
  11.     }
  12. }
  13. lazy_static! {
  14.     static ref APP_MANAGER: UPSafeCell = unsafe {
  15.         UPSafeCell::new({
  16.             let num_app = get_num_app();
  17.             AppManager {
  18.                 num_app,
  19.                 current_app: 0,
  20.             }
  21.         })
  22.     };
  23. }
复制代码
同样地,我们也需要定制一个上下文,使用__restore利用这个上下文 恢复(实际上可以理解为配置上下文)用户态 .
这时候脑子里的流出就不是单纯的sp和sscratch在 用户态内核态 互换了,而是__restore把第一个参数a0 里的函数入口entry送入了sp,然后又通过后续一系列操作把以这个sp为基准的sscratch也配置进去.这样就实现了多个APP上下文的切换.
这里截取一小段__restore:
  1. ...
  2. mv sp, a0
  3. ld t0, 32*8(sp)
  4. ld t1, 33*8(sp)
  5. ld t2, 2*8(sp)
  6. csrw sstatus, t0
  7. csrw sepc, t1
  8. csrw sscratch, t2
  9. ...
复制代码
那么怎么制定这个上下文呢,我们可以想到TrapContext结构体的两个组成部分一个是 用户栈的位置 一个是 APP入口 位置,这里偷取官方的代码,
  1. pub fn init_app_cx(app_id: usize) -> usize {
  2.     KERNEL_STACK[app_id].push_context(TrapContext::app_init_context(
  3.         get_base_i(app_id),
  4.         USER_STACK[app_id].get_sp(),
  5.     ))
  6. }
复制代码
然后改造上一章写得run_next_app即可,这里的关键点在于1. 去掉加载APP的环节 2. 因为去掉加载APP的环节,因此需要在切换而不是在加载的时候判断APP是不是运行结束:
  1. pub fn run_next_app() -> ! {
  2.     let mut app_manager = APP_MANAGER.exclusive_access();
  3.     let current_app = app_manager.get_current_app();
  4.     if current_app >= app_manager.num_app-1 {
  5.         println!("All applications completed!");
  6.         shutdown(false);
  7.     }
  8.     app_manager.move_to_next_app();
  9.     drop(app_manager);
  10.     // before this we have to drop local variables related to resources manually
  11.     // and release the resources
  12.     extern "C" {
  13.         fn __restore(cx_addr: usize);
  14.     }
  15.     unsafe {
  16.         __restore(init_app_cx(current_app));
  17.     }
  18.     panic!("Unreachable in batch::run_current_app!");
  19. }
复制代码
随后需要在代码里解决一些依赖问题,

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

根据评论区的经验,我建议大家先执行一下clean:
  1. cd user
  2. make clean
  3. make build
  4. cd ../os
  5. make run
复制代码
运行结果:
  1. [rustsbi] RustSBI version 0.3.1, adapting to RISC-V SBI v1.0.0
  2. .______       __    __      _______.___________.  _______..______   __
  3. |   _  \     |  |  |  |    /       |           | /       ||   _  \ |  |
  4. |  |_)  |    |  |  |  |   |   (----`---|  |----`|   (----`|  |_)  ||  |
  5. |      /     |  |  |  |    \   \       |  |      \   \    |   _  < |  |
  6. |  |\  \----.|  `--'  |.----)   |      |  |  .----)   |   |  |_)  ||  |
  7. | _| `._____| \______/ |_______/       |__|  |_______/    |______/ |__|
  8. [rustsbi] Implementation     : RustSBI-QEMU Version 0.2.0-alpha.2
  9. [rustsbi] Platform Name      : riscv-virtio,qemu
  10. [rustsbi] Platform SMP       : 1
  11. [rustsbi] Platform Memory    : 0x80000000..0x88000000
  12. [rustsbi] Boot HART          : 0
  13. [rustsbi] Device Tree Region : 0x87000000..0x87000f02
  14. [rustsbi] Firmware Address   : 0x80000000
  15. [rustsbi] Supervisor Address : 0x80200000
  16. [rustsbi] pmp01: 0x00000000..0x80000000 (-wr)
  17. [rustsbi] pmp02: 0x80000000..0x80200000 (---)
  18. [rustsbi] pmp03: 0x80200000..0x88000000 (xwr)
  19. [rustsbi] pmp04: 0x88000000..0x00000000 (-wr)
  20. [kernel] Hello, world!
  21. [kernel] trap init end
  22. Hello, world!
  23. [kernel] Application exited with code 0
  24. Into Test store_fault, we will insert an invalid store operation...
  25. Kernel should kill this application!
  26. [kernel] PageFault in application, kernel killed it.
  27. 3^10000=5079(MOD 10007)
  28. 3^20000=8202(MOD 10007)
  29. 3^30000=8824(MOD 10007)
  30. 3^40000=5750(MOD 10007)
  31. 3^50000=3824(MOD 10007)
  32. 3^60000=8516(MOD 10007)
  33. 3^70000=2510(MOD 10007)
  34. 3^80000=9379(MOD 10007)
  35. 3^90000=2621(MOD 10007)
  36. 3^100000=2749(MOD 10007)
  37. Test power OK!
  38. [kernel] Application exited with code 0
  39. Try to execute privileged instruction in U Mode
  40. Kernel should kill this application!
  41. [kernel] IllegalInstruction in application, kernel killed it.
  42. All applications completed!
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册