实验目的与要求:
实验目的:
(1)、掌握计算机操作系统管理进程、处理机、存储器、文件系统的基本方法。
(2)、了解进程的创建、撤消和运行,进程并发执行;自行设计解决哲学家就餐问题的并发线程,了解线程(进程)调度方法;掌握内存空间的分配与回收的基本原理;通过模拟文件管理的工作过程,了解文件操作命令的实质。
(3)、了解现代计算机操作系统的工作原理,具有初步分析、设计操作系统的能力。
(4)、通过在计算机上编程实现操作系统中的各种管理功能,在系统程序设计能力方面得到提升。
实验要求:
(1)、回答以下问题:
kmem中的freelist指针指向空闲物理块链表。空闲物理块链表中的节点为run结构体。但是:
可以看到这个结构体只有指向下一个节点的指针。请解释这个链表中的空闲物理块保存在哪里呢?
(2)、大作业partI-4其中一个问题:
(vm.c L151) kpgdir = setupkvm();
通过setupkvm函数,创建了调度器所用的页表。请深入setupkvm函数内部,确定在创建页表过程中,总共调用了多少次kalloc函数分配4K物理块用于存放页表项?
请编程验证大作业partI-4的理论推导结果,在终端中输出在setupkvm函数中调用kalloc函数的次数。
(1)、回答以下问题:
kmem中的freelist指针指向空闲物理块链表。空闲物理块链表中的节点为run结构体。但是:
可以看到这个结构体只有指向下一个节点的指针。请解释这个链表中的空闲物理块保存在哪里呢?
空闲物理块的实际数据保存在run结构体的后面,run结构体本身不包含任何与该数据相关的字段。具体分析过程如下:
查看kinit和freerange函数的定义,如图1-1所示。
图 1- 1 kinit和freerange函数定义
分析其具体执行步骤,freerange将pa_start地址向上取整到4096字节(一页)的边界,确保从整页开始释放,并遍历从pa_start到pa_end的每个4096字节的页,对每个页调用kfree,它的功能是将给定地址范围内的内存添加到空闲列表中。
Init函数使用initlock函数初始化kmem结构体中的lock,为物理内存分配器创建一个自旋锁,以提供同步,接着调用freerange函数,将从end(内核的结束地址)到PHYSTOP(物理内存的结束地址)的内存区域添加到空闲列表中,完成了对内核内存分配器的初始化。
Kfree的定义如图1-2所示。
图 1- 2 kfree函数定义
它首先检查pa是否是4096字节对齐的,并且是否位于合法的物理内存范围内,使用memset将该页的内容设置为1,以防止悬挂指针引用旧数据。接着将pa转换为run结构体指针r,获取kmem.lock以确保原子操作。并将r的next指针设置为当前的freelist头部。最后更新kmem.freelist为新的头部r并释放kmem.lock,完成将一个物理页标记为空闲,并添加到空闲列表中。
Kalloc的定义如图1-3所示。
图 1- 3 kalloc函数定义
它首先获取kmem.lock以确保原子操作,检查freelist是否为空。如果不为空,将freelist的头部节点保存到r,并更新freelist为下一个节点,释放kmem.lock。如果r不为空,使用memset将页内容设置为5,以确保页面被初始化。最后返回r作为物理页的指针,完成从空闲列表中分配一个物理页。
结合代码和注释,可以得知run结构体仅作为链表中的节点,它后面的内存(即同一物理页内的内存)就是空闲物理块的实际数据。由于所有的run结构体都位于它们各自代表的空闲物理页的开始位置,因此它们后面的内存就是可供分配的空闲空间。kmem结构体中的freelist指针指向空闲物理块链表,这个链表用于跟踪所有可用的物理页。每个空闲物理块由run结构体表示,该结构体仅包含一个指向下一个空闲块的指针。空闲物理块的实际数据保存在run结构体的后面,run结构体本身不包含任何与该数据相关的字段。这种设计允许使用整个物理页,没有额外的元数据开销,并且可以通过简单的链表操作来管理这些页。
(2)、大作业partI-4其中一个问题:
(vm.c L151) kpgdir = setupkvm();
通过setupkvm函数,创建了调度器所用的页表。请深入setupkvm函数内部,确定在创建页表过程中,总共调用了多少次kalloc函数分配4K物理块用于存放页表项?
请编程验证大作业partI-4的理论推导结果,在终端中输出在setupkvm函数中调用kalloc函数的次数。
一开始找不到源码和setupkvm函数感到很奇怪,查阅资料后发现该xv6版本没有这个函数,而在这个版本中,起到这个作用的函数为uvmalloc,如图2-1所示。
图 2- 1 uvmalloc函数
函数执行内容如图2-2所示。
图 2- 2 umv函数内容
其中,oldsz是一个uint64类型的值,表示进程已经占用的虚拟内存的末尾地址,函数会从这个地址继续往后分配新的内存。Newsz是一个uint64类型的值,表示要为进程分配到的新的内存的末尾地址。
下面是这个函数的步骤解释:
- 初始化:定义一个字符指针 mem 用于指向新分配的内存,以及一个 uint64 类型的变量 a 用于循环计数。
- 边界检查:如果 newsz 小于 oldsz,则直接返回 oldsz,不进行内存扩展。
- 内存对齐:使用 PGROUNDUP(oldsz) 将 oldsz 对齐到页面大小,确保它是页面大小的整数倍。
- 循环分配内存:从 oldsz 开始,每次增加一个页面大小 PGSIZE,直到达到 newsz。在循环中:
使用 kalloc() 分配一页物理内存,如果 kalloc() 返回 0 表示分配失败,则调用 uvmdealloc(pagetable, a, oldsz) 释放之前分配的所有内存,并返回 0 表示出错。
使用 memset 将新分配的内存初始化为 0。
使用 mappages 函数将新分配的物理内存 mem 映射到进程的虚拟地址空间中。mappages 的参数包括页表 pagetable,虚拟地址 a,页面大小 PGSIZE,物理地址 (uint64)mem,以及权限位 PTE_R|PTE_U|xperm。如果 mappages 返回非零值,表示映射失败,此时释放物理内存 mem,调用 uvmdealloc(pagetable, a, oldsz) 释放之前分配的内存,并返回 0。
- 打印信息:在每次成功分配和映射内存后,打印出相关信息,包括新分配的虚拟地址 a,物理内存的地址 mem,新分配的空间大小,页面大小,以及总共分配的页面数。
- 返回新大小:如果所有内存分配和映射都成功,函数最终返回新的内存大小 newsz。
每次执行一个指令,xv6就会调用这个函数,新分配一些空间。我们可以用一个变量记录每次执行时调用malloc的次数,如图2-3所示。
图 2- 3 记录调用malloc的次数
输出信息,进行检验。如图2-4所示。
图 2- 4 输出信息
进入系统输入ls命令,结果如图2-5所示。
图 2- 5 检验结果
之前的实验已经得知kalloc每次分配4KB,16*4096=65536 bytes,与之前的推导相符。
++++++++++++++++++++++++++++++++++++++++++++++++++++++
其他(例如感想、建议等等)。
通过这次实验,我对操作系统的内存管理有了更深入的理解。特别是,我学习了如何通过编程来管理和分配物理内存,以及如何设置页表来实现虚拟内存管理。这个过程不仅加深了我对操作系统底层工作原理的认识,也锻炼了我的编程技能和问题解决能力。
在实现 setupkvm 函数和理解 kalloc 调用次数的过程中,我意识到了内存管理的复杂性和精细性。每个函数调用和内存分配都需要精确控制,以确保系统的稳定性和效率。同时,我也体会到了调试和验证代码的重要性,这对于保证代码的正确性和性能至关重要。
此外,我还学习了如何通过阅读和分析源代码来理解程序的行为,这对于我未来的学习和工作都是非常宝贵的技能。总的来说,这次实验不仅提升了我的技术能力,也增强了我的分析和逻辑思维能力。