对于操作系统实验的最后感想,依托达芬
我毫无收获,看着这狗屁引导,从这狗屁的实现教程中,除了第一次的键盘输入,我没有亲手写哪怕一行代码!!!
https://github.com/heorion/npu-nwpu-oslab-geekos-project0-4
去吧,这里有前辈的实现整理,你还想什么呢?
我不是天才
我们也遇不到适合普通人的课程设计了
实验要求:
了解虚拟存储器管理设计原理,掌握请求分页虚拟存储管理的具体实现技术。
过程总览
- 段式先将逻辑地址映射成线性地址;
- 页式将线性地址映射成物理地址;
- 请求分页机制的实现;
- 创建页目录PGD和页表PT数据结构;
- 系统全局页链表g_pageList,s_freeList;
- 初始化页面文件数据结构;
- 实现页为页面文件分配与释放磁盘块;
- 实现页读写页数据函数的实现;
- 实现页内核缓冲区与用户缓冲区之间的数据交换;
- 实现用户级进程在分页系统中的创建,执行与销毁。
项目设计原理
为了实现分页存储系统的地址转换机制,系统增如了个新的寄存器CR3作为指向当前页目录的指针。这样,从线性地址到物现地址的映射过程为:
从CR3取得页目录的基地址;
以线性地址中的页目录位段为下标,在目录中取得相应页表的基地址;
以线性地址中的页表位段为下标,在所得到的页表中取得相应的页面描述项;
将页面描述项中给出的页面基地址与线性地址中的页内偏移位段相加得到物理地址。 上述映射过程可用图表示,具体如下图所示:
通过编写一个初始化页表和允许在处理器中使用分页模式的函数来为内核级进程创建一个页目录和页表入口,这个函数就是project4/src/geekos/paging.c中的Init_ VM函数。在paging.c的Init _VM的Hints (提示)中,用户可以看到此函数的功能主要有以下三个:
- 建立内核页目录表和页表;
- 调用Enable_Paging函数使分页机制有效;
- 加入一个缺页中断处理程序,并注册其中断号为14。
实现
1.在<src/geekos/paging.c>文件中编写代码完成以下函数:
Init_VM()(defined in )函数将建立一个初始的内存页目录和页表,并且安装一个页面出错处理程序。
1 | //通过为内核和物理内存构建页表来初始化虚拟内存。 |
假设当前要映射的线性地址是0x80001000,它的最高10位是0x200,中间10位是0x000,最低12位是0x000。那么:
cur_pde_entry指向页目录表中的第0x200项,假设它的地址是0xe000。
cur_pte指向页表中的第0x000项,假设它的地址是0xf000。
pageTableBaseAddr存储了页表所在物理页的基地址,假设它是0x1000。
(uint_t)cur_pte >> 12得到了页表所在物理页的页号,它是0xf000 / 4096 = 0x3。
这段代码就是将0x3存储到pageTableBaseAddr中,即:
cur_pde_entry->pageTableBaseAddr = 0x3;
这样,就建立了线性地址0x80001000到物理地址0x300000的一级映射关系。
Init_Paging()函数(定义在src/geekos/paging.c)初始化操作页面调度文件所需的所有数据结构。就如前面说到的,Get_Paging_Device()函数指定分页调度文件定位在哪一个设备和占用磁盘块的地址范围。
1 | void Init_Paging(void) |
Find_Space_On_Paging_File()函数应该在分页调度文件里面找到一个空闲的足够大的页空间。它将返回这个大块的索引,或者当没有合适的空间就返回-1。将释放由Find_Space_On_Paging_File()函数在分页调度文件里所分配的的磁盘块。
1 | // src/geekos/paging.c |
Write_To_Paging_File()函数将把存储在内存的一页数据写出到分页调度文件里。
1 | void Write_To_Paging_File(void *paddr, ulong_t vaddr, int pagefileIndex) |
Read_From_Paging_File()函数将读取分页调度文件里的一页数据到内存空间。
1 | void Read_From_Paging_File(void *paddr, ulong_t vaddr, int pagefileIndex) |
2.在<src/geekos/uservm.c>文件中编写代码完成以下函数(项目三已完成):
Destroy_User_Context()释放进程所占用的所有内存和其它资源。
Load_User_Program()装载可执行文件到内存里,创建一个就绪的用户地址空间,功能类似于分段系统的实现。
Copy_From_User()从一个用户缓冲区复制数据到一个内核缓冲区。
Copy_To_User()从一个内核缓冲区复制数据到一个用户缓冲区。
Switch_To_Address_Space()利用它装载相应页目录和LDT(局部描述符表)来切换到一个用户地址空间。
操作系统将需要在磁盘设备上创建一个page file文件暂时保存从内存中替换出去的页,实现一个类LRU(最近最少使用页面)算法在内存中选取一个替换页把它写到磁盘的page file文件中,然后根据表4.1 缺页处理表进行缺页中断处理。
在“/src/geekos/mem.c”文件中,已经定义了一个函数Alloc_Pageable_Page实现交换一页到磁盘的操作,具体执行步骤如下:
调用mem.c文件中已经实现的Find_Page_To_Page_Out函数来确定要替换的页(这个函数依赖于页数据结构中的clock域);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17static struct Page *Find_Page_To_Page_Out()
{
int i;
struct Page *curr, *best;
best = NULL;
for (i=0; i < s_numPages; i++){
if ((g_pageList[i].flags & PAGE_PAGEABLE) &&
(g_pageList[i].flags & PAGE_ALLOCATED)) {
if (!best) best = &g_pageList[i];
curr = &g_pageList[i];
if ((curr->clock < best->clock) && (curr->flags & PAGE_PAGEABLE)) {
best = curr;
}
}
}
return best;
}调用paging.c文件中已经实现的Find_Space_On_Paging_File函数在page file中找到空闲的存储空间;
调用paging.c文件中已经实现的Write_To_Paging_File函数把被替换的页写到page file文件中;
修改页表的相应表项,清除页存在的标志,标识为此页在内存为不存在;
修改页表项的页基地址为包含这一页的第一个磁盘块号;
修改页表项的kernelInfo位标识为KINFO_PAGE_ON_DISK状态(标识这一页是在磁盘上存在,而不是没有效);
调用lowlevel.asm文件中已经实现的Flush_TLB来刷新TLB。
运行结果
当前系统有5个进程已经创建,根据之前的项目可以知道这5个进程分别是什么geekos project 2。每个程序对应5个创建提示输出,标号1打印当前页目录表的入口起始地址,标号2打印当前页表当前起始地址,标号3打印当前所在页表位置,标号4打印物理地址起始地址,标号5打印线性地址。
页表要去内存中取值才能获取实际对应的物理地址。页表基地址字段只是一个指针,它指向页表所在的物理页的起始地址。要访问页表中的具体内容,还需要加上页表项的索引和偏移量。页表中的每个页表项也是一个指针,它指向实际对应的物理页的起始地址。要访问物理页中的具体内容,还需要加上页内偏移量。所以,要通过线性地址来访问物理内存中的数据,需要经过两级的转换,分别是页目录项和页表项。
1 |
|
例如:第一进程给出的线性地址是80001000,转换成二进制后是1000 0000 0000 0000 0001 0000 0000 0000,最高10位1000 0000 00的十进制是512,中间10位00 0000 0001的十进制是1,最低12位0000 0000 0000的十进制是0。 首先,需要从当前页目录表的入口起始地址d800开始,找到第512项,即d800 + 512 * 4 = e000。当前页表当前起始地址e000就是第512项的内容,表示页表的物理地址。 然后,需要从当前页表当前起始地址e000开始,找到第1项,即e000 + 1 * 4 = e004。当前所在页表位置e004就是第1项的内容,表示物理页的物理起始地址。 最后,需要从物理页的物理起始地址f000开始,加上字节偏移量0,即f000 + 0 = f000。所以当前线性地址对应的物理地址为f000。
输入命令rec 4后,可以得到如上图迭代递归下的 Project4运行截图所示结果。由结果可以看出,当前进程队列下共有7个进程,接下来5个标志输出位当前rec程序页面创建地址,最后输出此次迭代递归的搜索深度为4的提示过程,在这个过程中一共递归调用过4次进程,分别是调用递归进程4、进程3、进程2,以及进程1,最后结束递归调用,程序运行结束。
相关知识
- GeekOS 系统原始的内存管理方式是什么?
答:基于段式的内存管理。 GeekOS的存储器管理: ①分页分配方式:系统中所有存储器都分成大小相等的块,称为页。在X86系统中,页的大小是4KB。 ②堆分配方式:堆分配提供不同大小存储块的分配,使用函数Malloc()和Free()进行存储块的分配和回收。
- 在GeekOS 系统中,分页系统的地址转换机制是如何实现的?
答:分页机制把线性地址空间和物理地址空间分别划分为大小相同的块。这样的块称之为页。通过在线性地址空间的页与物理地址空间的页之间建立的映射,分页机制实现线性地址到物理地址的转换。线性地址空间的页与物理地址空间的页之间的映射可根据需要而确定,可根据需要而改变。线性地址空间的任何一页,可以映射为物理地址空间中的任何一页。采用分页管理机制实现线性地址到物理地址转换映射的主要目的是便于实现虚拟存储器。不像段的大小可变,页的大小是相等并固定的。根据程序的逻辑划分段,而根据实现虚拟存储器的方便划分页。
- 在GeekOS 系统中,如何为用户级线程创建一级目录表?
答:线性地址空间的页到物理地址空间的页之间的映射用表来描述。为避免映射表占用巨大的存储器资源,所以把页映射表分为两级。页映射表的第一级称为页目录表,存储在一个4K字节的物理页中。页目录表共有1K个表项,其中,每个表项为4字节长,包含对应第二级表所在物理地址空间页的页码。
- 在GeekOS 系统中,如何为用户级线程创建二级页表?
答:页映射表的第二级称为页表,每张页表也安排在一个4K字节的页中。每张页表都有1K个表项,每个表项为4字节长,包含对应物理地址空间页的页码。由于页目录表和页表均由1K个表项组成,所以使用10位的索引就能指定表项,即用10位的索引值乘以4加基地址就得到了表项的物理地址。
- 在GeekOS系统中,如何分页存储管理来运行用户级线程?
答:给每个进程分配一张页表,当要运行该用户线程时,只要将要执行进程的页表调入内存,使它驻留在内存中,就可以运行用户级线程。(其他进程的页表不必驻留在内存中) 两级表的第一级表称为页目录,存储在一个4K字节的页中,页目录表共有1K个表项,每个表项为4个字节,线性地址最高的10位(22-31)用来产生第一 级表索引,由该索引得到的表项中的内容定位了二级表中的一个表的地址,即下级页表所在的内存块号。第二级表称为页表,存储在一个4K字节页中,它包含了 1K字节的表项,每个表项包含了一个页的物理地址。二级页表由线性地址的中间10位(12-21)位进行索引,定位页表表项,获得页的物理地址。页物理地 址的高20位与线性地址的低12位形成最后的物理地址。 由于4G的地址空间划分为1M个页,因此,如果用一张表来描述这种映射,那么该映射表就要有1M个表项,若每个表项占用4个字节,那么该映射表就要占用4M字节。为避免映射表占用巨大的存储器资源,所以把页映射表分为两级。
习题
在计算机系统中,分页存储管理和虚拟内存机制是现代操作系统中用于管理内存的重要技术。一页的大小通常被定义为4KB(4096字节),这个选择是基于多方面的考虑:
硬件支持:早期的处理器架构设计时,为了简化硬件设计并提高效率,选择了4KB作为页面的标准大小。许多处理器都直接支持这种页面大小,这使得软件层面的操作更为简便。
平衡细粒度与开销:页面大小的选择需要在页面的细粒度和管理开销之间找到一个平衡点。如果页面太小,虽然可以更精确地分配和释放内存,但是会增加页面表的大小,从而增加内存管理和调度的开销。如果页面太大,则可能导致内存浪费,因为即使只需要很小一部分内存,也需要分配整个页面。4KB被认为是在这两者之间的一个良好折衷。
兼容性和标准化:随着4KB页面大小成为一种标准,它被广泛应用于不同的操作系统和硬件平台。这种标准化有助于提高不同系统之间的兼容性,减少因页面大小差异带来的复杂性。
性能优化:对于大多数应用场景而言,4KB页面大小能够提供良好的性能。它可以有效地减少页面错误(当请求访问不在物理内存中的页面时发生的中断)的发生频率,同时还能保持合理的页面表大小,有助于提高系统的整体性能。
当然,随着技术的发展,一些现代系统也开始支持更大的页面大小(如2MB或4MB的大页面),以适应特定的工作负载需求,特别是在处理大量数据或要求高吞吐量的应用场景下。这些大页面可以在某些情况下进一步减少页面表项的数量,降低TLB(Translation Lookaside Buffer,地址转换旁路缓存)的负担,从而提高性能。然而,4KB仍然是最常见的默认页面大小。
显示的是一个内存管理系统的调试信息,具体来说是在进行页面表(Page Table)和页目录(Page Directory)的操作。
存在的页面项(Existing Page Entry)
- 第一行显示了现有的页面项的第一个地址 (
first
) 是e000
。 - 接下来几行展示了几个具体的页面项的信息,包括物理地址、线性地址等。
- 第一行显示了现有的页面项的第一个地址 (
新创建的页面项(New Page Entry)
- 显示了新创建的页面项的第一个地址 (
first
) 是38000
。 - 同样给出了几个具体的页面项的信息。
- 显示了新创建的页面项的第一个地址 (
页目录(Page Directory)
- 列出了页目录条目(PDE)及其对应的值。
- 比如
dfdc
对应的值是38007
。
页面表(Page Table)
- 列出了页面表条目(PTE)的索引、地址、值以及对应的物理帧地址和值。
- 例如,索引为
1022
的 PTE 地址是38ff8
,其值指向物理地址3a007
,物理帧地址是3a000
,物理帧值是1475093c
。
写故障(Write Fault)
- 提示发生了写故障,并显示了相关的页面项信息。
存在的页面项(Existing Page Entry)
- 再次列出了现有页面项的一些信息。
加快页面替换的速度对于提高虚拟内存管理系统的性能至关重要。以下是一些有效的方法来加快页面替换的速度:
1. 优化磁盘 I/O 性能
- 使用高速存储设备:使用固态硬盘(SSD)而不是机械硬盘(HDD)可以显著提高读写速度,从而加快页面替换的速度。
- 优化文件系统:选择适合频繁读写的文件系统,例如 ext4 或 XFS,这些文件系统在处理大量小文件时表现更好。
- RAID 配置:使用 RAID 0 或 RAID 10 可以提高磁盘的读写性能,尽管 RAID 0 不提供冗余,但可以显著提升速度。
2. 改进页面替换算法
- 使用更高效的算法:选择更高效的页面替换算法,如 LRU(最近最少使用)、LFU(最不经常使用)或 Clock 算法。这些算法可以更准确地预测哪些页面将来不太可能被使用,从而减少不必要的页面替换。
- 自适应页面替换:使用自适应页面替换算法,根据系统负载和内存使用情况动态调整替换策略。
3. 预读取和预写入
- 预读取:操作系统可以预测应用程序未来可能需要的页面,并提前将它们加载到内存中。这可以减少页面缺失的次数,从而加快页面替换的速度。
- 预写入:在页面被替换之前,提前将脏页面(已修改的页面)写回到磁盘,可以减少页面替换时的等待时间。
4. 内存压缩
- 使用内存压缩技术:通过压缩内存中的页面,可以减少物理内存的使用量,从而减少页面替换的次数。压缩和解压操作可以在内存中快速完成,比从磁盘读写要快得多。
5. 页面共享
- 共享页面:对于多个进程共享相同的数据或代码段,操作系统可以只保留一份副本,而不是为每个进程复制一份。这可以减少总的内存使用量,从而减少页面替换的次数。
6. 优化 TLB(Translation Lookaside Buffer)
- 增大 TLB 大小:TLB 是一个高速缓存,用于存储最近使用的页表项。增大 TLB 的大小可以减少 TLB 未命中的次数,从而加快地址转换的速度。
- 使用大页面:使用较大的页面(如 2MB 或 4MB)可以减少 TLB 中需要存储的页表项数量,从而提高 TLB 的命中率。
7. 减少页面锁定
- 合理使用页面锁定:页面锁定可以防止某些关键页面被换出,但过度使用会导致内存资源紧张。合理使用页面锁定,确保只有真正需要的页面才被锁定。
8. 优化应用程序
- 减少内存碎片:内存碎片会导致可用内存空间的浪费,从而增加页面替换的次数。优化内存分配和释放策略,减少内存碎片。
- 合理管理内存:开发人员可以通过优化代码来减少内存使用,例如,避免创建过多的临时对象,合理管理内存池等。
9. 使用 swap 文件或分区
- 优化 swap 文件或分区:选择合适的 swap 文件或分区位置,避免将其放在性能较差的磁盘上。同时,可以配置多个 swap 分区来分散 I/O 负载。
通过上述方法,可以显著加快页面替换的速度,提高系统的整体性能和响应速度。