0%

Rust 具有安全和高效的特性,这使得它有希望被用于构建更好的操作系统

操作系统

在计算机的分层体系结构中,操作系统位于软件和硬件的分界处

操作系统的职责是帮助用户程序管理计算机硬件,这基于 CPU 提供 ISA 实现(在本次实验过程中,具体是 RISC-V)

操作系统的职责可以被进一步细分为几个方面——虚拟化、并发、持久性和驱动(和外部设备的通信)

在第二阶段,我们并没有太多的关于驱动的内容。虚拟化考虑如何在有限的硬件资源上为多个软件提供相互隔离的服务,并发考虑如何控制软件对共享资源的使用、持有性主要指文件系统

操作系统称得上是人类所设计的最复杂的一类程序,在这里你几乎能用上计算机中的所有知识

To Be Continue

这篇简短的博客作为2024春夏季开源操作系统训练营第二阶段的总结,我在其中简单概述了我对 Rust 用于构建操作系统的理解

因为我所学尚浅,且没有足够的实践经历,导致以上内容十分简略,甚至可能有不少错误

我并不希望这些内容就这样简短的结束并被掩埋和遗忘,所以我将在我的个人博客上持续更新我对使用 Rust 编写 OS 的理解

第一阶段总结

因为想学习 rust,所以找找有没有用 rust 开发的项目,就找到了 rCore,然后刚好看到有个春季训练营,就参加了。刚刷完官方的 Rustlings,所以第一阶段完成的还挺快的。

第二阶段总结

因为工作上也很忙,所以需要挤出时间来做,在 ddl 的最后一周才开始做,几乎一天一个 lab,也是有点忙坏了。

从课程上来看,因为在 21 年的时候有完成过 xv6-riscv ,所以 rCore 上手的时候主要难度还是在 rust 上面,经常用不明白 rust。又整体复习了一遍操作系统和 rust,希望后面能够继续去写 rust 代码,这感觉很有意思。

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

2024春夏rCore训练营第二阶段总结

参加过2023秋冬,所以ch3-ch5参考了之前的报告。

2023秋冬报告

这里讲讲我之前未接触过的lab。

Lab4-ch6

对文件系统的了解比较浅,之前也从未以代码的形式接触过文件系统,因此花了不少功夫理解easy-fs的设计。再和群友简短交流后,最终胜利完成了。

Lab5-ch8

手册给出的算法类似银行家算法,但不太一样。

我感觉没有理解到位,尤其是我自己打了打草稿后,发现不太能理解负值的semaphore要如何如何在这种方法下表示;同时我觉得用这种方法动态检测死锁,好像需要满足一些条件,比如线程p了一定会在后面v之类的。

然后在如何构建算法的矩阵上我也走了弯路,我首先很呆的在线程创建、mutex/sem创建的syscall里添加自定义struct的方法,来动态维护资源矩阵。这使得代码很乱,且static的struct导致运行多个程序时会出问题,需要特殊处理。这导致后面维护和debug很复杂。最后我选择推导重来,在进程这一层添加功能,并在pcb的mutex/sem_list获取各资源的数目,从task获取线程信息。这样功能几乎就集中在了少数地方,比之前轻松很多。

另外,文档中没有提示ch8需要实现时间的syscall,这导致即使死锁检测的逻辑出错,测试也必然出错。初见必然要debug一轮,感觉这个就有点坑了。

总结

想必于上一期此次要求500分全写完,要求提高了。不过这两个lab由于其内容我接触不多所以感觉还挺有意思的。

然后就是之前也提过的跨章代码合并,还有测试问题。我后来想了想,这好像也没什么办法;跨章合并是由rcore的风格导致的,上下两章代码可能是冲突的,不太好随着章节渐进更新,我见过的一些lab可以做到直接fetch + merge进入下一章;然后用测试框架每次都要恢复文件的问题,麻烦当然是有点麻烦的,但也许受制于某些因素只能这么设计的,如果这个能改了的话就很舒服了。

总的来说训练营还是很不错的,也有在更新维护;我想我们很多学生都应该多一些实际的代码经历,训练营就是这样一个机会。

RUST语言学习和Rcore实验感想

写在前面

​ 这篇博客可能长度有点长(),因为之前没get到要求,以为blog是从二阶段才开始要写hhhh,所以从来没写过就是了hhh~~

​ 这篇blog的主要内容是将我在学习RUST期间所做的笔记做一个整理(主要是来自于RUST语言圣经的节选)以便于自己后续回顾,以及将RUSTLINGS习题集中一些值得标注的语法问题做了记录。

​ :cry:二阶段得好好写了hhhh

RUST语言圣经笔记

数据类型

  1. 数据类型

    • 数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数
      • Nan表示未被定义的结果
      • debug模式检查整数溢出,release不会管
      • 浮点数不支持判等(eq操作未实现)
    • 字符串:字符串字面量和字符串切片 &str
    • 布尔类型: truefalse,1个字节
    • 字符类型: 表示单个 Unicode 字符,存储为 4 个字节
    • 单元类型: 即 () ,其唯一的值也是 ()

    一般来说不用显式声明,RUST编译器有变量推导

    比较逆天的话就不行了……

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let guess = "42".parse().expect("Not a number!");//推导不了

    //确定类型的三种方式
    // 编译器会进行自动推导,给予twenty i32的类型
    let twenty = 20;
    // 类型标注
    let twenty_one: i32 = 21;
    // 通过类型后缀的方式进行类型标注:22是i32类型
    let twenty_two = 22i32;
  2. 序列

    生成连续值,只允许用于数字和字符类型(编译器可在编译期确定类型和判空)

    1
    2
    3
    4
    5
    6
    7
    for i in 1..=5 {
    println!("{}",i);
    }

    for i in 'a'..='z' {
    println!("{}",i);
    }
  3. 函数

1
2
3
fn add(i: i32, j: i32) -> i32 {
i + j
}
  • 特殊返回类型

    • 无返回值

      • 函数没有返回值,那么返回一个 ()
      • 通过 ; 结尾的语句返回一个 ()
    • 发散函数:永不返回

      !作为函数的返回类型

所有权和借用

  1. C和RUST的内存管理差别

    1
    2
    3
    4
    5
    6
    7
    8
    int* foo() {
    int a; // 变量a的作用域开始
    a = 100;
    char *c = "xyz"; // 变量c的作用域开始
    return &a;
    } // 变量a和c的作用域结束
    //a是常数,被放在栈里,函数返回时出栈,a被回收,&a是悬空指针
    //c是字符串常量,在常量区,整个程序结束之后才会回收常量区
  2. 所有权规则

    • Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者

    • 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者

    • 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let x = 5;
    let y = x;
    //浅拷贝,两个变量都依然有效

    let s1 = String::from("hello");
    let s2 = s1;
    //变量移动,默认是只copy指针,不会复制其实际内容
    //s1失效,s2接管那片内存空间

    let s1 = String::from("hello");
    let s2 = s1.clone();
    //你真想赋值的时候复制其内容,用clone()方法

    let x: &str = "hello, world";
    let y = x;
    //浅拷贝,"hello, world"是字符串字面量

    Copy特征:一个旧的变量在被赋值给其他变量后仍然可用,也就是赋值的过程即是拷贝的过程。任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy

    • 所有整数类型,比如 u32
    • 布尔类型,bool,它的值是 truefalse
    • 所有浮点数类型,比如 f64
    • 字符类型,char
    • 元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32)Copy 的,但 (i32, String) 就不是
    • 不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意: 可变引用 &mut T 是不可以 Copy的
  3. 函数传值和返回——所有权的不断变化

    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
    fn main() {
    let s1 = gives_ownership(); // gives_ownership 将返回值
    // 移给 s1

    let s2 = String::from("hello"); // s2 进入作用域

    let s3 = takes_and_gives_back(s2); // s2 被移动到
    // takes_and_gives_back 中,
    // 它也将返回值移给 s3
    } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
    // 所以什么也不会发生。s1 移出作用域并被丢弃

    fn gives_ownership() -> String { // gives_ownership 将返回值移动给
    // 调用它的函数

    let some_string = String::from("hello"); // some_string 进入作用域.

    some_string // 返回 some_string 并移出给调用的函数
    }

    // takes_and_gives_back 将传入字符串并返回该值
    fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域

    a_string // 返回 a_string 并移出给调用的函数
    }
  4. 引用

    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
    fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);//传入的是引用而不是所有权

    println!("The length of '{}' is {}.", s1, len);
    }

    fn calculate_length(s: &String) -> usize {
    s.len()//拿到的是引用,因此函数结束的时候不会释放所有权
    }

    ————————————————
    //引用默认不可变(就是你不能动你借用的东西的值)
    fn main() {
    let mut s = String::from("hello");//可变引用(可以修改借用的东西)

    change(&mut s);
    }

    fn change(some_string: &mut String) {
    some_string.push_str(", world");
    }

    ————————————————
    //在同一个作用域只可以存在一个可变引用(互斥锁懂我意思吧……)
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;//r1的作用域还没寄,你怎么也搞个可变

    println!("{}, {}", r1, r2);


    ————————————————
    //可变和不可变引用不能同时存在
    let mut s = String::from("hello");

    let r1 = &s; // 没问题
    let r2 = &s; // 没问题
    let r3 = &mut s; // 大问题

    println!("{}, {}, and {}", r1, r2, r3);

    引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号结束。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);
    // 新编译器中,r1,r2作用域在这里结束

    let r3 = &mut s;
    println!("{}", r3);
    } // 老编译器中,r1、r2、r3作用域在这里结束
    // 新编译器中,r3作用域在这里结束
    //Non-Lexical Lifetimes(NLL)特性:用于寻找到某个引用在`}`之前就不再被使用的位置

    悬垂引用在Rust是不会存在的,因为当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn main() {
    let reference_to_nothing = dangle();
    }

    fn dangle() -> &String {
    let s = String::from("hello");

    &s//悬垂引用,会报错
    //解决办法是直接返回s,也就是交出其所有权
    }

复合类型

字符串和切片

  1. Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)

  2. 为啥 String 可变,而字符串字面值 str 却不可以?

    就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行的过程中动态生成的。

  3. String和&str的转换

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //从&str生成String
    String::from("hello,world")
    "hello,world".to_string()

    //String到&str 取切片即可
    fn main() {
    let s = String::from("hello,world!");
    say_hello(&s);
    say_hello(&s[..]);
    say_hello(s.as_str());
    }

    fn say_hello(s: &str) {
    println!("{}",s);
    }
  4. 字符串索引(Rust不支持

    1
    2
    3
    4
    5
    let s1 = String::from("hello");
    let hello = String::from("中国人");
    let h = s1[0];
    let h = hello[0];
    //不同字符的编码长度是不一样的,英文是1byte,中文是3byte,对特定单元的索引不一定有意义

    还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O(1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。

    字符串的区间切片Rust是支持的,但是必须谨慎

    1
    2
    let hello = "中国人";
    let s = &hello[0..2];
  5. 常见字符串操作

    • 追加和插入

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      //追加
      fn main() {
      let mut s = String::from("Hello ");//mut!

      s.push_str("rust");//追加字符串

      s.push('!');//追加单字符
      }

      //插入 insert需要插入位置和内容 位置越界会报错
      fn main() {
      let mut s = String::from("Hello rust!");//mut!
      s.insert(5, ',');
      s.insert_str(6, " I like");
      }
    • 替换

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      //返回一个新的字符串,而不是操作原来的字符串!!!
      //replace 参数是:被替换内容,用来替换的内容
      fn main() {
      let string_replace = String::from("I like rust. Learning rust is my favorite!");
      let new_string_replace = string_replace.replace("rust", "RUST");
      }

      //replacen 和前面差不多,不过是替换n个匹配到的
      fn main() {
      let string_replace = "I like rust. Learning rust is my favorite!";
      let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
      dbg!(new_string_replacen);
      }

      //方法是直接操作原来的字符串,不会返回新的字符串!!!
      //replace_range 替换特定范围
      fn main() {
      let mut string_replace_range = String::from("I like rust!");//mut!!!
      string_replace_range.replace_range(7..8, "R");
      }
    • 删除

      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
      //直接操作原来的字符串  mut!!!
      //pop 删除并返回最后一个字符 由于不确保存在,返回的是Option()类型 需要具体考察
      fn main() {
      let mut string_pop = String::from("rust pop 中文!");
      let p1 = string_pop.pop();
      let p2 = string_pop.pop();
      }

      //remove 删除指定位置的一个字符 注意给的索引要合法(表示字符的起始位置)
      fn main() {
      let mut string_remove = String::from("测试remove方法");
      println!(
      "string_remove 占 {} 个字节",
      std::mem::size_of_val(string_remove.as_str())
      );
      // 删除第一个汉字
      string_remove.remove(0);
      // 下面代码会发生错误
      // string_remove.remove(1);
      // 直接删除第二个汉字
      // string_remove.remove(3);
      dbg!(string_remove);
      }

      //truncate 从当前位置直接删除到结尾 注意给的索引
      fn main() {
      let mut string_truncate = String::from("测试truncate");
      string_truncate.truncate(3);
      }

      //clear 清空
      fn main() {
      let mut string_clear = String::from("string clear");
      string_clear.clear();
      dbg!(string_clear);
      }
    • 连接

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      //+或+=   +右边的必须是切片引用类型
      //返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰
      fn main() {
      let string_append = String::from("hello ");
      let string_rust = String::from("rust");
      // &string_rust会自动解引用为&str
      let result = string_append + &string_rust;
      let mut result = result + "!"; // `result + "!"` 中的 `result` 是不可变的
      result += "!!!";

      println!("连接字符串 + -> {}", result);
      }

      //format!() 格式化输出
      fn main() {
      let s1 = "hello";
      let s2 = String::from("rust");
      let s = format!("{} {}!", s1, s2);
      println!("{}", s);
      }

元组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//模式匹配解构元组
fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup;

println!("The value of y is: {}", y);
}

//用.访问元组
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;
}

结构体

  1. 结构体语法

    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
    //创建
    struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
    }

    //初始化 每个字段都要初始化
    let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };

    //通过.来访问结构体内部字段
    let mut user1 = User { //要改的话还是要mut
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");

    //当函数参数和结构体字段名称一样时,可以简写
    fn build_user(email: String, username: String) -> User {
    User {
    email,
    username,//缩略的初始化
    active: true,
    sign_in_count: 1,
    }
    }

    //更新
    let user2 = User {
    email: String::from("another@example.com"),
    ..user1 //未显式声明的字段都会从user1中获取 不过..user1只可以写在末尾
    };//也就是说你要赋值的写在前面

    //更新过程可能会有某些字段发生了所有权的转移,不会影响其他字段的访问
    let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
    };
    let user2 = User {
    active: user1.active,
    username: user1.username,
    email: String::from("another@example.com"),
    sign_in_count: user1.sign_in_count,
    };
    println!("{}", user1.active);
    // 下面这行会报错
    println!("{:?}", user1);
  2. 元组结构体

    为整个结构体提供名称,而字段不需要

    1
    2
    3
    4
    5
    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);

    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
  3. 单元结构体:没有任何字段和属性的结构体

枚举

  1. 任何数据类型都可以放到枚举中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    enum PokerCard {
    Clubs(u8),
    Spades(u8),
    Diamonds(char),//定义枚举成员时关联数据
    Hearts(char),
    }

    fn main() {
    let c1 = PokerCard::Spades(5);
    let c2 = PokerCard::Diamonds('A');
    }
  2. 枚举和结构体的对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    //使用枚举来定义这些消息
    enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
    }

    fn main() {
    let m1 = Message::Quit;
    let m2 = Message::Move{x:1,y:1};
    let m3 = Message::ChangeColor(255,255,0);
    }

    //使用结构体来定义这些消息
    struct QuitMessage; // 单元结构体
    struct MoveMessage {
    x: i32,
    y: i32,
    }
    struct WriteMessage(String); // 元组结构体
    struct ChangeColorMessage(i32, i32, i32); // 元组结构体

    //由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受 4 个不同的结构体作为参数。
  3. 取代NULL的方式——Option()枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    //Option()枚举定义
    enum Option<T> {
    Some(T), //T可以是任何类型
    None,
    }

    //示例
    ——————————————

    let some_number = Some(5);
    let some_string = Some("a string");

    let absent_number: Option<i32> = None;
    //当有个None值时,你需要告诉编译器T的类型,因为编译器无法通过None来推断本来应该是什么
  4. Option()枚举的好处

    1
    2
    3
    4
    let x: i8 = 5;
    let y: Option<i8> = Some(5);

    let sum = x + y;//报错!Option(i8)和i8并不是同一种类型

    当在 Rust 中拥有一个像 i8 这样类型的值时,编译器确保它总是有一个有效的值,我们可以放心使用而无需做空值检查。只有当使用 Option<i8>(或者任何用到的类型)的时候才需要担心可能没有值,而编译器会确保我们在使用值之前处理了为空的情况。

    换句话说,在对 Option<T> 进行 T 的运算之前必须将其转换为 T。通常这能帮助我们捕获到空值最常见的问题之一:期望某值不为空但实际上为空的情况。

  5. match表达式可以用于处理枚举

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
    None => None,
    Some(i) => Some(i + 1),
    }//如果接收到Some(i)类型,将其中的变量绑定到i上,计算i+1,再将其用Some()包裹
    }

    let five = Some(5);
    let six = plus_one(five);
    let none = plus_one(None);

数组

  1. 创建

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    //RUST的数组是定长的,被存储在栈上
    //变长的动态数组被存储在堆上
    //数组的长度也是类型的一部分
    let a: [i32; 5] = [1, 2, 3, 4, 5];

    //声明多个重复值
    let a = [3; 5];

    //非基础类型数组的创建

    //这样子写会报错,本质还是因为string不能浅拷贝
    let array = [String::from("rust is good!"); 8];

    //这样子写可以,但是很难看
    let array = [String::from("rust is good!"),String::from("rust is good!"),String::from("rust is good!")];

    //遇到非基本类型数组 调用std::array::from_fn
    let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));
  2. 支持索引访问,如果越界会崩溃

流程控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fn main() {
for i in 1..=5 {
println!("{}", i);
}
}

//如果想在循环中获取元素的索引,使用.iter()方法获得迭代器
fn main() {
let a = [4, 3, 2, 1];
// `.iter()` 方法把 `a` 数组变成一个迭代器
for (i, v) in a.iter().enumerate() {
println!("第{}个元素是{}", i + 1, v);
}
}
使用方法 等价使用方式 所有权
for item in collection for item in IntoIterator::into_iter(collection) 转移所有权
for item in &collection for item in collection.iter() 不可变借用
for item in &mut collection for item in collection.iter_mut() 可变借用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 第一种
let collection = [1, 2, 3, 4, 5];
for i in 0..collection.len() {
let item = collection[i];
// ...
}

// 第二种
for item in collection {

}


//while循环
fn main() {
let a = [10, 20, 30, 40, 50];
let mut index = 0;

while index < 5 {
println!("the value is: {}", a[index]);

index = index + 1;
}
}//用while循环来实现和第一种for循环是一样的

第一种方式是循环索引,然后通过索引下标去访问集合,第二种方式是直接循环集合中的元素,优劣如下:

  • 性能:第一种使用方式中 collection[index] 的索引访问,会因为边界检查(Bounds Checking)导致运行时的性能损耗 —— Rust 会检查并确认 index 是否落在集合内,但是第二种直接迭代的方式就不会触发这种检查,因为编译器会在编译时就完成分析并证明这种访问是合法的
  • 安全:第一种方式里对 collection 的索引访问是非连续的,存在一定可能性在两次访问之间,collection 发生了变化,导致脏数据产生。而第二种直接迭代的方式是连续访问,因此不存在这种风险( 由于所有权限制,在访问过程中,数据并不会发生变化)。

loop:简单的无限循环,需要搭配break跳出

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut counter = 0;

let result = loop {
counter += 1;

if counter == 10 {
break counter * 2;//break可以单独使用直接跳出,也可以带一个值返回(类似return)
}
};

println!("The result is {}", result);
}

模式匹配

match和if let

  1. 匹配

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
    }

    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => {
    println!("Lucky penny!");
    1
    },
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter => 25,
    }
    }
    //match匹配需要穷尽所有的可能,用_表示没有列出的其他可能性(如果没有穷尽可能性的话会报错)
    //match的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  2. 模式绑定

    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
    #[derive(Debug)]
    enum UsState {
    Alabama,
    Alaska,
    // --snip--
    }

    enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState), // 25美分硬币
    }

    fn value_in_cents(coin: Coin) -> u8 {
    match coin {
    Coin::Penny => 1,
    Coin::Nickel => 5,
    Coin::Dime => 10,
    Coin::Quarter(state) => {//这里将枚举类别Coin中的UsState值绑定给state变量
    println!("State quarter from {:?}!", state);
    25
    },
    }
    }
  3. if let匹配

    当我们只关注某个特定的值的匹配情况时,可以使用if let匹配代替match

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let v = Some(3u8);
    match v {
    Some(3) => println!("three"),
    _ => (),
    }

    //if let匹配
    if let Some(3) = v {
    println!("three");
    }
  4. matches!()宏

    将表达式和模式进行匹配,返回True或者False

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    enum MyEnum {
    Foo,
    Bar
    }

    fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
    }
    //对v进行过滤,只保留类型为MyEnum::Foo的元素
    v.iter().filter(|x| matches!(x, MyEnum::Foo));

    //更多例子
    let foo = 'f';
    assert!(matches!(foo, 'A'..='Z' | 'a'..='z'));

    let bar = Some(4);
    assert!(matches!(bar, Some(x) if x > 2));
  5. match和if let匹配导致的变量遮蔽

    ​ 尽量不要使用同名变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    fn main() {
    let age = Some(30);
    println!("在匹配前,age是{:?}",age);
    if let Some(age) = age {
    println!("匹配出来的age是{}",age);
    }

    println!("在匹配后,age是{:?}",age);
    }

    fn main() {
    let age = Some(30);
    println!("在匹配前,age是{:?}",age);
    match age {
    Some(age) => println!("匹配出来的age是{}",age),
    _ => ()
    }
    println!("在匹配后,age是{:?}",age);
    }

一些模式适用场景

  1. while let 只要匹配就会一直循环下去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Vec是动态数组
    let mut stack = Vec::new();

    // 向数组尾部插入元素
    stack.push(1);
    stack.push(2);
    stack.push(3);

    // stack.pop从数组尾部弹出元素
    while let Some(top) = stack.pop() {
    println!("{}", top);
    }
  2. let和if let

    1
    2
    3
    4
    5
    6
    let Some(x) = some_option_value;//报错,有可能是None
    //let,match,for都需要完全匹配(不可驳匹配)

    if let Some(x) = some_option_value {
    println!("{}", x);
    }//通过,只要有值的情况,其余情况忽略(可驳模式匹配)

全模式列表

  1. 用序列语法..=匹配区间内的值(还是只能用于数字和字符)

    1
    2
    3
    4
    5
    6
    let x = 5;

    match x {
    1..=5 => println!("one through five"),
    _ => println!("something else"),
    }
  2. 使用模式忽略值

    1
    2
    3
    4
    5
    6
    7
    8
    //忽略函数变量
    fn foo(_: i32, y: i32) {
    println!("This code only uses the y parameter: {}", y);
    }

    fn main() {
    foo(3, 4);
    }

    _忽略值和用_s的区别

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let s = Some(String::from("Hello!"));

    if let Some(_s) = s {
    println!("found a string");
    }

    println!("{:?}", s);//会报错,因为s的所有权已经转移给_s了

    ——————————————————————————

    let s = Some(String::from("Hello!"));

    if let Some(_) = s {
    println!("found a string");
    }

    println!("{:?}", s);//使用下划线本身是不会绑定值的
  3. 使用..忽略多个值需要保证没有歧义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn main() {
    let numbers = (2, 4, 8, 16, 32);

    match numbers {
    (.., second, ..) => {
    println!("Some numbers: {}", second)
    },
    }
    }//报错,编译器无法理解second具体指哪个
  4. 匹配守卫——为匹配提供额外条件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    fn main() {
    let x = Some(5);
    let y = 10;

    match x {
    Some(50) => println!("Got 50"),
    Some(n) if n == y => println!("Matched, n = {}", n),
    //通过匹配守卫,使得在匹配中也可以正常的使用外部变量,而不用担心变量遮蔽的问题
    _ => println!("Default case, x = {:?}", x),
    }

    println!("at the end: x = {:?}, y = {}", x, y);
    }

    ——————————————————
    //匹配守卫的优先级:会作用于所有的匹配项
    let x = 4;
    let y = false;

    match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
    }
  5. @绑定——提供在限定范围条件下,在分支代码内部使用变量的能力

    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
    enum Message {
    Hello { id: i32 },
    }

    let msg = Message::Hello { id: 5 };

    match msg {
    Message::Hello { id: id_variable @ 3..=7 } => {
    println!("Found an id in range: {}", id_variable)
    },//@变量绑定,限定范围且绑定变量
    Message::Hello { id: 10..=12 } => {
    println!("Found an id in another range")
    },//限定了范围,但是这样子只会匹配,而id这个量用不了
    Message::Hello { id } => {
    println!("Found some other id: {}", id)
    },//可以匹配并绑定到id上,但是这样子限制不了范围
    }


    ————————————————
    //绑定的同时对变量结构
    #[derive(Debug)]
    struct Point {
    x: i32,
    y: i32,
    }

    fn main() {
    // 绑定新变量 `p`,同时对 `Point` 进行解构
    let p @ Point {x: px, y: py } = Point {x: 10, y: 23};
    println!("x: {}, y: {}", px, py);
    println!("{:?}", p);

    let point = Point {x: 10, y: 5};
    if let p @ Point {x: 10, y} = point {
    println!("x is 10 and y is {} in {:?}", y, p);
    } else {
    println!("x was not 10 :(");
    }
    }

方法Method

  1. 定义和初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    struct Circle {
    x: f64,
    y: f64,
    radius: f64,
    }

    impl Circle {
    // new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
    // 这种方法往往用于初始化当前结构体的实例
    fn new(x: f64, y: f64, radius: f64) -> Circle {
    Circle {
    x: x,
    y: y,
    radius: radius,
    }
    }

    // Circle的方法,&self表示借用当前的Circle结构体
    fn area(&self) -> f64 {
    std::f64::consts::PI * (self.radius * self.radius)
    }
    }

    ​ 这种定义在 impl 中且没有 self 的函数被称之为关联函数: 因为它没有 self,不能用 f.read() 的形式调用,因此它是一个函数而不是方法,它又在 impl 中,与结构体紧密关联,因此称为关联函数。

    ​ 因为是函数,所以不能用 . 的方式来调用,我们需要用 :: 来调用,例如 let sq = Rectangle::new(3, 3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间。

    其他的语言往往将类型和方法一起定义,而Rust对这两者的定义是分开的。

  2. self和被实例化类型的关系

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    #[derive(Debug)]
    struct Rectangle {
    width: u32,
    height: u32,
    }

    impl Rectangle {//方法名称可以和结构体的名称相同
    fn area(&self) -> u32 {
    self.width * self.height
    }
    //self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
    //&self 表示该方法对 Rectangle 的不可变借用
    //&mut self 表示可变借用

    }

    fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
    "The area of the rectangle is {} square pixels.",
    rect1.area()
    );
    }
  3. 方法和字段同名的好处

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    pub struct Rectangle {
    width: u32,
    height: u32,
    }

    impl Rectangle {
    pub fn new(width: u32, height: u32) -> Self {
    Rectangle { width, height }
    }
    pub fn width(&self) -> u32 {
    return self.width;
    }
    }

    fn main() {
    let rect1 = Rectangle::new(30, 50);

    println!("{}", rect1.width());
    }

    ​ 方法和字段同名有助于我们实现访问器,我们可以将widthheight设置为私有属性,而通过pub关键字将Rectangle结构体对应的new方法和width方法设置为公有方法,这样子用户可以通过rect1.width()方法访问到宽度的数据,却无法直接使用rect1.width来访问。

  4. Rust中用自动引用/解引用机制代替了C/C++的->运算符

    ​ 在 C/C++ 语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法,这时需要先解引用指针。换句话说,如果 object 是一个指针,那么 object->something()(*object).something() 是一样的。

    ​ Rust 并没有一个与 -> 等效的运算符;相反,Rust 有一个叫 自动引用和解引用的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

    ​ 他是这样工作的:当使用 object.something() 调用方法时,Rust 会自动为 object 添加 &&mut* 以便使 object 与方法签名匹配。也就是说,这些代码是等价的:

    1
    2
    p1.distance(&p2);
    (&p1).distance(&p2);

    ​ 第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self 的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。

泛型和特征

泛型

  1. 代替值的泛型,而不是针对类型的泛型

    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
    //这段代码会报错,因为不同长度的数组在Rust中是不同的类型
    fn display_array(arr: [i32; 3]) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);

    let arr: [i32; 2] = [1, 2];
    display_array(arr);
    }

    //用切片的方式打印任意长度的数组,同时用泛型指代不同的类型
    fn display_array<T: std::fmt::Debug>(arr: &[T]) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(&arr);

    let arr: [i32; 2] = [1, 2];
    display_array(&arr);
    }

    //切片是一种引用,但是有的场景不允许我们使用引用,此时通过const泛型指代不同的长度
    fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
    println!("{:?}", arr);
    }
    fn main() {
    let arr: [i32; 3] = [1, 2, 3];
    display_array(arr);

    let arr: [i32; 2] = [1, 2];
    display_array(arr);
    }
  2. 泛型的性能

    编译器完成单态化的过程,增加了编译的繁琐程度,也让编译后的文件更大

    会对每一个具体用到的类型都生成一份代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //程序编写
    let integer = Some(5);
    let float = Some(5.0);

    //编译后
    enum Option_i32 {
    Some(i32),
    None,
    }

    enum Option_f64 {
    Some(f64),
    None,
    }

    fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
    }

特征

​ 一组可以被共享的行为,只要满足了特征,就可以做以下的行为。

  1. 定义特征

    ​ 只管定义,而往往不会提供具体的实现

    ​ 谁满足这个特征,谁来实现具体的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    pub trait Summary {
    fn summarize(&self) -> String;//以;结尾 只提供定义
    }

    pub trait Summary {
    fn summarize(&self) -> String { //也可以给一个默认实现
    String::from("(Read more...)")
    }//可以调用,也可以重载
    }

    默认实现允许调用相同特征中的其他方法,哪怕这些方法没有默认实现。如此,特征可以提供很多有用的功能而只需要实现指定的一小部分内容。例如,我们可以定义 Summary 特征,使其具有一个需要实现的 summarize_author 方法,然后定义一个 summarize 方法,此方法的默认实现调用 summarize_author 方法:

    1
    2
    3
    4
    5
    6
    7
    pub trait Summary {
    fn summarize_author(&self) -> String;//让实现Summary特征的类型具体实现吧

    fn summarize(&self) -> String {
    format!("(Read more from {}...)", self.summarize_author())
    }
    }
  2. 实现特征

    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
    pub trait Summary {
    fn summarize(&self) -> String;
    }
    pub struct Post {
    pub title: String, // 标题
    pub author: String, // 作者
    pub content: String, // 内容
    }

    impl Summary for Post {//为Post实现Summary特征
    fn summarize(&self) -> String {
    format!("文章{}, 作者是{}", self.title, self.author)
    }
    }

    pub struct Weibo {
    pub username: String,
    pub content: String
    }

    impl Summary for Weibo {
    fn summarize(&self) -> String {
    format!("{}发表了微博{}", self.username, self.content)
    }
    }
  3. 孤儿规则——特征定义和实现的位置关系

    ​ 关于特征实现与定义的位置,有一条非常重要的原则:如果你想要为类型 A 实现特征 T,那么 A 或者 T 至少有一个是在当前作用域中定义的! 例如我们可以为上面的 Post 类型实现标准库中的 Display 特征,这是因为 Post 类型定义在当前的作用域中。同时,我们也可以在当前包中为 String 类型实现 Summary 特征,因为 Summary 定义在当前作用域中。

    ​ 但是你无法在当前作用域中,为 String 类型实现 Display 特征,因为它们俩都定义在标准库中,其定义所在的位置都不在当前作用域,跟你半毛钱关系都没有,看看就行了。

  4. 使用特征作为函数的参数

    1
    2
    3
    pub fn notify(item: &impl Summary) {//实现了特征Summary的item参数
    println!("Breaking news! {}", item.summarize());//可以调用特征对应的方法
    }
  5. 特征约束

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //接收两个实现了Summary特征的参数,但是不能保证这两个参数的类型相同
    pub fn notify(item1: &impl Summary, item2: &impl Summary) {}

    //用泛型T指代
    //T:Summary要求其实现了特征Summary
    pub fn notify<T: Summary>(item1: &T, item2: &T) {}

    //多重约束
    //这里T被要求同时实现两个特征才行
    pub fn notify<T: Summary + Display>(item: &T) {}

    //Where约束,主要是用于简化函数的签名,将特征约束写在别处
    fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
    U: Clone + Debug
    {}
  6. 函数返回值中的impl Trait

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn returns_summarizable() -> impl Summary {
    //返回一个实现了Summary特征的类型,具体是什么类型不知道
    Weibo {
    username: String::from("sunface"),
    content: String::from(
    "m1 max太厉害了,电脑再也不会卡",
    )
    }
    }

    ​ 这种 impl Trait 形式的返回值,在一种场景下非常非常有用,那就是返回的真实类型非常复杂,你不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait 的方式简单返回。

  7. derive派生特征

    ​ 在本书中,形如 #[derive(Debug)] 的代码已经出现了很多次,这种是一种特征派生语法,被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。

    ​ 例如 Debug 特征,它有一套自动实现的默认代码,当你给一个结构体标记后,就可以使用 println!("{:?}", s) 的形式打印该结构体的对象。

    ​ 再如 Copy 特征,它也有一套自动实现的默认代码,当标记到一个类型上时,可以让这个类型自动实现 Copy 特征,进而可以调用 copy 方法,进行自我复制。

    ​ 总之,derive 派生出来的是 Rust 默认给我们提供的特征,在开发过程中极大的简化了自己手动实现相应特征的需求,当然,如果你有特殊的需求,还可以自己手动重载该实现。

特征对象

​ 指向了所有实现了某特征的对象,二者之间存在映射关系,可以通过特征对象找到该对象具体的实现方法。

  1. 可以通过 & 引用或者 Box<T> 智能指针的方式来创建特征对象

    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
    trait Draw {
    fn draw(&self) -> String;
    }

    impl Draw for u8 {
    fn draw(&self) -> String {
    format!("u8: {}", *self)
    }
    }

    impl Draw for f64 {
    fn draw(&self) -> String {
    format!("f64: {}", *self)
    }
    }

    // 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box<T> 可以被隐式转换成函数参数签名中的 Box<dyn Draw>
    fn draw1(x: Box<dyn Draw>) {
    // 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
    x.draw();
    }

    fn draw2(x: &dyn Draw) {
    x.draw();
    }

    fn main() {
    let x = 1.1f64;
    // do_something(&x);
    let y = 8u8;

    // x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box<T> 可以在函数调用时隐式地被转换为特征对象 Box<dyn Draw>
    // 基于 x 的值创建一个 Box<f64> 类型的智能指针,指针指向的数据被放置在了堆上
    draw1(Box::new(x));
    // 基于 y 的值创建一个 Box<u8> 类型的智能指针
    draw1(Box::new(y));
    draw2(&x);
    draw2(&y);
    }
    • draw1 函数的参数是 Box<dyn Draw> 形式的特征对象,该特征对象是通过 Box::new(x) 的方式创建的
    • draw2 函数的参数是 &dyn Draw 形式的特征对象,该特征对象是通过 &x 的方式创建的
    • dyn 关键字只用在特征对象的类型声明上,在创建时无需使用 dyn

    可以通过特征对象来代表具体的泛型。

  2. 使用泛型的实现和特征对象的对比

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    pub struct Screen<T: Draw> {
    pub components: Vec<T>,
    }

    impl<T> Screen<T>
    where T: Draw {
    pub fn run(&self) {
    for component in self.components.iter() {
    component.draw();
    }
    }
    }

    ​ 上面的 Screen 的列表中,存储了类型为 T 的元素,然后在 Screen 中使用特征约束让 T 实现了 Draw 特征,进而可以调用 draw 方法。

    ​ 但是这种写法限制了 Screen 实例的 Vec<T> 中的每个元素必须是 Button 类型或者全是 SelectBox 类型。如果只需要同质(相同类型)集合,更倾向于采用泛型+特征约束这种写法,因其实现更清晰,且性能更好(特征对象,需要在运行时从 vtable 动态查找需要调用的方法)。

  3. 特征对象的限制

    不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的:

    • 方法的返回类型不能是 Self
    • 方法没有任何泛型参数

    对象安全对于特征对象是必须的,因为一旦有了特征对象,就不再需要知道实现该特征的具体类型是什么了。如果特征方法返回了具体的 Self 类型,但是特征对象忘记了其真正的类型,那这个 Self 就非常尴尬,因为没人知道它是谁了。但是对于泛型类型参数来说,当使用特征时其会放入具体的类型参数:此具体类型变成了实现该特征的类型的一部分。而当使用特征对象时其具体类型被抹去了,故而无从得知放入泛型参数类型到底是什么。

    标准库中的 Clone 特征就不符合对象安全的要求:

    1
    2
    3
    pub trait Clone {
    fn clone(&self) -> Self;
    }

    因为它的其中一个方法,返回了 Self 类型,因此它是对象不安全的。

  4. 特征对象的动态分发

    ​ 静态分发:编译器会为每一个泛型参数对应的具体类型生成一份代码

    ​ 动态分发:直到运行时,才能确定需要调用什么方法。编译器无法知晓所有可能用于特征对象代码的类型,所以它也不知道应该调用哪个类型的哪个方法实现。

    img

    • 特征对象大小不固定:这是因为,对于特征 Draw,类型 Button 可以实现特征 Draw,类型 SelectBox 也可以实现特征 Draw,因此特征没有固定大小
    • 几乎总是使用特征对象的引用方式,如&dyn DrawBox<dyn Draw>
      • 虽然特征对象没有固定大小,但它的引用类型的大小是固定的,它由两个指针组成(ptrvptr),因此占用两个指针大小
      • 一个指针 ptr 指向实现了特征 Draw 的具体类型的实例,也就是当作特征 Draw 来用的类型的实例,比如类型 Button 的实例、类型 SelectBox 的实例
      • 另一个指针 vptr 指向一个虚表 vtablevtable 中保存了类型 Button 或类型 SelectBox 的实例对于可以调用的实现于特征 Draw 的方法。当调用方法时,直接从 vtable 中找到方法并调用。之所以要使用一个 vtable 来保存各实例的方法,是因为实现了特征 Draw 的类型有多种,这些类型拥有的方法各不相同,当将这些类型的实例都当作特征 Draw 来使用时(此时,它们全都看作是特征 Draw 类型的实例),有必要区分这些实例各自有哪些方法可调用

    简而言之,当类型 Button 实现了特征 Draw 时,类型 Button 的实例对象 btn 可以当作特征 Draw 的特征对象类型来使用,btn 中保存了作为特征对象的数据指针(指向类型 Button 的实例数据)和行为指针(指向 vtable)。

    一定要注意,此时的 btnDraw 的特征对象的实例,而不再是具体类型 Button 的实例,而且 btnvtable 只包含了实现自特征 Draw 的那些方法(比如 draw),因此 btn 只能调用实现于特征 Drawdraw 方法,而不能调用类型 Button 本身实现的方法和类型 Button 实现于其他特征的方法。也就是说,btn 是哪个特征对象的实例,它的 vtable 中就包含了该特征的方法。

特征进阶内容

  1. 关联类型

    在特征定义的语句块中,声明一个自定义类型,这样就可以在特征中使用这个类型。

    1
    2
    3
    4
    5
    pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
    }

Rustlings习题整理

map()的用法

1
2
3
4
5
6
7
8
fn vec_map(v: &Vec<i32>) -> Vec<i32> {
v.iter().map(|element| {
// TODO: Do the same thing as above - but instead of mutating the
// Vec, you can just return the new number!
*element * 2
}).collect()
}
//map方法由原来的迭代器生成一个新的迭代器,对旧迭代器的每一个方法都调用该闭包

字符串和切片操作

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
fn trim_me(input: &str) -> String {
// TODO: Remove whitespace from both ends of a string!
input.trim().to_string()
}

fn compose_me(input: &str) -> String {
// TODO: Add " world!" to the string! There's multiple ways to do this!
input.to_string() + " world!"
}

fn replace_me(input: &str) -> String {
// TODO: Replace "cars" in the string with "balloons"!
input.replace("cars","balloons").to_string()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn trim_a_string() {
assert_eq!(trim_me("Hello! "), "Hello!");
assert_eq!(trim_me(" What's up!"), "What's up!");
assert_eq!(trim_me(" Hola! "), "Hola!");
}

#[test]
fn compose_a_string() {
assert_eq!(compose_me("Hello"), "Hello world!");
assert_eq!(compose_me("Goodbye"), "Goodbye world!");
}

#[test]
fn replace_a_string() {
assert_eq!(replace_me("I think cars are cool"), "I think balloons are cool");
assert_eq!(replace_me("I love to look at cars"), "I love to look at balloons");
}
}

判断字符串和字符串切片的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn string_slice(arg: &str) {
println!("{}", arg);
}
fn string(arg: String) {
println!("{}", arg);
}

fn main() {
string_slice("blue");
string("red".to_string());
string(String::from("hi"));
string("rust is fun!".to_owned());
//to_owned()方法用于从借用的数据中创建一个具有所有权的副本
//和clone方法的区别是如果传入的参数是引用类型的,可以通过复制获得其所有权
string_slice("nice weather".into());
string(format!("Interpolation {}", "Station"));
string_slice(&String::from("abc")[0..1]);
string_slice(" hello there ".trim());
string("Happy Monday!".to_string().replace("Mon", "Tues"));
string("mY sHiFt KeY iS sTiCkY".to_lowercase());
//to_lowercase()返回此字符串切片的小写等效项,类型为string
}
clone() to_owned()
T T -> T T -> T
&T &T -> &T &T -> T

模块use

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mod delicious_snacks {
// TODO: Fix these use statements
pub use self::fruits::PEAR as fruit;//修改为pub才对外可见
pub use self::veggies::CUCUMBER as veggie;

mod fruits {
pub const PEAR: &'static str = "Pear";
pub const APPLE: &'static str = "Apple";
}

mod veggies {
pub const CUCUMBER: &'static str = "Cucumber";
pub const CARROT: &'static str = "Carrot";
}
}

fn main() {
println!(
"favorite snacks: {} and {}",
delicious_snacks::fruit,
delicious_snacks::veggie
);
}

比赛统计

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
use std::collections::HashMap;

// A structure to store the goal details of a team.
struct Team {
goals_scored: u8,
goals_conceded: u8,
}

fn build_scores_table(results: String) -> HashMap<String, Team> {
// The name of the team is the key and its associated struct is the value.
let mut scores: HashMap<String, Team> = HashMap::new();

for r in results.lines() {
let v: Vec<&str> = r.split(',').collect();
let team_1_name = v[0].to_string();
let team_1_score: u8 = v[2].parse().unwrap();
let team_2_name = v[1].to_string();
let team_2_score: u8 = v[3].parse().unwrap();
//注意Team是没有实现可加性的,只可以按照Team内部的元素来操作
let team1 = scores.entry(team_1_name).or_insert(Team{
goals_scored:0,
goals_conceded:0
});
team1.goals_scored += team_1_score;
team1.goals_conceded += team_2_score;

let team2 = scores.entry(team_2_name).or_insert(Team{
goals_scored:0,
goals_conceded:0
});
team2.goals_scored += team_2_score;
team2.goals_conceded += team_1_score;
}
scores
}

#[cfg(test)]
mod tests {
use super::*;

fn get_results() -> String {
let results = "".to_string()
+ "England,France,4,2\n"
+ "France,Italy,3,1\n"
+ "Poland,Spain,2,0\n"
+ "Germany,England,2,1\n";
results
}

#[test]
fn build_scores() {
let scores = build_scores_table(get_results());

let mut keys: Vec<&String> = scores.keys().collect();
keys.sort();
assert_eq!(
keys,
vec!["England", "France", "Germany", "Italy", "Poland", "Spain"]
);
}

#[test]
fn validate_team_score_1() {
let scores = build_scores_table(get_results());
let team = scores.get("England").unwrap();
assert_eq!(team.goals_scored, 5);
assert_eq!(team.goals_conceded, 4);
}

#[test]
fn validate_team_score_2() {
let scores = build_scores_table(get_results());
let team = scores.get("Spain").unwrap();
assert_eq!(team.goals_scored, 0);
assert_eq!(team.goals_conceded, 2);
}
}

quiz2

首先要观察代码判断其类型,随后用match表达式匹配枚举类型,做出相应的处理

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
pub enum Command {
Uppercase,
Trim,
Append(usize),
}

mod my_module {
use super::Command;

// TODO: Complete the function signature!
pub fn transformer(input: Vec<(String,Command)>) -> Vec<String> {
// TODO: Complete the output declaration!
let mut output: Vec<String> = vec![];
for (string, command) in input.iter() {
// TODO: Complete the function body. You can do it!
let applied_string:String = match command{
Command::Uppercase => string.to_uppercase(),
Command::Trim => string.trim().to_string(),
Command::Append(n) => format!("{}{}",string,"bar".repeat(*n)),
};
output.push(applied_string);
}
output
}
}

#[cfg(test)]
mod tests {
// TODO: What do we need to import to have `transformer` in scope?
use crate::my_module::transformer;
use super::Command;

#[test]
fn it_works() {
let output = transformer(vec![
("hello".into(), Command::Uppercase),
(" all roads lead to rome! ".into(), Command::Trim),
("foo".into(), Command::Append(1)),
("bar".into(), Command::Append(5)),
]);
assert_eq!(output[0], "HELLO");
assert_eq!(output[1], "all roads lead to rome!");
assert_eq!(output[2], "foobar");
assert_eq!(output[3], "barbarbarbarbarbar");
}
}

从Option中取出值

1
2
3
4
5
6
7
8
9
10
11
12
//如果你确定Option中是有值的,可以使用unwrap()方法直接取出来
let my_option: Option<i32> = Some(5); // 一个Option<i32>类型的变量

let value = my_option.unwrap();
println!("The value is: {}", value);

//如果要处理可能有None的情况,可以使用unwrap_or(初始值)
//为None的情况设置一个初始值
let my_option: Option<i32> = Some(5); // 一个Option<i32>类型的变量

let value = my_option.unwrap_or(0); // 如果my_option是None,则使用默认值0
println!("The value is: {}", value);

Option的类型问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let range = 10;
let mut optional_integers: Vec<Option<i8>> = vec![None];

for i in 1..(range + 1) {
optional_integers.push(Some(i));
}

let mut cursor = range;

//pop()函数会包着一层Some()在外面
while let Some(Some(integer)) = optional_integers.pop() {
assert_eq!(integer, cursor);
cursor -= 1;
}

所有权的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Point {
x: i32,
y: i32,
}

fn main() {
let y: Option<Point> = Some(Point { x: 100, y: 200 });

match y {
Some(ref p) => println!("Co-ordinates are {},{} ", p.x, p.y),
//加ref是为了防止所有权的转移
_ => panic!("no match!"),
}
y; // Fix without deleting this line.
}

? 表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
impl ParsePosNonzeroError {
fn from_creation(err: CreationError) -> ParsePosNonzeroError {
ParsePosNonzeroError::Creation(err)
}
// TODO: add another error conversion function here.
fn from_parseint(err: ParseIntError) -> ParsePosNonzeroError{
ParsePosNonzeroError::ParseInt(err)
}
}

fn parse_pos_nonzero(s: &str) -> Result<PositiveNonzeroInteger, ParsePosNonzeroError> {
// TODO: change this to return an appropriate error instead of panicking
// when `parse()` returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?;
PositiveNonzeroInteger::new(x).map_err(ParsePosNonzeroError::from_creation)
}

为动态数组Vector实现特征

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
trait AppendBar {
fn append_bar(self) -> Self;
}

// TODO: Implement trait `AppendBar` for a vector of strings.
impl AppendBar for Vec<String>{
fn append_bar(mut self) -> Self{ //声明可变mut
self.push("Bar".to_string());
self
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn is_vec_pop_eq_bar() {
let mut foo = vec![String::from("Foo")].append_bar();
assert_eq!(foo.pop().unwrap(), String::from("Bar"));
assert_eq!(foo.pop().unwrap(), String::from("Foo"));
}
}

特征约束代替类型

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
pub trait Licensed {
fn licensing_info(&self) -> String {
"some information".to_string()
}
}

struct SomeSoftware {}

struct OtherSoftware {}

impl Licensed for SomeSoftware {}
impl Licensed for OtherSoftware {}

// YOU MAY ONLY CHANGE THE NEXT LINE
fn compare_license_types(software:impl Licensed, software_two:impl Licensed) -> bool {
software.licensing_info() == software_two.licensing_info()
}
//下面有SomeSoftware和OtherSoftware两种类型
//用impl Licensed可以指代他们

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn compare_license_information() {
let some_software = SomeSoftware {};
let other_software = OtherSoftware {};

assert!(compare_license_types(some_software, other_software));
}

#[test]
fn compare_license_information_backwards() {
let some_software = SomeSoftware {};
let other_software = OtherSoftware {};

assert!(compare_license_types(other_software, some_software));
}
}

#[should_panic]

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
struct Rectangle {
width: i32,
height: i32
}

impl Rectangle {
// Only change the test functions themselves
pub fn new(width: i32, height: i32) -> Self {
if width <= 0 || height <= 0 {
panic!("Rectangle width and height cannot be negative!")
}
Rectangle {width, height}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn correct_width_and_height() {
// This test should check if the rectangle is the size that we pass into its constructor
let rect = Rectangle::new(10, 20);
assert_eq!(rect.width, 10); // check width
assert_eq!(rect.height, 20); // check height
}

#[test]
#[should_panic]
fn negative_width() {
// This test should check if program panics when we try to create rectangle with negative width
let _rect = Rectangle::new(-10, 10);

}

#[test]
#[should_panic]
fn negative_height() {
// This test should check if program panics when we try to create rectangle with negative height
let _rect = Rectangle::new(10, -10);
}
}

迭代器方法

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
pub fn capitalize_first(input: &str) -> String {
let mut c = input.chars();
match c.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + c.as_str(),
}
}

// Step 2.
// Apply the `capitalize_first` function to a slice of string slices.
// Return a vector of strings.
// ["hello", "world"] -> ["Hello", "World"]
pub fn capitalize_words_vector(words: &[&str]) -> Vec<String> {
words.iter().map(
|&word| {
capitalize_first(word)
}
).collect()
}

// Step 3.
// Apply the `capitalize_first` function again to a slice of string slices.
// Return a single string.
// ["hello", " ", "world"] -> "Hello World"
pub fn capitalize_words_string(words: &[&str]) -> String {
words.iter().map(
|&word| {
capitalize_first(word)
}
).collect()
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_success() {
assert_eq!(capitalize_first("hello"), "Hello");
}

#[test]
fn test_empty() {
assert_eq!(capitalize_first(""), "");
}

#[test]
fn test_iterate_string_vec() {
let words = vec!["hello", "world"];
assert_eq!(capitalize_words_vector(&words), ["Hello", "World"]);
}

#[test]
fn test_iterate_into_string() {
let words = vec!["hello", " ", "world"];
assert_eq!(capitalize_words_string(&words), "Hello World");
}
}

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
//宏的定义要在使用之前
macro_rules! my_macro {
() => {
println!("Check out my macro!");
};
}

fn main() {
my_macro!();
}

//使用分号区分不同的模式
#[rustfmt::skip]
macro_rules! my_macro {
() => {
println!("Check out my macro!");
}; //使用分号来区分不同的模式
($val:expr) => {
println!("Look at this other macro: {}", $val);
}
}

fn main() {
my_macro!();
my_macro!(7777);
}

Rcore实验感想

写在前面

由于实验要求不能够贴代码,因此本报告重点就是日记这种了……

会简单的讲一下过程

Lab3

看了一下自己写的东西……玛德我原本也贴了不少代码

行吧……那就放些记录性的东西

……

报错挺多的,反正就照着编译器一个一个来吧……

类型错误……

image-20240428141340827

missing documentations for functions

不写注释也不行

image-20240428142025031

Lab4

重写sys_get_time和sys_task_info

先看看原本的sys_get_time是如何实现的?

对指针*ts所指向的内存空间赋值时间信息,在引入虚存之前应用空间和内核空间之间不存在隔离,二者都可以直接访问到*ts所在位置。在引入虚存之后,每个应用以及内核本身都有独立的地址空间,没办法访问了。

因此我们需要想办法,使得OS能够访问到应用所在的位置,需要完成二者地址的翻译。

……

和mmap那个题目一样,sys_get_time以及sys_task_info也是需要在当前任务下才行

为什么突然写回来了呢……(因为在线CI测了sys_get_time还有sys_task_info 发现自己写的根本就不对

我知道哪里不行了 我的实现没问题 搞得我还重写了一次

每次执行系统调用的时候忘记调用add_syscall_num

mmap

insert_frame_area函数是比较值得参考的一个函数

不仅仅是函数实现的功能类似,用法也很值得学习

……

随后就是漫长的调试……

这里注意到特殊的一行

[kernel] PageFault in application, bad addr = {:#x}, bad instruction = {:#x}, kernel killed it.

先找到这行输出是哪里来的,发现在Trap_handler方法里面

然后再考虑,发现其实我的程序连mmap0都没正常跑完

但是正确分配了页面,要不然就不会有start_va:0x10000000~end_va:0x10001000 map_perm:0x16输出

这里的0x16完全没问题(之前看成10进制了)

0001 0110 代表U,W,R被置位 而测试用例mmap0给的是3 也就是011 也是对应W,R

image-20240518222104567

妈的 我知道怎么搞了 之所以会不断出现The Page you wanted has been alloced to others的报错信息是因为之前在sys_mmap方法中对MemorySet中mmap()的调用是这样子的

1
2
3
// 获取内核实例 取得所有权完成分配(这样不对 你并没有找到实际你要分配的位置) 实际上你是给内核多次分配了 所以才这样子报错
let num = KERNEL_SPACE.exclusive_access().mmap(start, len, port);
//之所以这样子写主要还是因为参考了前面insert_frame_area的用法 没有具体考虑他使用的上下文

事实上应该找到当前运行的任务,只有当前运行的任务是知道自己的地址空间信息的,具体在TCB里面有一项memory_set

这里也走了点弯路 一开始我的想法是在TaskControlBlock中实现一个get_current_tasks_area(类似于之前get_tasks_start_time一样拿到时间)拿到memory_set的所有权或者引用之后,在sys_mmap()里面再用得到的memory_set来调用(我个人感觉主要还是仿照了前面代码的思路,就非要拿到一个类似于KERNEL_SPACE的地址空间,事实上没必要)

……

实现了mmap之后munmap就比较简单了

这里写一个点 关于munmap的最后一个测试用例

下面给一个比较滑头的办法 检查一下是不是页对齐就行(start硬编码写死了 所以其实你怎么写都差不多

1
2
3
4
5
6
7
8
9
// YOUR JOB: Implement munmap.
pub fn sys_munmap(start: usize, len: usize) -> isize {
trace!("kernel: sys_munmap NOT IMPLEMENTED YET!");
if start % PAGE_SIZE != 0{
return -1;
}
let num = munmap_current_task(start, len);
num
}

按理说应该是实现一个检测解除映射范围和现有的映射区域是否完全一致的方法

明天再想吧……先看看能不能过在线CI

懂了 在线CI看不到报错 我就说为什么lab4的测例全过了assert断言还是不行

image-20240505014117148

这里可以看到比较详细的信息

这里记录一下回退的点 本地执行CI之后需要删除一些未跟踪的文件

1
2
git clean -f  删除未跟踪的文件(不包括目录)
git clean -fd 删除未跟踪的文件和目录

image-20240505171002449

解决了 太sb了

Lab5

之前的测例实现过程

经典内容……

注意一个点 在Lab5里面把TaskManager拆分成了TaskManager和processor两个数据结构

不过他们对进程信息的获取还是通过TaskControlBlock

关于初始化信息补全

忘记记录了……之前的测例实现基本就是cv,注意放到正确的数据结构里面重新实现一次就行

遇到一个新问题

image-20240506003949090

image-20240506004011708

这个Write系统调用莫名其妙多出这么多次数

我决定在增加系统调用的方法中加一行调试,打印一下系统调用编号

1
2
3
4
5
6
/// 添加系统调用
pub fn add_current_syscall_times(&mut self,syscall_id:usize){
let mut current_inner = self.current.as_mut().unwrap().inner_exclusive_access();
current_inner.syscall_times[syscall_id] += 1;
println!("{} + 1\n",syscall_id);
}

image-20240506010214962

出现了奇怪的输出

每次键入一个字符 对应着read waitpid yield write系统调用都+1了

虽然输出流打断了我的输入流 但是功能应该还是正常的 只是我没有键入回车键 所以用户程序没有被正常执行起来

应该是前面的实现有问题(

在父进程通过fork()系统调用创建子进程的时候,子进程不应该继承父进程的系统调用次数和开始时间

系统调用次数应该直接初始化为0才对(重新算

1
2
start_time:get_time_us() / 1000,
syscall_times:[0;MAX_SYSCALL_NUM]

ok 这里可以过了

现在又有新问题了 还是ch3_taskinfo的测例

image-20240506011435160

好像还是过不了 但是断言错误的次数确实是减少了……

好像是用println!()打印调试信息的问题,如果去掉的话Write系统调用的次数就不会增加

image-20240506013300058

还真是 回头看了一下console.rs里面对println!()的实现 很明显是基于Write

不然平白无故你的OS怎么能打印东西的……把这事情给忘记了

先一次性把时间信息都打印下来吧 后面就不看了

image-20240506013635316

我切换分支到ch4重新跑一下这个用例 t1=43 t2=544 t3=544 info.time=501是没有问题的

睡了 明天再说

……睡觉的时候突然想到Lab5的run_tasks()方法应该是没有修改 所以没有把时间信息记录下来

没错 就是在此处补一个记录时间的功能就ok了

Spawn系统调用实现

一遍过 感觉还是比较简单的…… 主要就是fork() new() 还有exec()的仿写

注意对parent字段特殊处理

spawn出来的进程的父进程应该为当前运行的进程

Stride调度

首先是在TCB里面增加进程优先级的字段priority和步长调度的参考数据stride

其实是对初始化信息的补全

一开始我是看到的processor.rsrun_tasks()模块,里面有一个fetch_tasks()的过程,取得目前应该运行的任务。但是fetch_tasks()是局限在Task Manager内部,缺少Processor结构,也就是当下的任务状态拿不到(就是有一种可能性是当下正在运行的进程stride还是最小)

然后继续看源码

感觉这几个函数之间的关系有点懵

……

后面感觉还是得在add位置实现,也就是fetch还是从队头把进程取出来,但是在add增加进程的时候维护所有进程的stride顺序

Lab6

之前的测例

首先就是要通过之前的测例 sys_spawn和之前会有一些区别

主要就是获得程序数据的方式有差异

本实验测试点

image-20240510225304112

差最后一个

???

byd什么勾八

怎么实验还不能复现的 本地跑差一个点 在线ci全过了是吧

image-20240510225444383

Lab8

如果启用死锁检测功能的话,主要的检测就是在上锁相关的操作检测是否合法

这里有个困惑的点,就是一开始没搞懂资源到底是什么

其实就是各类的lock……能够得到锁 就代表得到了某个特定的资源

初始化是一门玄学……

需要自己设置好两个常量MAX_THREADSMAX_RESOURES的数量,代表当前可以获得的资源

……

一些问题

image-20240516200029337

就卡在这里了,也不知道怎么回事

gpt问了一下

……死锁了 我就说为什么寄了

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.参考文档: