Linux 操作系统分析课程总结

Linux 操作系统分析课程总结

收获与感想

身为一个 Linux 用户和一个 Linux 开发者的感受是完全不一样的, 虽然 Linux 用户相对于其他系统来说已经很贴近开发者了, 然而由于抽象的存在, 大量的细节就已经对我们隐藏了.

在本门课程中, 我得以重拾本科操作系统课中的知识, 并以 Linux 内核为例, 探寻操作系统的设计决策与实际实现.

rCore-Tutorial

作为课程实验的一部分, 我同步参加了操作系统大赛, 并进行了 rCore-Tutorial 的实验, 这是一个从零开始用 Rust 语言写一个基于 RISC-V 架构的 类 Unix 内核的实验项目, 作为清华大学本科操作系统课程的实验部分. 由于时间关系我到目前为止仅完成了一~四章. 下面我将简单介绍一下本项目, 并适当地描述一些细节.

一个抽象

操作系统直接建立在硬件之上吗? 在计算机启动时执行的第一条指令就是操作系统的吗? 显然并不是.

BIOS/UEFI 提供了对硬件最基础的抽象, 在本实验中, RustSBI 提供了输入输出, 启动引导等最基本的功能, 同时其运行在 M 特权级下, 具有对计算机的最高控制权. (详见特权级小节)

Hello World

Rust 作为一种设计目标是取代 C 语言的语言, 对于裸机平台的支持已经非常好了, 其对于主流架构都提供了编译目标的支持, 如在 riscv64gc 指令集下 (即 64 位 RISC-V 的基本整数指令集加上整数乘除法, 浮点数等扩展) 的目标三元组为 riscv64gc-unknown-none-elf. 与之对比常见的 x86-64 Linux 平台的目标三元组为 x86_64-unknown-linux-gnu, 即在裸机的基础上封装了 Linux 系统调用.

我们的内核在 qemu 上运行, 一个典型的配置是 -machine virt, 这个平台物理内存起始物理地址为 0x80000000. 在 qemu 启动时, PC 会被初始化为 0x1000, 经过寥寥数条指令便会跳转到为 0x80000000, 而这是 RustSBI 放置的地址, RustSBI 完成初始化之后便会跳转到约定的 0x80200000 处, 而这便是我们内核放置的位置.

通过手动修改链接脚本, 我们得以让我们的操作系统拥有正确的内存布局:

不幸的是, 我们无法使用高级语言完成所有的任务, 内核的第一条指令便是由汇编来完成:

    .section .text.entry
    .globl _start
_start:
    la sp, boot_stack_top
    call rust_main

    .section .bss.stack
    .globl boot_stack
boot_stack:
    .space 4096 * 16
    .globl boot_stack_top
boot_stack_top:

在声明了符号 _start 的地址放置在名为 .text.entry 的段中之后, 我们便可以指示编译器将这个段放到最低的地址上, 故这段指令能被最先执行.

我们执行的第一条指令是将栈指针 sp 设置为栈顶的位置, 即预先分配的 4096 * 16 字节的空间. 这是内核的启动栈, 有了栈空间函数调用才能够被正常执行. 接着我们调用 rust_main 函数, 并在其中打印 Hello, World:

#[no_mangle]
pub fn rust_main() -> ! {
    clear_bss();
    println!("Hello, world!");
    panic!("Shutdown machine!");
}

这其中固然还隐藏了诸多细节, 比如对 bss 段的初始化, 调用 RustSBI 的接口实现字符输出, 创建 println! 宏等, 但到这里, 我们的程序已经能够作为支持显示字符串的"操作系统"执行起来了.

特权级

为了给操作系统带来更高的安全性, 硬件必须提供一套相关的机制. 在 RISC-V 中, 这套机制就是特权级. 处理
器通常大部分时间都运行在权限最低的模式下, 处理中断和异常时会将控制权移交到更高权限的模式. RISC-V 共有三种特权级:

  • 机器模式 (Machine Mode, M-Mode) 运行最可信代码, 其最重要的特性是拦截和处理异常的能力.
  • 用户模式 (User Mode, U-Mode) 运行不可信代码
  • 监管者模式 (Supervisor Mode, S-Mode) 的权限介于 M 模式和 U 模式之间, 为现代操作系统提供了虚拟内存管理的功能.

S 模式最重要的功能便是提供系统调用 (即所谓的 Trap 机制), 为此我们需要控制状态寄存器 (CSR, Control and Status Register) 的配合. 用户态程序通过 ecall 指令触发系统调用那么处理器和操作系统会完成到内核态执行环境的切换,并在操作系统完成服务后,再次切换回用户态执行环境,然后应用程序会紧接着 ecall 指令的后一条指令位置处继续执行, 如下图所示:

为了实现 Trap, 我们必须切换相关的上下文. 简而言之, 我们需要切换用户栈与内核栈, 保存和恢复所有通用寄存器以及两个 CSR, 然后调用 Trap 处理程序. 在恢复上下文后, 使用 sret 切换回用户态.

操作系统的发展史

本项目很大程度上是按照操作系统的发展来编排的, 从实现一个批处理系统 (第二章), 到分时多任务 (第三章), 到虚拟内存的出现 (第四章), 再到进程和文件系统的出现 (第五六章), 这也能够帮助我们更好地理解操作系统中的一些设计决策.

在接下来的几章中, 我们实现了任务切换上下文, 基于 SV39 多级页表的地址空间, 以及支撑进程运行的数个系统调用. 对于这些内容这里就不再展开了.

参考资料

posted @ 2022-07-05 22:11  AAAAlice  阅读(206)  评论(0)    收藏  举报