diff --git a/source/chapter1/1app-ee-platform.rst b/source/chapter1/1app-ee-platform.rst index 5a37943..9658e68 100644 --- a/source/chapter1/1app-ee-platform.rst +++ b/source/chapter1/1app-ee-platform.rst @@ -70,24 +70,12 @@ os文件夹下存放了所有我们构建操作系统的源代码。也是本次 实际上,main.c之中众多的extern声明的内存段是在ld文件之中定义的。 等待lab1重做。 +- printf.c以及sbi.c + 在printf.c之中大家可以看到框架实现了一个printf函数以及(染色的功能)。在函数之中我们完成了对format字符串的解析工作。那么我们是如何把字符串真正地打印到shell上的呢?大家可以发现我们调用了一个consputc函数来输出字符,而consputc函数又调用了sbi.c之中的console_putchar函数。这个console_putchar函数的本质是调用了sbi_call。剥开层层套娃,大家可以发现打印的最终实现是使用sbi帮助我们包装好的ecall汇编代码,通过指定ecall的idx为SBI_CONSOLE_PUTCHAR(1),并将我们的字符做为参数传入到ecall指定的寄存器之中完成一次系统调用来实现的。这个过程十分重要,大家一定要理解。 + bootloader文件夹 ------------------------------- 这个文件夹是用来存放bootloader的bin文件的,这一章以及之后都无需我们做任何修改。 硬件加电之后是处于M态,而rustsbi帮助我们完成了M态的初始化,到os运行的S态的迁移,以及PC移动至我们os开始执行的位置。同时,它也帮助我们完成了异常和中断的委托。会帮助S态的os完成其发出的ecall请求等等。具体的细节就是大家这一章的练习之一。 - -user文件夹(本章还莫得,需移动至lab2) ---------------------------------------- - -大家马上就会发现我们并没有这样一个文件夹啊?实际上user文件夹是我们用于存放测例的文件夹。大家可以通过clone如下仓库的得到: - -.. code-block:: bash - - git clone https://github.com/DeathWish5/riscvos-c-tests.git - -之后直接将user文件夹复制到我们的项目之中就可以了。 - -我们的测例是通过cmake来编译的。具体的指令可以参见其中的readme。在使用测例的时候要注意,由于我们使用的是自己的os系统,因此所有常见的C库,比如stdlib.h,stdio.h等等都不能使用C官方的版本。这里在user的include和lib之中我们提供了搭配本次实验的对应库。大家可以看到,所有测例代码调用的函数都是使用的这里的代码。而这些库会依赖我们编写的os提供的系统调用(syscall)来运行。 - -user的库是如何调用到os的系统调用的呢?实际上转lab2. diff --git a/source/chapter2/0intro.rst b/source/chapter2/0intro.rst index bd1da19..8fd50b7 100644 --- a/source/chapter2/0intro.rst +++ b/source/chapter2/0intro.rst @@ -12,7 +12,7 @@ - 通过批处理支持多个程序的自动加载和运行 - 操作系统利用硬件特权级机制,实现对操作系统自身的保护 -上一章,我们在 RV64 裸机平台上成功运行起来了 ``Hello, world!`` 。看起来这个过程非常顺利,只需要一条命令就能全部完成。但实际上,在那个计算机刚刚诞生的年代,很多事情并不像我们想象的那么简单。 当时,程序被记录在打孔的卡片上,使用汇编语言甚至机器语言来编写。而稀缺且昂贵的计算机由专业的管理员负责操作,就和我们在上一章所做的事情一样,他们手动将卡片输入计算机,等待程序运行结束或者终止程序的运行。最后,他们从计算机的输出端——也就是打印机中取出程序的输出并交给正在休息室等待的程序提交者。 +上一章,我们在 RV64 裸机平台上成功运行起来了 ``Hello, world!``并成功实现了染色的过程 。看起来这个过程非常顺利,只需要一条命令就能全部完成。但实际上,在那个计算机刚刚诞生的年代,很多事情并不像我们想象的那么简单。 当时,程序被记录在打孔的卡片上,使用汇编语言甚至机器语言来编写。而稀缺且昂贵的计算机由专业的管理员负责操作,就和我们在上一章所做的事情一样,他们手动将卡片输入计算机,等待程序运行结束或者终止程序的运行。最后,他们从计算机的输出端——也就是打印机中取出程序的输出并交给正在休息室等待的程序提交者。 实际上,这样做是一种对于珍贵的计算资源的浪费。因为当时的计算机和今天的个人计算机不同,它的体积极其庞大,能够占满一整个空调房间,像巨大的史前生物。管理员在房间的各个地方跑来跑去、或是等待打印机的输出的这些时间段,计算机都并没有在工作。于是,人们希望计算机能够不间断的工作且专注于计算任务本身。 @@ -24,10 +24,7 @@ 程序总是难免出现错误。但人们希望一个程序的错误不要影响到操作系统本身,它只需要终止出错的程序,转而运行执行序列中的下一个程序即可。如果后面的程序都无法运行就太糟糕了。这种 *保护* 操作系统不受有意或无意出错的程序破坏的机制被称为 **特权级** (Privilege) 机制,它实现了用户态和内核态的隔离,需要软件和硬件的共同努力。 - -本章主要是设计和实现建立支持批处理系统的泥盆纪“邓式鱼”操作系统,从而对可支持运行一批应用程序的执行环境有一个全面和深入的理解。 - -本章我们的目标让泥盆纪“邓式鱼”操作系统能够感知多个应用程序的存在,并一个接一个地运行这些应用程序,当一个应用程序执行完毕后,会启动下一个应用程序,直到所有的应用程序都执行完毕。 +本章我们的主要目的也是设计一个批处理的操作系统。毕竟将待执行的程序嵌入main.c之中是十分粗暴的,也不符合我们对操作系统的认知。这同时也意味着我们将开始使用独立的测例文件,并把它们打包到os之中。 .. image:: deng-fish.png :align: center @@ -36,14 +33,12 @@ 实践体验 --------------------------- -本章我们的批处理系统将连续运行三个应用程序,放在 ``user/src/bin`` 目录下。 +本章我们的批处理系统将连续运行三个应用程序,放在 ``user/target/bin`` 目录下。 获取本章代码: .. code-block:: console - $ git clone https://github.com/rcore-os/rCore-Tutorial-v3.git - $ cd rCore-Tutorial-v3 $ git checkout ch2 在 qemu 模拟器上运行本章代码: @@ -99,65 +94,6 @@ [kernel] Application exited with code 0 [kernel] Panicked at src/batch.rs:61 All applications completed! -本章代码树 -------------------------------------------------- - -.. code-block:: - - ./os/src - Rust 10 Files 311 Lines - Assembly 2 Files 58 Lines - - ├── bootloader - │   ├── rustsbi-k210.bin - │   └── rustsbi-qemu.bin - ├── LICENSE - ├── os - │   ├── build.rs(新增:生成 link_app.S 将应用作为一个数据段链接到内核) - │   ├── Cargo.toml - │   ├── Makefile(修改:构建内核之前先构建应用) - │   └── src - │   ├── batch.rs(新增:实现了一个简单的批处理系统) - │   ├── console.rs - │   ├── entry.asm - │   ├── lang_items.rs - │   ├── link_app.S(构建产物,由 os/build.rs 输出) - │   ├── linker-k210.ld - │   ├── linker-qemu.ld - │   ├── main.rs(修改:主函数中需要初始化 Trap 处理并加载和执行应用) - │   ├── sbi.rs - │   ├── syscall(新增:系统调用子模块 syscall) - │   │   ├── fs.rs(包含文件 I/O 相关的 syscall) - │   │   ├── mod.rs(提供 syscall 方法根据 syscall ID 进行分发处理) - │   │   └── process.rs(包含任务处理相关的 syscall) - │   └── trap(新增:Trap 相关子模块 trap) - │   ├── context.rs(包含 Trap 上下文 TrapContext) - │   ├── mod.rs(包含 Trap 处理入口 trap_handler) - │   └── trap.S(包含 Trap 上下文保存与恢复的汇编代码) - ├── README.md - ├── rust-toolchain - ├── tools - │   ├── kflash.py - │   ├── LICENSE - │   ├── package.json - │   ├── README.rst - │   └── setup.py - └── user(新增:应用测例保存在 user 目录下) - ├── Cargo.toml - ├── Makefile - └── src - ├── bin(基于用户库 user_lib 开发的应用,每个应用放在一个源文件中) - │   ├── 00hello_world.rs - │   ├── 01store_fault.rs - │   └── 02power.rs - ├── console.rs - ├── lang_items.rs - ├── lib.rs(用户库 user_lib) - ├── linker.ld(应用的链接脚本) - └── syscall.rs(包含 syscall 方法生成实际用于系统调用的汇编指令, - 各个具体的 syscall 都是通过 syscall 来实现的) - - 本章代码导读 ----------------------------------------------------- @@ -167,11 +103,10 @@ 应用程序运行中,操作系统要支持应用程序的输出功能,并还能支持应用程序退出。这需要完成 ``sys_write`` 和 ``sys_exit`` 系统调用访问请求的实现。 具体实现涉及到内联汇编的编写,以及应用与操作系统内核之间系统调用的参数传递的约定。为了让应用在还没实现操作系统之前就能进行运行测试,我们采用了Linux on RISC-V64 的系统调用参数约定。具体实现可参看 :ref:`系统调用 ` 小节中的内容。 这样写完应用小例子后,就可以通过 ``qemu-riscv64`` 模拟器进行测试了。 -写完应用程序后,还需实现支持多个应用程序轮流启动运行的操作系统。这里首先能把本来相对松散的应用程序执行代码和操作系统执行代码连接在一起,便于 ``qemu-system-riscv64`` 模拟器一次性地加载二者到内存中,并让操作系统能够找到应用程序的位置。为把二者连在一起,需要对生成的应用程序进行改造,首先是把应用程序执行文件从ELF执行文件格式变成Binary格式(通过 ``rust-objcopy`` 可以轻松完成);然后这些Binary格式的文件通过编译器辅助脚本 ``os/build.rs`` 转变变成 ``os/src/link_app.S`` 这个汇编文件的一部分,并生成各个Binary应用的辅助信息,便于操作系统能够找到应用的位置。编译器会把把操作系统的源码和 ``os/src/link_app.S`` 合在一起,编译出操作系统+Binary应用的ELF执行文件,并进一步转变成Binary格式。 +写完应用程序后,还需实现支持多个应用程序轮流启动运行的操作系统。这里首先能把本来相对松散的应用程序执行代码和操作系统执行代码连接在一起,便于 ``qemu-system-riscv64`` 模拟器一次性地加载二者到内存中,并让操作系统能够找到应用程序的位置。为把二者连在一起,需要对生成的应用程序进行改造,首先是把应用程序执行文件从ELF执行文件格式变成Binary格式(通过 ``rust-objcopy`` 可以轻松完成);然后这些Binary格式的文件通过编译器辅助脚本 ``os/pack.py`` 生成 ``os/link_app.S`` 这个汇编文件,并生成各个Binary应用的辅助信息,便于操作系统能够找到应用的位置。同时,makefile也会调用另外一个脚本``os/kernellld.py``来生一个新的规定程序空间的kernel_app.ld取代之前的kernel.ld。编译器会把把操作系统的源码和 ``os/link_app.S`` 合在一起,编译出操作系统+Binary应用的ELF执行文件,并进一步转变成Binary格式。 -操作系统本身需要完成对Binary应用的位置查找,找到后(通过 ``os/src/link_app.S`` 中的变量和标号信息完成),会把Binary应用拷贝到 ``user/src/linker.ld`` 指定的物理内存位置(OS的加载应用功能)。在一个应执行完毕后,还能加载另外一个应用,这主要是通过 ``AppManagerInner`` 数据结构和对应的函数 ``load_app`` 和 ``run_next_app`` 等来完成对应用的一系列管理功能。 +操作系统本身需要完成对Binary应用的位置查找,找到后(通过 ``os/link_app.S`` 中的变量和标号信息完成),会把Binary应用拷贝到 ``os/kernel_app.ld`` 指定的物理内存位置(OS的加载应用功能)。 - -这主要在 :ref:`实现批处理操作系统 ` 小节中讲解。 +更加详细的内容,主要在 :ref:`实现批处理操作系统 ` 小节中讲解。 为了让Binary应用能够启动和运行,操作系统还需给Binary应用分配好执行环境所需一系列的资源。这主要包括设置好用户栈和内核栈(在应用在用户态和内核在内核态需要有各自的栈),实现Trap 上下文的保存与恢复(让应用能够在发出系统调用到内核态后,还能回到用户态继续执行),完成Trap 分发与处理等工作。由于涉及用户态与内核态之间的特权级切换细节的汇编代码,与硬件细节联系紧密,所以 :ref:`这部分内容 ` 是本章中理解比较困难的地方。如果要了解清楚,需要对涉及到的CSR寄存器的功能有清楚的认识。这就需要看看 `RISC-V手册 `_ 的第十章或更加详细的RISC-V的特权级规范文档了。有了上面的实现后,就剩下最后一步,实现 **执行应用程序** 的操作系统功能,其主要实现在 ``run_next_app`` 函数中 。 diff --git a/source/chapter2/2application.rst b/source/chapter2/2application.rst index a862808..378b3ae 100644 --- a/source/chapter2/2application.rst +++ b/source/chapter2/2application.rst @@ -1,4 +1,4 @@ -实现应用程序 +实现应用程序以及user文件夹 =========================== .. toctree:: @@ -15,6 +15,39 @@ 从某种程度上讲,这里设计的应用程序与第一章中的最小用户态执行环境有很多相同的地方。即设计一个应用程序,能够在用户态通过操作系统提供的服务完成自身的功能。 +user文件夹以及测例简介 +--------------------------------------- + +大家马上就会发现我们目前的代码之中并没有这样一个文件夹。实际上user文件夹是我们用于存放测例的文件夹。大家可以通过clone如下仓库的得到: + +.. code-block:: bash + + git clone https://github.com/DeathWish5/riscvos-c-tests.git + +之后直接将user文件夹复制到我们的项目之中就可以了。 + +本章的测例文件,实际就是批处理操作系统中一个个待执行的文件。下面我们看一个测例来理解本章以及之后测例的本质: + +.. code-block:: c + + // ch2_hello_world.c + #include + #include + + int main(void) + { + puts("Hello world from user mode program!\nTest hello_world OK!"); + return 0; + } + +这个测例编译出来实际上就是一个可执行的打印helloworld的程序。如果是windows或者linux上它编译之后是可以直接执行的。反过来说,它也可以用来检查我们操作系统的实现是否有问题。 + +我们的测例是通过cmake来编译的。具体编译出测例的指令可以参见其中的readme。在使用测例的时候要注意,由于我们使用的是自己的os系统,因此所有常见的C库,比如stdlib.h,stdio.h等等都不能使用C官方的版本。这里在user的include和lib之中我们提供了搭配本次实验的对应库,里面实现了所有测例所需要的函数。大家可以看到,所有测例代码调用的函数都是使用的这里的代码。而这些函数会依赖我们编写的os提供的系统调用(syscall)来完成运行。 + +user的库是如何调用到os的系统调用的呢?在user/lib/arch/riscv下的syscall_arch.h为我们包装好了使用riscv汇编调用系统调用ecall的函数接口。lib之中的syscall.c文件就是用这些包装好的函数来进行系统调用实现完整的函数功能。在第一章中大家已经了解了异常委托的机制。U态的ecall指令会转到S态,也就是我们编写的os来进行处理,这样整个逻辑就打通了:为了使得测例成功运行,我们必须实现处理对应ecall的函数。 + +那么现在我们还面临一个理解上的问题:就是测例文件在调用ecall的时候的细节:程序是如何完成特权级切换的?在ecall完毕回到U态的时候,程序又是如何恢复调用ecall之前的执行流并继续执行的呢?这里其实和汇编课程对于异常的处理是一样的,下面我们来复习一下。 + 应用程序设计 -----------------------------