写在前面
本随笔是非常菜的菜鸡写的。如有问题请及时提出。
可以联系:1160712160@qq.com
GitHhub:https://github.com/WindDevil (目前啥也没有
本章目的
实现批处理操作系统,每当一个应用程序执行完毕,都需要将下一个要执行的应用的代码和数据加载到内存.
- 应用加载机制
- 在操作系统和应用程序需要被放置到同一个可执行文件的前提下,设计一种尽量简洁的应用放置和加载方式,使得操作系统容易找到应用被放置到的位置,从而在批处理操作系统和应用程序之间建立起联系的纽带。
- 具体而言,应用放置采用“静态绑定”的方式,而操作系统加载应用则采用“动态加载”的方式
- 静态绑定:通过一定的编程技巧,把多个应用程序代码和批处理操作系统代码“绑定”在一起。
- 动态加载:基于静态编码留下的“绑定”信息,操作系统可以找到每个应用程序文件二进制代码的起始地址和长度,并能加载到内存中运行。
- 硬件相关且比较困难的地方
- 如何让在内核态的批处理操作系统启动应用程序'
- 且能让应用程序在用户态正常执行
将应用程序链接到内核
目的是将应用程序的二进制文件(也就是上一章执行make build生成的.bin文件),链接到内核之中,此时内核需要知道:
知道以上信息就可以在运行时对它们进行管理并且可以加载到物理内存.
这里直接给出结论:
- 需要汇编代码.S文件,里边规定应用程序的 开始和结束位置 ,保存应用程序的 数量 和 应用程序开始位置指针 .
- 需要写出生成这个.S文件的代码.
这个.S文件就规定名字为link_app.S,使用build.rs模块来进行生成.
先看生成的~/App/rCore-Tutorial-v3/os/src/link_app.S:- .align 3
- .section .data
- .global _num_app
- _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
- .section .data
- .global app_0_start
- .global app_0_end
- app_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_end
- app_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_end
- app_2_start:
- .incbin "../user/target/riscv64gc-unknown-none-elf/release/02power.bin"
- app_2_end:
- .section .data
- .global app_3_start
- .global app_3_end
- app_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_end
- app_4_start:
- .incbin "../user/target/riscv64gc-unknown-none-elf/release/04priv_csr.bin"
- app_4_end:
复制代码 可以看到:
- .align是是一个伪指令,用于指定编译器对随后的变量或标签进行特定的内存对齐,这里.align 3则代表对齐到2^3或者8字节的边界.
- .quad是一个伪指令,用于定义一个 64 位(8 字节)的常量或变量
- .quad 5代表此时有5个应用程序
- 以.quad app_0_start为例,意味着记录app_0_start的指针
- .section是一个伪指令,用于定义或切换到一个新的段,.section .data 特别指定了数据段,这是程序中用于存储已初始化的全局变量和静态变量的区域.
- .global是一个伪指令,用于声明一个符号(通常是函数或变量)为全局可见.这意味着这个符号不仅在其定义的文件内部可用,而且在整个链接过程中都是可见的,也就是说,其他文件也可以引用它.
- .global app_1_start说明app_1_start是全局的,作为连接过程中被链接器识别的标号,标志第一个app的开始位置
- .global app_1_end说明app_1_end是全局的,作为连接过程中被链接器识别的标号,标志第一个app的结束位置
- .incbin 是汇编语言中的一个伪指令,用于将二进制文件的原始内容直接嵌入到当前的汇编文件中
- 以.incbin "../user/target/riscv64gc-unknown-none-elf/release/04priv_csr.bin"为例,将编译出的04priv_csr.bin嵌入到当前的汇编文件之中
因此,我们如果要生成这个文件,需要在_num_app标号后的段中储存app的数目和app的开启指针,并且为每个app生成包含开始标号和结束标号的段,并且在其中引入app的二进制文件.
这里是~/App/rCore-Tutorial-v3/os/build.rs的内容:- use std::fs::{read_dir, File};
- use std::io::{Result, Write};
- fn main() {
- println!("cargo:rerun-if-changed=../user/src/");
- println!("cargo:rerun-if-changed={}", TARGET_PATH);
- insert_app_data().unwrap();
- }
- static TARGET_PATH: &str = "../user/target/riscv64gc-unknown-none-elf/release/";
- fn insert_app_data() -> Result<()> {
- let mut f = File::create("src/link_app.S").unwrap();
- let mut apps: Vec<_> = read_dir("../user/src/bin")
- .unwrap()
- .into_iter()
- .map(|dir_entry| {
- let mut name_with_ext = dir_entry.unwrap().file_name().into_string().unwrap();
- name_with_ext.drain(name_with_ext.find('.').unwrap()..name_with_ext.len());
- name_with_ext
- })
- .collect();
- apps.sort();
- writeln!(
- f,
- r#"
- .align 3
- .section .data
- .global _num_app
- _num_app:
- .quad {}"#,
- apps.len()
- )?;
- for i in 0..apps.len() {
- writeln!(f, r#" .quad app_{}_start"#, i)?;
- }
- writeln!(f, r#" .quad app_{}_end"#, apps.len() - 1)?;
- for (idx, app) in apps.iter().enumerate() {
- println!("app_{}: {}", idx, app);
- writeln!(
- f,
- r#"
- .section .data
- .global app_{0}_start
- .global app_{0}_end
- app_{0}_start:
- .incbin "{2}{1}.bin"
- app_{0}_end:"#,
- idx, app, TARGET_PATH
- )?;
- }
- Ok(())
- }
复制代码 这里的代码有一些需要注意的点:
- r#和#允许创建一个不解析转义序列的字符串,而且允许字符串跨越多行.
- Vec是向量,Vec是一个类型推断的占位符,Rust编译器会根据上下文自动推断出向量元素的类型.
- 这里有一些复杂,read_dir读取之后为了处理两种情况:Ok()和Error()这里要注意到Rust的枚举类型的特质,即一个结果有某几种可以被列举出来的不同类型,而其中包裹着结果,使用unwarp()来获取其中包裹的内容,如果正确执行,那么返回的是一个ReadDir迭代器,再通过into_iter转换成迭代器.
- map 接收一个闭包(也称为匿名函数),并将这个闭包应用于迭代器的每一个元素,产生一个新的迭代器,其元素是原元素经过闭包转换后的结果.因此在map中,分别完成把读取的文件类转化成文件名,并且删除其拓展名.
这样我看就可以轻松读懂这段代码,它通过读取TARGET_PATH这个常量文件夹下的无后缀文件名,并且将其设置成asm文件中的label的名字,从而最后实现生成了link_app.S文件.
但是这时候很容易产生一个疑问,就是关于为什么在进行make的时候会先执行这个模块?
这时候尝试去看Makefile,发现其中似乎没有提到关于build.rs的内容,考虑到这个模块的名字被命名为build也许是一个特殊设置,通过查询Cargo的执行流程.得知,Cargo会按照以下步骤处理build.rs:
- 检查依赖关系:Cargo首先检查项目的依赖树,确保所有依赖项都是最新的,并且已经下载了所有必要的源代码和预编译的二进制文件。
- 执行build.rs:如果项目根目录下存在build.rs文件,Cargo会在构建任何其他目标之前先运行它。build.rs应该包含有效的Rust代码,通常用于生成或修改将在实际构建过程中使用的源代码或元数据。Cargo会编译并运行build.rs中的代码,这可能包括创建额外的源文件、生成绑定到外部库的代码、修改Cargo.toml文件等。
- 构建目标:一旦build.rs执行完成,Cargo将继续正常的构建过程,编译项目中的Rust源代码,链接库和可执行文件,最终产生一个或多个输出文件,如.rlib库文件或.exe可执行文件。
这样我们就知道原理了.
找到并加载应用程序二进制码
实现一个能够 找到 并且 加载 应用程序二进制码的应用管理器AppManager,通过在os中实现一个batch子模块获得:
- 保存应用数量和各自的位置信息,以及当前执行到第几个应用了。
- 根据应用程序位置信息,初始化好应用所需内存空间,并加载应用执行。
看了这个功能感觉头上浮现出一个汗珠,感觉这个在功能上仍然和直接顺序执行裸机代码是一样的,但是可能后续引入分时操作和文件系统之后,这种可以寻找和加载App的模式才是正道.
定义一个应用管理器的结构体AppManager:- // os/src/batch.rs
- struct AppManager {
- num_app: usize,
- current_app: usize,
- app_start: [usize; MAX_APP_NUM + 1],
- }
复制代码 这里可以看到,这个结构体是一个保存app的数量和当前的app以及每个app的开头指针位置的结构体,通过查看官方文档,可以看出在设计时,打算实例化一个AppManager作为全局变量,从而可以然任何函数来直接访问,也就是一个开放的单例.这时候就要考虑Rust的变量的生命周期问题,也就是所有权问题.
这里存在一个问题,就是如果使用了static修饰实例化的AppManager,就会导致AppManager.current_app是不可变的,但是使用static mut来修饰实例化的AppManager,则会导致对它的操作是unsafe的.因此需要使用更好的方法来解决问题.
发现关于 Rust所有权模型和借用检查 的问题,发现在官方文档中有比Rust语言圣经(Rust Course)中更好的解释,更详细的部分一定要去看官方文档:
我们这里简单介绍一下 Rust 的所有权模型。它可以用一句话来概括: 值 (Value)在同一时间只能被绑定到一个 变量 (Variable)上。这里,“值”指的是储存在内存中固定位置,且格式属于某种特定类型的数据;而变量就是我们在 Rust 代码中通过 let 声明的局部变量或者函数的参数等,变量的类型与值的类型相匹配。在这种情况下,我们称值的 所有权 (Ownership)属于它被绑定到的变量,且变量可以作为访问/控制绑定到它上面的值的一个媒介。变量可以将它拥有的值的所有权转移给其他变量,或者当变量退出其作用域之后,它拥有的值也会被销毁,这意味着值占用的内存或其他资源会被回收。
对于Rust中的内部可变性涉及到了对于 可变借用 的 运行时借用检查 ,在官方文档也有表述:
相对的,对值的借用方式运行时可变的情况下,我们可以使用 Rust 内置的数据结构将借用检查推迟到运行时,这可以称为运行时借用检查,它的约束条件和编译期借用检查一致。当我们想要发起借用或终止借用时,只需调用对应数据结构提供的接口即可。值的借用状态会占用一部分额外内存,运行时还会有额外的代码对借用合法性进行检查,这是为满足借用方式的灵活性产生的必要开销。当无法通过借用检查时,将会产生一个不可恢复错误,导致程序打印错误信息并立即退出。具体来说,我们通常使用 RefCell 包裹可被借用的值,随后调用 borrow 和 borrow_mut 便可发起借用并获得一个对值的不可变/可变借用的标志,它们可以像引用一样使用。为了终止借用,我们只需手动销毁这些标志或者等待它们被自动销毁。 RefCell 的详细用法请参考 2 。
因此可以使用RefCell来实现Rust的 内部可变性 , 也即在变量自身不可变或仅在不可变借用的情况下仍能修改绑定到变量上的值.
那么当前能不能声明一个全局变量的RefCell呢?在workspace/homework下创建ref_cell工程:- cd ~/workspace/homework
- cargo new ref_cell
复制代码 在main.rs中键入如下代码:- use std::cell::RefCell;
- static A: RefCell<i32> = RefCell::new(3);
- fn main() {
- *A.borrow_mut() = 4;
- println!("{}", A.borrow());
- }
复制代码 运行这个工程:- cd ~/workspace/homework/ref_cell
- cargo run
复制代码 出现报错:- error[E0277]: `RefCell<i32>` cannot be shared between threads safely
- --> src/main.rs:2:11
- |
- 2 | static A: RefCell<i32> = RefCell::new(3);
- | ^^^^^^^^^^^^ `RefCell<i32>` cannot be shared between threads safely
- |
- = help: the trait `Sync` is not implemented for `RefCell<i32>`
- = note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` instead
- = note: shared static variables must have a type that implements `Sync`
- For more information about this error, try `rustc --explain E0277`.
- error: could not compile `ref_cell` (bin "ref_cell") due to 1 previous error
复制代码 根据提示,我们可以看出RefCell不能在线程间共享,需要为它实现一个Sync特性才可以.
这里官方文档提到了我们单纯循规蹈矩的时候不会想到的设计逻辑:
目前我们的内核仅支持单核,也就意味着只有单线程,那么我们可不可以使用局部变量来绕过这个错误呢?
很可惜,在这里和后面章节的很多场景中,有些变量无法作为局部变量使用。这是因为后面内核会并发执行多条控制流,这些控制流都会用到这些变量。如果我们最初将变量分配在某条控制流的栈上,那么我们就需要考虑如何将变量传递到其他控制流上,由于控制流的切换等操作并非常规的函数调用,我们很难将变量传递出去。因此最方便的做法是使用全局变量,这意味着在程序的任何地方均可随意访问它们,自然也包括这些控制流。
这里我们可以看到,如果我们在Rust中使用局部变量可以通过巧妙地设计变量的借用来解决问题.
那么可以 退一步 , 设计一个可以在单核上可以安全使用的可变全局变量.
在os/src下创建sync模块,而且使用文件夹的方式,这里刚好复习Rust的模块的另一种使用方式---寻找包名文件夹下的mod.rs:- cd /os/src
- mkdir sync
- touch up.rs
- touch mod.rs
复制代码 这里除了mod.rs以外还定义了up.rs.
先看up.rs内容:
[code]// os/src/sync/up.rspub struct UPSafeCell { /// inner data inner: RefCell,}unsafe impl Sync for UPSafeCell {}impl UPSafeCell { /// User is responsible to guarantee that inner struct is only used in /// uniprocessor. pub unsafe fn new(value: T) -> Self { Self { inner: RefCell::new(value) } } /// Panic if the data has been borrowed. pub fn exclusive_access(&self) -> RefMut |