操作系统概念实验二:实现println宏

实验目的:
print() 函数是学习几乎任何一种软件开发语言时最先学习使用的函数,同时该函数也是最基本和原始的程序调试手段,但该函数的实现却并不简单。本实验的目的在于理解计算机(显卡)字符显示的原理,理解操作系统与硬件的接口方法,并实现一个可打印字符的宏(非系统调用),用于后续的调试和开发。

实验过程:
1)查找文献深入了解 VGA 的字符模式(VGA Text Mode),在实验报告中的合适部分进行记录和描述。
VGA(Video Graphics Array)是IBM于1987年提出的一个使用模拟信号的电脑显示标准,这个标准已对于现今的个人电脑市场已经十分过时。即使如此,VGA仍然是最多制造商所共同支援的一个低标准,个人电脑在加载自己的独特驱动程式之前,都必须支援VGA的标准。例如,微软Windows系列产品的开机画面仍然使用VGA显示模式,这也说明其分辨率和载色数的不足。
VGA这个术语常常不论其图形装置,而直接用于指称640×480的分辨率。VGA装置可以同时储存4个完整的EGA色版,并且它们之间可以快速转换,在画面上看起来就像是即时的变色
除了扩充为256色的EGA式色版,这256种色彩其实可以透过 VGA DAC(Digital-to-analog converter),任意的指定为任何一种颜色。这就程度上改变了原本EGA的色版规则,因为原本在EGA上,这只是一个让程式可以在每个频道(即红绿蓝)在2 bit以下选择最多种颜色的方式。但在VGA下它只是简单的64种颜色一组的表格,每一种都可以单独改变——例如EGA颜色的首两个bit代表红色的数量,在VGA中就不一定如此了。
VGA在指定色版颜色时,一个颜色频道有6个bit,红、绿、蓝各有64种不同的变化,因此总共有 262,144 种颜色。在这其中的任何 256 种颜色可以被选为色版颜色(而这 256 种的任何 16 种可以用来显示 CGA 模式的色彩)。
这个方法最终仍然使了VGA模式在显示EGA和CGA模式时,能够使用前所未有的色彩,因为VGA是使用模拟的方式来绘出EGA和CGA画面。提供一个色版转换的例子:要把文字模式的字符颜色设定为暗红色,暗红色就必须是 CGA 16 色集合中的一种颜色(譬如说,取代 CGA 默认的 7 号灰色),这个 7 号位置将被指定为 EGA 色版中的 42 号,然后 VGA DAC 将 EGA #42 指定为暗红色。则画面上的原本的 CGA 七号灰色,都会变成暗红色。这个技巧在 256 色的 VGA DOS 游戏中,常常被用来表示加载游戏的淡入淡出画面。
总结来说,CGA 和 EGA 同时只能显示 16 种色彩,而 VGA 因为使用了 Mode 13h 而可以一次显示 256 色版中的所有色彩,而这 256 种颜色又是从 262,144 种颜色中挑出的。
VGA的规格表如下:

  • 256 KiB 的 Video RAM
  • 16 色和 256 色模式
  • 总共 262144 种颜色的色版(红、绿、蓝三色各 6 bit,总共 (26)3 种)
  • 选择性的 25.2 MHz 或 28.3 MHz 处理频率
  • 最多 720 个水平像素
  • 最多 480 条线
  • 最高 70 Hz 的更新频率
  • Vertical Blanking interrupt(不是所有卡都支援)
  • 平面模式:最多 16 色(4 bit 面板)
  • Packed-pixel 模式:256 色(Mode 13h)
  • 顺畅卷动画面的能力
  • Some “Raster Ops” support
  • Barrel shifter
  • 支援分割画面
    VGA支援可单独操控像素的APA(All Points Addressable)模式,也支援字母与数字的文字模式。标准的图形模式如下:
  • 640×480×16色
  • 640×350×16色
  • 320×200×16色
  • 320×200×256色(Mode 13h)
    它也支援用模拟的方式画出以往规格的分辨率:EGA、CGA和MDA。
    标准的VGA文字模式使用 80×25 或 40×25 个字母或数字组成的平面。每个字符的块状区域可以选择16种前景色和8种背景色;8种背景色来自bit容量较低的集合(以今天的标准来说,例如 ffffff 或者是 000000)。而字符本身也可设定是否闪烁,而字符的闪烁动作都是同时的。画面的闪烁功能和选择背景颜色的功能是可交换的,换句话说两者只能择一。以上这些选项和IBM先前生产的 CGA 转换器是相同的。
    VGA虽然支援黑白和彩色的文字模式,但黑白模式很少使用。大多的VGA在显示黑白模式时使用彩色模式,即是将灰色字画在黑色背景上。而使用VGA 的单色显示器也能很好的支援这样的彩色模式。现代显示器和显卡若连接不当,偶尔会导致显卡的VGA部份侦测显示器为单色的,而这将使BIOS开机显示为黑白模式。通常在加载操作系统和适当的驱动程序以后,显卡的设定被覆盖,显示器就会变回彩色。
    在彩色的文字模式中,每个字符其实由两个byte代表。较低的一个byte用来显示字符,而较高的byte就用来代表彩色、闪烁等等属性。这种成对的byte模式是从CGA就一直传续下来的。 VGA的英文全称是Video Graphic Array,即显示绘图阵列。VGA支持在640X480的较高分辨率下同时显示16种色彩或256种灰度,同时在320X240分辨率下可以同时显示256种颜色.
    肉眼对颜色的敏感远大于分辨率,所以即使分辨率较低图像依然生动鲜明。VGA由于良好的性能迅速开始流行,厂商们纷纷在VGA基础上加以扩充,如将显存提高至1M并使其支持更高分辨率如800X600或1024X768,这些扩充的模式就称之为VESA(Video Electronics Standards Association,视频电子标准协会)的Super VGA模式,简称SVGA,现在的显卡和显示器都支持SVGA模式。不管是VGA还是SVGA,使用的连线都是15针的梯形插头,传输模拟信号。
    只有在电脑显卡驱动异常,进不去桌面的时候选择这个模式,这个模式会加载系统默认自带的驱动,以方便用户操作

2)在理解原理的基础上参照材料 https://os.phil-opp.com/vga-text-mode(https://zhuanlan.zhihu.com/p/53745617 中文)自行实现 println! 宏
步骤一:创建一个 Rust 模块来处理文字打印,输入文字打印。
在这里插入图片描述

这次代码定义了一个Rust模块,它的内容应当保存在vga_buffer.rs文件中。我们可以把模块的子模块(submodule)文件直接保存到 src/vga_buffer/ 文件夹下,与 vga_buffer.rs 文件共存,而无需创建一个 mod.rs 文件。我们的模块不需要添加子模块,所以我们将之创建为src/vga_buffer.rs文件。

步骤二:颜色
我们使用Rust的枚举表示一种颜色:
在这里插入图片描述

我们使用类似于C语言的枚举,给每一种颜色都明确指定一个数字。在这里每个用repr(u8)注记标注的枚举类型,都会以一个u8的形式存储.
为了描述包含前景色和背景色、完整的颜色代码,我们基于u8创建一个新类型:
在这里插入图片描述

ColorCode类型包装了一个完整的颜色代码字节,包含前景色和背景色信息。我们为他生成Copy和Debug等一系列trait。同时,添加repr(transparent)标记,使得Colorcode和u8有完全相同的内存布局。

步骤三:字符缓冲区
添加更多的结构体,来描述屏幕上的字符和整个字符缓冲区:
在这里插入图片描述

因为Rust并不保证按顺序布局变量。因此,我们需要使用#[rept©]标记结构体。这将按C语言约定的那个的顺序布局它的成员变量,让我们能正确的映射内存片段。对Buffer类型,我们再次使用repr(transparent),来确保类型和它的单个成员有相同的内存布局。
为了输出字符到屏幕,我们来创建一个Writer类型:
在这里插入图片描述

我们将让这个Writer类型将字符写入屏幕的最后一行,并在一行写满或接收到换行符\n的时候,将所有的字符向上位移一行。
在这里插入图片描述

将跟踪光标在最后一行的位置
在这里插入图片描述

前景和背景色将由color_code指定;
另外,我们存入一个VGA字符缓冲区的可变借用到buffer变量中。需要注意这里我们对借用使用显示生命周期,告诉编译器这个借用在何时有效,这个的static,意味着是在整个程序的运行期间有效。
步骤四:打印字符
现在我们使用Writer类型来更改缓冲区内的字符。首先,为了写入一个ASCII码字节,我们创建这样的函数:
在这里插入图片描述

如果这个字节是一个换行符字节\n,我们的Writer不应该打印新字符,相反,它将调用我们稍后会实现的new_line方法;其它的字节应该将在match语句的第二个分支中被打印到屏幕上。
当打印字节时,Writer将检查当前行是否已满。如果已满,它将首先调用new_liine方法将这一行字向上提升,然后写入一个新的ScreenChar到缓冲区,最终将当前的光标位置前进一位。
在这里插入图片描述

上面这部分代码是用来打印整个字符串,操作方式即转换为字节并依次输出。
因为VGA字符缓冲区只支持ASCII码和其他一些定义的字节,而Rust语言的字符串默认编码为UTF-8,所以可能有些字节,VGA缓冲区不支持。我们使用match语句,对不可打印的字节,打印一个■。
临时编写一个函数如下:

在这里插入图片描述

这个函数中,我们首先把整数0xb8000强制转换为一个裸指针;之后,通过运算符*,我们将这个裸指针解引用;最后,我们再通过&mut,再次获得他的可变借用。这里的转换需要unsafe语句块,因为编译器不能保证裸指针是有效的。
为了观察输出,我们需要在_start函数中调用print_something,代码如下:

在这里插入图片描述

编译运行后可以看到黄色的Hello World!字符串将会被打印在屏幕的左下角:
在这里插入图片描述

这里因为ö字符在UTF-8编码下是由两个字节表述的,而这两个字节并不处在可打印的ASCII码字节范围之内。
UTF-8编码的基本特点之一:如果一个字符占用多个字节,那么每个组成它的独立字节都不是有效的ASCII码字节。

步骤五:易失操作
因为rust未来暴力的优化,编译器也许会认为这些写入操作都没有必要,甚至会选择忽略这些操作!所以我们需要把这些操作指定为易失操作。
我们使用volatile库,这个包提供了一个名为Volatile的包装类型和它的read、write方法;这些方法包装了read_volatile和write_volatile函数,从而保证读或写操作不会被编译器优化。
在Cargo.toml文件的dependencies中添加如下内容:
在这里插入图片描述

0.2.6是一个语义版本号,我们使用它来完成VGA缓冲区的volatile写入操作。我们将Buffer类型的定义修改为下列代码:
在这里插入图片描述

在这里,Volatile类型是一个泛型,可以包装几乎所有的类型-这确保我们不会通过普通的写入操作,意外的向它写入数据;我们转而使用提供的write方法。
修改Writer::write_byte方法:
在这里插入图片描述

可以看出,将原先的普通的=赋值方法,修改为write,这样可以确保编译器不再优化写入操作。

步骤六:格式化宏
通过rust提供的格式化宏,我们可以轻松的打印不同类型的变量,如整数或者浮点数。为了支持它们,我们需要实现core::fmt::Write trait;要实现它,唯一需要提供的方法是write_str,它和我们之前编写的write_string方法差别不大,只是返回值类型变成了fmt_Result
在这里插入图片描述

这里Ok(())属于Result枚举类型中的OK,包含一个值为()的变量。
现在我们就可以使用Rust内置的格式化宏write!和writeln!了:
在这里插入图片描述

运行cargo xrun,结果如下:
在这里插入图片描述

步骤七:换行
我们之前因为没有考虑换行符,所以我们实际上是没有处理超过一行字符的情况的。当换行时,我们想要把每个字符向上移动一行——此时最顶上的一行将被删除——然后再最后一行的起始位置继续打印。所以我们要在Writer中实现一个新的new_line方法。
在这里插入图片描述

这里…符号时区间标号的一种,表示左闭右开的区间,因此不包含它的上界。在外层的枚举中,我们从第1行开始,省略了对第0行的枚举过程,因为这一行应该被移出屏幕,即被下一行的字符覆写。
所以我们实现的clear_row方法代码如下:
在这里插入图片描述

向对应缓冲区写入空格,相当于清空了所有字符。

步骤八:全局接口
编写其它模块时,我们希望无需随时拥有Writer实例,便能使用它的方法。
尝试创建一个静态的WRITER变量:
在这里插入图片描述

尝试编译,运行结果如下:
在这里插入图片描述

发生了编译错误。
一般的变量在运行时初始化,而静态变量在编译时初始化。虽然Rust编译器规定了一个称为常量求值器的组件,它应该在编译时处理这样的初始化工作。但是还不能在编译时直接转换裸指针到变量的引用。
问题一:
在这里碰到的问题是第一次编译并不是产生上图的报错,而是提醒函数clear_row和new_line重复定义,在这里并没有第一时间看出到底是哪里重复定义了,我确实之前是没有写这两个函数的啊。又仔细地回去看了一遍,发现有类似于下面这样的调用:
在这里插入图片描述

将这样的两处均进行注释掉之后,就形成了如上的报错信息

步骤九:延迟初始化
lazy_static 的包提供了一个很棒的解决方案:它提供了名为 lazy_static! 的宏,定义了一个延迟初始化的静态变量;这个变量的值将在第一次使用时计算,而非在编译时计算。这时,变量的初始化过程将在运行时执行,任意的初始化代码——无论简单或复杂——都是能够使用的。
将lazy_static包导入到我们的项目:
在这里插入图片描述

由于程序不连接标准库,我们需要启用spin_no_std特性。
使用lazy_static我们就可以定义一个不出问题的WRITER变量:
如下:
在这里插入图片描述

因为这个WRITER目前还是不可变变量:这意味着我们无法向它写入数据,因为所有与写入数据相关的方法都需要实例的可变引用&mut self。种解决方案是使用可变静态(mutable static)的变量,但所有对它的读写操作都被规定为不安全的(unsafe)操作,因为这很容易导致数据竞争或发生其它不好的事情——使用 static mut 极其不被赞成,甚至有一些提案认为应该将它删除。也有其它的替代方案,比如可以尝试使用比如 RefCell 或甚至 UnsafeCell 等类型提供的内部可变性;但这些类型都被设计为非同步类型,即不满足 Sync 约束,所以我们不能在静态变量中使用它们。

步骤十:自旋锁
有一种较为基础的互斥锁实现方式-自旋锁。它不会调用阻塞逻辑,而是在一个小的无限循环中反复尝试获得这个锁,也因此会一直占用CPU时间,直到被它的占用者释放。
添加spin包到项目的依赖项列表:
在这里插入图片描述

现在,我们能够使用自旋的互斥锁,为我们的WRITER类实现安全的内部可变性:
在这里插入图片描述

删除print_something,尝试_start函数中打印字符:
在这里插入图片描述

运行结果如下:
在这里插入图片描述

步骤十一:println!宏
我们把println!和print!两个宏复制过来,但修改部分代码,让这些宏使用我们定义的—_print函数:
在这里插入图片描述

我们首先修改了 println! 宏,在每个使用的 print! 宏前面添加了 $crate 变量。这样我们在只需要使用 println! 时,不必也编写代码导入 print! 宏。
就像标准库做的那样,我们为两个宏都添加了 #[macro_export] 属性,这样在包的其它地方也可以使用它们。需要注意的是,这将占用包的根命名空间,所以我们不能通过 use crate::vga_buffer::println 来导入它们;我们应该使用 use crate::println。
另外,_print 函数将占有静态变量 WRITER 的锁,并调用它的 write_fmt 方法。这个方法是从名为 Write 的 trait 中获得的,所以我们需要导入这个 trait。额外的 unwrap() 函数将在打印不成功的时候 panic;但既然我们的 write_str 总是返回 Ok,这种情况不应该发生。
如果这个宏将能在模块外访问,它们也应当能访问 _print 函数,因此这个函数必须是公有的。但这是一个私有的实现细节,我们添加一个 doc(hidden) 属性,防止它在生成的文档中出现。
修改_start函数如下,就可以在_start里使用 println! 了
在这里插入图片描述

运行结果如下:

在这里插入图片描述

步骤十二:打印panic信息
我们可以在panic处理信息中,使用它打印panic信息和产生位置
修改main.rs如下:
在这里插入图片描述

运行结果如下:
在这里插入图片描述

3)实现清屏(将屏幕上显示的字符全部清除)的功能
模仿clear_row函数,写了如下清屏函数:
在这里插入图片描述

但是这样写了之后,并不能在主函数中进行直接调用,刚开始想着不在impl Writer中写,模仿之前的print_something函数的形式,然后直接在主函数中通过vga_buffer::print_something()这种形式进行调用。如下:
在这里插入图片描述

仿照这个函数一样加入buffer那一行,最后在主函数中进行调用时,报错,提示函数不合法。
于是还是按照之前的方式去写,还是写在Writer中。
查询网上资料之后,发现可以将之用以下方式写成可以直接调用的函数:
在这里插入图片描述

加上这样的函数,在主函数中通过vga_buffer::clr_sc进行调用:
函数如下:
在这里插入图片描述

运行结果如下:
在这里插入图片描述

去掉清屏那一行后:
在这里插入图片描述

只注释掉最后一行之后:
在这里插入图片描述

可以看到清屏成功。
问题二:
在这里碰到的问题就是清屏的时候不能实现panic输出的清除。因为panic实际上是运行时错误的一个标志,那么调用这个函数,就意味着下面的主函数都不再进行。所以清屏函数自然而然也就不能进行。
所以自主的调用清屏函数只能在panic之前使用。

实验完整代码如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值