通过提供的底层模拟接口和细节,模拟实现操作系统的内存管理功能,包括存储保护、虚拟地址和物理地址的转换、实现虚拟内存等。
- 虚拟空间大小(磁盘可用交换区大小):512MB
- 物理空间大小(内存可用空间大小):128MB
- 字节可寻址
- 底层接口只负责:
- 数据的读和写
- 对模拟设备的访问的记录
- 需要实现的功能:
- 存储保护
- 虚拟地址到物理地址的转换
- 什么时候,将哪些数据,从内存写进磁盘,或从磁盘读进内存
- 读取内存某个物理地址上的字节,若
address
越界,会退出程序,开销小
data_unit mem_read(p_address address);
- 往内存某个物理地址写入字节,若
address
越界,会退出程序,开销小
void mem_write(data_unit data, p_address address);
- 将硬盘某个地址开始的一段连续的数据加载到内存,若
x_offset + size
越界,会退出程序;开销与size
大小有关,比内存读写开销大很多,且每次调用都有始动开销
void disk_load(p_address mem_offset, p_address disk_offset, m_size_t size);
- 将内存某个地址开始的一段连续的数据保存到磁盘,若
x_offset + size
越界,会退出程序;开销与size
大小有关,比内存读写开销大很多,且每次调用都有始动开销
void disk_save(p_address mem_offset, p_address disk_offset, m_size_t size);
- 获取内存和硬盘的读写开销情况,并往控制台输出
void evaluate(count_t *m_read, count_t *m_write, count_t *d_read, count_t *d_write);
- 进程号为 1-999,不会出现超过范围的
pid
。 - 每个进程可申请的内存空间无限制。
- 调用
allocate
的次数会有一个比较小的上限,因此不必太过担心一些 Corner Case。 - 大部分情况下,
read
和write
调用会根据之前返回的address
来调,但是有时候,测试用例里会故意调用错误的地址,这时候程序应该拒绝这样的非法调用。 - 在内存空间不足的时候(不管你有没有实现磁盘的虚拟内存),都可以返回申请内存失败。
- 测试用例会杂乱地访问各个进程的各个地址,但是对各个地址的访问会遵循一定的空间局部性原则,因此理想情况下程序不应该频繁地对磁盘数据换入换出。
- 实现的时候,不允许申请任何额外的内存,即不允许声明全局变量,不允许调用
malloc
函数。 - 需要使用数据记录内存使用状况的时候(如采用动态划分的话要用链表,用分页的话存储页表等),也需要调用底层模拟接口,不能自己申请内存来存储这些数据。
- 这样设计是保证为了不同实现方法之间的公平性,当然这样也使得实现起来需要额外花点功夫。建议根据自己的实现方式,抽象出一些工具函数并放到不同的文件里,这样会大大提高代码可读性和可维护性。
- 初始化函数,会在每个测试用例开始调用一次,你可以在这个函数里面做一些初始化操作。
void init();
- 进程号为
pid
的进程希望访问address
处的数据。如果访问合法,往data
指针里写入数据(通过*data = xxx
),并返回0;如果访问不合法,返回-1。
int read(data_unit *data, v_address address, m_pid_t pid);
- 进程号为
pid
的进程希望往address
处写入数据。如果访问合法,往address
处写入data
,并返回0;如果访问不合法,返回-1。
int write(data_unit data, v_address address, m_pid_t pid);
- 进程号为
pid
的进程希望申请大小为size
的空间。如果空间有剩余,往address
指针里写入申请到的地址(通过*address = xxx
),并返回0;如果空间不足,返回-1。
int allocate(v_address *address, m_size_t size, m_pid_t pid);
- 进程号为
pid
的进程希望归还address
处开始的空间。如果访问合法,回收空间,返回0;如果访问不合法,即address
处的空间不属于pid
进程,返回-1。
int free(v_address address, m_pid_t pid);
- test.c文件中的
main
方法包含一系列的测试用例,每个测试用例会先调用init
进行初始化。 - 每个测试用例内部会按各个测试用例的要求调用
read
、write
、allocate
和free
方法,并检查返回的结果。
- 正确性测试
- 进程 A 试图访问不属于它的地址的时候,应拒绝访问。
- 往一个地址
address
里写入数据data
,如果后面的访问没有修改address
上的值,那么读取address
的时候应返回data
。
- 时间测试
- 对同一个进程的相近几个位置的访问,除了第一次可能需要在磁盘上换入换出,后续的访问应该都要能在较短的时间内完成。
- 多个进程的多个距离较远的地址轮流访问,如果调度做的足够好,那么不应该在磁盘上频繁地换入换出。
- 容量测试
- 多次申请比较小的空间,理想情况下应该都要能够申请成功。
- 多次申请比较小的空间,然后退回部分,再次申请几个较小的空间,理想情况下应该都要能够申请成功,而且不需要在磁盘上换入换出。
- 多次申请比较小的空间,然后退回部分,再次申请一个较大的空间(总用量不超过128MB),理想情况下应该都要能够申请成功,而且不需要在磁盘上换入换出,考察的是对内存碎片的处理。
- 一次申请超过128MB的空间,若支持磁盘换入换出,应该能够申请成功。
- 综合测试:综合上面的几种进行测试。
由于测试用例是一个接着一个跑的,因此请注意在init
函数中把上次用过的变量重新初始化,避免上次的测试对本次测试造成干扰。
存储控制信息的数据全部位于物理内存的0MB~1MB上,因此物理内存实际可用于存储进程内存数据的区域为1MB~128MB,大小为127MB。
- 分页设定
- 逻辑内存分为4KB一页,共512MB/4KB = 128K页;
- 物理内存分为4KB一页,共128MB/4KB = 32K页;
- 物理磁盘分为4KB一页,共512MB/4KB = 128K页;
- 其中逻辑内存的页与物理磁盘的页一一对应。
- 页表:存储逻辑内存中页面和物理内存或磁盘中页框的对应表,一条记录占7个字节,包含逻辑内存中页面编号
v_page_id
、物理内存中页面编号p_page_id
、控制信息块control
,结构如下:
字节位 | 0 1 2 | 3 4 5 | 6 |
---|---|---|---|
保存信息 | v_page_id | p_page_id | control |
其中控制信息块包含载入位
in
(是否在内存中)、使用位use
(最近是否使用)。 需要存储128K条记录,所以共需要128K * 7B = 896KB来存储。放在内存的0~896K处。
- 逻辑页位示图:表示逻辑内存的页使用情况,共有512MB/4KB = 128K页,每页1位,所以一共需要16KB的空间来存储。放在内存的896K~912K处。
- 物理页位示图:表示物理存储空间的页使用情况,共有127MB/4KB < 32K页,每页1位,所以一共需要4KB的空间来存储。放在内存的912K~916K处。
- 数据交换区:内存中数据换出时,暂时保存数据的区域,大小为1页,即4KB,放在内存的916K~920K处。
- 内存分配表:存储内存分配信息的表,一条记录占10个字节,包含进程号
pid
、逻辑内存中起始地址start_address
、占用空间大小size
,结构如下:
字节位 | 0 1 | 2 3 4 5 | 6 7 8 9 |
---|---|---|---|
保存信息 | pid | start_address | size |
该表放在内存的924K~1M处,大小为100KB,最多可以存放10K条记录。
- 初始化
- 初始化数据区:清空物理内存和磁盘的数据区,每个
data_unit
都设置成0。 - 初始化页表:每个页表项根据
v_page_id
依次填入页表,每个页表项的p_page_id
都设为2^24-1 (即24位全为1),载入位in
设为0,写入位write
设为0,使用位use
设为0。 - 初始化页位示图:全部设为0,代表空闲。
- 初始化内存表:每条记录的
pid
都设为1000,代表空记录。
- 初始化数据区:清空物理内存和磁盘的数据区,每个
- 读取
- 检查要读取的内存是否越界(大小>512M)。
- 载入内存表,检查是否有权限访问这段内存区域(关于这段区域的记录中
pid
能够对应)。 - 根据逻辑地址计算出逻辑页号和偏移量,根据逻辑页号查页表获得对应的物理页框号。
- 如果这个物理页框不在内存中(
p_page_id
为2^24-1 ),则扫描页表,找出在内存中的使用位use
为0的页表项,执行物理页框对换,并把换进内存的页表项use
设为1,in
设为1,而换出内存的页表项in
设为0。 - 最后根据新的物理页框号和偏移量,读取需要的数据。
- 写入
- 检查要写入的内存是否越界(大小>512M)。
- 载入内存表,检查是否有权限访问这段内存区域(关于这段区域的记录中
pid
能够对应)。 - 根据逻辑地址计算出逻辑页号和偏移量,根据逻辑页号查页表获得对应的物理页框号。
- 如果这个物理页框不在内存中(
p_page_id
为2^24-1 ),则扫描页表,找出在内存中的使用位use
为0的页表项,执行物理页框对换,并把换进内存的页表项use
设为1,in
设为1,而换出内存的页表项in
设为0。 - 最后根据新的物理页框号和偏移量,写入需要的数据。
- 申请空间
- 检查要申请的内存是否越界(大小>512M)。
- 检查逻辑位示图,找出足够的连续可用页面,记录起始页号和页数(确定可用页面数足够后逻辑内存位示图设置占用)。
- 检查物理位示图,如果有空闲的页框,就分配给逻辑页面,更新页表和物理位示图占用。
- 添加内存表记录。
- 释放空间
- 检查要释放的内存是否越界(起始地址>512M)。
- 载入内存表,检查是否有权限释放这段内存区域(关于这段区域的记录中
pid
能够对应)。 - 根据页表更新逻辑位示图和物理位示图的占用信息,更新页表信息。
- 删除该条内存表记录,将后面的记录向前移动。