0%

2021操作系统夏令营结营报告-陈志扬 彭淳毅

2021年操作系统夏令营结营总结

学习rust语言

rust语言有很多独特的设计,在开始上手rust操作系统之前,最好先学习一些这些特性,否则遇到的时候会抓狂……

rust语言的学习资源目前已经很多,想快速入门的话,可以试一试Tour of Rust.

所有权

每次我们遇到所有权的问题是,都是根据编译器提示一通乱改,这样治标不治本。我们只要理解了设计所有权的目的,就可以少在这方面栽跟头。

rust宏不是简单地替换文本。可以看看这篇教程:

什么是 Rust 宏? —— 浅谈 Rust 宏 - HoshinoTented 的博客 - 洛谷博客 (luogu.com.cn)

最终成果总览

进程状态监控

系统调用的次数与时间获取

构建一个结构体用来存储系统调用的次数与时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pub struct SyscallCount{
    pub syscall_dup: (usize, usize),
    pub syscall_open: (usize, usize),
    pub syscall_close: (usize, usize),
    pub syscall_pipe: (usize, usize),
    pub syscall_read: (usize, usize),
    pub syscall_write: (usize, usize),
    pub syscall_yield: (usize, usize),
    pub syscall_get_time: (usize, usize),
    pub syscall_getpid: (usize, usize),
    pub syscall_fork: (usize, usize),
    pub syscall_exec: (usize, usize),
    pub syscall_waitpid: (usize, usize),
}

元组的第一个变量为次数,第二个为时间
在每个系统调用的开始时获取当前时间

在每个系统调用即将结束时再次获取当前时间并做差,将其赋给结构体中,并将系统次数加一

具体示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pub fn sys_dup(fd: usize) -> isize {
    let start_time = get_time_ms(); //获取开始时间
    let task = current_task().unwrap();
    let mut inner = task.acquire_inner_lock();
    if fd >= inner.fd_table.len() {
        return -1;
    }
    if inner.fd_table[fd].is_none() {
        return -1;
    }
    let new_fd = inner.alloc_fd();
    inner.fd_table[new_fd] = Some(Arc::clone(inner.fd_table[fd].as_ref().unwrap()));
    let mut syscall_inner = inner.syscall.lock();
    syscall_inner.syscall_dup.0 += 1; //系统调用次数加一
    syscall_inner.syscall_dup.1 += get_time_ms() - start_time; //系统调用时间增加
    new_fd as isize
}

注意:当前系统调用的统计只支持同步系统调用的时间获取,目前暂不支持异步

进程名称的获取

只有进程在sys_exec时会接触到进程的名字,所以我们选择在那里完成对进程名字的获取

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
pub fn sys_exec(path: *const u8, mut args: *const usize) -> isize {
    let start_time = get_time_ms();
    let token = current_user_token();
    let path = translated_str(token, path);
    let mut args_vec: Vec<String> = Vec::new();
    loop {
        let arg_str_ptr = *translated_ref(token, args);
        if arg_str_ptr == 0 {
            break;
        }
        args_vec.push(translated_str(token, arg_str_ptr as *const u8));
        unsafe { args = args.add(1); }
    }
    if let Some(app_inode) = open_file(path.as_str(), OpenFlags::RDONLY) {
        let all_data = app_inode.read_all();
        let task = current_task().unwrap();
        let argc = args_vec.len();
        task.set_name(path); //在这里获取进程名字
        task.exec(all_data.as_slice(), args_vec);
        argc as isize
    } else {
        -1
    }
}

pub fn set_name(&self, name: String){
    self.acquire_inner_lock().name = name;
}

进程 id 与父进程 id 的获取

进程控制块 TaskControlBlock 实现了 getpid() 方法,我们只需调用该方法即可得到进程的pid。在一个进程控制块被创建时,我们将它的父进程 id 写入 ppid 成员变量中,并编写 get_ppid() 方法来得到父进程 id

进程状态的获取

枚举类型 TaskStatus 表示了进程的状态:

1
2
3
4
5
6
#[derive(Copy, Clone, PartialEq,Serialize)]
pub enum TaskStatus {
Ready,
Running,
Zombie,
}

TaskControlBlockInner 里记录了 TaskStatus . 我们只需编写 get_status() 方法即可获得:

1
2
3
4
5
6
impl TaskControlBlockInner {
// ....
pub fn get_status(&self) -> TaskStatus {
self.task_status
}
}

cpu状态监控

cpu运行的顺序图

设计结构体来存储时间

1
2
3
4
5
6
7
struct CpuStatInner{
    time_stamp: usize
    sample_duration: usize,
    sample_busy_duration: usize,
    sample_busy_want: usize,
    busy_start: usize,
}

cpu运行总时间的获取

run函数中获取时间以100ms为一个时间段向结构体内存储数据

cpu忙运行时间的获取

在idle切换进入进程前记录时间,放入busy_start 中,当进程结束或主动yield时,进程会进入schedule函数并从进程重新切换回idle,所以在切换回idle之前取进程结束时间做差就可以得到cpu的忙运行时间

通过文件读取性能监测信息

rCore-Tutorial 已经实现了 pipe , stdio 两个抽象文件,我们再添加ProcInfoList 文件和CPUInfo文件。这两个文件的 read() 方法调用内核中的信息收集函数,将收集到的信息转换为 json 文本并写入缓冲区。

在新建进程控制块时,我们添加这两个文件。用户态程序读取这两个文件时使用 sys_read() 系统调用,该系统调用最终调用了ProcInfoList 文件和CPUInfo文件的 read() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
impl TaskControlBlock {
// ...
pub fn new(elf_data: &[u8]) -> Self {
// ...
fd_table: vec![
// 0 -> stdin
Some(Arc::new(Stdin)),
// 1 -> stdout
Some(Arc::new(Stdout)),
// 2 -> stderr
Some(Arc::new(Stdout)),
// 3 -> proc_info
Some(Arc::new(ProcInfoList {})),
// 4 -> cpu_info
Some(Arc::new(CPUInfo {})),
],

};

用户态监控程序

缓冲区

我们新建一个缓冲区来接收 read() 获取到的信息。

接下来,我们将缓冲区中的json数据解析并输出。

输出缓冲

我们可以在解析出数据后直接输出,但由于 println! 宏运行速度较慢,逐行输出会导致滚屏的现象。因此我们需要将要输出的字符串先存储在一个输出缓冲区里,待要更新的所有行都进入缓冲区再统一输出:

1
2
3
4
5
6
// ...
pool.push(format_to_string_ln!("---------")); // 向输出缓冲区添加字符串
let whole = pool.join("");//将输出缓冲区中的字符串合为一个字符串
print!("\x1b[2J"); //使用控制字符清屏
print!("\x1b[1;1H"); //光标移到左上角
print!("{}",whole); //

我们编写了 format_to_string_ln! 宏将格式化输出转换为自字符串:

1
2
3
4
5
6
7
8
9
10
macro_rules! format_to_string_ln {
($fmt: literal $(, $($arg: tt)+)?) => {
format_args!(concat!($fmt, "\n") $(, $($arg)+)?).to_string()
};
}
macro_rules! format_to_string {
($fmt: literal $(, $($arg: tt)+)?) => {
format_args!($fmt $(, $($arg)+)?).to_string()
};
}

制造负载

fork 多个子进程,进行性能消耗。

其他收获

合作

多问问题很重要。比如两位助教同学提醒我们可以用控制字符,使得软件界面好看不少。每个人的背景知识都不一样,因此对于同一个问题,不同人产生的思路也不一样。多人合作可以说是解决问题的一种捷径。

当前疫情管控比较严格,所以我们用腾讯会议交流代码。比起微信聊天,通过语音交流信息量会大一些。

坚持

一款精心设计的游戏,总能在适当的时候给予玩家激励,让玩家百玩不厌,沉迷其中。但操作系统(至少在入门阶段)并不是这样。操作系统机制复杂,理解起来需要花一些时间,编程的时候失败是常态。所以,想像写前端应用那样,十分钟搭好框架,一天就能出个大致成果,是不太现实的。和其他组相比,我们的题目简单一些,但是从选题到写出第一个能勉强运行的demo也尽全力花了六天时间才搞定。因此,除去对技术本身的热爱,坚持也是十分重要的。

坚持的秘诀在于休息。感到压力太大的话,可以吃点甜食或者出门走走。休息的时候,人脑也会无意识地进行思考,这时往往能产生一些意想不到的突破(这叫做发散模式)。

记笔记

人遗忘得很快,因此很有必要记录下自己的思路,方便后期复用。概念图,思维导图,大纲笔记······只要是能呈现自己思路的笔记方法都可以。有兴趣的话,还可以浏览一下《卡片笔记写作法》这本书。