MIT 6.828 Lab 02
MIT 6.828 Lab02
前期准备
- 切换git环境
-
切回lab1分支,提交了lab1
-
在mit-6.828的提交页申请了提交API key
-
Exercise 01
首先需要理解物理内存的分布
内存分为用户空间和内核空间
-
4G的进程地址空间被人为的分为两个部分--用户空间与内核空间。用户空间从0到3G(0xc0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址。例外情况只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。
-
内存结构:
+------------------+ <- 0xFFFFFFFF (4GB) | 无效空间 | | | +------------------+ <- addr:3G+256M | 256MB | | IO外设地址空间 | | | +------------------+ <- 0xC0000000(3GB) | | /\/\/\/\/\/\/\/\/\/\ /\/\/\/\/\/\/\/\/\/\ | 无效空间 | +------------------+ <- 0x40000000(1GB) | | | 实际有效内存 | | | +------------------+ <- 0x00100000 (1MB) | BIOS ROM | +------------------+ <- 0x000F0000 (960KB) | 16-bit devices, | | expansion ROMs | +------------------+ <- 0x000C0000 (768KB) | VGA Display | +------------------+ <- 0x000A0000 (640KB) | | | Low Memory | | | +------------------+ <- 0x00000000
来自:https://www.bookstack.cn/read/ucore_os_docs/lab1-lab1_3_2_2_address_space.md
ucore实验指导书
lab1结束后,我将物理内存的分布总结如下:
大致上可以分为三部分:
- 0x00000~0xA0000:这部分叫做basemem,是可用的。
- 接着是0xA0000~0x100000:这部分叫做IO Hole,不可用。
- 再接着就是0x100000以上的部分:这部分叫做extmem,可用。
(IDT: 中断描述符表)
- kern/pmap.c中的i386_detect_memory()统计有多少可用的物理内存,将总共的可用物理内存页数保存到全局变量npages中,basemem部分可用的物理内存页数保存到npages_basemem中。
/*
* Virtual memory map: Permissions
* kernel/user
*
* 4 Gig --------> +------------------------------+
* | | RW/--
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* : . :
* : . :
* : . :
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
* | | RW/--
* | Remapped Physical Memory | RW/--
* | | RW/--
* KERNBASE, ----> +------------------------------+ 0xf0000000 --+
* KSTACKTOP | CPU0's Kernel Stack | RW/-- KSTKSIZE |
* | - - - - - - - - - - - - - - -| |
* | Invalid Memory (*) | --/-- KSTKGAP |
* +------------------------------+ |
* | CPU1's Kernel Stack | RW/-- KSTKSIZE |
* | - - - - - - - - - - - - - - -| PTSIZE
* | Invalid Memory (*) | --/-- KSTKGAP |
* +------------------------------+ |
* : . : |
* : . : |
* MMIOLIM ------> +------------------------------+ 0xefc00000 --+
* | Memory-mapped I/O | RW/-- PTSIZE
* ULIM, MMIOBASE --> +------------------------------+ 0xef800000
* | Cur. Page Table (User R-) | R-/R- PTSIZE
* UVPT ----> +------------------------------+ 0xef400000
* | RO PAGES | R-/R- PTSIZE
* UPAGES ----> +------------------------------+ 0xef000000
* | RO ENVS | R-/R- PTSIZE
* UTOP,UENVS ------> +------------------------------+ 0xeec00000
* UXSTACKTOP -/ | User Exception Stack | RW/RW PGSIZE
* +------------------------------+ 0xeebff000
* | Empty Memory (*) | --/-- PGSIZE
* USTACKTOP ---> +------------------------------+ 0xeebfe000
* | Normal User Stack | RW/RW PGSIZE
* +------------------------------+ 0xeebfd000
* | |
* | |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* . .
* . .
* . .
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
* | Program Data & Heap |
* UTEXT --------> +------------------------------+ 0x00800000
* PFTEMP -------> | Empty Memory (*) | PTSIZE
* | |
* UTEMP --------> +------------------------------+ 0x00400000 --+
* | Empty Memory (*) | |
* | - - - - - - - - - - - - - - -| |
* | User STAB Data (optional) | PTSIZE
* USTABDATA ----> +------------------------------+ 0x00200000 |
* | Empty Memory (*) | |
* 0 ------------> +------------------------------+ --+
inc/memlayout.h
ucore清华实验说明
逻辑
- 从lab1知道,进入内核后首先调用的是i386_init(),该函数会调用mem_init()。然后mem_init()调用其他工具函数实现内核内存管理。
- 首先调用
i386_detect_memory()
统计有多少能用的物理内存,并将总共的可用物理内存页数保存到全局变量npages
里面,basemen
部分可用的物理内存页数保存到npages_basemen
;然后调用boot_alloc()
,返回当前的空闲指针,并且将nextfree更新至新的空闲空间开始处
boot_alloc()函数
🚩注意:boot_alloc()返回的是虚拟地址
static void *
boot_alloc(uint32_t n) //n是大小,uint32_t 长度4字节,但是这里只是说n的数据类型,而不是说n代表的数据长度;这里n代表n bytes
{
static char *nextfree; // virtual address of next byte of free memory
char *result;
// Initialize nextfree if this is the first time.
// 'end' is a magic symbol automatically generated by the linker,
// which points to the end of the kernel's bss segment: //bss segment:存放程序中未初始化的全局变量的一块静态内存分配区域
// the first virtual address that the linker did *not* assign //这段bss segment不会被assign to 任何kernel/global code
// to any kernel code or global variables.
if (!nextfree) { //第一次initialize nextfree
extern char end[];
nextfree = ROUNDUP((char *) end, PGSIZE); //bss segment末端开始寻找,说明从内核的末尾开始分配物理内存 ,end是定义在/kern/kernel.ld中定义的符号
}
// Allocate a chunk large enough to hold 'n' bytes, then update //?为啥这里是n bytes?
// nextfree. Make sure nextfree is kept aligned
// to a multiple of PGSIZE.//PGSIZE 的倍数
//
// LAB 2: Your code here.
result = nextfree;
if(n!=0)
{
nextfree = ROUNDUP((char *)(nextfree+n),PGSIZE); //为下一次更新nextfree
cprintf("n!=0,nextfree = %x, result = %x\n",nextfree,result);
return result;
}
else return nextfree; //当n=0时不用分配内存,直接返回nextfree
}
mem_init()函数
- 把panic语句注释掉
- 代码处需要做的事情:为pages结构找到合适的空间,并且将每个内存初始化为0
//////////////////////////////////////////////////////////////////////
// Allocate an array of npages 'struct PageInfo's and store it in 'pages'.
// The kernel uses this array to keep track of physical pages: for
// each physical page, there is a corresponding struct PageInfo in this
// array. 'npages' is the number of physical pages in memory. Use memset
// to initialize all fields of each struct PageInfo to 0. //把PageInfo结构中的all fields置为0
// Your code goes here:
pages = (struct PageInfo*)boot_alloc(sizeof (struct PageInfo)*npages); //调用boot_alloc()函数找到空闲空间起点,并且大小n= PageInfo结构大小*npages
memset(pages,0,sizeof (struct PageInfo)*npages);
-
- 调用boot_alloc()函数找到空闲空间起点,并且大小n= PageInfo结构大小*npages
- 猜测memset()函数:如
memset(pages,0,sizeof (struct PageInfo)*npages);
中,以pages指针所指的地方为起点,总长度为sizeof (struct PageInfo)*npages
之间的每个内存初始化为0
page_init()函数
这个函数的主要作用是初始化之前分配的pages数组,并且构建一个PageInfo链表,保存空闲的物理页,表头是全局变量page_free_list。具体实现看注释。
-
PageInfo结构的定义——
inc/memlayout.h
中struct PageInfo { // Next page on the free list. struct PageInfo *pp_link; //指向free list的下一页 // pp_ref is the count of pointers (usually in page table entries) // to this page, for pages allocated using page_alloc. // Pages allocated at boot time using pmap.c's // boot_alloc do not have valid reference count fields. uint16_t pp_ref; };
-
- 每个页page,都有一个PageInfo结构;
- pages[]数组是记录所有页面的PageInfo的,初始生成时,每个内存位置都是默认free的
- page_init()函数就是按照要求初始化pages数组,将不可分配的内存改为1,并将link调为NULL
-
关于
IOPHYSMEM
EXTPHYSMEM
// At IOPHYSMEM (640K) there is a 384K hole for I/O. From the kernel, // IOPHYSMEM can be addressed at KERNBASE + IOPHYSMEM. The hole ends // at physical address EXTPHYSMEM. #define IOPHYSMEM 0x0A0000 #define EXTPHYSMEM 0x100000
函数填补:
void
page_init(void)
{
// The example code here marks all physical pages as free.
// However this is not truly the case. What memory is free?
// 1) Mark physical page 0 as in use.
// This way we preserve the real-mode IDT and BIOS structures
// in case we ever need them. (Currently we don't, but...)
// 2) The rest of base memory, [PGSIZE, npages_basemem * PGSIZE)
// is free.
// 3) Then comes the IO hole [IOPHYSMEM, EXTPHYSMEM), which must
// never be allocated. //I/O hole 不可以被分配
// 4) Then extended memory [EXTPHYSMEM, ...).
// Some of it is in use, some is free. Where is the kernel
// in physical memory? Which pages are already in use for
// page tables and other data structures?
//
// Change the code to reflect this.
// NB: DO NOT actually touch the physical memory corresponding to
// free pages!
// 这里初始化pages中的每一项,建立page_free_list链表
// 已使用的物理页包括如下几部分:
// 1)第一个物理页是IDT所在,需要标识为已用
// 2)[IOPHYSMEM, EXTPHYSMEM)称为IO hole的区域,需要标识为已用。
// 3)EXTPHYSMEM是内核加载的起始位置,终止位置可以由boot_alloc(0)给出(理由是boot_alloc()分配的内存是内核的最尾部)
size_t i;
size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE;
size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE; //注意boot_alloc()返回虚拟地址
for (i = 0; i < npages; i++) {
if(i==0) //如上面要求1
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else if(i >= io_hole_start_page && i <= kernel_end_page)
{
pages[i].pp_ref = 1;
pages[i].pp_link = NULL;
}
else
{
pages[i].pp_ref = 0;
pages[i].pp_link = page_free_list;
page_free_list = &pages[i];
}
}
}
❗❗❗:第二个分支——else if里面 的判断条件应该有等于符号,第一次test的时候没有通过,回想发现这里[IOPHYSMEM, EXTPHYSMEM] 两个闭区间是不予分配的
❓❓❓:page_free_list 是表头,但是是从pages最后一项开始的??
从中得到的关于内存的结构消息
-
第0页、IO hole已被占用,内存extended部分被占用
-
其中,第0页为物理地址的前4k,存放real-mode IDT和BIOS structures;
-
IO hole为0x0A0000-0x100000的区域,被硬件保留用于特殊用途;
-
extended memory部分被占用的区域如kernel,以及我们刚刚分配的页表目录
page_alloc()函数
该函数的作用是:从page_free_list指向的链表中取一个PageInfo结构返回,根据参数alloc_flags决定要不要将这块内存初始化为0。需要注意的是,不需要增加PageInfo的pp_ref字段。
struct PageInfo *
page_alloc(int alloc_flags)
{
// Fill this function in
struct PageInfo *ret = page_free_list;
if (ret == NULL) //没有空余page了
{
cprintf("page_alloc: out of free memory! \n");
return NULL;
}
page_free_list = ret ->pp_link;
ret->pp_link = NULL;
if(alloc_flag && ALLOC_ZERO)
{
memset(page2kva(ret),0,PGSIZE);
}
return ret;
}
page_free()函数
将指针pp所指向的pageinfo对应的内存重新设为空,并加入page_free_list
//
// Return a page to the free list.
// (This function should only be called when pp->pp_ref reaches 0.)
//
void
page_free(struct PageInfo *pp)
{
// Fill this function in
// Hint: You may want to panic if pp->pp_ref is nonzero or
// pp->pp_link is not NULL.
if(pp->pp_link != NULL | pp->pp_ref !=0)
{
panic("Illegally call function page_free! \n");
}
else
{
pp->pp_link = page_free_list;
page_free_list = pp;
}
}
执行完mem_init()后
🚩:这里[end,end+PGSIZE]这一段是pages[0] 是单独留出来的一页;源码注释里面说,
- Mark physical page 0 as in use. This way we preserve the real-mode IDT and BIOS structures in case we ever need them. (Currently we don't, but...)
test
- 第一次test没有通过,报错:
★ 第二个分支——else if里面 的判断条件应该有等于符号,第一次test的时候没有通过,回想发现这里[IOPHYSMEM, EXTPHYSMEM] 两个闭区间是不予分配的
- 添加等号后通过
Exercise 02
exercise2.2是了解segmentation 和 page 机制,之前已经学过。
总的来说:
整个lab中,都不用分段,而是将segment base addresses 设为0,limits to 0xffffffff。linear address = virtual address's offset
。例外:lab3中需要一点点分段机制set up privilege levels
lab1的part3中,我们设置了simple page table
,让kernel可以运行在link address of 0xf0100000
处,而实际上load in physical address just above the BIOS at 0x00100000
; 但是 这个simple page table
只映射了4MB
Exercise 03
GDB access QEMU's memory only by virtual address; 但建立虚拟内存时最好可以看到物理地址——qemu指令 xp
可以看到物理地址!
在terminal中Ctrl-a c
可以调出 qemu monitor;
xp command
,info pg
提供当前页表信息(包括all mapped memory ranges,permissions, and flags);info mem
;在QEMU monitor里面使用;
x command
在GDB里面使用
当从bootloader进入保护模式后,所有memory reference 都是虚拟地址,经过MMU翻译,没有直接用吸纳型地址和物理地址的地方!——所有C指针都是虚拟地址
地址转换
这张图展示了x86体系中虚拟地址,线性地址,物理地址的转换过程。在boot/boot.S中我们设置了全局描述符表(GDT),设置所有段的基地址都是0x0,所有虚拟地址的offset和线性地址都是相等的。
在lab1中已经安装了一个简易的页目录和页表,将虚拟地址[0, 4MB)映射到物理地址[0, 4MB),[0xF0000000, 0xF0000000+4MB)映射到[0, 4MB)。所以内核kernel运行在它的link address
——0xf0100000
处,即使他被加载到了物理地址0x00100000
处。具体实现在kern/entry.S中,临时的页目录线性地址为entry_pgdir,定义在kern/entrypgdir.c中。
图示
地址指针
JOS 内核区分虚拟地址和物理地址的方法:
uintptr_r
表示 opaque vitrual addressesphysaddr_t
表示物理地址
以上两种类型都是uint32_t
的同义词,
①如果混用了这两种数据类型,编译器不会报错,但是引用的内容会出错!
② 他们本质是integer,而非指针,需要进行前期转换不能直接引用
③ 不能尝试将 physaddr_t转化为指针然后寻址,这样不会报错,但内容是错误的!(因为MMU默认所有的地址都是虚拟地址)
转换 KADDR(pa) PADDR(va)
JOS内核有时候在仅知道物理地址的情况下,想要访问该物理地址,但是没有办法绕过MMU的线性地址转换机制,所以没有办法用物理地址直接访问。JOS将虚拟地址0xf0000000映射到物理地址0x0处的一个原因就是希望能有一个简便的方式实现物理地址和虚拟地址的转换。在知道物理地址pa的情况下可以 加 0xf0000000得到对应的虚拟地址,可以用 KADDR(pa)
宏实现。在知道虚拟地址va的情况下 减 0xf0000000可以得到物理地址,可以用宏PADDR(va)
实现。
Exercise 04
page table
和 page directory
:页表和页目录,页目录是第一级页表(看上图)
首先,根据inc/mmu.h 里面的宏定义,linear address 的结构 (la)
// A linear address 'la' has a three-part structure as follows: // // +--------10------+-------10-------+---------12----------+ // | Page Directory | Page Table | Offset within Page | // | Index | Index | | // +----------------+----------------+---------------------+ // \--- PDX(la) --/ \--- PTX(la) --/ \---- PGOFF(la) ----/ // \---------- PGNUM(la) ----------/ // // The PDX, PTX, PGOFF, and PGNUM macros decompose linear addresses as shown. // To construct a linear address la from PDX(la), PTX(la), and PGOFF(la), // use PGADDR(PDX(la), PTX(la), PGOFF(la)). // page number field of address #define PGNUM(la) (((uintptr_t) (la)) >> PTXSHIFT) // page directory index #define PDX(la) ((((uintptr_t) (la)) >> PDXSHIFT) & 0x3FF) // page table index #define PTX(la) ((((uintptr_t) (la)) >> PTXSHIFT) & 0x3FF) // construct linear address from indexes and offset #define PGADDR(d, t, o) ((void*) ((d) << PDXSHIFT | (t) << PTXSHIFT | (o)))
同样,inc/mmu中还提到了
// Page table/directory entry flags.
#define PTE_P 0x001 // Present
#define PTE_W 0x002 // Writeable
#define PTE_U 0x004 // User
#define PTE_PWT 0x008 // Write-Through
#define PTE_PCD 0x010 // Cache-Disable
#define PTE_A 0x020 // Accessed
#define PTE_D 0x040 // Dirty
#define PTE_PS 0x080 // Page Size
#define PTE_G 0x100 // Global
为了完成这个练习,需要深入理解x86的段页机制,从这个图中可以看出P
mit-lec
看了这个图顿悟:(之前操作系统课程用中文学的,名称也不完全对应)
- page directory 和 page table都是属于从线性地址到物理地址的步骤,属于分页机制;而GDT/LDT对应的是逻辑地址到线性地址;
- PDE(page directory entry)和PTE 都是页表的一项,页表里不是直接存储页地址,而是存储
(page table) physical page number
,offset从线性地址那里直接送过来,然后才拼接成为物理地址 - 逻辑地址(16+32)共48位;线性和物理地址共32位
- (不是这个图得来的,但重要):pages[]保存的是物理页面 也转化成物理地址!
重要的辅助函数,来自kern/pmap.h
static inline physaddr_t
page2pa(struct PageInfo *pp) //已知pages[i]的地址,转化为第i个页面的首地址(pages[]保存的是物理页面 也转化成物理地址!
{ return (pp - pages) << PGSHIFT;}
static inline void* //将pages[i]地址转化为第i个页面的虚拟地址
page2kva(struct PageInfo *pp)
{ return KADDR(page2pa(pp));}
pgdir_walk()
传入pgdir(指向page directory的指针),该函数返回线性地址为va的指向PTE的指针。如果页表不存在且需要create,则用page_alloc()分配一张新的页表,并且需要将page directory里面对应项更新。x86 MMU 在page directory 和 page table两个地方都会检查权限,所以在page directory处可以不那么严格。所以在更新PDE时,*pde_ptr = page2pa(pp) | PTE_P | PTE_W | PTE_U;
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create) //注意:一级页表(page directory)存的是物理地址和权限,二级页表(page table)存的是虚拟地址且没有权限位
{
// Fill this function in
pde_t *pde_ptr = pgdir + PDX(va); //进入页目录找到directory entry
if(!(*pde_ptr && PTE_P)) //该页表还没有初始化
{
if(create)
{
//分配一个页面作为页表page table,并且初始化为0
struct PageInfo * pp = page_alloc(1);
if(pp == NULL) return NULL;
pp->pp_ref++ ;
*pde_ptr = page2pa(pp) | PTE_P | PTE_W | PTE_U; //创建新页表之后,page directory里面的内容也需要更新,PDE不是只有地址,且后12位默认为0,加上的这三个是将对应项置为1的
//return page2pa(pp)+PTX(va); //不能直接返回这个 是错误的!
return page2pa(pp)+PTX(va);
}
else
{
return NULL;
}
}
else
{
pte_t *pte_str = (pte_t *)(*pde_ptr) + PTX(va); //进入页表找到page table entry
return pte_str;
}
}
boot_map_region()
通过修改pgdir指向的树,将[va, va+size)对应的虚拟地址空间映射到物理地址空间[pa, pa+size)。va和pa都是页对齐的。
static void
boot_map_region(pde_t *pgdir, uintptr_t va, size_t size, physaddr_t pa, int perm)
{
// Fill this function in
//计算共有多少页
size_t num_pgs = size / PGSIZE;
if (size % PGSIZE !=0)
{
num_pgs++;
}
for(int i=0;i<num_pgs;i++)
{
pte_t *pte = pgdir_walk(pgdir,(void*)va,1); //获取va对应的PTE入口
if(pte ==NULL)
{
panic("boot_map_region():out of memory! \n");
}
*pte = pa | perm | PTE_P;
pa +=PGSIZE;
va +=PGSOZE;
}
}
page_lookup()
PageInfo* 作用:通过查找pgdir指向的树结构,返回va对应的PTE所指向的物理地址对应的PageInfo结构地址。
※:注意这里的pte_store 其实是结合了pgdir_walk()函数的功能,然后返回pte的地址的指针,且可以方便修改pte内容!
static inline struct PageInfo* //传入物理地址,(返回线性地址的前20位(即页目录项+页表项),找到page在pages[]数组中的序号,然后返回该PageInfo的地址)
pa2page(physaddr_t pa)
{
if (PGNUM(pa) >= npages)
panic("pa2page called with invalid pa");
return &pages[PGNUM(pa)];
}
// Address in page table or page directory entry
//已知入口 返回pde/pte的内容
#define PTE_ADDR(pte) ((physaddr_t) (pte) & ~0xFFF)
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
// Fill this function in
pte_t* pte_str = pgdir_walk(pgdir,va,false);
if(pte_str == NULL)
{
return NULL;
}
if(!(*pte_str) & PTE_P)
{
return NULL; //如果pte内容为空且PTE_P(即页表存在)
}
physaddr_t *pa = PTE_ADDR(pte_str);
if(pte_str !=NULL) *pte_str = pte;
return pa2page(*pa);
//return NULL;
}
page_remove()
修改pgdir指向的树结构,解除va的映射关系。
快表
快表,又称联想寄存器(TLB,translation lookaside buffer ),是一种访问速度比内存快很多的 高速缓存(TLB不是内存!),用来存放近访问的页表项的副本,可以加速地址变换的速度。 与此对应,内存中的页表常称为慢表
什么时候可能会遇到tlb_flush
对一个模块的优化往往需要对该模块的特性进行更细致的分析、归类,上一节,我们采用进程地址空间这样的术语,其实它可以被进一步细分为内核地址空间和用户地址空间。对于所有的进程(包括内核线程),内核地址空间是一样的,因此对于这部分地址翻译,无论进程如何切换,内核地址空间转换到物理地址的关系是永远不变的,其实在进程A切换到B的时候,不需要flush掉,因为B进程也可以继续使用这部分的TLB内容(上图中,橘色的block)。对于用户地址空间,各个进程都有自己独立的地址空间,在进程A切换到B的时候,TLB中的和A进程相关的entry(上图中,青色的block)对于B是完全没有任何意义的,需要flush掉。
在这样的思路指导下,我们其实需要区分global和local(其实就是process-specific的意思)这两种类型的地址翻译,因此,在页表描述符中往往有一个bit来标识该地址翻译是global还是local的,同样的,在TLB中,这个标识global还是local的flag也会被缓存起来。有了这样的设计之后,我们可以根据不同的场景而flush all或者只是flush local tlb entry。
// Decrement the reference count on a page,
// freeing it if there are no more refs.
//
void
page_decref(struct PageInfo* pp)
{
if (--pp->pp_ref == 0)
page_free(pp);
}
void
page_remove(pde_t *pgdir, void *va)
{
// Fill this function in
pte_t *pte_store;
struct PageInfo* pp = page_lookup(pgdir,va,&pte_store);
if(pp == NULL) return; //该虚拟页面还没有完成映射
page_decref(pp);
*pte_store=0; //这里的pte_store就是一个用于直接获得pte地址和内容的机制,是从page_lookup()函数这里来的
tlb_invalidate(pgdir,va);
return;
}
page_insert()
使va映射到pp对应的物理页处。
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
// Fill this function in
pte_t * pte_str = pgdir_walk(pgdir,va,1);//找到pte_t PTE entry
if(pte_str == NULL) return -E_NO_MEM;
if(page2pa(pp) != 0)
{
page_remove(pgdir,va); //修改页表,接触va的映射关系,且对tlb做必要的修改
}
*pte_str = (page2pa(pp)) | perm | PTE_P; //修改pte内容
pp->pp_ref++;
return 0;
}
测试
一开始无法通过,调用gdb调试:
错误提示:
调用gdb调试
当程序被停住了,你需要做的第一件事就是查看程序是在哪里停住的。当你的程序调用了一个函数,函数的地址,函数参数,函数内的局部变量都会被压入“栈”(Stack)中。可以用GDB命令来查看当前的栈中的信息。
例如:
下面是一些查看函数调用栈信息的GDB命令:
backtrace
bt
打印当前的函数调用栈的所有信息。如:
(gdb) bt
#0 func (n=250) at tst.c:6
#1 0x08048524 in main (argc=1, argv=0xbffff674) at tst.c:30
#2 0x400409ed in __libc_start_main () from /lib/libc.so.6
从上可以看出函数的调用栈信息:__libc_start_main --> main() --> func()
报错信息:
Program received signal SIGTRAP, Trace/breakpoint trap.
The target architecture is assumed to be i386
=> 0xf010129e <page_insert+93>: mov %eax,(%ebx)
0xf010129e in page_insert (pgdir=0x0, pp=0x1, va=0xf011730c, perm=-267268120)
at kern/pmap.c:482
482 *pte_str = (page2pa(pp)) | perm | PTE_P; //修改pte内容
调试自己的程序:
(gdb) bt
#0 0xf010129e in page_insert (pgdir=0x0, pp=0x1, va=0xf011730c,
perm=-267268120) at kern/pmap.c:482
#1 0xf011a000 in ?? ()
#2 0xf01019bb in mem_init () at kern/pmap.c:810
#3 0xf011a000 in ?? ()
#4 0xf010008b in i386_init () at kern/init.c:30
#5 0xf0103f40 in ?? ()
#6 0xf010003e in relocated () at kern/entry.S:80
可以看出调用顺序是relocated()-->i386_init()-->mem_init()-->当前函数
triple fault
https://blog.csdn.net/weixin_43344725/article/details/89209403
JOS此时尚未设置硬件允许从用户空间转换到内核态,当CPU发现这个它没办法处理这个中断时,它引发一个异常;当它又发现它没办法处理这个异常时,它又引发了一个异常;但它发现它还是没办法处理这个异常,只好放弃,因此最终引发了“三重异常”,在qemu中得到Triple fault
的输出。
所以,意识到自己之前对于虚拟地址、物理地址等,以及哪个函数该返回什么类型的地址没有厘清。重新检查一次。修改了后面的函数的虚实地址转换,但还是无法通过make grade
崩溃了?!! 我该怎么办!!!
对照网上大神的代码,先把自己代码保存一下,然后将大神代码替换进来!居然check_page():ok
debug记录
大神代码:
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
assert(pgdir);
pde_t *pde = &pgdir[PDX(va)];
if (!(*pde & PTE_P)) {
if (!create) return NULL;
struct PageInfo *page = page_alloc(ALLOC_ZERO);
if (!page) return NULL;
page->pp_ref++;
assert(page->pp_ref == 1);
assert(page->pp_link == NULL);
*pde = page2pa(page) | PTE_P | PTE_U | PTE_W;
}
// 获取页表项
return (pte_t*)(KADDR(PTE_ADDR(*pde))) + PTX(va);
}
我debug前的代码:
pte_t * //返回的肯定是可以寻址的虚拟地址
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
// Fill this function in
pde_t *pde_ptr = pgdir + PDX(va); //进入页目录找到directory entry
if(!(*pde_ptr & PTE_P)) //该页表还没有初始化
{
if(create)
{
//分配一个页面作为页表page table,并且初始化为0
struct PageInfo * pp = page_alloc(1);
if(pp == NULL) return NULL;
pp->pp_ref++ ;
*pde_ptr = page2pa(pp) | PTE_P | PTE_W | PTE_U ; //创建新页表之后,page directory里面的内容也需要更新,PDE不是只有地址
//return page2pa(pp)+PTX(va); //不能直接返回这个 是错误的!
//pte_t *pte_str = (pte_t*)(page2pa(pp)+PTX(va)); //page2pa()函数返回的是页面对应的物理地址,而不是page table对应的物理地址!!
//pte_t pte_str = KADDR(PTE_ADDR((physaddr_t)*pde_ptr));
//return (pte_t*)pte_str;
return (pte_t*)(KADDR(PTE_ADDR((physaddr_t)*pde_ptr)));
}
else
{
return NULL;
}
}
//else
//{
//pte_t pte_str = PTE_ADDR((physaddr_t)*pde_ptr); //进入页表找到page table entry
// return (pte_t*)pte_str;
//}
return ((pte_t *)KADDR(PTE_ADDR(*pde_ptr)+PTX(va)));
}
step01:最开始 return这里是直接PTE_ADDR(),而没有用KADDR——没有理解:PTE_ADDR()返回的是(physaddr_t) 所以一定要用KADDR转化为虚拟地址,在保护模式中,所有地址引用都要经过mmu处理,都是虚拟地址
step02:对照代码,① 第一个return没有加PTX(va)偏移量,但加上之后还是报错,②尝试把return中的PTX(va)地位升级,不需要KADDR转,发现:成了!!!——所以,PTX(va) 返回值是uintptr_t 类型的,是偏移量,不应该加入KADDR()
回顾part2 知识点
1. 首先得区分Virtual, Linear, and Physical Addresses
🚩🚩 在x86中,所有的内存引用都会被解释为虚拟地址,然后由MMU(内存管理单元)
来转换成物理地址从而找到对应的物理页。所以所有的C指针都是虚拟地址。
虚拟地址由a segment selector 和 an offset两部分组成,经过segment translation
转成线性地址,再经过page translation
变成物理地址(指向某个物理页的首地址)
在lab2里面可以暂时不要管段翻译,可以看成整个系统是一个段的,即其段基址是0,段范围(limit)是0xffffffff.整个系统就被分成了一个段,所以系统可以看成是纯分页形式的。在分页中,地址的转化也比较简单,JOS被设计成0-256M的物理地址映射到以0xf0000000开始的256M虚拟地址,所以从物理地址转化虚拟地址比较方便简单。
谢谢fang92
所以第二部分的主要任务就是要非常清晰线性地址与物理地址的互相转换过程,从而完成物理地址与虚拟地址之间的映射关系
(其实就是将物理页的物理基地址
赋给虚拟地址在页表中的表项
,并设好权限位
)
由第一部分我们可以知道,物理页地址由PageInfo结构体数组pages保存,而物理页的管理是通过二级页表机制pgdir+pgtable实现。
线性地址va由dir | table | offset
三个字段组成,分别是10bit、10bit、12bit,其中dir=PDX(va)是页目录表pgdir中的索引值,table=PEX(va)是页表pgtable中的索引值。
🚩 那么得先找到虚拟地址va对应在页表中的表项,由pgdir_walk()
完成。pgdir[PDX(va)]
内取出对应二级页表的物理基地址
(20bit),补全到32bit然后转成虚拟地址
得到pgtable(页表首地址),pgtable[PEX(va)]
就是va在页表中的表项了,&pgtable[PEX(va)]
就是该表项的地址,是个内核虚拟地址(前面说了,x86所有地址引用都得是虚拟地址)。
找到表项后,想建立物理地址与虚拟地址的映射只需将物理地址的物理基地址存到表项内并设好权限值即可。同样取消映射只需将表项内容清零即可,由page_insert()
与page_remove()
实现。建立或取消映射都会改变该物理页的引用值pp_ref
,减为0时应当把物理页free掉。
之前我在想既然有虚拟地址-KERNBASE=物理地址,那为什么还需要有虚拟内存与分页转换这么多此一举。首先下面列出了虚拟内存的好处,其次应该只有[KERNBASE,4GB]才符合这段映射,最后分页机制下可能减去KERNBASE得不到物理地址(但是页目录表项取出二级页表物理基地址转成虚拟地址就是加上KERNBASE实现的,所以这个理由保留意见)。🚩🚩 验证这句话!!
2. kern_pgdir
在内存初始化阶段,内核为页目录表分配了PGSIZE
的内存空间,以kern_pgdir
作为页目录表的首地地址,因此在内核的线性地址中,我们可以通过kern_pgdir
访问页目录表。那么页目录到底保存在物理内存的哪个区域呢? [页目录表是存储在内核的,内核级的数据结构]
从lab/kern/entrypgdir.c
中得知,在内核早期初始化阶段所建立的初级页表结构中:虚拟地址[KERNBASE,KERNBASE+4MB]映射到了物理地址[0,4MB]
处,虚拟地址[0,4MB]
映射到物理地址[0,4MB]
。在这套映射机制下,虚拟地址和物理地址的转换关系可以通过宏PADDR(kva)
与KADDR(pa)
进行转换,本质上是借助(虚拟地址=物理地址+KERNBASE
)这一转化关系。
说回到上面那个问题,页目录到底保存在物理内存的哪个区域?
kern_pgdir
的逻辑地址由kern_pgdir=(pde_t *)boot_alloc(PGZISE)
决定,确定逻辑地址后通过PADDR(kva)
即得到系统页目录表对应的物理内存地址。
Part 03: Kernel Address Space
Exercise 05
JOS将线性地址空间分为两部分,用户空间在lower part, 内核部分在upper part。由定义在inc/memlayout.h中的ULIM分割,预留了256MB的虚拟地址空间给内核。ULIM以上的部分用户没有权限访问,内核有读写权限。
❓
- lab book里面说 留了256MB 的空间给内核,这解释了为什么lab1中我们需要给内核一个很高的链接地址,不然就没有足够的内核虚拟空间去map到比它低的用户空间 ——???
/*
* Virtual memory map: Permissions
* kernel/user
*
* 4 Gig --------> +------------------------------+
* | | RW/--
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* : . :
* : . :
* : . :
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| RW/--
* | | RW/--
* | Remapped Physical Memory | RW/--
* | | RW/--
* KERNBASE, ----> +------------------------------+ 0xf0000000 --+
* KSTACKTOP | CPU0's Kernel Stack | RW/-- KSTKSIZE |
* | - - - - - - - - - - - - - - -| |
* | Invalid Memory (*) | --/-- KSTKGAP |
* +------------------------------+ |
* | CPU1's Kernel Stack | RW/-- KSTKSIZE |
* | - - - - - - - - - - - - - - -| PTSIZE
* | Invalid Memory (*) | --/-- KSTKGAP |
* +------------------------------+ |
* : . : |
* : . : |
* MMIOLIM ------> +------------------------------+ 0xefc00000 --+
* | Memory-mapped I/O | RW/-- PTSIZE
* ULIM, MMIOBASE --> +------------------------------+ 0xef800000
* | Cur. Page Table (User R-) | R-/R- PTSIZE
* UVPT ----> +------------------------------+ 0xef400000
* | RO PAGES | R-/R- PTSIZE
* UPAGES ----> +------------------------------+ 0xef000000
* | RO ENVS | R-/R- PTSIZE
* UTOP,UENVS ------> +------------------------------+ 0xeec00000
* UXSTACKTOP -/ | User Exception Stack | RW/RW PGSIZE
* +------------------------------+ 0xeebff000
* | Empty Memory (*) | --/-- PGSIZE
* USTACKTOP ---> +------------------------------+ 0xeebfe000
* | Normal User Stack | RW/RW PGSIZE
* +------------------------------+ 0xeebfd000
* | |
* | |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* . .
* . .
* . .
* |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|
* | Program Data & Heap |
* UTEXT --------> +------------------------------+ 0x00800000
* PFTEMP -------> | Empty Memory (*) | PTSIZE
* | |
* UTEMP --------> +------------------------------+ 0x00400000 --+
* | Empty Memory (*) | |
* | - - - - - - - - - - - - - - -| |
* | User STAB Data (optional) | PTSIZE
* USTABDATA ----> +------------------------------+ 0x00200000 |
* | Empty Memory (*) | |
* 0 ------------> +------------------------------+ --+
虚拟空间中的权限:
复习:内核虚拟地址空间到物理地址空间映射如下:
填补函数
//////////////////////////////////////////////////////////////////////
// Now we set up virtual memory
//////////////////////////////////////////////////////////////////////
// Map 'pages' read-only by the user at linear address UPAGES
// Permissions:
// - the new image at UPAGES -- kernel R, user R
// (ie. perm = PTE_U | PTE_P)
// - pages itself -- kernel RW, user NONE
// Your code goes here:
boot_map_region(kern_pgdir,UPAGES,PTSIZE,PADDR(pages),PTE_U);//把pages数组的起点开始PTSIZE大小的空间映射到kern_pgdir的对应的
//////////////////////////////////////////////////////////////////////
// Use the physical memory that 'bootstack' refers to as the kernel //将bootstack指向的物理内存用作 kernel stack
// stack. The kernel stack grows down from virtual address KSTACKTOP. //kernel stack 从虚拟地址 KSTACKTOP向下生长
// We consider the entire range from [KSTACKTOP-PTSIZE, KSTACKTOP)
// to be the kernel stack, but break this into two pieces: //整个kernel stack被分为两部分
// * [KSTACKTOP-KSTKSIZE, KSTACKTOP) -- backed by physical memory //正常映射了的地址
// * [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) -- not backed; so if
// the kernel overflows its stack, it will fault rather than
// overwrite memory. Known as a "guard page". //保护页,没有映射,当栈的增长超出其所在页时就会产生异常(因为无法翻译这个错误的地址)
// Permissions: kernel RW, user NONE
// Your code goes here:
boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
//////////////////////////////////////////////////////////////////////
// Map all of physical memory at KERNBASE. //映射所有在 KERNBASE的物理内存
// Ie. the VA range [KERNBASE, 2^32) should map to
// the PA range [0, 2^32 - KERNBASE)
// We might not have 2^32 - KERNBASE bytes of physical memory, but
// we just set up the mapping anyway.
// Permissions: kernel RW, user NONE
// Your code goes here:
boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
//把KERNBASE及以上的部分(内核部分)映射到[0,2^32 - KERNBASE)
通过测试代码!!
总结
- 区分清除虚拟地址和虚拟内存,这里讲的是虚拟地址,而不是操作系统课程中学的虚拟内存。
- 虚拟地址:x86指令(用户和内核都是如此)计算的是虚拟地址,是指程序使用的地址
- 物理地址:是DRAM中的存储单元,每个字节的物理内存都有一个地址,成为物理地址。机器的RAM,物理内存都是用物理地址标记的。
- x86的页表硬件通过映射机制将虚拟地址和物理地址联系起来
- 进程地址空间:
- entry中建立了一个超级页(4MB),解决了自举的问题:存放页表的页放在哪儿?——内核数据部分的后面开辟了一个4MB的超级页,一起映射到了虚拟地址上(可以理解为在内核代码段?)
- 每个进程都有自己的页表,xv6在进程切换的时候通知分页硬件切换页表。
- xv6在每个进程的页表中都包含了内核运行所需要的所有映射,这些映射出现在
KERNBASE
之上。 将KERNBASE:KERNBASE+PHYSTOP
映射到0:PHYSTOP
。 有一些使用内存映射的I/O设备的物理内存在0xFE000000
之上,对于这些设备xv6采用了直接映射。
- 虚拟空间权限:
-
虚拟空间到物理空间的映射
virutal physical [KERNBASE, 2^32) [0, 2^32 - KERNBASE) [KSTACKTOP-KSTKSIZE, KSTACKTOP) PADDR(bootstack) [KSTACKTOP-PTSIZE, KSTACKTOP-KSTKSIZE) 不映射,保护页 [UPAGES,UPAGES+PTSIZE] PADDR(pages)