2022-3-12 MIT6.828 Lab 2: Memory Management | Part 2: Virtual Memory | Exercise 2-4

Exercise 2. Look at chapters 5 and 6 of the Intel 80386 Reference Manual, if you haven’t done so already. Read the sections about page translation and page-based protection closely (5.2 and 6.4). We recommend that you also skim the sections about segmentation; while JOS uses the paging hardware for virtual memory and protection, segment translation and segment-based protection cannot be disabled on the x86, so you will need a basic understanding of it.

Chapter 5 Memory Management

  • Segment translation, in which a logical address (consisting of a segment selector and segment offset) are converted to a linear address.

  • Page translation, in which a linear address is converted to a
    physical address. This step is optional, at the discretion of
    systems-software designers.
    在这里插入图片描述
    5.1 Segment Translation
    (个人感觉这一块有些像物理地址寻址,由段地址+偏移得到地址,只不过开启了分页之后得到的是虚拟地址,没有开启分页得到的是物理地址。)
    在这里插入图片描述

  • Descriptors
    下面是两种形式的段描述符
    BASE 分段的起始位置
    LIMIT 段的总长度,有两种长度可以选择,(一种是 1 字节为粒度单位,用20位二进制能够表示的段的最大长度为1 M;一种是 4k 字节为单位,用 20 位进制所表示的最大长度为 4 G。)
    Granularity bit 粒度大小(置为1的话,粒度为1字节;置为0的话,粒度为 4k 字节。)
    (个人理解:其实粒度和存放没有啥关系,并且可以灵活的调整段的长度。)
    TYPE 段描述符种类
    DPL (Descriptor Privilege Level) 和地址访问保护有关(个人认为是区分用户和内核的特权)
    Segment-Present bit 为0则意味着不可以使用。地址无法转换。有两种情况会导致地址无法转换:1)地址没有映射,2)地址不在内存当中。)
    Accessed bit 标识是否处于正在访问之中。
    在这里插入图片描述

  • Descriptor tables
    段描述符表用于存放段描述符
    一个是全局段描述符表(GDT)
    一个是局部段描述符表 (LDT)
    (我个人认为,全局段描述符表是由操作系统统一管理,LDT 是各项进程人手一个,并且能够通过GDT 访问到 LDT )
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    参考文章:GDT(全居描述符表)和LDT(局部描述符表)

  • Selectors
    (我个人认为段选择子对线性地址的作用相当于实模式下寻址的段地址)
    Index 处理器简单地将这个索引值乘以8(一个描述符的长度),然后将其结果加到描述符的基址上,用来访问一个表中所描述的一个段。
    (和段地址一样,在使用的时候需要先偏移走一波)
    Table Indicator 0 是GDT表格里面找,1在 LDT 表格里面找
    Requested Privilege Level 和保护模式有关
    由于段选择子的第一个地址(索引为0)是不使用的,所以将没有引用对象的指针置为0。可以防止非法引用 —— 会因为无法正确引用而报错。
    在这里插入图片描述

  • Segment Registers
    将段选择子的前 16 位导入寄存器当中,剩下的部分交给程序来处理,这样就避免了多次访问描述符。
    在这里插入图片描述

5.2 Page Translation
Page translation is in effect only when the PG bit of CR0 is set.记得是在 entry.S 当中设置的。
在这里插入图片描述
5.2.1 Page Frame
a 4K-byte unit of contiguous addresses of physical memory.

5.2.2 Linear Address
在这里插入图片描述

5.2.3 Page Tables
A page table is itself a page, and therefore contains 4 Kilobytes of memory or at most 1K 32-bit entries.
在这里插入图片描述

The physical address of the current page directory is stored in the CPU register CR3, also called the page directory base register (PDBR). CR3里面存放了当前进程的页表所在的地址,一个进程可能有多个页表。

5.2.4 Page-Table Entries
在这里插入图片描述
5.2.4.1 Page Frame Address
用来放置页的物理地址。
由于每页的大小为 4kb,所以页表地址的后 12 位都是 0 (这些为0的位置就可以利用起来),
page directory 用这个 12 位来存放二级物理页表的地址。
a second-level page table 用这12位来放置内存操作情况(个人认为和缺页错误,磁盘页面切换有些关系)。

5.2.4.2 Present Bit
The Present bit indicates whether a page table entry can be used in address translation. P=1 indicates that the entry can be used.
在这里插入图片描述
When P=0 in either level of page tables, the entry is not valid for address translation, and the rest of the entry is available for software use;
不可以使用的页面二级结构直接为空
在这里插入图片描述
试图访问这样的页面会报错异常。(怀疑和缺页错误有关)
the page directory itself 是没有这一位的。
注意,页目录本身没有存在位。当相关任务被挂起时,页目录可以不存在。但操作系统必须确保TSS映像中CR3所引用的页目录在任务被分派时可用。

5.2.4.3 Accessed and Dirty Bits
The processor sets the corresponding accessed bits in both levels of page tables to one before a read or write operation to a page.

The processor sets the dirty bit in the second-level page table to one before a write to an address covered by that page table entry. The dirty bit in directory entries is undefined.

用于 缺页错误和是否写回,写直达

5.2.4.4 Read/Write and User/Supervisor Bits
used for page-level protection

5.2.5 Page Translation Cache
为了方便,页表会放在高速缓冲区当中。(比如说 TLB ,translation lookaside buffer)。当页表不在高速缓冲区当中,需要刷新所有级的页表。

5.3 Combining Segment and Page Translation
5.3.1 “Flat” Architecture
可以通过向段寄存器中加入一个特定的段选择子来关闭段结构。让那个选择子所指向的描述符占用整个地址空间,便可以达到同样的效果。(个人认为这种想法不是不分段,而是让所有的地址空间成为唯一的一整段。)

5.3.2 Segments Spanning Several Pages
一段可以被分成多个页。反正程序不知道它的某一段被放在物理地址当中的多个页面,反正知道了也无所谓。
在这里插入图片描述
5.3.3 Pages Spanning Several Segments
多个段可能放在一个页当中。比如说信号量可能单独放在不同的段当中,这些段比较小。可能会放在同一个页面当中。

5.3.4 Non-Aligned Page and Segment Boundaries
我个人是这样理解的,进程要多少空间,就将多少页分给进程。对于进程来说,它看到的是一大块连续的地址空间,然后在这块连续的地址空间当中放置代码和数据,这些代码和数据可能是分段放置。具体同一段的全部是不是放在同一页当中,它不知道,也没有必要知道。

5.3.5 Aligned Page and Segment Boundaries
将页起始和段起始相绑定能够简化内存管理软件。(我有个疑问,如果段的大小超过了页的大小怎么办?如果段的大小远远小于页的大小怎么办?第一种情况未必能够简化内存空间管理软件的复杂程度,第二种情况浪费空间)

5.3.6 Page-Table per Segment
在这里插入图片描述
这个部分没有咋看懂。

Chapter 6 Protection
6.1 Why Protection?
能够限制有问题的程序因访问内存不当所带来的损失。

6.2 Overview of 80386 Protection Mechanisms
保护模式的五个方面:
Type checking
Limit checking
Restriction of addressable domain
Restriction of procedure entry points
Restriction of instruction set
地址形成,内存访问之前就会对地址涉及保护模式的五个检查方面进行检查。在程序开始的周期之前就会完成对程序的检查。

保护模式的后3个方面便是所谓的特权。对于程序而言,特权衡量的是被信任的程度;对于数据而言,特权衡量的是被保护的程度。

6.3 Segment-Level Protection
段保护模式涉及的五个方面:
Type checking
Limit checking
Restriction of addressable domain
Restriction of procedure entry points
Restriction of instruction set
这五个方面会在将段选择子载入寄存器的时候由 CPU 进行检查。
寄存器里会存放段选择子的特权级别。

6.3.1 Descriptors Store Protection Parameters
用户程序不需要考虑到代码的拥有啥特权(这部分是系统自动生成的?)。
为啥检查地址的权限级别不需要额外的时钟周期?
因为段的权限级别会在段加载的时候一同导入到寄存器当中。(想来,不需要额外的时钟周期是指访存吧?)

在这里插入图片描述

6.3.1.3 Privilege Levels

6.4 Page-Level Protection
分页级别的保护
Restriction of addressable domain.
Type checking

6.4.1 Page-Table Entries Hold Protection Parameters
highlights the fields of PDEs and PTEs that control access to pages.
在这里插入图片描述
6.4.1.1 Restricting Addressable Domain
Supervisor level (U/S=0) – for the operating system and other systems software and related data.
User level (U/S=1) – for applications procedures and data.
具体在那个模式下执行和 CPL 有关。如果 CPL 为 0,1,2,程序运行在 Supervisor level 上面,所有的地址都可以访问。
如果 CPL 为 3 ,程序运行在 User level 上面,只有用户级别的地址可以访问。

6.4.1.2 Type Checking
Read-only access (R/W=0)
Read/write access (R/W=1)
Supervisor level 啥都可读可写
User level 只有属于 User level 的可读可写,其他不可读也不可写。

6.4.2 Combining Protection of Both Levels of Page Tables
不同级别的页表权限可能不一样,只有通过所有级别页表的权限检测,才ok;

6.4.3 Overrides to Page Protection
无论是啥特权级。下面这些特权级别都会被检查。
LDT, GDT, TSS, IDT references.
Access to inner stack during ring-crossing CALL/INT.

Exercise 3. While GDB can only access QEMU’s memory by virtual address, it’s often useful to be able to inspect physical memory while setting up virtual memory. Review the QEMU monitor commands from the lab tools guide, especially the xp command, which lets you inspect physical memory. To access the QEMU monitor, press Ctrl-a c in the terminal (the same binding returns to the serial console).

Use the xp command in the QEMU monitor and the x command in GDB to inspect memory at corresponding physical and virtual addresses and make sure you see the same data.

Our patched version of QEMU provides an info pg command that may also prove useful: it shows a compact but detailed representation of the current page tables, including all mapped memory ranges, permissions, and flags. Stock QEMU also provides an info mem command that shows an overview of which ranges of virtual addresses are mapped and with what permissions.

暂时不知道如何将 qemu 切换到 monitor 模式,下一个下一个。
在这里插入图片描述
在这里插入图片描述
Question

Assuming that the following JOS kernel code is correct, what type should variable x have, uintptr_t or physaddr_t?

mystery_t x;
char* value = return_a_pointer();
*value = 10;
x = (mystery_t) value;

是 uintptr_t 。在开启虚拟地址之后,内核无法直接使用物理地址。不能绕开虚拟地址转换为物理地址这一步。故在程序中取得的地址大概率是虚拟地址。内核也提供了物理地址和虚拟地址互相转换的方法,通过 KADDR(pa) 指令来获取物理地址 pa 所映射成的虚拟地址。通过 PADDR(va) 指令来通过 虚拟地址的值 va 求得物理地址的值。

Exercise 4. In the file kern/pmap.c, you must implement code for the following functions.

    pgdir_walk()
    boot_map_region()
    page_lookup()
    page_remove()
    page_insert()

check_page(), called from mem_init(), tests your page table management routines. You should make sure it reports success before proceeding.

  • pgdir_walk()
    这个函数的目标是从一级页表当中找到对应的二级页表的指针
    要进行如下的判断。
    是否在内存中
  • 如果在,增加引用计数,返回二级页表指针的地址
  • 如果不在,就创建一个
    • 如果创建成功,就增加引用计数,返回指针
    • 创建失败,返回 NULL

(上面的理解好像不太对)
inc/mmu.h 是包含了一些有用的宏

// page directory index
#define PDX(la)		((((uintptr_t) (la)) >> PDXSHIFT) & 0x3FF)

// page table index
#define PTX(la)		((((uintptr_t) (la)) >> PTXSHIFT) & 0x3FF)

用这个宏来查看页面是否在内存当中
// Page table/directory entry flags.
#define PTE_P		0x001	// Present

//将物理地址转化为二级页表或者页目录当中的地址
//也就是物理地址的后 12 位偏移地址全部为 0 ,剩下 20 位分别表示 pt 和 pd
// Address in page table or page directory entry
#define PTE_ADDR(pte)	((physaddr_t) (pte) & ~0xFFF)
// Given 'pgdir', a pointer to a page directory, pgdir_walk returns
// a pointer to the page table entry (PTE) for linear address 'va'.
// This requires walking the two-level page table structure.
//
// The relevant page table page might not exist yet.
// If this is true, and create == false, then pgdir_walk returns NULL.
// Otherwise, pgdir_walk allocates a new page table page with page_alloc.
//    - If the allocation fails, pgdir_walk returns NULL.
//    - Otherwise, the new page's reference count is incremented,
//	the page is cleared,
//	and pgdir_walk returns a pointer into the new page table page.
//
// Hint 1: you can turn a PageInfo * into the physical address of the
// page it refers to with page2pa() from kern/pmap.h.
//
// Hint 2: the x86 MMU checks permission bits in both the page directory
// and the page table, so it's safe to leave permissions in the page
// directory more permissive than strictly necessary.
//
// Hint 3: look at inc/mmu.h for useful macros that manipulate page
// table and page directory entries.
//
pte_t *
pgdir_walk(pde_t *pgdir, const void *va, int create)
{
	// Fill this function in
	//找到页在页目录当中的地址
	pde_t *dir = pgdir + PDX(va);
	
	if (!(*dir & PTE_P)) {
		if (!create) return NULL;
		struct PageInfo* pp = page_alloc(1);
		if (pp == NULL) return NULL;
		pp->pp_ref++;
		*dir = page2pa(pp) | PTE_P | PTE_U | PTE_W;
	}

	return (pte_t *) KADDR(PTE_ADDR(*dir)) + PTX(va);
}

其他的代码:

pte_t * pgdir_walk(pde_t *pgdir, const void * va, int create)
{
      unsigned int page_off;
      pte_t * page_base = NULL;
      struct PageInfo* new_page = NULL;

      unsigned int dic_off = PDX(va);
      //PDX()是啥
      pde_t * dic_entry_ptr = pgdir + dic_off;
      //大概是找到页表项在pgdir 里面的具体位置

      if(!(*dic_entry_ptr & PTE_P))
      //判断 page 是否在内存当中
      {
            if(create)
            {
            		//分配内存
                   new_page = page_alloc(1);
                   if(new_page == NULL) return NULL;
                   //内存引用计数增加1
                   new_page->pp_ref++;
                   *dic_entry_ptr = (page2pa(new_page) | PTE_P | PTE_W | PTE_U);
                   //page_alloc返回的是PageInfo* ,可以用page2pa得到对应的物理地址.得到物理地址后赋予相应的权限
            }
           else
               return NULL;
      }

      page_off = PTX(va);
      //虚拟地址在page table 里的index
      //PTE_ADDR(*dic_entry_ptr)将物理地址的后12位全部变成0,也就是page table 里面的地址当中的权限位
      page_base = KADDR(PTE_ADDR(*dic_entry_ptr));
      //通过kaddr 来映射成虚拟地址
      //虚拟地址和物理地址的偏移量是一致的
      return &page_base[page_off];
}

2.boot_map_region()

建立物理地址和虚拟地址的联系(很遗憾,这样想是错误的)
为了设置虚拟地址UTOP之上的地址范围,这一部分的地址映射是静态的,在操作系统的运行过程中不会改变,所以这个页的PageInfo结构体中的pp_ref域的值不会发生改变

//
// Map [va, va+size) of virtual address space to physical [pa, pa+size)
// in the page table rooted at pgdir.  Size is a multiple of PGSIZE, and
// va and pa are both page-aligned.
看到这里的page 对齐,大概明白了为啥这里不用建立 pa 和 va 的映射关系了
这里也不需要记录页的引用计数
// Use permission bits perm|PTE_P for the entries.
//
// This function is only intended to set up the ``static'' mappings
// above UTOP. As such, it should *not* change the pp_ref field on the
// mapped pages.
//
// Hint: the TA solution uses pgdir_walk
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 pieces = ROUNDUP(size, PGSIZE) / PGSIZE;
	//有多少页
	//ROUNDUP 是用来取整的
	for (size_t i = 0; i < pieces; i++) {
		pte_t *pte = pgdir_walk(pgdir, (void *) va, 1);
		//为虚拟地址在二级页表中找到位置
		if (pte == NULL) {
			panic("boot_map_region: out of memory!\n");
		}

		*pte = pa | PTE_P | perm;
		//给物理地址相应的权限
		//pgdir page table 里面都是物理地址
		va += PGSIZE;
		pa += PGSIZE;
		//虚拟地址和物理地址同时增长
		//有些东西我没有明白?
		物理地址和虚拟地址的映射关系在哪里建立起来的?
	}
}

3.page_insert()

//
// Map the physical page 'pp' at virtual address 'va'.
// The permissions (the low 12 bits) of the page table entry
// should be set to 'perm|PTE_P'.
//
// Requirements
//   - If there is already a page mapped at 'va', it should be page_remove()d.
//   - If necessary, on demand, a page table should be allocated and inserted
//     into 'pgdir'.
//   - pp->pp_ref should be incremented if the insertion succeeds.
//   - The TLB must be invalidated if a page was formerly present at 'va'.
//
// Corner-case hint: Make sure to consider what happens when the same
// pp is re-inserted at the same virtual address in the same pgdir.
// However, try not to distinguish this case in your code, as this
// frequently leads to subtle bugs; there's an elegant way to handle
// everything in one code path.
//
// RETURNS:
//   0 on success
//   -E_NO_MEM, if page table couldn't be allocated
//
// Hint: The TA solution is implemented using pgdir_walk, page_remove,
// and page2pa.
//
int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
	// Fill this function in
	pte_t *pte = pgdir_walk(pgdir, va, 1);
	//在页表中找到某个地址对应的页表
	if (!pte) {
		return -E_NO_MEM;
	}
	//如果没有找到就返回错误

	pp->pp_ref++;
	//为啥引用计数依然要增加?
	
	if (*pte & PTE_P) {
		page_remove(pgdir, va);
		//如果有映射就把原先的映射移走 
		//这个可以理解当页重新使用的时候需要这样操作
		//但是当多个页映射到同一个物理地址怎么办?
		//不会有这种鼓励,因为多个页表项映射到同一个物理地址的话pgdir 是不一致的
		tlb_invalidate(pgdir, va);
		//上面这个函数干什么用的?我没有看懂
	}

	*pte = page2pa(pp) | PTE_P | perm;
	//给地址加权限
	return 0;
}

更好的版本

int
page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
{
    pte_t *entry = NULL;
    entry =  pgdir_walk(pgdir, va, 1);    //Get the mapping page of this address va.
    if(entry == NULL) return -E_NO_MEM;

    pp->pp_ref++;
    if((*entry) & PTE_P)             //If this virtual address is already mapped.
    {
        tlb_invalidate(pgdir, va);
        page_remove(pgdir, va);
    }
    *entry = (page2pa(pp) | perm | PTE_P);
    pgdir[PDX(va)] |= perm;
    //为什么需要在这里赋值
                      //Remember this step!

    return 0;
}

4、page_lookup()
返回虚拟地址va所映射的物理页的PageInfo结构体的指针,如果pte_store参数不为0,则把这个物理页的页表项地址存放在pte_store中。
也就是传出参数的实现。

//
// Return the page mapped at virtual address 'va'.
// If pte_store is not zero, then we store in it the address
// of the pte for this page.  This is used by page_remove and
// can be used to verify page permissions for syscall arguments,
// but should not be used by most callers.
//
// Return NULL if there is no page mapped at va.
//
// Hint: the TA solution uses pgdir_walk and pa2page.
//
struct PageInfo *
page_lookup(pde_t *pgdir, void *va, pte_t **pte_store)
{
	// Fill this function in
	pte_t *pte = pgdir_walk(pgdir, va, 0);
	if (!pte || !(*pte & PTE_P)) {
		cprintf("page_lookup: can not find out the page mapped at virtual address 'va'.\n");
		return NULL;
	}

//传出参数
	if (pte_store) {
		*pte_store = pte;
	}

	return pa2page(PTE_ADDR(*pte));
}

5.page_remove()
建立映射关系的时候引用计数+1,那接触映射关系的时候,引用计数自然得减一。
那为什么 pgdir_walk()当中 create 为 true 的时候引用计数也得增加。

//
// Unmaps the physical page at virtual address 'va'.
// If there is no physical page at that address, silently does nothing.
//
// Details:
//   - The ref count on the physical page should decrement.
//   - The physical page should be freed if the refcount reaches 0.
//   - The pg table entry corresponding to 'va' should be set to 0.
//     (if such a PTE exists)
//   - The TLB must be invalidated if you remove an entry from
//     the page table.
//
// Hint: The TA solution is implemented using page_lookup,
// 	tlb_invalidate, and page_decref.
//
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) {
		page_decref(pp);
		*pte_store = 0;
		tlb_invalidate(pgdir, va);
	}
}

最终测试的运行结果
在这里插入图片描述
不知道check_va2pa()这个是啥?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实验概述 本次实验是MIT 6.828操作系统课程的第一次实验,主要内容是编写一个简单的操作系统内核,并在QEMU虚拟机上运行。本次实验共有9个练习,其中练习9要求实现一个简单的用户程序并运行。 练习9要求我们实现一个简单的用户程序,该程序能够在屏幕上输出一些信息,并等待用户输入,输入结束后将输入内容输出到屏幕上。用户程序的具体要求如下: - 输出一些信息,例如“Hello World!”。 - 等待用户输入,可以使用getchar()函数实现。 - 将用户输入内容输出到屏幕上。 实验过程 1. 编写用户程序 我们首先在lab1目录下创建一个user文件夹,用于存放用户程序。然后创建一个名为“test.c”的文件,编写用户程序的代码如下: ``` #include <stdio.h> int main() { printf("Hello World!\n"); char c = getchar(); printf("You entered: %c\n", c); return 0; } ``` 这段代码的功能是输出“Hello World!”并等待用户输入,输入结束后将输入内容输出到屏幕上。 2. 修改Makefile文件 为了能够编译用户程序,我们需要修改Makefile文件。具体修改如下: ``` UPROGS=\ _cat\ _echo\ _forktest\ _grep\ _init\ _kill\ _ln\ _ls\ _mkdir\ _rm\ _sh\ _stressfs\ _usertests\ _wc\ _test\ # 添加用户程序的名称 $(OBJDIR)/_test: $(OBJDIR)/test.o $(LIBDIR)/ulib.o | $(OBJDIR) $(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $@ $^ $(OBJDIR)/test.o: test.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< ``` 在UPROGS变量中添加上刚刚编写的用户程序的名称“_test”,然后在Makefile文件的末尾添加如上代码。 3. 编译内核和用户程序 在终端运行命令“make”,编译内核和用户程序。 4. 运行QEMU虚拟机 在终端运行命令“make qemu”,启动QEMU虚拟机。 5. 运行用户程序 在QEMU虚拟机中,输入“test”,即可运行刚刚编写的用户程序。运行结果如下: ``` Hello World! This is a test. You entered: T ``` 可以看到,程序首先输出了“Hello World!”这个信息,然后等待用户输入。我们输入了“This is a test.”这个字符串,然后按下回车键,程序将输入内容输出到了屏幕上。 实验总结 本次实验要求我们实现一个简单的用户程序并运行。通过编写代码、修改Makefile文件、编译内核和用户程序、启动虚拟机以及运行用户程序等步骤,我们成功地完成了本次实验,并学会了如何在操作系统内核中运行用户程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值