修改物理页面内存属性_物理内存管理——建立分页管理机制(二)

afa96b3b8445998c609b11100dab0d81.png

【原理】分页内存管理

在分页内存管理中,一方面把实际物理内存(也称主存)划分为许多个固定大小的内存块,称为物理页面,或者是页框(page frame);另一方面又把CPU(包括程序员)看到的虚拟地址空间也划分为大小相同的块,称为虚拟页面,或者简称为页面、页(page)。页面的大小要求是2的整数次幂,一般在256个字节到4M字节之间。在本书中,页面的大小设定为4KB。在32位的86x86中,虚拟地址空间是4GB,物理地址空间也也是4GB,因此在理论上程序可访问到1M个虚拟页面和1M个物理页面。软件的每一物理页面都可以放置在主存中的任何地方,分页系统(需要CPU等硬件系统提供相应的分页机制硬件支持,详见下一节)提供了程序中使用的虚地址和主存中的物理地址之间的动态映射。这样当程序访问一个虚拟地址时,支持分页机制的相关硬件自动把CPU访问的虚拟地址虚拟地址拆分为页号(可能有多级页号)和页内偏移量,再把页号映射为页帧号,最后加上页内偏移组成一个物理地址,这样最终完成对这个地址的读/写/执行等操作。

假设程序在运行时要去读地址0x100的内容到寄存器1(用REG1表示)中,执行如下的指令:

mov 0x100, REG1

虚拟地址0x100被发送给CPU内部的内存管理单元(MMU),然后MMU通过支持分页机制的相关硬件逻辑就会把这个虚拟地址是位于第0个虚拟页面当中(设页大小为4KB),页内偏移是0x100;而操作系统的分页管理子系统已经设置好第0个虚拟页面对应的是第2个物理页帧,物理页帧的起始地址是0x2000,然后再加上页内的偏移地址0x100,所以最后得到的物理地址就是0x2100。然后MMU就会把这个真正的物理地址发送到计算机系统中的地址总线上,从而可正确访问相应的物理内存单元。

如果操作系统的分页管理子系统没有设置第0个虚拟页面对应的物理页帧,则表示第0个虚拟页面当前没有对应的物理页帧,这会导致CPU产生一个缺页异常,由操作系统的缺页处理服务例程来选择如何处理。如果缺页处理服务例程认为这是一次非法访问,它将报错,终止软件运行;如果它认为是一次合理的访问,则它会采用分配物理页等手段建立正确的页映射,使得能够重新正确执行产生异常的访存指令。

【背景】X86的分页硬件支持

X86 CPU对实际物理内存的访问是通过连接着CPU和北桥芯片的前端总线来完成的,在前端总线上传输的内存地址是物理内存地址。物理内存地址被北桥映射到实际的内存条中的内存单元相应位置上。然而,在CPU内执行带来软件所使用的是虚拟内存地址(也称逻辑内存地址),它必须被转换成物理地址后,才能用于实际内存访问。

前面已经讲过了80x86的分段机制,80x86的分页机制建立在其分段机制基础之上,提供了更加强大的内存管理支持。需要注意的是,在x86中,必须先有分段机制,才能有分页机制。在分段机制中,虚地址会转换为线性地址。如果不启动分页机制,那么线性地址就是最终在前端总线上的物理地址;如果启动了分页机制,则线性地址还会经过页映射被转换为物理地址。

那如果启动分页机制呢?在80x86中有一个CR0控制寄存器,它包含一个PG位,如果PG=1,启用分页机制;如果 PG=0,禁用分页机制。不像分段机制管理大小不固定的内存卡,分页机制以固定大小的存储块为最小管理单位,即把整个地址空间(包括线性地址和物理地址)都看成由固定大小的存储块组成。在80x86中,这个固定大小一般设定为4096字节。在线性地址空间中的最小管理单位(称为页(page)),可以映射到物理地址空间中的任何一个最小管理单位(称为页帧(page frame))。页/页帧的32位地址由20位的页号/页帧号和12位的页/页帧内偏移组成。

80x86分页机制中的分页转换功能(即线性地址到物理地址的映射功能)需采用驻留在内存中的数组来描述,该数组称为页表(page table)。每个数组项就是一个页表项。由于页/页帧基地址按4096字节对齐,因此页/页帧的基地址的低12位是0。页地址<->页帧地址的转换过程以简单地看做80x86对页表的一个查找过程。页地址(线性地址)的高20位(即页号,or页的基地址)构成这个数组的索引值,用于选择对应页帧的页帧号(即页帧的基地址)。页地址的低12位给出了页内偏移量,加上对应的页帧基地址就最终形成对应的页帧地址(即物理地址)。

由于80x86的地址空间可达到4GB,按页大小(4KB)划分为1M个页。如果用一个页表来描述这种映射,那么该也表就要有1M个表项,若每个表项占用4个字节,那么该映射表就要占用4M字节。考虑到将来一个进程就需要一个地址映射表,若有多个进程,那地址映射表所占的总空间将非常巨大。为避免地址映射表占用过多的内存资源,80x86把地址映射表设定为两级。地址映射表的第一级称为页目录表,存储在一个4KB的物理页中,页目录表共有1K个表项,其中每个表项为4字节长,页表项中包含对应第二级表所在的基地址。地址映射表的第二级称为页表,每个页表也安排在一个4K字节的页中,每张页表中有1K个表项,每个表项为4字节长,包含对应页帧的基地址。由于页目录表和页表均由1K个表项组成,所以使用10位的索引就能指定表项,即用10位的索引值乘以4加基地址就得到了表项的物理地址。按上述的地址转换描述,一个页表项只需20位,但实际的页表项是32位,那其他的12位有何用途呢?

在80x86中的的页目录表项结构定义如下所示:

866c68f37aaf35ab94e90ce346331e37.png

在80x86中的的页表项结构定义如下所示:

7a5254ef8136df3daaee1774c6a6e42c.png

其中低12位的相应属性位含义如下:

  • P位:存在(Present)标志,用于指明此表项是否有效。P=1表示有效;P=0表示无效。如果80x86访问一个无效的表项,则会产生一个异常。如果P=0,那么除表示表项无效外,其余位用于其他用途(比如swap in/out中,用来保存已存储在磁盘上的页面的序号)。
  • R/W:读/写(Read/Write)标志,如果R/W=1,表示页的内容可以被读、写或执行。如果R/W=0,表示页的内容只读或可执行。当处理器运行在特权级(级别0、1或2)时,则R/W位不起作用。
  • U/S:是用户态/特权态(User/Supervisor)标志。如果U/S=1,那么在用户态和特权态都可以访问该页。如果U/S=0,那么只能在特权态(0、1或2)可访问该页。
  • A:是已访问(Accessed)标志。当CPU访问页表项映射的物理页时,页表项的这个标志就会被置为1。可通过软件把该标志位清零,并且操作系统可通过该标志来统计页的使用情况,用于页替换策略。
  • D:是页面已被修改(Dirty)标志。当CPU写页表项映射的物理页内容时,页表项的这个标志就会被置为1。可通过软件把该标志位清零,并且操作系统可通过该标志来统计页的修改情况,用于页替换策略。

下图显示了由页目录表和页表构成的二级页表映射架构。

4c99d66b0c43a597e906487c86296ad6.png

图 页目录表和页表构成的二级页表映射架构

从图中可见,控制寄存器CR3的内容是对应页目录表的物理基地址;页目录表可以指定1K个页表,这些页表可以分散存放在任意的物理页中,而不需要连续存放;每张页表可以指定1K个任意物理地址空间的页。存储页目录表和页表的基地址是按4KB对齐。当采用上述页表结构后,基于分页的线性地址到物理地址的转换过程如下图所示:

首先,CPU把控制寄存器CR3的高20位作为页目录表所在物理页的物理基地址,再把需要进行地址转换的线性地址的最高10位(即22~ 31位)作为页目录表的索引,查找到对应的页目录表项,这个表项中所包含的高20位是对应的页表所在物理页的物理基地址;然后,再把线性地址的中间10位(即12~21位)作为页表中的页表项索引,查找到对应的页表项,这个表项所包含的的高20位作为线性地址的基地址(即页号)对应的物理地址的基地址(即页帧号);最后,把页帧号作为32位物理地址的高20位,把线性地址的低12位不加改变地作为32位物理地址的低12位,形成最终的物理地址。

如果每次访问内存单元都要访问位于内存中的页表,则访存开销太大。为了避免这类开销,x86 CPU把最近使用的地址映射数据存储在其内部的页转换高速缓存(页转换查找缓存,简称TLB)中。这样在访问存储器页表之前总是先查阅高速缓存,仅当必须的转换不在高速缓存中时,才访问存储器中的两级页表。

【实现】实现分页内存管理

重新建立段映射

前面已经介绍了如何探测物理内存,接下来ucore需要根据物理内存的情况来建立分页管理机制。首先观察一下tools/kernel.ld文件在proj4.1和proj5中的区别,在proj4.1中:

ENTRY(kern_init)

SECTIONS {
    /* Load the kernel at this address: "." means the current address */
    . = 0x100000;

    .text : {
        *(.text .stub .text.* .gnu.linkonce.t.*)
    }

在porj5中:

ENTRY(kern_entry)

SECTIONS {
    /* Load the kernel at this address: "." means the current address */
    . = 0xC0100000;

    .text : {
        *(.text .stub .text.* .gnu.linkonce.t.*)
    }

在意味着gcc编译出ucore的起始地址从0xC0100000开始,入口函数为kern_entry函数。这与proj4.1有很大差别。这实际上说明ucore在建立好页映射关系后,虚拟地址空间和物理地址空间之间存在如下的映射关系:

Virtual Address=LinearAddress=0xC0000000+Physical Address

另外,ucore的入口地址也改为了kern_entry函数,这个函数位于init/entry.S中,分析代码可以看出,entry.S重新建立了段映射关系,从以前的

Virtual Address= Linear Address

改为

Virtual Address=Linear Address-0xC0000000

由于gcc编译出的虚拟起始地址从0xC0100000开始,ucore被bootloader放置在从物理地址0x100000处开始的物理内存中。所以当kern_entry函数完成新的段映射关系后,且ucore在没有建立好页映射机制前,CPU按照ucore中的虚拟地址执行,能够被分段机制映射到正确的物理地址上,确保ucore运行正确。

初始化物理内存页分配管理

为了与以后的分页机制配合,我们首先需要建立对整个计算机的页级物理内存分配管理。这部分代码的实现在kern/default_pmm.[ch]。首先我们需要用一个数据结构来描述每个物理页(也称页帧),这里用了双向链表结构来表示每个页。链表头用free_area_t结构来表示,包含了一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。

/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
    list_entry_t free_list;            // the list header
    unsigned int nr_free;            // # of free pages in this free list
} free_area_t;

每一个物理页的属性用结构Page来表示,它包含了映射此物理页的虚拟页个数,描述物理页属性的flags和双向链接各个Page结构的page_link双向链表。

struct Page {
    atomic_t ref;    // page frame's reference counter
    uint32_t flags;    // array of flags that describe the status of the page frame
    list_entry_t page_link;    // free list link
};

有了这两个数据结构,ucore就可以管理起来整个以页为单位的物理内存空间。接下来需要解决两个问题:

  • 管理页级物理内存空间所需的Page结构的内存空间从哪里开始,占多大空间?
  • 空闲内存空间的起始地址在哪里?

对于这两个问题,我们首先根据bootloader给出的内存布局信息找出最大的物理内存地址maxpa(定义在page_init函数中的局部变量),由于x86的起始物理内存地址为0,所以可以得知需要管理的物理页个数为

npage = maxpa / PGSIZE

这样,我们就可以预估出管理页级物理内存空间所需的Page结构的内存空间所需的内存大小为:

sizeof(struct Page) * npage)

由于bootloader加载ucore的结束地址(用全局指针变量end记录)以上的空间没有被使用,所以我们可以把end按页大小为边界去整后,作为管理页级物理内存空间所需的Page结构的内存空间,记为:

pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);

为了简化起见,从地址0到地址pages+ sizeof(struct Page) npage)结束的物理内存空间设定为已占用物理内存空间(起始0~640KB的空间是空闲的),地址pages+ sizeof(struct Page) npage)以上的空间为空闲物理内存空间,这时的空闲空间起始地址为

uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);

为此我们需要把这两部分空间给标识出来。对于已占用物理空间,通过如下语句即可实现占用标记:

for (i = 0; i < npage; i ++) {
    SetPageReserved(pages + i);
}

对于空闲物理空间,通过如下语句即可实现空闲标记:

//获得空闲空间的起始地址begin和结束地址end
……
init_memmap(pa2page(begin), (end - begin) / PGSIZE);

其实SetPageReserved只需把物理地址对应的Page结构中的flags标志设置为PG_reserved ,表示这些页已经被使用了。而init_memmap函数则是把空闲物理页对应的Page结构中的flags和引用计数ref清零,并加到free_area.free_list指向的双向列表中,为将来的空闲页管理做好初始化准备工作。

物理内存页分配与释放

关于内存分配的操作系统原理方面的知识有很多,但在proj5中只实现了最简单的内存页分配算法,即每次只分配一页或释放一页的内存页分配算法。相应的实现在default_pmm.c中的default_alloc_pages函数和default_free_pages函数,相关实现很简单,这里就不具体分析了,直接看源码,应该很好理解。

其实proj5在内存分配和释放方面最主要的作用是建立了一个物理内存页管理器框架,这实际上是一个函数指针列表,定义如下:

struct pmm_manager {
    const char *name; //物理内存页管理器的名字
    void (*init)(void); //初始化内存管理器
    void (*init_memmap)(struct Page *base, size_t n); //初始化管理空闲内存页的数据结构
    struct Page *(*alloc_pages)(size_t n); //分配n个物理内存页
    void (*free_pages)(struct Page *base, size_t n); //释放n个物理内存页
    size_t (*nr_free_pages)(void); //返回当前剩余的空闲页数
    void (*check)(void); //用于检测分配/释放实现是否正确的辅助函数
};

重点是实现init_memmap/ alloc_pages/ free_pages这三个函数。当完成物理内存页管理初始化工作后,计算机系统的内存布局如下图所示:

a673d5276558c6db698d74a9169b5750.png

chapt3-proj5-memory.vsd

读者可进一步通过分析proj5.1/5.1.1/5.1.2/5.2中firstfit_pmm[ch]/bestfit_pmm[ch]/ worstfit_pmm[ch]/ buddy_pmm[ch]文件中对应函数实现来体会原理课中的连续空间内存分配中各种分配算法的设计思路和实现。

建立二级页表

为了实现分页机制,需要建立好虚拟内存和物理内存的页映射关系,即建立二级页表。这需要解决如下问题:

  • 对于哪些物理内存空间需要建立页映射关系?
  • 具体的页映射关系是什么?
  • 页目录表的起始地址设置在哪里?
  • 页表的起始地址设置在哪里,需要多大空间?
  • 如何设置页目录表项的内容?
  • 如何设置页目录表项的内容?

下面我们逐一解决上述问题。由于物理内存页管理器管理了从0到实际可用物理内存大小的物理内存空间,所以对于这些物理内存空间都需要建立好页映射关系。由于目前ucore只运行在内核空间,所以可以建立一个一一映射关系。假定虚拟内核地址的起始地址为0xC0000000,这虚拟内存和物理内存的具体页映射关系为:

Virtual Address=Physical Address+0xC0000000

由于我们已经具有了一个物理内存页管理器default_pmm_manager,我们就可以用它来获得所需的空闲物理页。在二级页表结构中,页目录表占4KB空间,ucore就可通过default_pmm_manager的default_alloc_pages函数获得一个空闲物理页,这个页的起始物理地址就是页目录表的起始地址。同理,ucore也通过这种方式获得各个页表所需的空间。页表的空间大小取决与页表要管理的物理页数n,一个页表项(32位,即4字节)可管理一个物理页,页表需要占n/256个物理页空间。这样页目录表和页表所占的总大小为4096+1024*n字节。

为把0~KERNSIZE(明确ucore设定实际物理内存不能超过KERNSIZE值,即0x38000000字节,896MB,3670016个物理页)的物理地址一一映射到页目录表项和页表项的内容,其大致流程如下:

  1. 先通过default_pmm_manager获得一个空闲物理页,用于页目录表;
  2. 调用boot_map_segment函数建立一一映射关系,具体处理过程以页为单位进行设置,即
    Virtual Address=Physical Address+0xC0000000
  • 设一个逻辑地址la(按页对齐,故低12位为零)对应的物理地址pa(按页对齐,故低12位为零),如果在页目录表项(la的高10位为索引值)中的存在位(PTE_P)为0,表示缺少对应的页表空间,则可通过default_pmm_manager获得一个空闲物理页给页表,页表起始物理地址是按4096字节对齐的,这样填写页目录表项的内容为
    页目录表项内容 = 页表起始物理地址| PTE_U | PTE_W | PTE_P
  • 进一步对于页表中对应页表项(la的中10位为索引值)的内容为
    页表项内容 = pa | PTE_P | PTE_W
    其中:
    • PTE_U:位3,表示用户态的软件可以读取对应地址的物理内存页内容
    • PTE_W:位2,表示物理内存页内容可写
    • PTE_P:位1,表示物理内存页存在

建立好一一映射的二级页表结构后,接下来就要使能分页机制了,这主要是通过enable_paging函数实现的,这个函数主要做了两件事:

  • 通过lcr3指令把页目录表的起始地址存入CR3寄存器中;
  • 通过lcr0指令把cr0中的CR0_PG标志位设置上。

执行完enable_paging函数后,计算机系统进入了分页模式!但到这一步还不够,还记得ucore在最开始通过kern_entry函数设置了临时的新段映射机制吗?这个临时的新段映射机制不是最简单的对等映射,导致虚拟地址和线性地址不相等。而刚才建立的页映射关系是建立在简单的段对等映射,即虚拟地址=线性地址的假设基础之上的。所以我们需要进一步调整段映射关系,即重新设置新的GDT,建立对等段映射。

这里需要注意:在进入分页模式到重新设置新GDT的过程是一个过渡过程。在这个过渡过程中,已经建立了页表机制,所以通过现在的段机制和页机制实现的地址映射关系为:

Virtual Address=Linear Address + 0xC0000000 = Physical Address +0xC0000000+0xC0000000

在这个特殊的阶段,如果不把段映射关系改为Virtual Address = Linear Address,则通过段页式两次地址转换后,无法得到正确的物理地址。为此我们需要进一步调用gdt_init函数,根据新的gdt全局段描述符表内容(gdt定义位于pmm.c中),恢复以前的段映射关系,即使得Virtual Address = Linear Address。这样在执行完gdt_init后,通过的段机制和页机制实现的地址映射关系为:

Virtual Address=Linear Address = Physical Address +0xC0000000

这里存在的一个问题是,在调用enable_page函数使能分页机制后到执行完毕gdt_init函数重新建立好段页式映射机制的过程中,内核使用的还是旧的段表映射,也就是说,enable paging 之后,内核使用的是页表的低地址 entry。 如何保证此时内核依然能够正常工作呢?其实只需让低地址目录表项的内容等于以KERNBASE开始的高地址目录表项的内容即可。目前内核大小不超过 4M (实际上是3M,因为内核从 0x100000 开始编址),这样就只需要让页表在0~4MB的线性地址与KERNBASE ~ KERNBASE+4MB的线性地址获得相同的映射即可,都映射到 0~4MB 的物理地址空间,具体实现在pmm.c中pmm_init函数的语句:

boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];

实际上这种映射也限制了内核的大小。当内核大小超过预期的3MB 就可能导致打开分页之后内核 crash,在后面的试验中,也的确出现了这种情况。解决方法同样简单,就是拷贝更多的高地址项到低地址。

当执行完毕gdt_init函数后,新的段页式映射已经建立好了,上面的0~4MB的线性地址与0~4MB的物理地址一一映射关系已经没有用了。所以可以通过如下语句解除这个老的映射关系。

boot_pgdir[0] = 0;

自映射机制

上一小节讲述了通过boot_map_segment函数建立了基于一一映射关系的页目录表项和页表项,这里的映射关系为:

Virtual addr (KERNBASE~KERNBASE+KMEMSIZE) = Physical_addr (0~KMEMSIZE)

这样只要给出一个虚地址和一个物理地址,就可以设置相应PDE和PTE,就可完成正确的映射关系。

如果我们这时需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样过程比较繁琐。我们有没有一个简洁的方法来实现这个查找呢?ucore做了一个很巧妙的地址自映射设计,把页目录表和页表放在一个连续的4MB虚拟地址空间中,并设置页目录表自身的虚地址<-->物理地址映射关系。这样在已知页目录表起始虚地址的情况下,通过连续扫描这特定的4MB虚拟地址空间,就很容易访问每个页目录表项和页表项内容。

具体而言,ucore是这样设计的,首先设置了一个常量(memlayout.h):

VPT=0xFAC00000,

这个地址的二进制表示为:

1111 1010 1100 0000 0000 0000 0000 0000

高10位为1111 1010 11,即10进制的1003,中间10位为0,低12位也为0。在pmm.c中有两个全局初始化变量

pte_t * const vpt = (pte_t *)VPT;
pde_t * const vpd = (pde_t *)PGADDR(PDX(VPT), PDX(VPT), 0);

NaN. 并在pmm_init函数执行了如下语句:

boot_pgdir[PDX(VPT)] = PADDR(boot_pgdir) | PTE_P | PTE_W;

这些变量和语句有何特殊含义呢?其实vpd变量的值就是页目录表的起始虚地址0xFAFEB000,且它的高10位和中10位是相等的,都是10进制的1003。当执行了上述语句,就确保了vpd变量的值就是页目录表的起始虚地址,且vpt是页目录表中第一个目录表项指向的页表的起始虚地址。此时描述内核虚拟空间的页目录表的虚地址为0xFAFEB000,大小为4KB。页表的理论连续虚拟地址空间0xFAC00000~0xFB000000,大小为4MB。因为这个连续地址空间的大小为4MB,可有1M个PTE,即可映射4GB的地址空间。

但ucore实际上不会用完这么多项,在memlayout.h中定义了常量

#define KMEMSIZE            0x38000000

表示ucore只支持896MB的物理内存空间,这个896MB只是一个设定,可以根据情况改变。则最大的内核虚地址为常量

#define KERNTOP             (KERNBASE + KMEMSIZE)=0xF8000000

所以最大内核虚地址KERNTOP的页目录项虚地址为

vpd+0xF8000000/0x400000=0xFAFEB000+0x3E0=0xFAFEB3E0

最大内核虚地址KERNTOP的页表项虚地址为: vpt+0xF8000000/0x1000=0xFAC00000+0xF8000=0xFACF8000

在pmm.c中的函数print_pgdir就是基于ucore的页表自映射方式完成了对整个页目录表和页表的内容扫描和打印。注意,这里不会出现某个页表的虚地址与页目录表虚地址相同的情况。

自映射机制还可方便用户态程序访问页表。因为页表是内核维护的,用户程序很难知道自己页表的映射结构。VPT 实际上在内核地址空间的,我们可以用同样的方式实现一个用户地址空间的映射(比如 pgdir[UVPT] = PADDR(pgdir) | PTE_P | PTE_U,注意,这里不能给写权限,并且 pgdir 是每个进程的 page table,不是 boot_pgdir),这样,用户程序就可以用和内核一样的 print_pgdir 函数遍历自己的页表结构了。

在page_init函数建立完实现物理内存一一映射和页目录表自映射的页目录表和页表后,一旦使能分页机制,则ucore看到的内核虚拟地址空间如下图所示:

a867206c8fd6e05c97fc5470bda52f43.png

proj5使能分页机制后的虚拟地址空间图

【原理】页内存分配算法

在proj5中进行在动态分配内存时,存在很多限制,效率很低。在操作系统原理中,为了有效地分配内存,首先需要了解和跟踪空闲内存和分布情况,一般可采用位图(bit map)和双向链表两种方式跟踪内存使用情况。若采用位图方式,则每个页对应位图区域的一个bit,如果此位为0,表示空闲,如果为1,表示被占用。采用位图方式很省空间,但查找n个长度为0的位串的开销比较大。而双向链表在查询或修改操作方面灵活性和效率较高,所以ucore采用双向链表来跟踪跟踪内存使用情况。

假设整个物理内存空闲空间的以页为单位被一个双向链表管理起来,每个表项管理一个物理页。这需要设计某种算法来查找空闲页和回收空闲页。ucore实现了首次适配(first fit)算法、最佳适配(best fit)算法、最差适配(worst fit)算法和兄弟(buddy)算法,这些算法都可以实现在ucore提供的物理内存页管理器框架pmm_manager下。

首次适配(first fit)算法的分配内存的设计思路是物理内存页管理器顺着双向链表进行搜索空闲内存区域,直到找到一个足够大的空闲区域,这是一种速度很快的算法,因为它尽可能少地搜索链表。如果空闲区域的大小和申请分配的大小正好一样,则把这个空闲区域分配出去,成功返回;否则将该空闲区分为两部分,一部分区域与申请分配的大小相等,把它分配出去,剩下的一部分区域形成新的空闲区。其释放内存的设计思路很简单,只需把这块区域重新放回双向链表中即可。

最佳适配(best fit)算法的设计思路是物理内存页管理器搜索整个双向链表(从开始到结束),找出能够满足申请分配的空间大小的最小空闲区域。找到这个区域后的处理以及释放内存的处理与上面类似。最佳适配算法试图找出最接近实际需要的空闲区,名字上听起来很好,其实在查询速度上较慢,且较易产生多的内存碎片。

最差适配(worst fit)算法与最佳适配(best fit)算法的设计思路相反,物理内存页管理器搜索整个双向链表,找出能够满足申请分配的空间大小的最大空闲区域,使新的空闲区比较大从而可以继续使用。在实际效果上,查询速度上也较慢,产生内存碎片相对少些。

上述三种算法在实际应用中都会产生碎片较多,效率不高的问题。为此一般操作系统会采用buddy算法来改进上述问题。buddy算法的基本设计思想是:在buddy系统中,被占用的内存空间和空闲内存空间的大小均为2的k次幂(k是正整数)。这样在ucore中,若申请n个页的内存空间,则实际可能分配的空间大小为2K个页(2k-1<n<=2k)。若初始化时的空闲内存空间容量为2m个页,这空闲块的大小只可能是20、21、…、2m个页。

33291edd7df381aff14fb44adbfccd03.png

假定内存一开始是一个连续地址空间(大小为2^k个页)的大空闲块,且最小分配单位为1个页(4KB),则buddy system初始化时将生成一个长度为k + 1的可用空间表List, 并将全部可用空间作为一个大小为2^k个页的空闲块Bk挂接在空闲块数组链表List的最后一个节点上, 如下图:

d32a350e18d9800e70609cd1ce599a7f.png

当ucore其他子系统申请n个字节的存储空间时, buddy system分配的空闲块大小为2^ m个页,m满足条件:2^ (m-1) < n <= 2^ m

此时buddy system将在list中的m位置寻找可用的空闲块。初始化时List中这个位置为空, 于是buddy system就向上查找m+1,…,直到达到k位置为止. 找到k位置后, 便得到可用空闲块Bk, 此时Bk将分裂成两个大小为2^(k-1)的空闲块Bk-1a和Bk-1b, 并将其中一个插入到List中k-1位置, 同时对另外一个继续进行分裂. 如此以往直到得到两个大小为2^m个页的块为止,并把其中一个空闲块分配给需求方。此时的内存如下图所示:

6b43d49721bf320a834c12e24bf8313b.png

如果buddy system在运行一段时间之后, List中某个位置t可能会出现多个块, 则将其他块依次链接可用块链表的末尾。当buddy system要在t位置取可用块时, 直接从链表头取一个即可。

当一个存储块被释放时, buddy system将把此内存块回收到空闲块链表List中。此时buddy system系统将根据此存储块的大小计算出其在List中的位置, 然后插入到空闲块链表的末尾。在这一步完成后, 系统立即开始合并尝试操作, 该操作是将地址相邻且大小相等的空闲块(简称buddy,即"伙伴"空闲块)合并到一起, 形成一个更大的空闲块,并重新放到空闲块链表List的对应位置中, 并继续对更大的块进行合并, 直到无法合并为止。

严蔚敏老师的“数据结构”一书第8章第4节对buddy算法有详尽的解释,“understanding linux kernel”此书对此也有很好的描述,读者可以进一步参考。

对于上述4个内存分配算法,可参考对应的proj5.1/5.1.1/5.1.2/5.2中的kern/mm/*_pmm.[ch]的具体实现来进一步了解。

(可以进一步描述三种算法的具体实现)


本文转载自:https://chyyuu.gitbooks.io/simple_os_book/content/zh/chapter-2/proj4_intr_in_ucore.html ,原作者为陈渝(清华大学计算机科学与技术系副教授)老师,转载已获原作者认证。

微信公众号:迪捷数原

联系电话:010-56131268;15600442810、13260299730 (微信同号)

联系邮箱:contact@digiproto.com

工作地址:北京市海淀区中关村软件园

浙江省绍兴市越城区中关村•水木湾区科学园3#802

公司网址:www.digiproto.com

公司简介:浙江迪捷软件科技有限公司2013年成立于北京,专注于安全关键领域数字化转型,提供军工领域的MBSE产品和解决方案,遵循中立开放的商业理念,为我国防务等安全关键领域提供MBSE和数字装备等解决方案。我司软件产品全部为自主研发,具有核心知识产权,涉及了高端装备的设计、研发和测试等环节。公司注册资金为1000万,总部位于浙江省绍兴市,在北京、上海等地设有分公司。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值