0%

2024年开源操作系统训练营第一阶段学习总结-卓堂越

首先,我要衷心感谢所有给予我帮助助教导师。每次我有困惑的时候他们的专业知识和耐心指导帮我解决一系列棘手的问题。通过这个平台可以很好的交到对这方面感兴趣的朋友,在群里跟朋友们讨论让我可以更好地理解和应用 Rust 语法。此外,也离不开Rust 社区分享的宝贵的学习资料。同时课堂上分享的The Rust Reference 不失为一本经典巨作给我前期的语法学习提供很有力的帮助。
在开始开发操作系统之前,先熟悉 Rust 语言的基础知识,包括语法、类型系统、所有权和生命周期等概念为首要目标。选择Rust是因为他的安全性和性能使其成为编写操作系统的理想选择。在正式做练习之前花了很多时间在环境配置上面,之前学过的harmony os 都是他们给的镜像直接导入,第一次在上面搭建问了很多导师问题,他们都耐心的回答我,让我在这方面更有兴趣。比如在当推送本地更改时,如果远程仓库已经有了更新,可能会出现合并冲突,需要手动解决这些冲突。通过这些学习能更好的完成后面几个阶段的学习。
和其他语言对比在生命周期这个方面Rust 强制使用生命周期来检查引用的有效性,以避免悬空引用和数据竞争。生命周期注解使得 Rust 的借用系统变得更加严格和安全。C++ 中也有生命周期的概念,但它通常通过使用智能指针、RAII(资源获取即初始化)等技术。相比rust在这方面更不容易出错。而且在所有权上面Rust 引入了所有权和借用的概念,这是与 C++ 最明显的区别之一。在 Rust 中,每个值都有一个所有者,并且在任何时候只能有一个可变借用或任意数量的不可变借用。这确保了在编译时不会出现数据竞争。在以前学过的 C++ 中,没有类似的所有权和借用系统,开发人员需要手动管理内存和资源,这可能导致内存泄漏、悬空指针和其他常见的错误

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let result;
{
let x = 5;
result = compute(&x);
}
println!("Result: {}", result);
}

fn compute<'a>(value: &'a i32) -> &'a i32 {
value
}

有一个 main 函数,它调用了一个 compute 函数来计算一个整数的引用,并将结果赋值给 result 变量。然后我们尝试在 main 函数中打印 result 的值。然而,compute 函数返回了一个指向局部变量 x 的引用,而 x 的生命周期只在包含它的代码块内有效。由于 x 在 compute 函数返回后就会被销毁,所以返回的引用将指向无效的内存,导致悬垂引用错误。后期我发现需要通过修改函数签名来指定一个更长的生命周期,或者避免返回对局部变量的引用

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let result;
{
let x = 5;
result = compute(x);
}
println!("Result: {}", result);
}

fn compute(value: i32) -> i32 {
value
}

通过上面代码的修改这样就避免了使用引用并返回了整数值本身,从而避免了悬垂引用错误。

第二部分学习总结

实验是按照rCore-Tutorial-Book-v3来完成的
环境搭建我是用了VMware 安装虚拟机在文档提供的直接使用

lab1

这个任务主要是为了获取任务信息,通过引入新的系统调用 sys_task_info,操作系统增强了对任务的监控和管理能力。现在,系统管理员可以根据任务 ID 查询到任务的详细信息,包括当前任务的状态、已经执行的系统调用及其调用次数,以及任务的总运行时长。这个功能使得系统能够更加细致地了解任务的运行情况,有助于优化系统资源的分配,提高系统的整体性能和稳定性。同时,对于开发人员来说,这也提供了一个更好的调试工具,可以更方便地跟踪和分析任务的行为,帮助定位和解决潜在的问题。

lab2

sys_get_time 函数的重写,我们重新设计了获取系统时间的逻辑,使其能够适应新的系统架构。这样,我们成功恢复了获取系统时间的功能,确保系统在引入新特性的同时不影响原有功能的正常运行。sys_mmap 和 sys_munmap 系统调用的实现,则为系统增加了动态内存管理的功能。通过 sys_mmap 系统调用,我们可以在虚存中映射一段指定长度的物理内存,为进程提供所需的内存空间。而 sys_munmap 则提供了取消内存映射的功能,帮助释放不再需要的内存空间,有效管理系统资源。

lab3

实现了一个完全 DIY 的系统调用 sys_spawn,用以创建一个新进程。与传统的 fork 和 execve 组合不同,sys_spawn 系统调用直接创建一个新的子进程,并使其执行指定的目标程序。这种方式更加直接,省去了复制父进程地址空间的步骤,从而提高了效率。在实现 sys_spawn 系统调用的过程中,我们需要处理一些可能的错误情况,比如无效的文件名或者系统资源不足导致的错误。通过正确处理这些错误,我们可以保证系统的稳定性和可靠性。通过这次实践作业,我们深入理解了进程创建的原理和实现方式,加深了对操作系统内核的理解。同时,也提高了我们对系统调用的理解和实现能力。

lab4

在本次实践中,我们要用到三个系统调用:sys_linkat、sys_unlinkat 和 sys_stat,分别用于创建硬链接、取消链接以及查询文件状态。通过这些系统调用,我们可以实现文件系统中文件的硬链接操作,并获取文件的状态信息,从而提高文件管理的便利性和灵活性。

lab5

本次实验旨在实现一个新的系统调用 sys_eventfd2,用于在 Linux 系统中创建 eventfd,其功能是创建一个带有计数器的文件描述符,用于进程间事件通知。通过参数解析和错误处理,我们成功实现了该系统调用,并能够根据传入的参数创建相应的 eventfd,从而为进程间通信提供了一种有效的机制。

这次实验为我们提供了一个很好的机会,加深了对操作系统的理解,并提高了我们的系统编程能力。

2024春夏季开源操作系统训练营Blog-第二阶段-罗健峰

开头

对于操作系统这门课上,我是比较薄弱的,就目前在写此总结之余,想起来我还是感觉有些吃力,主要的困点和难点在于我在训练营开始学习之前,学习过操作系统但没有这么深入,仅仅停留在简单的进程、线程和一些简单的调度算法,再一个就是我本身的学习和工作都在应用层,CPU指令集、硬件这一块的理解还是不太深入。但总体来讲我还是比较高兴自己能够坚持下来,回首一看,真是不容易,要补的,要学习的还是非常多。我接下来的打算就是反复细致的再回首,好好吃透rcore,在rcore的基础上想实现一些其他的功能,同时迎接第三阶段的学习。

好了,回归正题,我将以各章所遇到的情况分别总结。

第一章

第一章的代码树比较简单,主要的内容还是让同学们对操作系统有个大致的启动流程。

  1. qemu来模拟基于risc-v指令集的裸机环境。
  2. qemu的启动地址设置为rustsbi的地址来引导操作系统的入口函数。同时接触到第一次risc-v汇编语言(entry.asm)。
  3. 剥离rust基于特定操作系统的用户标准库。
  4. 一些OS的基础库(日志、标准输出、sbi、异常处理、内存布局等等)

最后运行裸机环境下的os,输出hello world!,此时的os其实不叫os,可能更像是一个库函数。整个计算机的资源都为它一个程序服务。

第一章还是比较简单,没有遇到太大的问题。

第二章 批处理系统

第二章的主要内容是risc-v指令集的特权级机制。

由于第一章是运行单个程序、更多的是一个函数库的使用。

第二章在基于特权级机制下,我们将多个程序一起打包在一起,在第一个程序执行完后,就会自动的执行下一个程序,但要注意的是目前执行的程序都不是全部加载到内存里面的,而是在我们约定好的一个具体的物理地址上,同时特权级机制保障了程序在运行过程中发生错误时,能够触发异常陷入trap进而从用户态转入到内核态去执行和处理异常,进而保证程序错误不会影响接下来的程序运行。

第二章也比较好理解和简单。

第三章 多道程序与分时任务

第三章在第二章的基础上,从一个一个加载到固定内存地址去执行,到一次性全部加载到内存里执行,减少任务切换的开销,同时可以并发的执行多个用户程序。

最重要的是实现了yield系统调用来主动放弃处理器的执行权,

进而可以让OS来调度一个任务的执行还是强制打断,其中打断需要保存上下文,在下一个时间片时恢复原先程序的上下文状态,采用时间片轮转算法调度用户程序。

同时引申出了其他的调度算法,也是一个比较值得研究的地方。后面有时间了还是想看看windows和linux内核的调度算法,学习学习。

第四章 地址空间

由于前面的OS,应用程序都是运行在真实的物理内存上的,任务多了,给应用程序员带来了很大的麻烦,他们在写程序的时候得仔细考虑一下应用该在哪个内存区间内执行,不会影响其他程序和自身程序的运行,再一个就是操作系统有义务来管理程序的运行,和减少上层应用程序员的心智负担,让他们不需要考虑程序的具体运行和内存的分布。

所以说我们引入了一个新的抽象层,地址空间。

实现地址空间不单单是os来做的,在risc-v指令集的硬件设计上也有实现,主要的参与者是MMU,其中stap寄存器是来开启risc-v指令集的分页机制的

通过设置stap寄存器来开启,目前OS实现的是SV39分页机制。

基于SV39的分页机制,就可以通过 va -> vpn -> ppn -> pa的关系,从虚拟地址转换到物理地址。

其中os维护了这一映射关系,在代码中的体现是PageTable和相关的工具函数。

注:
运行时,内核要运行在内核的自己的地址空间中,同时它还得需要知道具体的物理内存地址,能读取到应用中的数据。我们引入了跳板机制。

在每一个程序的最高位地址空间中都注入了跳板,以达到操作系统能读取到应用程序的方便,同时补上risc-v寄存器不够存储上下文的情况。

第五章 进程及进程管理

在第四章地址空间的基础上,我们把前面所谓的任务,升级为进程,其实本质上来讲就是进程。

我们对任务建立新的抽象:进程

引入进程相关的状态。

我们引入了进程相关的内容,整个项目的进程管理是一颗树,同时实现了一些进程相关的系统调用,forkexecwaitpid 等。

这颗进程管理树,是通过用户初始化进程展开的,这个进程是被内置在我们操作系统中的,其他的进程皆可以通过fork + exec 创建和执行。

同时实现了新的stride调度算法,同时设置相关的进程优先级。

这一章的内容是比较多的,当时我在学习的时候还是废了老大劲了,目前准备在写完总结之后再仔细研究研究,准备换个新的进程调度算法。

第六章 文件系统与I/O重定向

这一章,我还没太细致的研究,但总体来讲主要是要明白文件系统的管理方式,其中目录和文件的区分。

easy-fs的磁盘布局

sb | bitmap | diskInode | 数据块位图 | 数据块区域 DataBlock

第七章 进程间通讯

  1. 主要是进程与管道。
  2. 一些进程之间的通讯方式,通过管道

后面的想法是通过消息队列来实现进程之间的通讯,目前有一些语言的同步方式就是使用的消息队列。

第八章 并发

没有和前面的系统前向兼容,主要是为os实现锁机制和检测死锁,以及如何恢复,解开锁。

一些死锁算法。

总结

还是非常感谢开源操作系统社区这个平台,文档V3和2024s写的非常好和细致,在整个阅读下来非常舒服,同时也学到了很多的新东西。RAII的设计模式有了更加深刻的体会,也学会了qemu、gdb、make等工具的使用,加深了对rust语言的学习。

最后,非常感谢!

第二阶段总结

在第二阶段的学习过程中,是我对进程的概念有了更加深刻的认识,之前对进程和线程的认知还是比较模糊,且之前没有看过多线程的源码,一直不太能理解进程和线程的本质区别,而这次经过rCore的学习,让我从源码级别真正触碰到了进程和线程,让我对这个概念的理解更加深刻。同时在学习过程中也加强了我的调试能力,特别是对gdb的使用,还记得做lab4的时候因为一个bug调试了几天,虽然最后也没能调试出来换了别的方案实现,但是还是让我受益匪浅。同时我对RAII的这种设计模式也有了更加深刻的体会,我以前也用c写过x86的内核,但这次用rust来写内核给我的体验是完全不一样的,这种RAII的设计模式让我感觉我在用写开发项目的方式来写操作系统,比用c写要方便许多。

5.1放假结束后才有时间开始做,两个礼拜的时间每天只要没有事情,就是肝lab,虽然学习过程感觉很艰辛,但是每次看到lab测试全部通过就会感到很兴奋。

这是第一次学习rust并用rust实现需求,很多写法思路都是照着rcore代码实现的,感觉从里面学到了很多,对RAII的思想了解的更加深刻了,体会到了它的魅力

在做lab的过程中,逐渐学习如何对代码结构进行优化,虽然现在代码写的还是一坨,但是有努力在学习好的想法。希望能在未来的学习中进一步规范自己的代码规范,进一步学习如何写出优雅的代码。

lab5是最让人感觉没头绪的实验,但是我选择了先实现能实现的,在实践过程中我发现这样的方法能让我更容易克服困难,但是在完成后我发现问题,这样往往会导致一些结构上的设计不够完美。所以最好的选择估计是完成后,在对整体进行优化,重构所写的功能。

在参加项目之前,我还没有感受到阅读源码的魅力,在学习过程中,在读代码的过程中发现能让我对知识有更好的理解,有时候光看手册,会觉得非常的枯燥,很那理解,当结合编译器对代码一个个进行跳转查看的时候,我发现理解起来好了很多。阅读代码也能让我学习更多好的代码桂发,写出一些比较好的代码。

一阶段

起源

之前几次训练营我没赶上, 这次过年后就关注了.

Rust

目前实习工作是用 rust 的算懂一点, 不过马上离职了.
一阶段的写链表还是有意思的哈哈哈

二阶段

收获

老哥们在二阶段刚刚开始的时候通宵冲榜太猛了.
个人感觉 lab4 的复杂度是最高的, 主要是在 block cache 这一块, 如果是萌新确实理解起来难度比较大.
20 年的时候我做过 xv6-riscv 的实验, COW, lazy fork 之类的实验都被放在了选做题部分, 但是选做我都是不做的.
rCore对我帮助最大的是从零开始怎么写一个 kernel, 而 xv6 是在一个 kernel 上加 feature .
希望之后 rCore 能够加入其他的架构, 我感觉 riscv 是可以自学的, 比如从 61c 开始, 但是x86真得有老师指导, 实模式,保护模式,长模式, apic, aipc, multiboot, 人已经晕了.
还希望有个微内核版本.

感谢清华大学, 感谢 rCore 社区

一阶段学习参考


在学习os开发的时候如何使我们能够愉快的开始学习,而不在配置环境折腾的时间太长,以下是我的实践。

我的系统是windows11,上边安装了dockerdesktop.

1. docker 容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 检出源码

git clone git://github.com/rcore-os/rCore-Tutorial-v3

# 切换到源码目录,我把源码检出到 本机以下目录

cd D:\work20220906\gitee\rusttest\rCore-Tutorial-v3

# 制作 docker 镜像
# 这里我们把镜像的名称设置为rcore
docker build -t rcore --target build .

# 运行docker dev container
# 为了能够在容器内看到源码,需要把包含源码的目录 隐射到容器内部
# 我为了方便 把源码的上层目录隐射到了容器内部 /mnt
docker run --name os1 --hostname os1 -v D:/work20220906/gitee/rusttest/:/mnt -w /mnt -d rcore sleep infinity
# docker rm -f os1


# 在cmd 中, 进入容器ssh
docker exec -it os1 /bin/bash

# 测试运行
# 切换到目录
cd /mnt/rCore-Tutorial-v3/os
运行
make run

docker 镜像

开发容器

2. dev container 开发环境

2.1 安装vscode 及其插件

为了支持docker dev container 需要安装以下插件

点击以下红色标识位置连接到dev container

然后打开/mnt/ 目录下的项目文件夹

2.2 安装调试插件

3. 在vscode 中支持远程调试

3.1 在容器os1 内编译 gdb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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
76
77
78
79
80
81
82
83
84
85
86
87
```shell
#1. 进入docker
docker exec -it os1 /bin/bash
#2. 安装终端复用工具
apt install tmux
apt-get install libncurses5-dev texinfo libreadline-dev
# 在 configure 发现少了2个库,补上
apt-get install libgmp-dev libmpfr-dev
# 缺少python-dev,没有也可
apt install python3.8-dev

# 好像docker 环境已经预装了python3.8
#apt-get install python3
#which python3

#ll $(which python3)

# 建立符号连接 到/usr/bin/python
#ln -s /usr/bin/python3.8* /usr/bin/python
#root@os1:/usr/bin# which python
#/usr/bin/python

#whereis python3

#python3 --version
#Python 3.8.10

#4. 下载gdb
wget https://mirrors.tuna.tsinghua.edu.cn/gnu/gdb/gdb-14.2.tar.xz
#解压
tar -xvf gdb-14.2.tar.xz
# 进入目录
cd gdb-
14.2

#查看当前目录 pwd
/mnt/gdb-14.2
# 创建编译目录
mkdir build-riscv64

cd build-riscv64



#5. 配置编译选项,可以不配置with-python
../configure --prefix=/mnt/gdb-14.2/build-riscv64 --with-python=/usr/bin/python3 --target=riscv64-unknown-elf --enable-tui=yes

# 6.编译
make -j$(nproc)
# 7.安装
make install

# 8. 编译好的 GDB 存放在 build-riscv64/bin/ 目录下,你可以只保留这个目录,然后添加这个目录到环境变量。
# 确认 GDB 可以运行
./bin/riscv64-unknown-elf-gdb --version


root@os1:/mnt/gdb-14.2/build-riscv64# ./bin/riscv64-unknown-elf-gdb --version
GNU gdb (GDB) 14.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
root@os1:/mnt/gdb-14.2/build-riscv64# ll ./bin/
total 238428
drwxr-xr-x 1 root root 4096 May 13 03:10 ./
drwxr-xr-x 1 root root 4096 May 13 03:10 ../
-rwxr-xr-x 1 root root 232530576 May 13 03:10 riscv64-unknown-elf-gdb*
-rwxr-xr-x 1 root root 4627 May 13 03:10 riscv64-unknown-elf-gdb-add-index*
-rwxr-xr-x 1 root root 11605272 May 13 03:10 riscv64-unknown-elf-run*




# 我们直接拷贝到/usr/local/bin 目录,这样直接可以全局使用
ll /usr/local/bin

cp /mnt/gdb-14.2/build-riscv64/bin/* /usr/local/bin


# 9. 安装 gdb-dashboard:仅仅是下载一个 python 文件到 ~/.gdbinit 来做 gdb 的启动拓展
wget -P ~ https://github.com/cyrus-and/gdb-dashboard/raw/master/.gdbinit
# 以下是 gdbinit 文件的存放目录
root@os1:~# pwd
/root
root@os1:~# ll -la
-rw-r--r-- 1 root root 93928 May 13 03:15 .gdbinit

3.2 使编译的os文件支持调试信息

以rcore ch3 为例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#1. os/Makefile 文件中 修改和添加以下内容
# MODE := release 保证是debug编译 会保留符号信息
MODE := debug
# 包装gdb 命令否则rustc源码无法对应
GDB_PATH := riscv64-unknown-elf-gdb
gdb := RUST_GDB=$(GDB_PATH) rust-gdb

debug: build
@tmux new-session -d \
"qemu-system-riscv64 -machine virt -nographic -bios $(BOOTLOADER) -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) -s -S" && \
tmux split-window -h "$(gdb) -ex 'file $(KERNEL_ELF)' -ex 'set arch riscv:rv64' -ex 'target remote localhost:1234'" && \
tmux -2 attach-session -d

#2. user/Makefile
# MODE := release 保证编译debug
MODE := debug


#3. user/build.py

#mode = os.getenv("MODE", default = "release")
mode = os.getenv("MODE", default = "debug")

# 4.user/src/linker.ld 文件中
/DISCARD/ : {
*(.eh_frame)
/* *(.debug*) */ 注释掉这行,不删除调试信息

用以下命令确定os文件支持调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
root@os1:/mnt/2024s-rcore-xuejianxinokok/os/target/riscv64gc-unknown-none-elf/debug# file os
os: ELF 64-bit LSB executable, UCB RISC-V, version 1 (SYSV),
statically linked,

with debug_info, not stripped <<<<<<< 注意是这行



# 或者直接读取section 信息,发现有debug_info 这些section
root@os1:/mnt/2024s-rcore-xuejianxinokok/os/target/riscv64gc-unknown-none-elf/debug# readelf -SW os
There are 18 section headers, starting at offset 0x2952a8:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 0000000080200000 001000 00a340 00 AX 0 0 4
[ 2] .rodata PROGBITS 000000008020b000 00c000 0334ab 00 AM 0 0 4096
[ 3] .data PROGBITS 000000008023f000 040000 028920 00 WA 0 0 8
[ 4] .bss NOBITS 0000000080268000 068920 038660 00 WA 0 0 8
[ 5] .debug_abbrev PROGBITS 0000000000000000 068920 006a82 00 0 0 1
[ 6] .debug_info PROGBITS 0000000000000000 06f3a2 073718 00 0 0 1
[ 7] .debug_aranges PROGBITS 0000000000000000 0e2aba 0061b0 00 0 0 1
[ 8] .debug_str PROGBITS 0000000000000000 0e8c6a 08d4ab 01 MS 0 0 1
[ 9] .comment PROGBITS 0000000000000000 176115 000048 01 MS 0 0 1
[10] .riscv.attributes RISCV_ATTRIBUTES 0000000000000000 17615d 00003e 00 0 0 1
[11] .debug_frame PROGBITS 0000000000000000 1761a0 007f08 00 0 0 8
[12] .debug_line PROGBITS 0000000000000000 17e0a8 040627 00 0 0 1
[13] .debug_ranges PROGBITS 0000000000000000 1be6cf 033b00 00 0 0 1
[14] .debug_loc PROGBITS 0000000000000000 1f21cf 000b72 00 0 0 1
[15] .symtab SYMTAB 0000000000000000 1f2d48 096048 18 17 25465 8
[16] .shstrtab STRTAB 0000000000000000 288d90 0000b5 00 0 0 1
[17] .strtab STRTAB 0000000000000000 288e45 00c462 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
p (processor specific)

这时我们就可以在 容器内的命令行进行调试了,但这样还是不太方便

进入gbd 后

先回车后 ,然后再输入 b rust_main

1
2
b rust_main 
Breakpoint 1 at 0x8020618a: file src/main.rs, line 98.

再输入 c

3.3 配置vscode 调试

虽然我们就可以在命令行进行调试了,但这样还是不太方便,我们接着配置vscode中的调试

按F5 启动调试添加 .vscode/launch.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"version": "0.2.0",
"configurations": [
{
"name": "gdb Remote Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceRoot}/os/target/riscv64gc-unknown-none-elf/debug/os",
"args": [],
"stopAtEntry": true,
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"miDebuggerPath": "riscv64-unknown-elf-gdb",
"miDebuggerServerAddress": "localhost:1234",
"miDebuggerArgs": "gdb",

"setupCommands": [
{
"text": "set arch riscv:rv64",
"ignoreFailures": true
},
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"cwd": "${workspaceFolder}",
}
]
}

在另一个终端窗口启动 make gdbserver

在vscode 启动调试

为了每次按F5 时能够自动打开gbdserver

需要在 .vscode/launch.json配置一个preLaunchTask

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
"version": "0.2.0",
"configurations": [
{
"name": "gdb Remote Launch",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceRoot}/os/target/riscv64gc-unknown-none-elf/debug/os",
"args": [],
// 在入口处停止
"stopAtEntry": true,
"environment": [],
"externalConsole": true,
// 调试会话开始前执行的任务,一般为编译程序。与tasks.json的label相对应
// 参考 https://blog.csdn.net/BlizCp/article/details/111054747
"preLaunchTask": "startGdbserverTask",
"MIMode": "gdb",
//调试器路径,Windows下后缀不能省略,Linux下则去掉
"miDebuggerPath": "riscv64-unknown-elf-gdb",
"miDebuggerServerAddress": "localhost:1234",
"miDebuggerArgs": "gdb",

"setupCommands": [
{
"text": "set arch riscv:rv64",
"ignoreFailures": true
},
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"cwd": "${workspaceFolder}",
}
]
}

在.vscode/tasks.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"version": "2.0.0",
"tasks": [
{
"type": "shell",
"label": "startGdbserverTask",
"command": "nohup",
"args": [
"make",
"gdbserver",
"-w",

],
"options": {
"cwd": "${workspaceRoot}/os"
},
"hide": true
}
]

}

为了能使 gdbserver在后台运行,os/Makefile,在命令结尾-s -S 后边添加了 & ,否则阻塞client启动

1
2
gdbserver: build
@qemu-system-riscv64 -machine virt -nographic -bios $(BOOTLOADER) -device loader,file=$(KERNEL_BIN),addr=$(KERNEL_ENTRY_PA) -s -S &

有时候gdbserver 没有被杀死导致启动不了,需要找到进程然后手动kill

1
2
3
4
root@os1:/mnt/2024s-rcore-xuejianxinokok/os# lsof -i:1234
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
qemu-syst 5049 root 9u IPv4 8134403 0t0 TCP *:1234 (LISTEN)
qemu-syst 5049 root 10u IPv6 8134404 0t0 TCP *:1234 (LISTEN)

这样就可以愉快的调试了。

路漫漫…

感谢训练营的老师们!!!


4.参考文档:

本阶段的收获

经过半月多的赶ddl(,加上老师群友们的无私帮助,我成功的完成了第二阶段的任务。

不仅对Rust的掌握程度更深了,并且对操作系统中很多概念有了实际上的理解。

包括且不限于:

  • 特权态的切换
  • 地址表,页表的管理
  • 进程和线程的区别
  • 执行流切换和恢复
  • 线程调度

同时,这些知识很大程度上帮助我消除了对裸机编程的恐惧。对计算机底层的运行方式有了清楚的认知。

一个尝试-Rust in ch32v003

在前几天结束第二阶段的作业后,我是打算了摸几天鱼,正好看到桌边吃灰的ch32v003单片机。

ch32v003是一块携带了riscv指令集芯片的单片机,我手头上这块,实现了riscv标准中的mai,以及f。
美中不足的是,它只有S态和M态,并没有实现U态。

于是我萌生出想法——为什么不试试将Rust代码运行在这上面呢。

ch32-hal

Rust在嵌入式方面的库,在2024年的今天已经较为完善了。在createio中查找一番后,我看到了ch32-hal,以及与其配套的qingke-rt

ch32-hal将ch32系列的单片机的外设进行了rust封装,使得在代码中能方便的进行调用和更改

如修改SYSTICK的ctlr位,只需要以下代码

1
SYSTICK.ctlr().modify(|w| w.set_ste(false));

qingke-rt则包含了运行时相关的内容,可以在main函数前添加#[qingke_rt::entry],使得该函数称为编译后程序的入口函数

并且qingke-rt也可以通过类似的方法定义中断处理函数。类似的工作,我们在rcore实验中是通过内联汇编手动修改相关寄存器实现的。

代码

以下附上一个点灯代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#[qingke_rt::entry]
fn main() -> ! {
let mut config = hal::Config::default();
config.rcc = hal::rcc::Config::SYSCLK_FREQ_48MHZ_HSE;
let p = hal::init(config);

let mut delay = Delay;

let mut led = Output::new(p.PC2, Level::Low, Default::default());
loop {
led.toggle();

delay.delay_ms(500);
let val = hal::pac::SYSTICK.cnt().read();
}
}

理论和实践

之前在学习体系结构的知识时,书本上只有几张图来描述代码执行流的切换,于我而言并不直观。甚至很长一段时间我是当做文科的内容来学的。

经过了本次实验后,令我印象最深的是__switch函数,通过寄存器内容的切换,便可切换到另一个执行流上。

而执行流,在某一时刻无非是:

  • pc指针的位置
  • 栈指针的位置
  • 若干存储在当前寄存器中的变量

而对执行流,进一步抽象成包含若干线程的进程。

对若干的进程进行管理,并为进程提供服务,就是操作系统。

文件系统

在此之前,我对ntfs之类的文件系统没有一点了解。在这次实验中,了解了一个简单文件系统的实现,并且通过easyfs实现了一些功能。

同时,简单了解到了对于硬件驱动的编写,以及将不同的硬件驱动,抽象成相同的软件内模型。在软件内进行多层抽象,每层抽象负责简单的几件事情。

这样能够方便错误判断,多层抽象的思想我最先从网络原理中了解。easyfs的实现方式,进一步的说明多层抽象在计算机软件编写中,可以称得上是最佳实践了。

进程&线程

不去亲自阅读操作系统源码,是很难对进程和线程有明确的理解的。

甚至很长一段时间,我对进程和线程并没有区分。而在OS内部,进程可以拥有多个线程,线程之间共享内存空间。

对于这点的了解,不仅是系统编程领域,对我普通的软件编程也大有裨益。

第1~2章

总的来说这两张花的时间是比较久的,因为完全没有学过操作系统,对概念上的理解用了一些时间,真的跟着代码学习的时候,不知道这些代码之间的联系,不知道用户程序和操作系统是怎么联系起来的。

因此总结了一个大概流程:

build.py编译user里面的用户应用程序:

img

启动qemu和rustsbi,编译os

img

执行build.rs,将bin文件插入到link_app.S,生成link_app.S

链接

main.rs运行batch.rs,开始批处理

batch.rs首先初始化一个实例:APP_MANAGER: UPSafeCell

从link_app.S里面找到_num_app符号,找到每个app的地址存进app_start数组,然后将其打印出来

然后依次执行应用程序,其中应用程序出现错误或者开始下一个应用程序时,需要有用户态到内核态的切换。需要用到Trap.S

img

开始批处理

img

保存Trap上下文,之后进入trap_handler,正常情况下会返回并继续执行下一个应用程序

img

这个函数不会返回,会一直将加载应用程序并把当前的应用程序的上下文压入内核栈,sret后就会自动执行下一个应用程序,直到完成

第3章

主要是学习分时操作系统,概念上还是比较好理解的

第4章

学习了地址空间,一开始对于这几个概念比较混淆:

  • 虚拟地址
  • 虚拟页号
  • 物理地址
  • 物理页号
  • 地址空间

后来根据这个图搞懂了

img

  • 虚拟地址指的是在地址空间里面,数据的地址

  • 虚拟页号指的是虚拟地址字段里面的一个字段,这个字段可以在物理页表上查到一个物理页号,最终通过多级页表,查找到这个虚拟地址对应的物理地址,从而真正在主存读写数据

  • 每个应用的地址空间可以被分成若干个(虚拟) 页面 (Page) ,而可用的物理内存也同样可以被分成若干个(物理) 页帧 (Frame) ,虚拟页面和物理页帧的大小相同

  • 地址空间:一系列有关联的逻辑段:

    地址空间是一系列有关联的逻辑段,这种关联一般是指这些逻辑段属于一个运行的程序(目前把一个运行的程序称为任务,后续会称为进程)。 用来表明正在运行的应用所在执行环境中的可访问内存空间,在这个内存空间中,包含了一系列的不一定连续的逻辑段。 这样我们就有任务的地址空间、内核的地址空间等说法了

    也就是说,地址空间的逻辑段不一定连续,这就对应了编程题,如果要写的内容太大导致分页,应该直接将内容也分页塞入到其物理地址里面去

第5章

需要记的就是进程是怎么调度的,switch函数的作用

就是最初,先创建一个idle进程,然后将switch的参数设置为idle进程和第一个程序,使得第一个程序可以执行

然后就调用 task 子模块提供的 suspend_current_and_run_next 函数可以暂停当前任务,并切换到下一个任务

这一章完成后,一个应用程序可以主动交出CPU的使用权,这样一来,它需要等待某个资源的时候,CPU就可以去执行其他程序了

第6章

这一章的概念也看得我很晕

主要是:

  • 索引节点和文件有什么关系?
  • 目录和索引节点又有什么关系?
  • 一级索引存在哪里?二级索引指向哪里?
  • inode和DiskInode是如何映射起来的?

因此画了这个示意图:

img

对于lab,感觉比较难的是理解几个inode之间的关系,比如通过文件名如何查找其对应的inode_id?

第7~8章

终于要结束了

第8章的概念比较好理解,对我来说比较难的是理解lab里面死锁检测算法的含义

一开始写的时候只考虑了能否解开一个锁,总感觉哪里不对。后来发现,其实要考虑能否解开所有的锁,因为解锁的顺序是要考虑的,如果只考虑能否解开一个锁,那么这个解锁顺序可能无法实现。

2024s春季操作系统训练营第一阶段心得

在学校相关社群里经常有学长在讨论rust这门语言,什么生命周期,智能指针,对于我这种刚接触编程的小白来说就是看天书。在大二陆续学习了几门语言以后慢慢学习新语言的速度也变快了,也可以更好的理解一些概念了,恰巧训练营又开始了,于是报名参加了这个训练营。有句话说的没错,参加这个训练营唯一的坏处(?)就是让你多学会了一门rust,不过rust确实是我接触的这些语言里个人认为best的。之前一直听go是极致的简单rust是极致的复杂,现在看来还真是,不过依然蛮喜欢!希望可以继续学习更多变成更好的自己啦!

第二次参加训练营了,之前已经写过一段时间的Rust了,题目和记忆中差不太多很快就做完了。
遗憾是每次参加训练营的时候,学校的安排都很紧,希望这次有时间能进三阶段吧