目录
1. 阅读XV6 guide book第一、第二章或自行查阅相关资料,了解XV6系统初始化阶段内存的分配以及分页式内存管理的实现。
2.阅读XV6系统中的mmu.h头文件,分析64行到104行定义的各种常量及define的意义,描述每一个常量和定义代表什么意义。
3.阅读XV6系统中的main.c文件的97行到最后的数组初始化,分析XV6系统初始化阶段的单级页表的构成与映射关系,着重分析数组中每一项数据分别对应着什么信息,存储进页表的两个0的值意味着什么。
1. entry.S文件中新加的汇编代码将现有的单级页表改为二级页表,逐行分析这段x86汇编代码,每一行都要说明其目的与意义,每个常量、数值都要分析其含义
任务要求:
一. 理解XV6内核源码
1. 阅读XV6 guide book第一、第二章或自行查阅相关资料,了解XV6系统初始化阶段内存的分配以及分页式内存管理的实现。
2. 阅读XV6系统中的mmu.h头文件,分析64行到104行定义的各种常量及define的意义,描述每一个常量和定义代表什么意义。
3. 阅读XV6系统中的main.c文件的97行到最后的数组初始化,分析XV6系统初始化阶段的单级页表的构成与映射关系,着重分析数组中每一项数据分别对应着什么信息,存储进页表的两个0的值意味着什么。
二、修改XV6内核源码
请下载修改版的entry.S,entryOthers.S,以及main.c文件并替换原文件内容。
1. entry.S文件中新加的汇编代码将现有的单级页表改为二级页表,逐行分析这段x86汇编代码,每一行都要说明其目的与意义,每个常量、数值都要分析其含义。
2. 在main.c中,有一个空的func()函数。在其中用c语言在5行代码以内复刻entry.S中汇编代码的操作,并描述你的思路,论证为何你的C代码与汇编代码是效果完全相同的。注意!本任务严禁使用0以外的任何数值,所有0以外的数值应调用mmu.h等头文件中定义的常量,并说明你选择这个常量的原因。常量选择错误(如混淆PTE_T和PDE_T)将被扣分。
一、理解XV6内核源码
1. 阅读XV6 guide book第一、第二章或自行查阅相关资料,了解XV6系统初始化阶段内存的分配以及分页式内存管理的实现。
x86上的操作系统xv6,是32位操作系统,而32位分页存储支持1级页表、2级页表。因此首先我们来搞清楚几个概念,什么是一级页表,什么是二级页表。
一级页表:一级页表就是页表项的指针直接指向页面,没有什么间接的东西,关系如下图,可以推断出一共有【1024 * 1 = 1024】个页面
二级页表:二级页表其实就相当于一个页表再指向第二个页表,而第二个页表直接指向目录,关系如下图,可以推断出一共有【1024 * 1024 * 1 = 】个页面
在一级页表中进行xv6的初始化:xv6运行时至少要4M内存,启动时自动创建一个页目录表,即一级页表。一开始只能工作在低地址端,即页目录项0。因为分页系统还没启动,所以xv6要自己造个页表系统,只填充一个4M的页面,映射到页目录表项0,也就是物理地址。初始化完后,跳转到高地址,也就是页目录项512(内核地址从512开始),也映射到刚刚那个4M的页面。总的来说,一个最低地址0,一个地址512(内核地址开始的位置),都映射到同一个页面。 关系如下图所示:
在二级页表中进行xv6的初始化:xv6运行时至少要4M内存,创建1024个4K的页面,映射到1024个页表中的页表项,所以一共有【1024 * 4K = 4M】的内存。
2.阅读XV6系统中的mmu.h头文件,分析64行到104行定义的各种常量及define的意义,描述每一个常量和定义代表什么意义。
#define PDX(va) (((uint)(va) >> PDXSHIFT) & 0x3FF)
含义:从传入的虚拟地址va中获取页目录的索引
详解:PDX(va):PDX这个宏定义接收一个参数va,即虚拟地址virtual address。
(uint)(va):这个虚拟地址强转为uint类型以方便后续的位操作。
((uint)(va) >> PDXSHIFT):虚拟地址va右移 PDXSHIFT位,即将虚拟地址的部分高位移到低位。
PDXSHIFT:PDXSHIFT为22位,而虚拟地址中页目录索引为第22到第31位,因此上文的右移PDXSHIFT位也就是右移22位,就能将高10位移动到低10位了,也就是将页目录索引移动到低10位。
(((uint)(va) >> PDXSHIFT) & 0x3FF):0x3FF这个十六进制数转换为2进制数即1111111111,虚拟地址右移PDXSHIFT位的结果与上0x3FF即保留低10位的结果,因为1与上任何一个数,都保持原本的数不变,也就是保留页目录的索引。
#define PTX(va) (((uint)(va) >> PTXSHIFT) & 0x3FF)
含义:从传入的虚拟地址va中获取页表的索引
详解:PTX(va):PTX这个宏定义接收一个参数va,即虚拟地址virtual address。
(uint)(va):这个虚拟地址强转为uint类型以方便后续的位操作。
((uint)(va) >> PTXSHIFT):虚拟地址va右移 PTXSHIFT位,即将虚拟地址的部分高位移到低位。
PDXSHIFT:PTXSHIFT为12位,而虚拟地址中页表索引为第22到第12位,因此上文的右移PTXSHIFT位也就是右移12位,就能将第22到第12位移动到低10位了,也就是将页表索引移动到低10位。
(((uint)(va) >> PTXSHIFT) & 0x3FF):0x3FF这个十六进制数转换为2进制数即1111111111,虚拟地址右移PTXSHIFT位的结果与上0x3FF即保留低10位的结果,因为1与上任何一个数,都保持原本的数不变,也就是保留页表的索引。
#define PGADDR(d, t, o) ((uint)((d) << PDXSHIFT | (t) << PTXSHIFT | (o)))
含义:从传入的页目录索引、页表索引、页内偏移量中,通过页目录索引、页表索引的左移,将页目录索引、页表索引分别放在高10位和中间10位,再拼接上页内偏移量作为低12位,组合还原成虚拟地址。
详解:PGADDR(d, t, o):PGADDR这个宏定义接收3个参数d、t、o。
d:d代表虚拟地址中的页目录索引,也就是Page Directory Index
t:t代表虚拟地址中的页表索引,也就是Page Table Index
o:o代表虚拟地址中的页内偏移量,也就是Offset within Page
(d) << PDXSHIFT:把d也就是页目录索引左移PDXSHIFT位,也就是左移22位,也就是将页目录索引放在第31位到22位。
(t) << PTXSHIFT:把t也就是页表索引左移PTXSHIFT位,也就是左移12位,也就是将页目录索引放在第22位到12位。
(o):o也就是页内偏移量不变,也就是放在第12位到第0位。
((uint)((d) << PDXSHIFT | (t) << PTXSHIFT | (o))):把页目录索引和页表索引进行位操作的结果,以及o按位或在一起,也就是把3者组合在一起,最后强转为uint类型。
#define NPDENTRIES 1024
含义:页目录表中的项数为1024项
#define NPTENTRIES 1024
含义:页表中的项数为1024项
#define PGSIZE 4096
含义:一个页面的大小为4096字节,也就是4kb
#define PTXSHIFT 12
含义:页表索引的位移量为12
#define PDXSHIFT 22
含义:页目录索引的位移量为22
#define PGROUNDUP(sz) (((sz)+PGSIZE-1) & ~(PGSIZE-1))
含义:将给定的大小 sz 向上舍入到页面大小的倍数。
详解:((sz)+PGSIZE-1):sz加上页面大小-1
~(PGSIZE-1):PGSIZE是4096,转换为二进制即10000000000。PGSIZE-1是4095,转换为二进制即111111111111。对于无符号数,最高位为0,所以取反后的最高位为1,所以~(PGSIZE-1)为1000000000000。
(((sz)+PGSIZE-1) & ~(PGSIZE-1)):两者按位与,因为~(PGSIZE-1)为1000000000000,所以与之相与意味着低12位全部清0,只留12位以上的数。
#define PGROUNDDOWN(a) (((a)) & ~(PGSIZE-1))
含义:将给定的大小 a 向下舍入到页面大小的倍数。
详解:~(PGSIZE-1):PGSIZE是4096,转换为二进制即10000000000。PGSIZE-1是4095,转换为二进制即111111111111。对于无符号数,最高位为0,所以取反后的最高位为1,所以~(PGSIZE-1)为1000000000000。
(((a)) & ~(PGSIZE-1)):两者按位与,因为~(PGSIZE-1)为1000000000000,所以与之相与意味着低12位全部清0,只留12位以上的数。
#define PTE_P 0x001
含义: 标志PTE 是否陈列在页表中。当PTE_P为1时,则在列表中,否则不在列表中,那么一个对该页的引用会引发错误(也就是:不允许被使用)。0x001是十六进制数,转换为2进制也就是001。
#define PTE_W 0x002
含义:标志着能否对页执行写操作;如果不能,则只允许对其进行读操作和取指令。0x002是十六进制数,转换为2进制也就是010。
#define PTE_U 0x004 // User
含义:标志着用户程序能否使用该页;如果不能,则只有内核能够使用该页。0x004是十六进制数,转换为2进制也就是100。
#define PTE_PS 0x080 // Page Size
含义:标志着这个页面是大页面还是普通页面大小,若被设置,则表示这个页面表项对应的页面是大页面,否则表示页面是普通大小。0x080是十六进制数,转换为2进制也就是1000 0000。
#define PTE_ADDR(pte) ((uint)(pte) & ~0xFFF)
含义:把页表项的标志位清0,获取页面基本地址。
详解:(uint)(pte):将pte强转为uint类型
pte:pte是页表项,页表项由页面基本地址和标志位组成,标志位即上文提到的PTE_W等,在页表项的低12位,页面基本地址在页表项的高20位。
~0xFFF:0xFFF是十六进制数,转换为二进制数是1111 1111 1111,所以~0xFFF是0000 0000 0000。
((uint)(pte) & ~0xFFF):pte按位与~0xFFF意思是把pte的低12位,也就是标志位清零,只留下高20位,也就是页面基本地址。
#define PTE_FLAGS(pte) ((uint)(pte) & 0xFFF)
含义:把页表项的页面基本地址清0,获取标志位。
详解: (uint)(pte):将pte强转为uint类型
pte:pte是页表项,页表项由页面基本地址和标志位组成,标志位即上文提到的PTE_W等,在页表项的低12位,页面基本地址在页表项的高20位。
0xFFF:0xFFF是十六进制数,转换为二进制数是1111 1111 1111。因为无符号数默认符号位为0,所以除了低12位是1,其他位都是0。
((uint)(pte) & 0xFFF):pte按位与0xFFF意思是把pte的低12位,也就是标志位保留,因为1与上任何数都是数本身,也就是页表项的标志位保留,而把其他位都清0。
#ifndef __ASSEMBLER__
typedef uint pte_t;
含义:定义了一个uint类型 的变量pte_t,用于表示页面表项,在 C 代码中可以使用这个类型来声明变量、参数等。
详解:#ifndef:用于判断标识符是否未被定义,如果未被定义,则执行后续的代码,否则不执行后续代码。
#ifndef __ASSEMBLER__:指示是否正在处理ASSEMBLER也就是汇编器assembler代码。如果未定义 __ASSEMBLER__,则表示当前正在处理 C 代码。
typedef uint pte_t:若未定义__ASSEMBLER__,则执行该行代码。定义了一个uint类型的pte_t变量,也就是页表项类型。
3.阅读XV6系统中的main.c文件的97行到最后的数组初始化,分析XV6系统初始化阶段的单级页表的构成与映射关系,着重分析数组中每一项数据分别对应着什么信息,存储进页表的两个0的值意味着什么。
(1)XV6系统初始化阶段的单级页表的构成与映射关系:
①单级页表的构成:单级页表通过一个数组来表示,每个数组元素都是一个页表项PTE,且页表的大小是4MB。
②页表项的构成:页表项存储了虚拟地址到物理地址的映射关系,以及一些控制位用于指示页的状态,如 PTE_P(标志页表项是否效)、PTE_W(标志页表项是否允许写操作)和PTE_PS(标志页面大小是4MB)。页表项的内容被设置为 0,表示对应的虚拟地址没有映射到任何物理页框。
③虚拟地址到物理地址的映射关系:设置了两个特殊的映射关系:将虚拟地址 0 映射到物理地址 0和将内核地址的起始位置映射到物理地址 0。也就是虚拟地址空间和内核空间都映射到物理地址的同一个地方,也就是都映射到同一个页面。
(2)代码分析:
__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
// Map VA's [0, 4MB) to PA's [0, 4MB)
[0] = (0) | PTE_P | PTE_W | PTE_PS,
// Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};
__attribute__((__aligned__(PGSIZE))):将entrypgdir数组按照PGSIZE也就是页大小对齐。
[0] = (0) | PTE_P | PTE_W | PTE_PS:[0]的意思是该数组的索引为0的项,也就是虚拟地址的起始地址为0,(0)意思是物理地址的起始位置为0,因为使用了单级页表,所以页表本身也是物理地址0。[0] = (0)意思是把虚拟地址的起始映射为物理地址的起始。又因为限定了PTE_PS也就是页表项类型为大页面,也就是4MB的页面来表示,所以是将虚拟地址[0, 4MB)映射到物理地址[0,4MB)。PTE_P表示页表项有效,PTE_W表示可进行写操作,PTE_PS表示使用4MB的大页面。
[KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS:KERNBASE 是内核的起始地址,定义了内核代码和数据在虚拟地址空间中的起始位置。而KERNBASE >> PDXSHIFT 是将 KERNBASE 这个虚拟地址右移 PDXSHIFT 位,以获得对应的页目录项索引,也就是计算了内核地址空间的起始地址在页目录中的索引。(0) 是物理地址的初始位置0,因为还没有映射任何内核的物理页面,所以内核页表也是物理地址0,总体是表示将内核地址空间映射到物理地址空间的起始位置(0),所以这行代码将虚拟地址范围[KERNBASE, KERNBASE+4MB)映射到物理地址范围[0, 4MB)。
二、修改XV6内核源码
请下载修改版的entry.S,entryOthers.S,以及main.c文件并替换原文件内容。
1. entry.S文件中新加的汇编代码将现有的单级页表改为二级页表,逐行分析这段x86汇编代码,每一行都要说明其目的与意义,每个常量、数值都要分析其含义
就是一些汇编指令,这里就不分析了。
2. 在main.c中,有一个空的func()函数。在其中用c语言在5行代码以内复刻entry.S中汇编代码的操作,并描述你的思路,论证为何你的C代码与汇编代码是效果完全相同的。注意!本任务严禁使用0以外的任何数值,所有0以外的数值应调用mmu.h等头文件中定义的常量,并说明你选择这个常量的原因。常量选择错误(如混淆PTE_T和PDE_T)将被扣分。
(1)代码:
void init_entrypgdir(void) {
// 初始化页表内容
for (int i = 0; i < NPTENTRIES; i++) {
page_table[i] = ((i < < PTXSHIFT) | PTE_P | PTE_W);
}
// 初始化页目录表内容
for (int i = 0; i < NPDENTRIES; i++) {
entrypgdir[i] = (V2P_WO(page_table)) | PDE_P | PDE_W;
}
}
(2)解释:
①初始化页表内容时:
NPTENTRIES:1024,表示页表中的项数为1024项。循环1024次,也就是对每个页表项执行相同的操作,也就是初始化第0项到第1023项。
PTXSHIFT:12,表示页内偏移量为12。因为页内偏移量存储在低12位上,通过左移12位可以空出低12位,用来存储页表的标志位,也就是通过按位或 | 拼接页表的标志位,也就是PTE_P和PTE_W。
PTE_P:标志页表项是否陈列在页表中。因为在初始化页表内容,所以选择页表项的标志,也就是PTE_xxx。
PTE_W:标志页表项能否进行写操作。因为在初始化页表内容,所以选择页表项的标志,也就是PTE_xxx。
②初始化页目录表内容时:
NPDENTRIES:1024,表示页目录表中的项数为1024项。循环1024次,也就是对每个页目录项执行相同的操作,也就是初始化页目录的第0项到第1023项。
V2P_WO:将page_table也就是页表指针指向的页面的物理地址与页目录项的标志按位或进行拼接。
PDE_P:标志页目录项是否陈列在页表中。因为在初始化页目录表内容,所以选择页目录项的标志,也就是PDE_xxx。
PDE_W:标志页目录项能否进行写操作。因为在初始化页目录表内容,所以选择页目录项的标志,也就是PDE_xxx。