• MIT_JOS_Lab2


    内存管理

    内存管理的内容

    内核页表机制的启动过程: 详细内容可以这篇博客 传送门 ,这里我们简单概述下, BIOS 与 boot loader 都是工作在实模式下的, 启用页表机制是在 entry.S 文件中启动的, 并且将页目录的物理地址存入 CR3 寄存器中, 这是一个静态初始化的页目录与页表, 映射的内存空间为 4MB, 但是我们知道, 内核的大小远大于 4MB, 这 4MB 的部分是用来执行 KERNBASE 上面 4MB 部分的代码, 这一部分代码一定包含了下面的构造真正的内核页表部分的代码.

    物理内存分配器: 管理内存分配出去的页表以及有多少个进程分享每一个已经被分配的页表. 如何收回与释放内存页表.

    虚拟内存分配器: 将内核与用户程序的虚拟地址与物理地址进行对应, 实现这一功能的就是 memory management unit (MUM), 使用的是页表

    物理内存管理

    memlayout.h 是虚拟内存的描述, 之前的笔记中已经记录了用法, 下面是一个十分重要的结构, 用来表示一个物理页的状态:

    /*
     * Page descriptor structures, mapped at UPAGES.
     * Read/write to the kernel, read-only to user programs.
     * 存出来一个物理页的基本信息, 并不是物理页本身, 而且我们也不必对物理页的所有信息进行描述
     * 这个结构体与物理页一一对应, 可以通过 page2pa() 获得一个物理页描述符的物理地址
     */
    // 描述了一个物理页的状态
    struct PageInfo {
        // 这是一个虚拟地址, 指向一个物理页描述符结构体
    	// Next page on the free list.
    	struct PageInfo *pp_link;
        
    	// 表示有多少个虚拟地址指向该页
    	uint16_t pp_ref;
    };
    

    物理页的状态可以是空闲的, 此时 pp_ref = 0, 也可以是被占用, 此时 pp_ref = 1, 可以这么理解, PageInfo 的物理地址就是该页的物理地址.

    内存管理的部分与前面的内核启动的部分不一样, 内核启动的部分代码需要在启动的时候执行, 而内存管理的部分编译后存储在内核文件中(ELF 文件, 代码段与数据段), 所以而我们写的部分是编译之前的代码, 相当于对内核进行内存管理的功能描述. 这些代码的执行是在启动之后.

    首先我们需要明确一些变量的定义:

    size_t npages;			// Amount of physical memory (in pages), 表示物理内存的大小
    pde_t *kern_pgdir;		// Kernel's initial page directory, 内核的页目录
    struct PageInfo *pages;		// Physical page state array, 物理页状态数组, 记录了物理页的状态, 
    // 问题: 如何获得物理页的物理地址, 根据下面的内容我们直到, 直接使用 pages 数组的下标就可以访问物理地址了, 因为物理页以及以 PGSIZE 为单位划分
    static struct PageInfo *page_free_list;	// Free list of physical pages, 空闲物理页链表, 将空闲的物理页连成一个链表
    

    当计算机刚开始启动虚拟地址机制的时候, 因为要构造一个二级页表, 首先分配的是一个页目录, 内核文件执行的时候的页目录是一个静态的目录, Lab1 描述的, 已经存在在内核文件的数据段中, 可以直接访问, 那么在构建页表机制的时候, 构建页目录的方法是, 在内核空闲数据段的最末端分配一个页(4KB)大小的空间, 作为页目录, 分配这样一个空间的函数是:

    static void *
    boot_alloc(uint32_t n)
    {
    	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: 根据 ELF 文件的格式, 这里就是内核数据段的末尾
        // 也可以根据前面所使用的查看内核 ELF 文件的格式来查看, 得到 bss 段的内容如下:
        // Idx Name          Size      VMA       LMA       File off  Algn
        // 9 .bss          00000648  f0113060  00113060  00014060  2**5
        // 内核文件在虚拟内存中, 数据段在代码段的上面, bss 就是数据段的结尾
    	// the first virtual address that the linker did *not* assign
    	// to any kernel code or global variables.
    	if (!nextfree) {
    		extern char end[];
    		// 将地址 end 向上以页面大小对齐
    		nextfree = ROUNDUP((char *) end, PGSIZE);
    	}
    	// Allocate a chunk large enough to hold 'n' bytes, then update
    	// nextfree.  Make sure nextfree is kept aligned to a multiple of PGSIZE.
    	//
    	// LAB 2: Your code here.
    	// 这里的 free memory 是在 KERNBASE 上面的虚拟地址空间中的 free memory, 也就是内核的数据段
    	result = nextfree;
        // 这里是一个虚拟地址
    	if(n > 0)
    	{
    		nextfree = ROUNDUP(result+n, PGSIZE);
            // 这里相当于在 free memory 分出一部分内存, 以 PGSIZE 对齐
    	}
    	cprintf("boot_alloc memory at %x, next memory allocate at %x
    ", result, nextfree);
    	return result;
    }
    

    分配页目录之后就是建立二级页表, 这里需要先初始化一下内存, 以及虚拟内存中的一些数据:

    	// create initial page directory.
    	// 分配一个物理页大小的虚拟空间, 对于页目录来说, 这里使用 boot_alloc, kern_pgdir 就是页目录的虚拟地址
    	kern_pgdir = (pde_t *) boot_alloc(PGSIZE);
    	memset(kern_pgdir, 0, PGSIZE);
    	// 下面对页目录的内容进行初始化
    	// Permissions: kernel R, user R
    	// 假设 UVPT 是页目录开头的虚拟地址, 左边得到的应该是页目录的物理地址
    	// 这一步相当于将页目录的虚拟地址 kern_pgdir 固定在 UVPT, 访问UVPT就是访问页目录
    	kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
    	// 下面是描述物理内存的状态
    	// 物理页描述符的数组, 
    	pages = (struct PageInfo*)boot_alloc(sizeof(struct PageInfo) * npages);
    	// pages 是物理内存状态数组
    	memset(pages, 0, sizeof(struct PageInfo) * npages);
    	// 这一部分是确定物理内存的状态以及页目录初始信息
    

    接下来对物理内存的描述进行初始化, 在分配物理内存, 设置页目录机制之前, 物理内存中有很多地方已经不是空闲的了, 所以对 pages 的描述就会有所改变, 比如说物理内存中分配给 IO 段的内存, 直到内核的数据段的末尾都是已经已经分配过的物理内存, 这一部分已经被分配, 所以不在空闲链表中. 下面就初始化 pages 数组,

    void page_init(void)
    {
        // 获取 IO 数据段的物理地址
    	size_t io_hole_start_page = (size_t)IOPHYSMEM / PGSIZE;
    	// If n==0, returns the address of the next free page without allocating anything.
    	// 使用 boot_alloc(0) 找出未被分配的物理地址, 就是内核分配的末尾的物理地址
    	size_t kernel_end_page = PADDR(boot_alloc(0)) / PGSIZE;
    
    	size_t i;
    	for (i = 0; i < npages; i++) {
    		if(i == 0){
    			pages[i].pp_ref = 1;
    			pages[i].pp_link = NULL;
    		}
    		if(io_hole_start_page <= i && 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];
    		}
    		
    	}
    }
    

    分配了物理页空闲链表就实现了对物理内存的更好的管理, 我们不仅知道了物理内存的状态, 还知道了哪些物理页是空闲的, 注意: page_free_list 空闲链表构成的方法是通过 pages[i] 的 pp_link 构造的, page_free_list 是空闲链表的开头, 内容是物理页描述符, 而不是真正的物理页. 描述了物理页之后, 分配一个物理页就很简单了,

    struct PageInfo * page_alloc(int alloc_flags)
    {
    	// Fill this function in
    	struct PageInfo *new_alloc = page_free_list;
    	if(new_alloc == NULL)
    	{
    		// 分配失败, 没有空闲的空间
    		cprintf("page_alloc: out of free memory
    ");
    	}
        // 将 page_free_list 向后移动一步, 表示一个物理页被占用
    	page_free_list = new_alloc->pp_link;
        // 这一页已经被分配了, 所以 pp_link == NULL
    	new_alloc->pp_link = NULL;
    	if(alloc_flags & ALLOC_ZERO)
    	{
    		memset(page2kva(new_alloc), 0, sizeof(struct PageInfo));
    	}
    	return new_alloc;
    }
    

    我们是重要注意的是, 物理内存的描述就是 pages, 所以释放与分配的过程对 pages 的操作相反:

    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_ref == 0 && pp->pp_link == NULL)
    	{
    		pp->pp_link = page_free_list;
    		page_free_list = pp;
    	}
    	else
    	{
            // 释放一个页之前, 要判断是否被使用
    		panic("page_free: pp->pp_ref is nonzero or pp->pp_link is not NULL
    ");
    	}
    	
    }
    

    虚拟内存管理

    虚拟地址, 线性地址, 物理地址

    对于 x86 而言, 虚拟地址段选择器与段内偏移, 准确的说, 线性地址就是段内偏移, 也就是进行页表映射之前的地址, 物理地址是访问 RAM 内存的地址, 是转换后的地址, 对于xv6 JOS 而言, 没有使用段机制, 在前面的 Lab1 中在页表机制启动之前, boot loader 阶段 cpu 仍然工作在实模式下. 启动页表机制与在保护模式下, 程序的内存引用, 也就是程序中的偏移与指针,都是建立在虚拟地址上的. 而进入了保护模式之后, 内核有时候有需要读或者更改他知道的物理地址对应的内存, 这时候就需要将物理地址再转化为虚拟地址, 然后引用虚拟地址.

    统计页面的引用个数

    在页表的构造中, 我们知道虚拟地址向物理地址映射的时候, 一定会有多个虚拟地址映射到同一个物理地址, 我们需要计算对于每个物理页面有多少个虚拟页面指向该物理页面, 通常情况下, 这个比较好计算, 直接拿虚拟空间大小除以物理空间的大小就可以了, 我们真正考虑这个引用的个数的问题, 比如说为什么引用可以是 0, 因为虚拟内存空间没有数据的话, 物理内存对应的位置一定是空.

    下面的内容主要是页表的管理, 最重要的工作就是将虚拟地址与物理地址对应起来, 在保护模式下, 对应的方式就是通过页表来实现. 需要实现的函数如下:

    // 对于一个虚拟地址找到页表中对应页表项地址, 页目录项的内容就是页表对应的物理地址, 
    // 该函数返回的页表项地址还是一个虚拟地址
    pte_t * pgdir_walk(pde_t *pgdir, const void *va, int create)
    {
    	// Fill this function in
    	pte_t *result;
    	// 如果页目录对应的目录项内容为空, 表示页表中该页还未创造
    	if (!(pgdir[PDX(va)] & PTE_P)) {
    		// create == 0, return NULL
    		if (create == 0) {
    			return NULL;
    		}
    		else {
    			// 新建一页, 在物理内存中新分配一页
    			struct PageInfo *temp = page_alloc(ALLOC_ZERO);
    			if(temp == NULL) {
    				return NULL;
    			}
    			else {
    				temp->pp_ref += 1;
    				// 新分配的一页是页表, 下面设置页目录 目录项的内容, 其内容是页表的物理地址
    				pgdir[PDX(va)] = page2pa(temp) | PTE_P | PTE_W |PTE_U;
    			}
    		}        
    	}
    	// PTE_ADDR(pgdir[PDX(va)])这一步是根据页表项的内容得到物理地址, 因为页表项的内容存的就是物理地址
    
    	result = (pte_t *)KADDR(PTE_ADDR(pgdir[PDX(va)])) + PTX(va);
    	return result;
    }
    

    整个过程中最重要的数据是:

    变量及含义 内容 例子
    *pgdir, 页目录的起始地址 页目录的内容应该是对应的页表的物理地址 pgdir[PDX(va)] = page2pa(temp)
    pgdir[PDX(va)] 页表的物理地址与权限信息 页目录项的内容, (PTE_ADDR(pgdir[PDX(va)])) 这里使用PTE_ADDR的原因也是,目录项中不仅有物理地址,还有权限信息,
    pgdir_walk(pgdir, (void *)temp_va, 1) 页表的虚拟地址 存储的是虚拟地址对应的物理地址 pte_t *pte = pgdir_walk(pgdir, (void *)temp_va, 1); //获取当前va对应的页表的页表项地址
    // 将物理地址与虚拟地址对应起来
    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 pgs = size / PGSIZE;
    	if (size % PGSIZE != 0) {
    		pgs++;
    	}	
    	for (int i = 0; i < pgs; i++) {
    		pte_t *pte = pgdir_walk(pgdir, (void *)va, 1);
    		if (pte == NULL) {
    			panic("boot_map_region(): out of memory
    ");
    		}
    		*pte = pa | PTE_P | perm;
    		pa += PGSIZE;
    		va += PGSIZE;
    	}
    }
    

    上面的部分, 相当于将物理地址与虚拟地址进行对应, 也就是 map, 也就是说, 访问虚拟地址 va, 等同于访问物理地址 pa, 在下面的向页表中插入一页, 物理内存中的页是使用 pages 数组管理的, 所以本质就是将物理页对应的物理地址与虚拟地址 map起来.

    // 向页表中插入一页, 本质是将一个物理页与虚拟地址对应起来
    int page_insert(pde_t *pgdir, struct PageInfo *pp, void *va, int perm)
    {
    	// Fill this function in
    	// 查找虚拟地址对应的页表的页表项地址
    	pte_t *temp = pgdir_walk(pgdir, va, 1);
    	if(temp == NULL) {
    		return -E_NO_MEM;
    	}
    	pp->pp_ref += 1;
    	// *temp 是页表项存储的数据, 判断 PTE_P 位是否为 1, 
    	// 为 1表示页表中该页表项已经存储了物理地址, 虚拟地址 va 已经被映射
    	if((*temp) & PTE_P) {
    		page_remove(pgdir, va); 
    	}
    	// pp - pages 实际是物理页号, 将其转化为物理地址
    	physaddr_t pa = page2pa(pp); 
    	// 更新页表中页表项的数据
    	*temp = pa | perm | PTE_P;    //修改PTE
    	pgdir[PDX(va)] |= perm;
    	
    	return 0;
    }
    

    look up, 找出虚拟地址对应的物理页的过程, 这个过程相当于将虚拟地址与物理页之间的映射关系复现了一遍, 如果理解其中的关键参数, 那么整个过程其实十分的流畅与明确,

    // 返回虚拟地址对应的物理页面, 也就是物理页号
    struct PageInfo *
    page_lookup(pde_t *pgdir, void *va, pte_t* *pte_store)
    {
    	// Fill this function in
    	// 注意这里传进来了一个二级指针, 目的是修改原指针
    	pte_t *temp_pte = pgdir_walk(pgdir, va, 0);
    
    	if (temp_pte == NULL) {
    		return NULL;
    	}
        // 表示页表项的内容为空
    	if (!(*temp_pte) & PTE_P) {
    		return NULL;
    	}
    	// 这里是从页表项的内容中中获取物理地址
    	physaddr_t pa = PTE_ADDR(*temp_pte);
    	struct PageInfo* pp = pa2page(pa);								//物理地址对应的PageInfo结构地址
        // 在后面的函数调用中, 我们知道这里的 pre_store, 与 temp_pte 是对于同一个虚拟地址对应的页表项地址, 所以应该是相同的
    	if (pte_store != NULL) {
    		*pte_store = temp_pte;
    	}
    	return pp;
    }
    

    内核空间

    在 JOS 中, 进程的地址空间被分为两部分, 用户进程与内核进程, 这个分布在 inc/memlayout.h 描述的十分详细, 划分空间的主要原因是内核程序与用户程序的功能不同, 内核程序主要负责资源的调度与内存的分配, 而这里的很多操作对用户来说是透明的, 所以对内核空间与用户空间的权限也是不同的, 内核的代码与数据用户是没有权限修改与访问的, 但是 [UTOP,ULIM] 这一部分是页表与页目录部分, 用户是可读的,至于为什么这里是页表与页目录, 其实在 mem_init 函数中决定的:

    // 假设 UVPT 是页目录开头的虚拟地址, 左边得到的应该是页目录的物理地址
    	// 这一步相当于将页目录的虚拟地址 kern_pgdir 固定在 UVPT, 访问UVPT就是访问页目录
    	kern_pgdir[PDX(UVPT)] = PADDR(kern_pgdir) | PTE_U | PTE_P;
    

    Exercise 5. Fill in the missing code in mem_init() after the call to check_page().

    对于后面的问题. 其实这部分就是将虚拟地址空间与物理地址空间进行映射, 这里的映射方法是使用函数 boot_map_region ,实现的方法就是:

    	// 将虚拟地址的  UPAGES 与 pages映射
    	boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
    	// 这一部分是内核栈部分, KSTACKTOP 是内核栈顶部的虚拟地址
    	boot_map_region(kern_pgdir, KSTACKTOP-KSTKSIZE, KSTKSIZE, PADDR(bootstack), PTE_W);
    	// 这一部分是内核的 kernbase 向上部分, 也就是内核部分的数据, 
    	boot_map_region(kern_pgdir, KERNBASE, 0xffffffff-KERNBASE, 0, PTE_W);
    
    1. What entries (rows) in the page directory have been filled in at this point? What
      addresses do they map and where do they point? In other words, fill out this table
      as much as possible:
    Entry Base Virtual Address Points to (logically):
    1023 0xffc00000 Page table for top 4MB of phys memory
    ... ... ...
    960 0xf0000000 Page table for [0,4)MB of phys memory
    959 0xefc00000 Kernel Stack and Invalid Memory
    ... ... ...
    957 0xef400000 UVPT, User read-only virtual page table
    956 0xef000000 UPAGES, Read-only copies of the Page structures

    上面的表格对应来自上面使用 boot_map_region 的三种情况, 分别是,

    1. UPAGES 与 pages映射, 将UPAGES 与 pages的物理地址 map 起来, pages本身是虚拟地址, 页表是在内存中的, 但是是使用 pages描述的
    2. UVPT, 将页目录与虚拟地址对应
    3. 内核栈, 将内核栈的虚拟地址部分与物理地址部分对应
    4. KERNBASE 上面的一部分, 使用的是 boot_map_region, 直接将地址对应, 映射的物理内存的大小为256MB.

    中间的几个问题都十分简单,我们来看看最后一个

    Revisit the page table setup in kern/entry.S and kern/entrypgdir.c. Immediately after we turn on paging, EIP is still a low number (a little over 1MB). At what point do we transition to running at an EIP above KERNBASE? What makes it possible for us to continue executing at a low EIP between when we enable paging and when we begin running at an EIP above KERNBASE? Why is this transition necessary?

    这一步我们需要理解 JOS 页表机制的启动过程, 在 Bootloader 之后, 进入内核开始执行, 这一部分的代码在物理内存中就是在 1MB 上方, 然后是在一部分代码中, 首先执行 entry.S , 该过程通过 kern/entrypgdir.c 建立了一个临时的页目录, 这个页目录映射的空间大小为 4MB, 但是建立页目录的过程与页目录建立完, 我们CPU使用的都是一个 low EIP, 可以看做此时, EIP 由物理地址表示. 顺序上, 我们设置了 cr0 寄存器后, 表示开启页表机制.

      mov $relocated, %eax
      jmp *%eax
    

    之后,eip到了KERNBASE之上. 既能在低eip分页,也能在高位上运行的原因在于, kern/entrypgdir.c 建立的页表不仅虚拟地址[KERNBASE, KERNBASE+4MB) 被映射到物理地址[0, 4MB),同时虚拟地址[0, 4MB)也被映射到同一段物理地址. 所以当eip在物理地址[0, 4MB)上时,它既在低位的[0, 4MB),也在高位[KERNBASE, KERNBASE+4MB)。这是一个过渡的状态,过一会儿页索引会被加载进来而虚拟地址[0, 4MB)就被舍弃了.
    最后我们开下开机进行内存初始化的过程,用一张表示就是:

    page_init()

    page_init()
    Initialize page structure
    and memory free list.
    Initialize page structure...
    mem_init()
    mem_init()

    boot_alloc()

    a kernel pgdir

    boot_alloc()...
    the position is end of
    kernel's data segment
    the position is end of...

    map UVPT with physical
    address of kern_pgdir

    map UVPT with physical...
    pages[]
    pages[]
    kern_pgdir
    kern_pgdir
    kernel
    Data_segment
    kernel...
    kernel
    Code_segment
    kernel...
    KERNBASE
    KERNBASE
    UVPT
    UVPT
    Pagetable[PDX(va)]
    Pagetable[PDX(va)]
    startup_APs
    startup_APs
    kernel_end_page
    kernel_end_page
    kernel_stack
    kernel_stack
    ~~~~~
    ~~~~~
    io_hole_start_page
    io_hole_start_page
    phy_pages[0]
    phy_pages[0]
    page_free_list
    page_free_list
    kern_pgdir
    kern_pgdir
    pages[]
    pages[]
    envs
    envs

    boot_alloc()

    a pages[]

    boot_alloc()...
    UPAGES
    UPAGES
    UENVS
    UENVS
    npages_num
    npages_num
    KERN_Stack1
    KERN_Stack1
    KERN_Stack2
    KERN_Stack2
    boot_stack
    boot_stack
    boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
    boot_map_region(kern_pgdir, UPAGES, PTSIZE, PADDR(pages), PTE_U);
    boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U);
    boot_map_region(kern_pgdir, UENVS, PTSIZE, PADDR(envs), PTE_U);
    boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
    boot_map_region(kern_pgdir, KERNBASE, 0xffffffff - KERNBASE, 0, PTE_W);
    MMU
    MMU
    Physical_Memory
    Physical_Memory
    Virtual_Memory
    Virtual_Memory

    PageInfo

    PageInfo
  • 相关阅读:
    Leetcode 283. Move Zeroes
    算法总结
    随机森林
    BRICH
    DBSCAN算法
    k-means算法的优缺点以及改进
    soket编程
    手电筒过河
    字符串反转
    URAL 1356. Something Easier(哥德巴赫猜想)
  • 原文地址:https://www.cnblogs.com/wevolf/p/12634262.html
Copyright © 2020-2023  润新知