BUAA-OS-2023-Lab5-1-Exam
本次上机还算中规中矩,能够完全理解题意后,写起来还是很顺手的。(实际上,根本读题的信息都读不全)
Exam - 时间查询与计算
题目背景
在 Lab5 与 Lab2 中,我们分别实现了不同种类外设的读写操作,现在我们需要实现一个能够获取当前物理时间的函数,和一个要求进程必须等待指定时间后才能运行的函数。现在我们介绍如何获取实时间。
GXemul 中除了磁盘 disk
还存在另一类外设——实时时钟 rtc
(Real-Time Clock),其中记录了 Unix 时间戳,即从格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒起至现在的总秒数。为了进一步精确 Unix 时间戳, rtc
中还记录了一个微秒字段。
当我们需要读取 rtc
中存放的时钟的值时,需要先触发时钟更新,才能获取到正确的数据,未更新时首次读取默认是 0(别问我怎么知道的)
在 GXemul 的手册中我们能查阅到 rtc
的起始地址为 0x15000000
,且不同偏移量下的读写效果如下:
偏移 | 效果 | 数据位宽 |
---|---|---|
0x0000 |
读/写:触发时钟更新 | 4 bytes |
0x0010 |
读:从格林威治时间 1970 年 01 月 01 日 00 时 00 分 00 秒起至现在的总秒数 | 4 bytes |
0x0020 |
读:为精确 Unix 时间戳而记录的微秒数,范围为 0 到 999999 微秒(1秒内) | 4 bytes |
题目要求
本题需要完成两个用户态中的函数,并要求在 user/include/lib.h
中声明,user/lib/ipc.c
中实现
u_int get_time(u_int *us)
从 rtc
中读取总秒数和微秒数,总秒数作为该函数的返回值,微秒数通过指针参数 us
返回。
void usleep(u_int us)
在任一用户态进程进入 usleep
函数后,必须停留 us
微秒后才能退出函数。这其中允许时间片的轮转,也就是说在等待调度的过程中仍然计时
- 请避免单位换算时可能发生的整数溢出
u_int
为无符号整数,如果需要进行减法得到有符号整数,请在运算前使用(int)
进行强制类型转换- 课程组给出的一种
usleep
的实现:
void usleep(u_int us) { |
一种可行的做法
ipc.c
u_int get_time(u_int *us) { return syscall_get_time(us); } |
syscall_all.c
:增加了一个系统调用,用于直接访问用户态无权访问的 KSEG1 段内存
u_int sys_get_time(u_int *us) { |
以上。
思考
其实读取时间的那部分还蛮简单的,直接写系统调用进入内核态,再向 KSEG1 区用 memcpy
就能实现读外设。真正能卡人的部分在时间比较上。哦,还有人忘了更新时钟
关于时间比较上,群友们提供了以下几种可行的方法:
u_int
溢出?我直接上long long
!
直接用 long long
类型存储时间,这样在 秒 → 微秒 的换算上就能避免溢出的问题,直接就能比较
- 我是守序善良者,我要选用合理的方法处理溢出
先获取微秒,再计算时间差就会溢出,那我先把大单位的秒相减,再移动不就溢出不了了?也就是先进行秒相减,随后再换算成微秒单位进行比较。在这个基础上还有混乱善良者,让(秒 - 秒)、(微秒 - 微秒),最后再换算相加作比较,实际上是一个意思
- 无所谓,我会
% 100
还有一些同学对大单位的秒进行了取模运算,实际上这并不安全,有可能两次比较的时间的后三位分别跨在 100 这个线两边,然后相减可能就出锅了。不过没关系,我们的测评很宽松,而且这种看脸的几率也很小,因为题目要求 us 大小上小于 5 秒
Extra - SSD 硬盘模拟
题目背景
在 MOS 的实现中,我们使用的是 GXemul 提供的 IDE 磁盘作为外挂的文件存储。现在我们使用固态硬盘 SSD 的概念对 IDE 磁盘进行模拟,即按照操作 SSD 的方法去操作我们的磁盘。
SSD 的特点是每个块存储数据后必须先擦除才能写入。同时闪存块是有擦写次数上限的,为了平衡各个块的擦写次数,SSD 引入了磨损平衡机制。我们在本次模拟过程中规定,模拟 SSD 的读、写、擦除均以块为单位,每块的大小都是 512 Bytes;且对于每个物理块,必须先擦除,才能写入数据。
我们定义如下几个概念:
逻辑块号:文件系统读写的逻辑硬盘块编号,在物理上不一定连续,每个块的大小为512Bytes
物理块号:SSD实际读写的闪存的物理编号,在物理上连续,每个块的大小都是512Bytes
闪存映射表:记录逻辑块号对应物理块号信息的表
物理块位图:记录各个物理块的状态信息
实际上,我们要先实现一个逻辑硬盘块到物理硬盘块的映射表,同时维护每个物理块的使用情况与擦除次数。并根据这些信息模拟正常的 SSD 工作流程。
首先说明,外界访问 SSD 时使用的是其逻辑块号,我们需要经闪存映射表获得其物理块号后,在 IDE 磁盘的对应扇区执行相关操作。
我们先规定 SSD 硬盘可能出现的四种操作:
- 初始化硬盘:清空闪存映射表,在物理块位图中将SSD的所有物理块初始化为可写状态,SSD各物理块的累计擦写次数清 0。(保证只在开始时调用一次)
- 读取逻辑块:查询闪存映射表,找到逻辑块号对应的表项。如果为空,则返回
-1
;若不为空,就去读取对应的物理块,返回物理块内的数据,并返回0
。 - 写入逻辑块:查询闪存映射表,找到逻辑块号对应的表项。若为空(初次写),则分配一个可写的物理块,在闪存映射 表中填入此映射关系,将数据写入该物理块,并标记此物理块为不可写;若不为空(覆盖写),则擦除原来的物理块并清除该逻辑块号的映射,并按相同方法分配一 个物理块向其写入数据。
- 擦除逻辑块:查询闪存映射表,找到逻辑块号对应的表项。若为空,不做处理;若不为空,则将对应的物理块擦除(擦除表示将物理块数据全部清 0,下同)并清除此映射关系。
为了描述上面需要的状态,我们需要在代码中定义如下数据结构:
- 闪存映射表:记录逻辑块号对应物理块号信息的表
- 物理块位图:记录各个物理块是否可写。每次向可写的物理块中写入数据后,都需要标记物理块为不可写
- 物理块累计擦除次数表:记录各个物理块的累计擦除次数
在完成上述基础机制后,我们还需要实现一个简单实现磨损平衡的分配函数,在这里就不展开逻辑了。
题目要求
在本题中,你需要使用IDE 0号硬盘(diskno = 0
)的第0~31个扇区来模拟SSD的第0~31个物理块,之后根据 题目描述部分 的描述,在 fs/ide.c
中实现下列函数:
1. 初始化硬盘:
void ssd_init(); |
- 功能:初始化闪存映射表、物理块位图、物理块累计擦除次数表。
2. 读取逻辑块:
int ssd_read(u_int logic_no, void *dst); |
logic_no
:逻辑块号dst
: 硬盘数据要加载到的内存地址- 功能:读取逻辑块
logic_no
的数据到dst
,成功返回0
,失败返回-1
3. 写入逻辑块:
void ssd_write(u_int logic_no, void *src); |
logic_no
:逻辑块号src
: 要写入到硬盘中的数据的起始内存地址- 功能:向逻辑块号对应的硬盘块写入以
src
为起始地址的512字节数据
4. 擦除逻辑块
void ssd_erase(u_int logic_no); |
logic_no
:逻辑块号- 功能:擦除逻辑块
logic_no
上的数据,并取消logic_no
到物理块号的映射关系
评测时将会调用上面的接口做一系列操作,验证操作执行结果是否正确,并会在操作的任何阶段读取硬盘数据以检查实现的正确与否。
为了实现上面的函数,你可能还需要自行实现以下两个函数,但我们不对函数的定义做具体要求:
- 擦除物理块:将全 0 的数据写入到物理块中,物理块累计擦除次数加一,并置为可写状态
- 分配新物理块:按“题目描述”部分的方法分配,需要用到物理块位图和物理块累计擦除次数两个信息
一种可行的做法
其实闪存映射表直接用一维的数组就足够了,下标代表逻辑号,内容代表物理号,无所谓,写完了才意识到。
首先按照合适的逻辑规定好数据结构:
u_int ssd_bitmap[32]; // 标记物理块是否可用,1 = free, 0 = using |
然后顺手写好初始化函数:
ssd_init
初始化标记可用的位图,闪存映射表。
擦除次数默认为 0,又写成了全局变量,所以直接跳过初始化。注意不要让映射表不存在映射时保存 0,否则可能会识别为第 0 块存在的某个映射,这里按照课程组建议写了 0xffffffff
void ssd_init() { |
随后可以先实现比较简单的读函数,因为它不需要检查/改变硬盘块状态
ssd_read
为了给这个蹩脚的数据结构一些便利,我自己又写了个获取映射块的函数,多此一举。
注意下真正的读取函数还是 ide_read
,要注意读取的扇区号和大小,其他的比较简单
int ssd_read(u_int logic_no, void *dst) { |
然后是两个比较复杂的操作,我的建议是先写擦除的函数,因为写之前免不了要进行擦除
ssd_erase
这里又写了一个专门擦除的 erase
函数,没什么实际意义,需要注意别忘了消除原有的映射。同时,当且仅当磁盘块在进行擦除时(erase
函数)会发生 cleanmap
数组值的变化,其他地方就不要写这个数组了。
void ssd_erase(u_int logic_no) { |
最后是相对最复杂的写操作。因为这个函数中,不仅要完成写操作,主要是在写之前需要分配物理块,这其中的重点是实现磨损平衡的分配函数。
首先讲讲磨损平衡的实现方法:
- 在低负荷状态下,我们优先选取擦除次数最少的、当前状态为可用的(
ssd_bitmap[i] = 1
)磁盘块(若同次数选物理块号小的),并将其返回,直接结束 - 在高负荷状态下,即当前可用块最小擦除次数大于等于 5,此时就需要考虑非可用块了(避免长期驻留的数据影响块的擦除数)。首先按照和第一步相同的逻辑,选出非可用块中擦除次数最少的块。随后我们进行一个简单的内容替换:
将选出的非可用块包含的内容转存到选出的可用块中,这也包含将映射到该物理块的闪存表项同时更改;同时擦除非可用块,并进行分配。(有点像 Lab2 Extra 那味了)
总结一下,高负荷状态下需要:
- 将原可用块状态标记为不可写
- 更新替换块的原有映射
- 复制替换块的内容到原可用块中
- 对替换块执行一次擦除操作,标记为可用
alloc_physics
这个函数写的很臃肿,而且废话很多,怎么说呢,能跑就行(
u_int alloc_physics() { |
- Step 5 里完全忘记用
erase
函数了,无所谓 - 第一次写的时候,到后面逻辑就乱了,直接把 可用块 的内容复制给 替换块 了,导致最后一个高负荷的点跑不出来
ssd_write
实现分配逻辑函数后,写入就变得很简单了
void ssd_write(u_int logic_no, void *src) { |
思考
其实说到底,最后实现的核心就是模拟,说是模拟 SSD 硬盘,但实际上完成的内容又和之前的页表映射有点像。 Lab2 Extra 要求实现的换入换出,到这里就变成了写磁盘读磁盘。不过之前 Extra 并没有限制换页的分配函数,这次却着重写了这个分配的逻辑,只能说侧重点不同吧。
这次 Extra 里数据结构的选取也相对关键,比如标记使用情况的结构就选用了 bitmap
(虽然当成 intmap
来用了,不过总归还是位图);但标记映射实际上应该用个一维数组就够了,我仓皇之中选用的结构给之后写代码无形中增添了很多负担(主要是每次查都要遍历)。没关系,能跑就行下次一定
最后附一张课上的疯狂涂鸦,也许能提供一些结构上的思路,等告一段落之后再换成能看的图罢: