使用Rust开发操作系统(自旋锁以及print!和println!宏实现)

释放双眼,带上耳机,听听看~!

print!和println!宏实现

  • 自旋锁

  • 原子操作

    • Rust中的原子操作
    • Ordering
    • 顺序一致性
    • 获取 – 释放
    • Relaxed
    • 自旋锁的实现
  • 改造之前的代码

  • print!和println!

  • 接下来要做什么

在上一章中我们实现了基本的打印功能,现在的打印功能使用起来不是很方便,因此我们对之前编写打印功能进行优化

自旋锁

原子操作

为了更好理解自旋锁,我们需要了解一下原子操作,原子操作指在执行过程中不会被任何其它任务或事件中断,一个任务要么做要么不做,不能在做的过程中被打断,这个特性需要硬件支持在x86平台上,CPU提供了在指令执行期间对总线加锁的手段

Rust中的原子操作

在Rust中我们可以使用std::sync::atomic包来使用原子操作,在#![no_std]环境中我们可以使用core::sync::atomic使用

atomic包提供了AtomicBool,AtomicIsize,AtomicUsize,AtomicI8,AtomicU16等类型的原子操作

每个方法都会使用Ordering枚举表明内存屏障的强度,Rust的原子顺序LLVM原子顺序一致,原子类型可以存储在静态变量中,可以使用常量初始化程序(如AtomicBool :: new)进行初始化

原子访问可以告诉硬件和编译器,我们的程序是多线程的。每一个原子访问都关联一种 “排序方式”,以确定它和其他访问之间的关系。归根结底,就是告诉编译器和硬件什么是它们不能做的。对于编译器,主要指的是命令的重排。而对于硬件,指的是写操作的结果如何同步到其他的线程
— 《Rust高级编程》

Ordering

内存顺序(Memory orderings)指定原子操作同步内存的方式,以下是常用的几种状态

  • Relaxed
  • Release(获取)
  • Acquire(释放)
  • AcqRel
  • SeqCst(顺序一致性)

以下内容摘自《Rust高级编程》,感觉解释比我好,PS:才不是因为懒 (笑)

顺序一致性

顺序一致性是所有排序方式中最强大的,包含了其他所有排序方式的约束条件。直观上看,顺序一致性操作不能被重排:在同一个线程中,SeqCst 之前的访问永远在它之前,之后的访问永远在它之后。只使用顺序一致性原子操作和数据访问就可以构建一个无数据竞争的程序,这种程序的好处是它的命令在所有线程上都有着唯一的执行流程。而且这个执行流程又很容易推导:它就是每个线程各自执行流程的交叉。如果你使用更弱的原子排序方式的话,这一点并不一定继续有效。

顺序一致性给开发者的便利并不是免费的。即使是在强顺序平台上,顺序一致性也会产生内存屏障 (memory fence)。

事实上,顺序一致性很少是程序正确性的必要条件。但是,如果你对其他内存排序方式模棱两可的话,顺序一致性绝对是你正确的选择。程序执行得稍微慢一点总比执行出错要好!将它变为具有更弱一致性的原子操作也很容易,只要把 SeqCst 变成 Relaxed 就完工了!当然,证明这种变化的正确性就是另外一个问题了。

获取 – 释放

获取和释放经常成对出现。它们的名字就提示了它们的应用场景:它们适用于获取和释放锁,确保临界区不会重叠。

直观看起来,acquire 保证在它之后的访问永远在它之后。可在它之前的操作却有可能被重排到它后面、类似的,release 保证它之前的操作永远在它之前。但是它后面的操作可能被重排到它前面。

当线程 A 释放了一块内存空间,紧接着线程 B 获取了同一块内存,这时因果关系就确定了。在 A 释放之前的所有写操作的结果,B 在获取之后都能看到。但是,它们和其他线程之间没有确定因果关系。同理,如果 A 和 B 访问的是不同的内存,它们也没有因果关系。

所以,释放 – 获取的基本用法很简单:你获取一块内存并进入临界区,然后释放内存并离开临界区


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1use std::sync::Arc;
2use std::sync::atomic::{AtomicBool, Ordering};
3use std::thread;
4
5fn main() {
6    let lock = Arc::new(AtomicBool::new(false)); // 我上锁了吗?
7
8    // ...用某种方式把锁分发到各个线程...
9
10    // 设置值为true,以尝试获取锁
11    while lock.compare_and_swap(false, true, Ordering::Acquire) {}
12    // 跳出循环,表明我们获取到了锁!
13
14    // ...恐怖的数据访问...
15
16    // 工作完成了,释放锁
17    lock.store(false, Ordering::Release);
18}
19
20

在强顺序平台上,大多数的访问都有释放和获取的语义,释放和获取通常是无开销的。不过在弱顺序平台上不是这样。

Relaxed

Relaxed 访问是最弱的。它们可以被随意重排,也没有先后关系。但是 Relaxed 操作依然是原子的。也就是说,它并不算是数据访问,所有对它的读 – 修改 – 写操作都是原子的。Relaxed 操作适用于那些你希望发生但又并不特别在意的事情。比如,多线程可以使用 Relaxed 的 fetch_add 来增加计数器,如果你不使用计数器的值去同步其他的访问,这个操作就是安全的。

在强顺序平台上使用 Relaxed 没什么好处,因为它们通常都有释放 – 获取语义。不过,在弱顺序平台上,Relaxed 可以获取更小的开销。

自旋锁的实现

根据之前的理论,我们现在就开始实现自旋锁

我们创建一个新的项目称为system,我们编写的代码最终要以库的形式使用,因此在创建的时候选择创建库(library)而不是可执行(executable)

通过一下命令便可以创建


1
2
3
1cargo new system --lib
2
3

我们的项目结构为


1
2
3
4
5
6
7
8
9
10
11
1system
2|
3|__ src
4|  |
5|  |__ lib.rs
6|
7|__ Cargo.toml
8|
9|__ .gitignore
10
11

我们创建一个src/mutex.rs文件(别忘记在lib.rs中声明模块),然后写入以下内容


1
2
3
4
5
6
7
8
9
10
11
12
13
1use core::cell::UnsafeCell;
2use core::sync::atomic::AtomicBool;
3
4struct Mutex<T: ?Sized> {
5    lock: AtomicBool,
6    data: UnsafeCell<T>,
7}
8pub struct MutexGuard<'a, T: ?Sized + 'a> {
9    lock: &'a AtomicBool,
10    data: &'a mut T,
11}
12
13

在Mutex中我们使用lock表示是否获得锁,使用data,存储需要同步访问的资源,在这里我们的泛型使用了Sized约束,Sized表示该类型在编译时已经确定大小,结构体中所有参数都必须实现了Sized绑定,特殊语法是?Sized表示如果绑定不适合使用将会移除,MutexGuard获取被保护的资源,当我们获取锁时,将会返回MutexGuard,当MutexGuard的声明周期结束时将会自动释放锁

紧接着我们为Mutex实现几个方法


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1impl<T> Mutex<T> {
2    pub const fn  new(data: T) -> Mutex<T> {
3        Mutex {
4            lock: AtomicBool::new(false),
5            data: UnsafeCell::new(data),
6        }
7    }
8
9    pub fn into_runner(self) -> T {
10        // 注意data的变量名一定要跟Mutex中的成员名一致
11        // 这里只获取Mutex.data
12        let Mutex { data, .. } = self;
13        data.into_inner()
14    }
15}
16
17

into_runner方法将获取data中的原始数据

现在我们开始实现自旋锁


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
1use core::sync::atomic::{AtomicBool, Ordering, spin_loop_hint};
2
3impl<T: ?Sized> Mutex<T> {
4    fn obtain_lock(&self) {
5        // 尝试获得锁
6        while self.lock.compare_and_swap(false, true, Ordering::Acquire) {
7            // 循环判断是否已经解锁如果没有解锁
8            while self.lock.load(Ordering::Relaxed) {
9                // 向处理器发出信号,表明现在处于自旋状态
10                spin_loop_hint();
11            }
12        }
13        // 跳出循环后表明获得锁
14    }
15    pub fn lock(&self) -> MutexGuard<T> {
16        self.obtain_lock();
17        MutexGuard {
18            lock: &self.lock,
19            data: unsafe { &mut *self.data.get() },
20        }
21    }
22}
23
24

obtain_lock用于获取锁是私有方法,仅供lock方法使用,当一个线程进入obtain_lock后会尝试获取锁,如果没获取它将一直在内层循环中不断的判断是否已经解锁lock=false,为了过度消耗CPU资源我们使用spin_loop_hint来告诉CPU线程处于自旋状态,spin_loop_hint使用llvm.x86.sse2.pause指令来完成,pause指令向CPU发送自旋信号,CPU收到自旋信号后,处理器可以通过节省电源或切换超线程等优化

lock为用户调用方法,如果锁获取成功obtain_lock将会执行完毕,最后将原始资源返回

我们提供的函数有些"暴躁"了,如果没获取到会一直获取,我们应该提供一个"温柔"点的方法来尝试获取


1
2
3
4
5
6
7
8
9
10
11
12
1pub fn try_lock(&self) -> Option<MutexGuard<T>> {
2    if self.lock.compare_and_swap(false, true, Ordering::Acquire) == false {
3        Some(MutexGuard {
4            lock: &self.lock,
5            data: unsafe { &mut *self.data.get() },
6        })
7    } else {
8        None
9    }
10}
11
12

try_lock将会判断是否获取到锁,如果获取到返回原始数据,否则返回None

我们获取锁实现了接下来实现解锁操作,我们只要将之前的锁住状态true改为解锁状态false


1
2
3
4
5
6
7
1impl<'a, T: ?Sized> Drop for MutexGuard<T> {
2    fn drop(&mut self) {
3        self.lock.store(false, Ordering::Release);
4    }
5}
6
7

之后我们需要实现Deref和DerefMut来完成不可变解引用操作


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1impl<'a, T: Sized> Deref for MutexGuard<T> {
2    type Target = T;
3
4    fn deref(&self) -> &T {
5        &*self.data
6    }
7}
8impl<'a, T: Sized> DerefMut for MutexGuard<'a, T> {
9    fn deref_mut(&mut self) -> &mut T { &mut *self.data }
10}
11
12unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<T> {}
13unsafe impl<T: ?Sized + Sync> Send for MutexGuard<T> {}
14
15

我们以后可以使用自旋锁来完成多线程操作,因此实现了Sync和Send

最终我们的mutex.rs文件内容如下


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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
1use core::sync::atomic::{AtomicBool, Ordering, spin_loop_hint};
2use core::cell::UnsafeCell;
3use core::marker::Sync;
4use core::ops::{Drop, Deref, DerefMut};
5use core::option::Option::{self, None, Some};
6use core::default::Default;
7
8// 在编译时已经确定大小
9// 所有参数都必须实现了Sized绑定
10// 特殊语法是?Sized表示如果绑定不适合使用将会移除
11pub struct Mutex<T: ?Sized> {
12    lock: AtomicBool,
13    data: UnsafeCell<T>,
14}
15
16pub struct MutexGuard<'a, T: ?Sized + 'a> {
17    lock: &'a AtomicBool,
18    data: &'a mut T,
19}
20
21
22impl<T> Mutex<T> {
23    pub const fn new(data: T) -> Mutex<T> {
24        Mutex {
25            lock: AtomicBool::new(false),
26            data: UnsafeCell::new(data),
27        }
28    }
29
30    pub fn into_runner(self) -> T {
31        // 注意data的变量名一定要跟Mutex中的成员名一致
32        // 这里只获取Mutex.data
33        let Mutex { data, .. } = self;
34        data.into_inner()
35    }
36}
37
38unsafe impl<T: ?Sized + Sync> Sync for Mutex<T> {}
39
40unsafe impl<T: ?Sized + Sync> Send for Mutex<T> {}
41
42impl<T: ?Sized> Mutex<T> {
43    fn obtain_lock(&self) {
44        // 尝试获得锁
45        while self.lock.compare_and_swap(false, true, Ordering::Acquire) {
46            // 循环判断是否已经解锁如果没有解锁
47            while self.lock.load(Ordering::Relaxed) {
48                // 向处理器发出信号,表明现在处于自旋状态
49                spin_loop_hint();
50            }
51        }
52        // 跳出循环后表明获得锁
53    }
54
55    pub fn lock(&self) -> MutexGuard<T> {
56        self.obtain_lock();
57        MutexGuard {
58            lock: &self.lock,
59            data: unsafe { &mut *self.data.get() },
60        }
61    }
62
63    pub unsafe fn force_unlock(&self) {
64        self.lock.store(false, Ordering::Release)
65    }
66
67    pub fn try_lock(&self) -> Option<MutexGuard<T>> {
68        if self.lock.compare_and_swap(false, true, Ordering::Acquire) == false {
69            Some(MutexGuard {
70                lock: &self.lock,
71                data: unsafe { &mut *self.data.get() },
72            })
73        } else {
74            None
75        }
76    }
77}
78
79impl<T: Sized + Default> Default for Mutex<T> {
80    fn default() -> Mutex<T> {
81        Mutex::new(Default::default())
82    }
83}
84
85impl<'a, T: Sized> DerefMut for MutexGuard<'a, T> {
86    fn deref_mut(&mut self) -> &mut T { &mut *self.data }
87}
88
89impl<'a, T: Sized> Deref for MutexGuard<'a, T> {
90    type Target = T;
91
92    fn deref(&self) -> &T {
93        &*self.data
94    }
95}
96
97impl<'a, T: ?Sized> Drop for MutexGuard<'a, T> {
98    fn drop(&mut self) {
99        self.lock.store(false, Ordering::Release);
100    }
101}
102
103

改造之前的代码

回到我们的droll_os项目,我们需要引用我们之前写的库,在Cargo.toml文件中添加如下内容


1
2
3
4
1[dependencies]
2system={path="/home/admins/Rust/lib/",version="0.1.0"}
3
4

启用system是我们创建的cargo项目,path是我们cargo项目的路径

我们在上一篇中实现的代码是这样子的


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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
1// vga.rs
2#[allow(dead_code)]
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4// 指定字节对齐方式
5#[repr(u8)]
6pub enum Color {
7    // 黑色
8    Black = 0,
9    // 蓝色
10    Blue = 1,
11    // 绿色
12    Green = 2,
13    // 青色
14    Cyan = 3,
15    // 红色
16    Red = 4,
17    // 品红色
18    Magenta = 5,
19    // 棕色
20    Brown = 6,
21    // 浅灰色
22    LightGray = 7,
23    // 深灰色
24    DarkGray = 8,
25    // 浅蓝色
26    LightBlue = 9,
27    // 浅绿色
28    LightGreen = 10,
29    // 浅青色
30    LightCyan = 11,
31    // 亮红色
32    LightRed = 12,
33    // 粉色
34    Pink = 13,
35    // 黄色
36    Yellow = 14,
37    // 白色
38    White = 15,
39}
40
41#[derive(Debug, Copy, Clone, Eq, PartialEq)]
42#[repr(transparent)]
43pub struct ColorCode(u8);
44
45impl ColorCode {
46    pub fn new(foreground: Color, background: Color) -> ColorCode { // foreground: 前景色 // background: 背景色
47        ColorCode((background as u8) << 4 | foreground as u8)
48    }
49}
50
51
52#[derive(Debug, Copy, Clone, Eq, PartialEq)]
53#[repr(C)]
54struct ScreenChar {
55    // ASCII 字符
56    ascii_char: u8,
57    // 字符颜色
58    color: ColorCode,
59}
60
61pub struct Buffer {
62    // 二维数组,使用VGA 模式显示文本
63    chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
64}
65
66// Writer总是以\n结尾
67pub struct Writer {
68    // 当前行号
69    pub row_position: usize,
70    // 当前列号
71    pub column_position: usize,
72    // 字符颜色
73    pub color_code: ColorCode,
74    // 屏幕缓冲区 生命周期位整个程序运行时间
75    pub buffer: &'static mut Buffer,
76}
77
78
79impl Writer {
80    pub fn write_bytes(&mut self, byte: u8) {
81        match byte {
82            b'\n' => self.new_line(),
83            byte => {
84                // 检查是否超过每行最大字符数
85                if self.column_position >= BUFFER_WIDTH {
86                    // 超过需要换行
87                    self.new_line()
88                }
89
90                let row = self.row_position;
91                let col = self.column_position;
92                let color = self.color_code;
93                // 使用write方法来代替= 保证编译器将永远不会优化写操作。
94                self.buffer.chars[row][col].write(ScreenChar {
95                    ascii_char: byte,
96                    color,
97                });
98
99                self.column_position += 1;
100            }
101        }
102    }
103
104    pub fn write_string(&mut self, s: &str) {
105        for byte in s.bytes() {
106            match byte {
107                //32(空格) - 126(~)
108                0x20..=0x7e | b'\n' => self.write_bytes(byte),
109                // 不是ASCII可打印字符 会打印■
110                _ => self.write_bytes(0xfe),
111            }
112        }
113    }
114
115    fn new_line(&mut self) {
116        // 如果超过最大行数
117        if self.row_position >= BUFFER_HEIGHT {
118            // 清空屏幕
119            self.clear_screen();
120            self.row_position = 0;
121        }
122        self.row_position += 1;
123        self.column_position = 0;
124    }
125
126    fn clear_screen(&mut self) {
127        let blank = ScreenChar {
128            ascii_char: b' ',
129            color: self.color_code,
130        };
131        for row in 0..BUFFER_HEIGHT {
132            for col in 0..BUFFER_WIDTH {
133                self.buffer.chars[row][col].write(blank);
134            }
135        }
136    }
137}
138
139

因此我们使用的时候是这样子的


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1#![no_std]
2#![no_main]
3
4#[macro_use]
5extern crate kernel;
6use core::panic::PanicInfo;
7use kernel::vga::{Writer, ColorCode, Color, Buffer};
8
9#[no_mangle]
10pub extern "C" fn _start() -> ! {
11    let mut w = Writer {
12        row_position: 0,
13        column_position: 0,
14        color_code: ColorCode::new(Color::White, Color::Black),
15        buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
16    };
17    w.write_string("Hello World");
18      loop{}
19}
20
21

假设我们在不同的文件中都需要使用打印字符串,这样我们要写很多次这样的代码,所以我们要把Writer声明成静态全局的,要注意的是,writer会被多个任务同时调用(虽然现在没有进程以后会有的)这样打印的字符将会混乱不堪,因此我们使用我们已经实现的自旋锁

所以我们在vga.rs文件中添加一下内容


1
2
3
4
5
6
7
8
9
1pub static ref WRITER: Mutex<Writer> = Mutex::new(
2    Writer {
3        row_position: 0,
4        column_position: 0,
5        color_code: ColorCode::new(Color::White, Color::Black),
6        buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
7    });
8
9

如果我们现在编译的话会发生错误


1
2
3
1calls in statics are limited to constant functions, tuple structs and tuple variants
2
3

要了解这里发生的情况,我们需要知道静态变量是在编译时初始化的,而不是在运行时初始化的普通变量,Rust编译器评估此类初始化表达式的组件称为“const evaluator”。它的功能仍然有限,但是现在还在不断地优化,例如“允许常量panic”

这里的根本问题是Rust的const求值器无法在编译时将原始指针转换为引用。因此我们需要找到其他的替代方案

因此我们使用lazy_static,lazy_static定义惰性初始化的静态宏。惰性初始化不会在编译时计算其值,而是在首次访问时会初始化自身

我们在Cargo.toml文件中添加一下内容


1
2
3
4
5
1[dependencies.lazy_static]
2version = "1.0"
3features = ["spin_no_std"]
4
5

因此我们可以这样初始化我们的代码


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1use lazy_static::lazy_static;
2// static 懒加载无需在编译时计算其值,而是在首次访问时进行初始化
3lazy_static! {
4    // 保证内部可变性是安全的
5    pub static ref WRITER: Mutex<Writer> = Mutex::new(
6    Writer {
7        row_position: 0,
8        column_position: 0,
9        color_code: ColorCode::new(Color::White, Color::Black),
10        buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
11    });
12}
13
14

这样我们就可以使用WRITER来打印字符了

print!和println!

接下来我们开始去编写print!和prinln!宏我们可以参考一下标准库中的实现


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1#[macro_export]
2#[stable(feature = "rust1", since = "1.0.0")]
3#[allow_internal_unstable(print_internals, format_args_nl)]
4macro_rules! println {
5    () => ($crate::print!("\n"));
6    ($($arg:tt)*) => ({
7        $crate::io::_print($crate::format_args_nl!($($arg)*));
8    })
9}
10
11#[macro_export]
12#[stable(feature = "rust1", since = "1.0.0")]
13#[allow_internal_unstable(print_internals)]
14macro_rules! print {
15    ($($arg:tt)*) => ($crate::io::_print($crate::format_args!($($arg)*)));
16}
17
18

我们可以看到标准库实现中都会调用crate::io::_print来完成_print实现比较复杂因为它们要支持Stdout设备,我们只需要向VGA中打印字符串即可因此我们拷贝这两个宏,并实现自己的_print


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1// vga.rs
2
3#[macro_export]
4macro_rules! println {
5    () => ($crate::print!("\n"));
6    ($($arg:tt)*) => ({
7        $crate::vga::print!("{}\n", format_args!($($arg)*));
8    })
9}
10
11#[macro_export]
12macro_rules! print {
13    ($($arg:tt)*) => ($crate::vga::_print(format_args!($($arg)*)));
14}
15
16#[doc(hidden)]
17pub fn _print(arg: fmt::Arguments) {
18      // 一定要导入Write trait否则会提示没有实现write_fmt方法
19      use core::fmt::Write;
20      WRITER.lock().write_fmt(arg).unwrap();
21}
22
23

我们将#[macro_export]属性添加到两个宏,以使其在crate中的任何位置都可用

请注意,这会将宏放置在crate的根namespace中,因此无法通过使用crate::vga::println导入它们。我们必须使用crate::println来导入即可或者在extern crate <name>上面添加#[macro_use]即可

现在在我们的入口函数中就可以这样使用了


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1#![no_std]
2#![no_main]
3
4#[macro_use]
5extern crate kernel;
6
7use core::panic::PanicInfo;
8
9#[no_mangle]
10pub extern &quot;C&quot; fn _start() -&gt; ! {
11    println!(&quot;Hello World&quot;);
12    loop {}
13}
14
15/// 用于运行过程中异常处理
16#[panic_handler]
17fn panic(info: &amp;PanicInfo) -&gt; ! {
18    println!(&quot;Error: {}&quot;,info);
19    loop {}
20}
21
22

好了,这样我们就可以使用我们自己实现的print!和println!宏了!

接下来要做什么

在下一章中我们开始编写一些工具函数,常用的位操作,描述符等等(大家加油!)

给TA打赏
共{{data.count}}人
人已打赏
安全技术

c++ vector

2022-1-11 12:36:11

病毒疫情

福建省新型冠状病毒肺炎疫情情况

2020-3-18 8:17:00

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索