3batch-system.rst 11.6 KB
Newer Older
chyyuu1972's avatar
chyyuu1972 已提交
1
实现批处理操作系统
Y
Yifan Wu 已提交
2 3 4 5
==============================

.. toctree::
   :hidden:
Y
Yifan Wu 已提交
6 7
   :maxdepth: 5

chyyuu1972's avatar
chyyuu1972 已提交
8 9 10 11
本节导读
-------------------------------

目前本章设计的批处理操作系统--泥盆纪“邓式鱼”操作系统,还没有文件/文件系统的机制与设计实现,所以还缺少一种类似文件系统那样的松耦合灵活放置应用程序和加载执行应用程序的机制。这就需要设计一种简洁的程序放置和加载方式,能够在批处理操作系统与应用程序之间建立联系的纽带。这主要包括两个方面:
chyyuu1972's avatar
chyyuu1972 已提交
12

chyyuu1972's avatar
chyyuu1972 已提交
13 14 15 16 17
- 静态编码:通过一定的编程技巧,把应用程序代码和批处理操作系统代码“绑定”在一起。
- 动态加载:基于静态编码留下的“绑定”信息,操作系统可以找到应用程序文件二进制代码的起始地址和长度,并能加载到内存中运行。

这里与硬件相关且比较困难的地方是如何让在内核态的批处理操作系统启动应用程序,且能让应用程序在用户态正常执行。本节会讲大致过程,而具体细节将放到下一节具体讲解。

Y
Yifan Wu 已提交
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
将应用程序链接到内核
--------------------------------------------

在本章中,我们把应用程序的二进制镜像文件作为内核的数据段链接到内核里面,因此内核需要知道内含的应用程序的数量和它们的位置,这样才能够在运行时
对它们进行管理并能够加载到物理内存。

在 ``os/src/main.rs`` 中能够找到这样一行:

.. code-block:: rust

    global_asm!(include_str!("link_app.S"));

这里我们引入了一段汇编代码 ``link_app.S`` ,它一开始并不存在,而是在构建的时候自动生成的。当我们使用 ``make run`` 让系统成功运行起来
之后,我们可以先来看一看里面的内容:

.. code-block:: asm
    :linenos:
    
    # os/src/link_app.S

38
        .align 3
Y
Yifan Wu 已提交
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
        .section .data
        .global _num_app
    _num_app:
        .quad 3
        .quad app_0_start
        .quad app_1_start
        .quad app_2_start
        .quad app_2_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:

可以看到第 13 行开始的三个数据段分别插入了三个应用程序的二进制镜像,并且各自有一对全局符号 ``app_*_start, app_*_end`` 指示它们的
开始和结束位置。而第 3 行开始的另一个数据段相当于一个 64 位整数数组。数组中的第一个元素表示应用程序的数量,后面则按照顺序放置每个应用
程序的起始地址,最后一个元素放置最后一个应用程序的结束位置。这样每个应用程序的位置都能从该数组中相邻两个元素中得知。这个数组所在的位置
同样也由全局符号 ``_num_app`` 所指示。

这个文件是在 ``cargo build`` 的时候,由脚本 ``os/build.rs`` 控制生成的。有兴趣的读者可以参考其代码。

76
找到并加载应用程序二进制码
chyyuu1972's avatar
chyyuu1972 已提交
77 78 79
-----------------------------------------------

能够找到并加载应用程序二进制码的应用管理器 ``AppManager`` 是“邓式鱼”操作系统的核心组件。我们在 ``os`` 的 ``batch`` 子模块中实现一个应用管理器,它的主要功能是:
80

chyyuu1972's avatar
chyyuu1972 已提交
81 82
- 保存应用数量和各自的位置信息,以及当前执行到第几个应用了。
- 根据应用程序位置信息,初始化好应用所需内存空间,并加载应用执行。
Y
Yifan Wu 已提交
83

chyyuu1972's avatar
chyyuu1972 已提交
84
应用管理器 ``AppManager`` 结构体定义
Y
Yifan Wu 已提交
85 86 87 88 89 90 91 92 93 94 95 96
如下:

.. code-block:: rust

    struct AppManager {
        inner: RefCell<AppManagerInner>,
    }
    struct AppManagerInner {
        num_app: usize,
        current_app: usize,
        app_start: [usize; MAX_APP_NUM + 1],
    }
Y
Yifan Wu 已提交
97
    unsafe impl Sync for AppManager {}
Y
Yifan Wu 已提交
98 99 100 101 102 103 104

这里我们可以看出,上面提到的应用管理器需要保存和维护的信息都在 ``AppManagerInner`` 里面,而结构体 ``AppManager`` 里面只是保存了
一个指向 ``AppManagerInner`` 的 ``RefCell`` 智能指针。这样设计的原因在于:我们希望将 ``AppManager`` 实例化为一个全局变量使得
任何函数都可以直接访问,但是里面的 ``current_app`` 字段表示当前执行到了第几个应用,它会在系统运行期间发生变化。因此在声明全局变量
的时候一种自然的方法是利用 ``static mut``。但是在 Rust 中,任何对于 ``static mut`` 变量的访问都是 unsafe 的,而我们要尽可能
减少 unsafe 的使用来更多的让编译器负责安全性检查。

Y
Yifan Wu 已提交
105 106 107 108 109 110 111 112 113 114
此外,为了让 ``AppManager`` 能被直接全局实例化,我们需要将其标记为 ``Sync`` 。

.. note::

    **为什么对于 static mut 的访问是 unsafe 的**

    **为什么要将 AppManager 标记为 Sync**

    可以参考附录A:Rust 快速入门的并发章节。

Y
Yifan Wu 已提交
115 116 117 118
.. _term-interior-mutability:

于是,我们利用 ``RefCell`` 来提供 **内部可变性** (Interior Mutability),
所谓的内部可变性就是指在我们只能拿到 ``AppManager`` 的不可变借用,意味着同样也只能
Y
Yifan Wu 已提交
119 120
拿到 ``AppManagerInner`` 的不可变借用的情况下依然可以修改 ``AppManagerInner`` 里面的字段。
使用 ``RefCell::borrow/RefCell::borrow_mut`` 分别可以拿到 ``RefCell`` 里面内容的不可变借用/可变借用, 
121
``RefCell`` 会在运行时维护当前它管理的对象的已有借用状态,并在访问对象时进行借用检查。于是 ``RefCell::borrow_mut`` 就是我们实现内部可变性的关键。
Y
Yifan Wu 已提交
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146

我们这样初始化 ``AppManager`` 的全局实例:

.. code-block:: rust

    lazy_static! {
        static ref APP_MANAGER: AppManager = AppManager {
            inner: RefCell::new({
                extern "C" { fn _num_app(); }
                let num_app_ptr = _num_app as usize as *const usize;
                let num_app = unsafe { num_app_ptr.read_volatile() };
                let mut app_start: [usize; MAX_APP_NUM + 1] = [0; MAX_APP_NUM + 1];
                let app_start_raw: &[usize] = unsafe {
                    core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1)
                };
                app_start[..=num_app].copy_from_slice(app_start_raw);
                AppManagerInner {
                    num_app,
                    current_app: 0,
                    app_start,
                }
            }),
        };
    }

147 148
初始化的逻辑很简单,就是找到 ``link_app.S`` 中提供的符号 ``_num_app`` ,并从这里开始解析出应用数量以及各个应用的开头地址。注意其中对于切片类型的使用能够很大程度上简化编程。

Y
Yifan Wu 已提交
149 150 151 152 153 154 155 156 157 158 159
这里我们使用了外部库 ``lazy_static`` 提供的 ``lazy_static!`` 宏。要引入这个外部库,我们需要加入依赖:

.. code-block:: toml

    # os/Cargo.toml

    [dependencies]
    lazy_static = { version = "1.4.0", features = ["spin_no_std"] }

``lazy_static!`` 宏提供了全局变量的运行时初始化功能。一般情况下,全局变量必须在编译期设置一个初始值,但是有些全局变量依赖于运行期间
才能得到的数据作为初始值。这导致这些全局变量需要在运行时发生变化,也即重新设置初始值之后才能使用。如果我们手动实现的话有诸多不便之处,
160 161 162
比如需要把这种全局变量声明为 ``static mut`` 并衍生出很多 unsafe code。这种情况下我们可以使用 ``lazy_static!`` 宏来帮助我们解决
这个问题。这里我们借助 ``lazy_static!`` 声明了一个 ``AppManager`` 结构的名为 ``APP_MANAGER`` 的全局实例,且只有在它第一次被使用到
的时候才会进行实际的初始化工作。
Y
Yifan Wu 已提交
163

164
因此,借助 Rust 核心库提供的 ``RefCell`` 和外部库 ``lazy_static!``,我们就能在避免 ``static mut`` 声明的情况下以更加优雅的Rust风格使用全局变量。
Y
Yifan Wu 已提交
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192

``AppManagerInner`` 的方法中, ``print_app_info/get_current_app/move_to_next_app`` 都相当简单直接,需要说明的是 ``load_app``:

.. code-block:: rust
    :linenos:

    unsafe fn load_app(&self, app_id: usize) {
        if app_id >= self.num_app {
            panic!("All applications completed!");
        }
        println!("[kernel] Loading app_{}", app_id);
        // clear icache
        llvm_asm!("fence.i" :::: "volatile");
        // clear app area
        (APP_BASE_ADDRESS..APP_BASE_ADDRESS + APP_SIZE_LIMIT).for_each(|addr| {
            (addr as *mut u8).write_volatile(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);
    }

chyyuu1972's avatar
chyyuu1972 已提交
193
这个方法负责将参数 ``app_id`` 对应的应用程序的二进制镜像加载到物理内存以 ``0x80400000`` 开头的位置,这个位置是批处理操作系统和应用程序
194 195 196
之间约定的常数地址,回忆上一小节中,我们也调整应用程序的内存布局以同一个地址开头。第 8 行开始,我们首先将一块内存清空,然后找到待加载应用
二进制镜像的位置,并将它复制到正确的位置。它本质上是把数据从一块内存复制到另一块内存,从批处理操作系统的角度来看是将它数据段的一部分复制到了它
程序之外未知的地方。在这一点上也体现了冯诺依曼计算机的 ``代码即数据`` 的特征。
Y
Yifan Wu 已提交
197

198 199 200
.. _term-dcache:
.. _term-icache:

Y
Yifan Wu 已提交
201 202 203 204 205 206 207
注意第 7 行我们插入了一条奇怪的汇编指令 ``fence.i`` ,它是用来清理 i-cache 的。我们知道缓存是存储层级结构中提高访存速度的很重要一环。
而 CPU 对物理内存所做的缓存又分成 **数据缓存** (d-cache) 和 **指令缓存** (i-cache) 两部分,分别在 CPU 访存和取指的时候使用。在取指
的时候,对于一个指令地址, CPU 会先去 i-cache 里面看一下它是否在某个已缓存的缓存行内,如果在的话它就会直接从高速缓存中拿到指令而不是通过
总线和内存通信。通常情况下, CPU 会认为程序的代码段不会发生变化,因此 i-cache 是一种只读缓存。但在这里,我们会修改会被 CPU 取指的内存
区域,这会使得 i-cache 中含有与内存中不一致的内容。因此我们这里必须使用 ``fence.i`` 指令手动清空 i-cache ,让里面所有的内容全部失效,
才能够保证正确性。 

Y
Yifan Wu 已提交
208 209 210 211 212 213 214
.. warning:: 

   **模拟器与真机的不同之处**

   至少在 Qemu 模拟器的默认配置下,各类缓存如 i-cache/d-cache/TLB 都处于机制不完全甚至完全不存在的状态。目前在 Qemu 平台上,即使我们
   不加上刷新 i-cache 的指令,大概率也是能够正常运行的。但在 K210 真机上就会看到错误。

Y
Yifan Wu 已提交
215 216 217
``batch`` 子模块对外暴露出如下接口:

- ``init`` :调用 ``print_app_info`` 的时候第一次用到了全局变量 ``APP_MANAGER`` ,它也是在这个时候完成初始化;
chyyuu1972's avatar
chyyuu1972 已提交
218
- ``run_next_app`` :批处理操作系统的核心操作,即加载并运行下一个应用程序。当批处理操作系统完成初始化或者一个应用程序运行结束或出错之后会调用
Y
Yifan Wu 已提交
219
  该函数。我们下节再介绍其具体实现。