物理内存管理
(1)编译运行 uCore Lab2的工程代码
(2)完成uCore Lab2 练习 1-3的编程作业
思考如何实现uCore Lab2扩展练习1-2
提交uCore Lab2实验报告的具体要求,包括必要的运行截图
实验目的:
- 理解基于段页式内存地址的转换机制
- 理解页表的建立和使用方法
- 理解物理内存的管理方法
实验内容:
- 了解如何发现系统中的物理内存
- 了解如何建立对物理内存的初步管理,即了解连续物理内存管理
- 了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个全面的了解。
练习
练习0
需要填写的地方:
kdebug.c:
trap.c:
练习1:实现 first-fit 连续物理内存分配算法(需要编程)
在实现first fit 内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时,需要按照空闲页块起始地址来排序,形成一个有序的链表。可能会修改default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数。请仔细查看和理解default_pmm.c中的注释。
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
你的first fit算法是否有进一步的改进空间
first-fit算法其实就是顺着连续分布的物理内存链表进行搜索,直到找到一块满足要求的空闲区域,然后就将这块区域分配出去,不考虑内存资源的浪费和管理难度问题,这样可以减少搜索物理内存的时间和分配内存的时间。当空闲的内存区域区域大于申请分配的大小,就将该空闲区与划分成两部分,一部分的大小正好跟申请分配的大小一致,另一部分就还留在链表中作为空闲区域。
释放内存的设计:直接将这块内存放回链表中即可。
为了实现first fit连续物理内存分配算法,我们先要了解与内存相关的数据结构
/* *
* struct Page - Page descriptor structures. Each Page describes one
* physical page. In kern/mm/pmm.h, you can find lots of useful functions
* that convert Page to other data types, such as phyical address.
* */
struct Page {
int ref; // page frame's reference counter
uint32_t flags; // array of flags that describe the status of the page frame
unsigned int property; // the num of free block, used in first fit pm manager
list_entry_t page_link; // free list link
};
由注释我们可以知道,这是物理页的数据结构,该数据结构中有4个成员。
- ref,物理页的引用计数器。这个页被页表所引用的次数,也就是映射此物理页的虚拟页个数。如果这个页被页表引用,即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系,就会把Page的ref加一;反之,若页表项取消,即映射关系解除,就会把Page的ref减一。
- flags,此物理页的状态标记,有两个标志位状态,其为1的时候,代表这一页是free状态,可以被分配,但不能对它进行释放;如果为0,那么说明这个页已经分配了,不能被分配,但是可以被释放掉。
- property,用来记录某连续空闲页的数量,需要注意的是用到此成员变量的Page一定是连续内存块的开始地址。
- page_link,用于把多个连续内存空闲连接在一起的双向链表指针,连续内存空闲块利用这个页的成员变量page_link来链接比它地址小和大的其他连续内存空闲块,释放的时候只要将这个空间通过指针放回到双向链表中。
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
这个结构用于管理所有连续的空闲内存空间块。
在初始情况下,也许这个物理内存的空闲物理页都是连续的,这样就形成了一个大的连续内存空闲块。但随着物理页的分配与释放,这个大的连续内存空闲块会分裂为一系列地址不连续的多个小连续内存空闲块,且每个连续内存空闲块内部的物理页是连续的。
那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构,包含了一个list_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr_free。其中的链表指针指向了空闲的物理页。
该数据结构中含有两个成员:
- free_list:一个list_entry结构的双向链表指针。
- nr_free:记录当前空闲页的个数。
了解完物理页管理所涉及到的数据结构之后,接下来我们就要开始设计物理页的分配算法。
在kern/mm/pmm.h中定义了一个物理内存管理类:
struct pmm_manager {
const char *name; // XXX_pmm_manager's name
void (*init)(void); // initialize internal description&management data structure
// (free block list, number of free block) of XXX_pmm_manager
void (*init_memmap)(struct Page *base, size_t n);
// setup description&management data structcure according to the initial free physical memory space
struct Page *(*alloc_pages)(size_t n);
// allocate >=n pages, depend on the allocation algorithm
void (*free_pages)(struct Page *base, size_t n);
// free >=n pages with "base" addr of Page descriptor structures(memlayout.h)
size_t (*nr_free_pages)(void); // return the number of free pages
void (*check)(void); // check the correctness of XXX_pmm_manager
};
一个物理内存管理类需要完成的事情包括:
- 初始化
- 分配页
- 释放页
接下来就是实现题目中提到的完成“default_pmm.c中的default_init,default_init_memmap,default_alloc_pages, default_free_pages等相关函数”。
初始化:defaullt_init
初始化的过程也只是调用库函数list_init初始化掉free_area_t的双向链表和空闲块数
#define free_list (free_area.free_list)
#define nr_free (free_area.nr_free)
default_init(void) {
list_init(&free_list);
nr_free = 0;
}
default_init_memmap
这个函数是用来初始化空闲页链表的,初始化每一个空闲页,然后计算空闲页的总数。
根据注释我们完成代码如下:
static void default_init_memmap(struct Page *base, size_t n) {
assert(n > 0);
struct Page *p = base;
for (; p != base + n; p ++) {
assert(PageReserved(p));//确认本页是否为保留页
//设置标志位
p->flags = 0;
SetPageProperty(p);
p->property = 0;
set_page_ref(p, 0);//清空引用
list_add_before(&free_list, &(p->page_link));//插入空闲页的链表里面
}
nr_free += n; //说明连续有n个空闲块,属于空闲链表
base->property=n; //连续内存空闲块的大小为n,属于物理页管理链表
}
default_init_memmap
主要就是从空闲页块的链表中去遍历,找到第一块大小大于n的块,然后分配出来,把它从空闲页链表中除去,然后如果有多余的,把分完剩下的部分再次加入会空闲页链表中即可。
static struct Page *default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) { //如果所有的空闲页的加起来的大小都不够,那直接返回NULL
return NULL;
}
list_entry_t *le, *len;
le = &free_list; //从空闲块链表的头指针开始
while((le=list_next(le)) != &free_list) {//依次往下寻找直到回到头指针处,即已经遍历一次
struct Page *p = le2page(le, page_link);//将地址转换成页的结构
if(p->property >= n){ //由于是first-fit,则遇到的第一个大于N的块就选中即可
int i;
for(i=0;i<n;i++){//递归把选中的空闲块链表中的每一个页结构初始化
len = list_next(le);
struct Page *pp = le2page(le, page_link);
SetPageReserved(pp);
ClearPageProperty(pp);
list_del(le);//从空闲页链表中删除这个双向链表指针
le = len;
}
if(p->property>n){
(le2page(le,page_link))->property = p->property - n;//如果选中的第一个连续的块大于n,只取其中的大小为n的块
}
ClearPageProperty(p);
SetPageReserved(p);
nr_free -= n;//当前空闲页的数目减n
return p;
}
}
return NULL;//没有大于等于n的连续空闲页块,返回空
}
init_memmap
是如何被调用的呢?
kern_init --> pmm_init-->page_init-->init_memmap
当进入操作系统时第一个执行的函数就是kern_init,用于对内和进行初始化,中间会对pmm_init函数进行调用。pmm_init函数用于对整个物理内存进行初始化,它会调用page_init函数对页进行初始化。Page_init函数顾名思义,就是对整个物理地址的初始化,其中包括页初始化,于是调用了init_memmap函数。
page_init给init_memmap传送了两个参数:pa2page(begin),end-begin/PGSIZE
- pa2page(begin):
static inline struct Page *
pa2page(uintptr_t pa) {
if (PPN(pa) >= npage) {
panic("pa2page called with invalid pa");
}
return &pages[PPN(pa)];
}
其中PNN是物理地址页号,用于返回传入参数pa开始的第一个物理页,也就是基地址
- end-begin/PGSIZE
由于end和begin都是循环中记录位置的标记,PGSIZE为4KB
default_alloc_pages
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}
list_entry_t *le, *len;
le = &free_list;
while((le=list_next(le)) != &free_list) {//寻找一个可分配的连续页
struct Page *p = le2page(le, page_link);
if(p->property >= n){
int i;
for(i=0;i<n;i++){
len = list_next(le);
struct Page *pp = le2page(le, page_link);
SetPageReserved(pp);
ClearPageProperty(pp);
list_del(le);//删链表
le = len;
}
if(p->property>n){
(le2page(le,page_link))->property = p->property - n;
}
ClearPageProperty(p);
SetPageReserved(p);
nr_free -= n;
return p;
}
}
return NULL;
}
alloc_page,这个函数是用来分配空闲页的。
首先判断空闲页的大小是否大于所需的页块大小。如果需要分配的页面数量n,已经大于了空闲页的数量,那么直接return NULL分配失败。
过了这一个检查之后,遍历整个空闲链表。如果找到合适的空闲页,即p->property >= n(从该页开始,连续的空闲页数量大于n),即可认为可分配,重新设置标志位。具体操作是调用SetPageReserved(pp)和ClearPageProperty(pp),设置当前页面预留,以及清空该页面的连续空闲页面数量值。然后从空闲链表,即free_area_t中,记录空闲页的链表,删除此项。如果当前空闲页的大小大于所需大小。则分割页块。具体操作就是,刚刚分配了n个页,如果分配完了,还有连续的空间,则在最后分配的那个页的下一个页(未分配),更新它的连续空闲页值。如果正好合适,则不进行操作。最后计算剩余空闲页个数并返回分配的页块地址。
default_free_pages
static void
default_free_pages(struct Page *base, size_t n) {
assert(n > 0);
assert(PageReserved(base));
list_entry_t *le = &free_list;
struct Page * p;
while((le=list_next(le)) != &free_list) {
p = le2page(le, page_link);
if(p>base){
break;
}
} //找到释放的位置
//list_add_before(le, base->page_link);
for(p=base;p<base+n;p++){
list_add_before(le, &(p->page_link));
} //在这个位置开始,插入释放数量的空页
base->flags = 0;
set_page_ref(base, 0);//引用次数
ClearPageProperty(base);
SetPageProperty(base);
base->property = n;
p = le2page(le,page_link) ; //此时,p已经到达了插入完释放数量空页的后一个页的位置上。此时,一般会满足base+n==p,因此,尝试向后合并空闲页。如果能合并,那么base的连续空闲页加上p的连续空闲页,且p的连续空闲页置为0,;如果之后的页不能合并,那么p的property一直为0,下面的代码不会对它产生影响。
if( base+n == p ){
base->property += p->property;
p->property = 0;
}
le = list_prev(&(base->page_link)); //获取基地址页的前一个页,如果为空,那么循环查找之前所有为空,能够合并的页
p = le2page(le, page_link);
if(le!=&free_list && p==base-1){
while(le!=&free_list){
if(p->property){
p->property += base->property;
base->property = 0;
break; //不断更新前一个页p的property值,并清除base
}
le = list_prev(le);
p = le2page(le,page_link);
}
}
nr_free += n; //最后的最后,空闲页数量加n
return ;
}
default_free_pages主要完成的是对于页的释放操作,首先有一个assert语句断言这个基地址所在的页是否为预留,如果不是预留页,那么说明它已经是free状态,无法再次free,也就是之前所述,只有处在占用的页,才能有free操作。之后,声明一个页p,p遍历一遍整个物理空间,直到遍历到base所在位置停止,开始释放操作。找到了这个基地址之后呢,就可以将空闲页重新加进来(之前在分配的时候,删除了),之后就是一系列与初始化空闲页一样的设置标记位操作了。之后,如果插入基地址附近的高地址或低地址可以合并,那么需要更新相应的连续空闲页数量,向高合并和向低合并。
运行结果
缺陷/改进空间
- 空闲链表是升序的,从合并空闲块时从链表头开始遍历,最多只能够合并一个在base块之前(低地址)的空闲块。为了将base块之前的空闲块全部合并,不得不在第一次合并后,从base块之前的空闲块向链表头遍历。
- 使用链表,分配、合并都要遍历链表,时间复杂度为O(n)。可以使用平衡二叉树替代链表,将时间复杂度降低到O(n*logn)。
- 在进行free操作中,我们需要寻找需要free的base地址的时候,依靠的是遍历,通过改进算法,可以直接将base地址传入,无需遍历,直接找到位置开始操作,减少时间开销。
练习2:实现寻找虚拟地址对应的页表项(需要编程)
通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:
图1 get_pte函数的调用关系图
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
请描述页目录项(Page Directory Entry)和页表项(Page Table Entry)中每个组成部分的含义以及对ucore而言的潜在用处。
页目录项:
bit 0(P)
: resent 位,若该位为 1 ,则 PDE 存在,否则不存在。bit 1(R/W)
: read/write 位,若该位为 0 ,则只读,否则可写。bit 2(U/S)
: user/supervisor位。bit 3(PWT)
: page-level write-through,若该位为1则开启页层次的写回机制。bit 4(PCD)
: page-level cache disable,若该位为1,则禁止页层次的缓存。bit 5(A)
: accessed 位,若该位为1,表示这项曾在地址翻译的过程中被访问。bit 7(PS)
: 这个位用来确定 32 位分页的页大小,当该位为 1 且 CR4 的 PSE 位为 1 时,页大小为4M,否则为4K。bit 11:8
: 这几位忽略。bit 32:12
: 页表的PPN(页对齐的物理地址)。
页表项:
页表项除了第 7 , 8 位与 PDE 不同,其余位作用均相同。
bit 7(PAT)
: 如果支持 PAT 分页,间接决定这项访问的 4 K 页的内存类型;如果不支持,这位保留(必须为 0 )。bit 8(G)
: global 位。当 CR4 的 PGE 位为 1 时,若该位为 1 ,翻译是全局的;否则,忽略该位。
其中被忽略的位可以被操作系统用于实现各种功能;和权限相关的位可以用来增强ucore的内存保护机制;access 位可以用来实现内存页交换算法。
如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
- 硬件陷入内核,在堆栈中保存程序计数器。大多数机器将当前指令的各种状态信息保存在特殊的CPU寄存器中。
- 启动一个汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏。这个例程将操作系统作为一个函数来调用。
- 当操作系统发现一个缺页中断时,尝试发现需要哪个虚拟页面。通常一个硬件寄存器包含了这一信息,如果没有的话,操作系统必须检索程序计数器,取出这条指令,用软件分析这条指令,看看它在缺页中断时正在做什么。
- 一旦知道了发生缺页中断的虚拟地址,操作系统检查这个地址是否有效,并检查存取与保护是否一致。如果不一致,向进程发出一个信号或杀掉该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
- 如果选择的页框“脏”了,安排该页写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。无论如何,该页框被标记为忙,以免因为其他原因而被其他进程占用。
- 一旦页框“干净”后(无论是立刻还是在写回磁盘后),操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面被装入后,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
- 当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映它的位置,页框也被标记为正常状态。
- 恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
- 调度引发缺页中断的进程,操作系统返回调用它的汇编语言例程。
- 该例程恢复寄存器和其他状态信息
x86体系结构有三种内存地址:逻辑地址,线性地址,物理地址。
- 逻辑地址:逻辑地址是程序指令中使用的地址
- 物理地址:物理地址是实际访问内存的地址
- 线性地址:线性地址通过页式管理的地址映射的到物理地址。get_pte函数是给出了线性地址
三者的关系是:线性地址(Linear Address)是逻辑地址到物理地址变换之间的中间层。程式代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地址。
get_pte
get_pte作用:根据页目录pgdir来获取或创建指向线性地址la的PTE,是否创建页表取决与create
pte_t *
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
assert(pgdir != NULL);
struct Page *struct_page_vp; // virtual address of struct page
uint32_t pdx = PDX(la), ptx = PTX(la); // index of PDE, PTE
pde_t *pdep, *ptep;
pte_t *page_pa; // physical address of page
pdep = pgdir + pdx;
ptep = (pte_t *)KADDR(PDE_ADDR(*pdep)) + ptx;
// if PDE exists
if (test_bit(0, pdep)) {
return ptep;
}
/* if PDE not exsits, allocate one page for PT and create corresponding PDE */
if ((!test_bit(0, pdep)) && create) {
struct_page_vp = alloc_page(); // allocate page for PT
assert(struct_page_vp != NULL); // allocate successfully
set_page_ref(struct_page_vp, 1); // set reference count
page_pa = (pte_t *)page2pa(struct_page_vp); // convert virtual address to physical address
ptep = KADDR(page_pa + ptx); // virtual address of PTE
*pdep = (PADDR(ptep)) | PTE_P | PTE_U | PTE_W; // set PDE
memset(ptep, 0, PGSIZE); // clear PTE content
return ptep;
}
return NULL;
首先尝试使用PDX函数,获取一级页表的位置,如果获取成功,可以直接返回一个东西。如果获取不成功,那么需要根据create标记位来决定是否创建这一个二级页表(注意,一级页表中,存储的都是二级页表的起始地址)。如果create为0,那么不创建,否则创建。既然需要查找这个页表,那么页表的引用次数就要加一。之后,需要使用memset将新建的这个页表虚拟地址,全部设置为0,因为这个页所代表的虚拟地址都没有被映射。
接下来是设置控制位。这里应该设置同时设置上PTE_U、PTE_W和PTE_P,分别代表用户态的软件可以读取对应地址的物理内存页内容、物理内存页内容可写、物理内存页存在。
如果原来就有二级页表,或者新建立了页表,最后,只需返回对应项的地址即可。
练习3:释放某虚地址所在的页并取消对应二级页表项的映射(需要编程)
当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。page_remove_pte函数的调用关系图如下所示:
图2 page_remove_pte函数的调用关系图
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
数据结构Page的全局变量的每一项与页表中的页目录项和页表项有对应关系。
所有的物理页都有一个描述它的Page结构。所有的页表都是通过alloc_page()分配的,每个页表项都存放在一个Page结构描述的物理页中;如果 PTE 指向某物理页,同时也有一个Page结构描述这个物理页。
(1)可以通过 PTE 的地址计算其所在的页表的Page结构
(2)可以通过 PTE 指向的物理地址计算出该物理页对应的Page结构。
- 将虚拟地址向下对齐到页大小,换算成物理地址(减 KERNBASE), 再将其右移 PGSHIFT12位获得在pages数组中的索引PPN,&pages[PPN]就是所求的Page结构地址。
- PTE 按位与 0xFFF获得其指向页的物理地址,再右移 PGSHIFT(12)位获得在pages数组中的索引PPN,&pages[PPN]就 PTE 指向的地址对应的Page结构。
如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题
因为在编译链接时 ld 脚本 kern/tools/kernel.ld设置链接地址(虚拟地址),代码段基地址为0xC0100000(对应物理地址0x00100000),必须将该地址修改为0x00100000以确保内核加载正确。
- ,ucore 设置了虚拟地址 0 ~ 4M 到物理地址 0 ~ 4M 的映射以确保开启页表后
kern_entry
能够正常执行,在将eip
修改为对应的虚拟地址(加KERNBASE
)后就取消了这个临时映射。因为我们要让物理地址等于虚拟地址,所以保留这个映射不变(将清除映射的代码注释掉)。 - 在
boot_map_segment()
中,先清除boot_pgdir[1]
的present
位,再进行其他操作。这是get_pte
会分配一个物理页作为boot_pgdir[1]指向的页表。
page_removw_pte
函数作用:清除pte指向的内存对应的pte和page结构
static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {
assert(pgdir != NULL);
assert(ptep != NULL);
pde_t *pdep = pgdir + PDX(la); // virtual address of PDE
// PTE pointed by ptep must reside in page pointed by PDE
assert(PDE_ADDR(*pdep) == PADDR(ROUNDDOWN(ptep, PGSIZE)));
// if PDE exists
if (test_bit(0, ptep)) {
// Page struct related with la pointed by PTE
struct Page *page = pte2page(*ptep);
// decrease page reference and free this page when page reference reachs 0
page_ref_dec(page);
if (page_ref(page) == 0)
free_page(page);
// clear PTE pointed by ptep
clear_bit(PTE_P, ptep);
// flush TLB
tlb_invalidate(pgdir, la);
}
// for debug
else
cprintf("test_bit(PTE_P, ptep) error\n");
}
具体步骤:先判断ptep指向的pte是否存在,如果不存在就不需要处理。如果ptep指向的pte存在,计算其指向的内存对应的page结构,递减引用计数,如果没有虚拟地址指向该页就将其释放,最后清除pte并刷新TLB
扩展练习Challenge:buddy system(伙伴系统)分配算法(需要编程)
Buddy System算法把系统中的可用存储空间划分为存储块(Block)来进行管理, 每个存储块的大小必须是2的n次幂(Pow(2, n)), 即1, 2, 4, 8, 16, 32, 64, 128…
参考伙伴分配器的一个极简实现, 在ucore中实现buddy system分配算法,要求有比较充分的测试用例说明实现的正确性,需要有设计文档。
buddy system算法:
- 分配内存:寻找大小合适的内存块:
- 如果找到了,分配给应用程序。
- 如果没找到,分出合适的内存块。
- 对半分离出高于所需大小的空闲内存块
- 如果分到最低限度,分配这个大小。
- 回溯到步骤1(寻找合适大小的块)
- 重复该步骤直到一个合适的块
- 释放内存
- 寻找相邻的块,看其是否释放了。
- 如果相邻块也释放了,合并这两个块,重复上述步骤直到遇上未释放的相邻块,或者达到最高上限(即所有内存都释放了)。
具体实现过程可以用完全二叉树来实现。
#include <pmm.h>
#include <list.h>
#include <string.h>
#include <default_pmm.h>
#include <buddy.h>
//来自参考资料的一些宏定义
#define LEFT_LEAF(index) ((index) * 2 + 1)
#define RIGHT_LEAF(index) ((index) * 2 + 2)
#define PARENT(index) ( ((index) + 1) / 2 - 1)
#define IS_POWER_OF_2(x) (!((x)&((x)-1)))
#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define UINT32_SHR_OR(a,n) ((a)|((a)>>(n)))//右移n位
#define UINT32_MASK(a) (UINT32_SHR_OR(UINT32_SHR_OR(UINT32_SHR_OR(UINT32_SHR_OR(UINT32_SHR_OR(a,1),2),4),8),16))
//大于a的一个最小的2^k
#define UINT32_REMAINDER(a) ((a)&(UINT32_MASK(a)>>1))
#define UINT32_ROUND_DOWN(a) (UINT32_REMAINDER(a)?((a)-UINT32_REMAINDER(a)):(a))//小于a的最大的2^k
static unsigned fixsize(unsigned size) {
size |= size >> 1;
size |= size >> 2;
size |= size >> 4;
size |= size >> 8;
size |= size >> 16;
return size+1;
}
struct buddy2 {
unsigned size;//表明管理内存
unsigned longest;
};
struct buddy2 root[80000];//存放二叉树的数组,用于内存分配
free_area_t free_area;
#define free_list (free_area.free_list)
#define nr_free (free_area.nr_free)
struct allocRecord//记录分配块的信息
{
struct Page* base;
int offset;
size_t nr;//块大小
};
struct allocRecord rec[80000];//存放偏移量的数组
int nr_block;//已分配的块数
static void buddy_init()
{
list_init(&free_list);
nr_free=0;
}
//初始化二叉树上的节点
void buddy2_new( int size ) {
unsigned node_size;
int i;
nr_block=0;
if (size < 1 || !IS_POWER_OF_2(size))
return;
root[0].size = size;
node_size = size * 2;
for (i = 0; i < 2 * size - 1; ++i) {
if (IS_POWER_OF_2(i+1))
node_size /= 2;
root[i].longest = node_size;
}
return;
}
//初始化内存映射关系
static void
buddy_init_memmap(struct Page *base, size_t n)
{
assert(n>0);
struct Page* p=base;
for(;p!=base + n;p++)
{
assert(PageReserved(p));
p->flags = 0;
p->property = 1;
set_page_ref(p, 0);
SetPageProperty(p);
list_add_before(&free_list,&(p->page_link));
}
nr_free += n;
int allocpages=UINT32_ROUND_DOWN(n);
buddy2_new(allocpages);
}
//内存分配
int buddy2_alloc(struct buddy2* self, int size) {
unsigned index = 0;//节点的标号
unsigned node_size;
unsigned offset = 0;
if (self==NULL)//无法分配
return -1;
if (size <= 0)//分配不合理
size = 1;
else if (!IS_POWER_OF_2(size))//不为2的幂时,取比size更大的2的n次幂
size = fixsize(size);
if (self[index].longest < size)//可分配内存不足
return -1;
for(node_size = self->size; node_size != size; node_size /= 2 ) {
if (self[LEFT_LEAF(index)].longest >= size)
{
if(self[RIGHT_LEAF(index)].longest>=size)
{
index=self[LEFT_LEAF(index)].longest <= self[RIGHT_LEAF(index)].longest? LEFT_LEAF(index):RIGHT_LEAF(index);
//找到两个相符合的节点中内存较小的结点
}
else
{
index=LEFT_LEAF(index);
}
}
else
index = RIGHT_LEAF(index);
}
self[index].longest = 0;//标记节点为已使用
offset = (index + 1) * node_size - self->size;
while (index) {
index = PARENT(index);
self[index].longest =
MAX(self[LEFT_LEAF(index)].longest, self[RIGHT_LEAF(index)].longest);
}
//向上刷新,修改先祖结点的数值
return offset;
}
static struct Page*
buddy_alloc_pages(size_t n){
assert(n>0);
if(n>nr_free)
return NULL;
struct Page* page=NULL;
struct Page* p;
list_entry_t *le=&free_list,*len;
rec[nr_block].offset=buddy2_alloc(root,n);//记录偏移量
int i;
for(i=0;i<rec[nr_block].offset+1;i++)
le=list_next(le);
page=le2page(le,page_link);
int allocpages;
if(!IS_POWER_OF_2(n))
allocpages=fixsize(n);
else
{
allocpages=n;
}
//根据需求n得到块大小
rec[nr_block].base=page;//记录分配块首页
rec[nr_block].nr=allocpages;//记录分配的页数
nr_block++;
for(i=0;i<allocpages;i++)
{
len=list_next(le);
p=le2page(le,page_link);
ClearPageProperty(p);
le=len;
}//修改每一页的状态
nr_free-=allocpages;//减去已被分配的页数
page->property=n;
return page;
}
void buddy_free_pages(struct Page* base, size_t n) {
unsigned node_size, index = 0;
unsigned left_longest, right_longest;
struct buddy2* self=root;
list_entry_t *le=list_next(&free_list);
int i=0;
for(i=0;i<nr_block;i++)//找到块
{
if(rec[i].base==base)
break;
}
int offset=rec[i].offset;
int pos=i;//暂存i
i=0;
while(i<offset)
{
le=list_next(le);
i++;
}
int allocpages;
if(!IS_POWER_OF_2(n))
allocpages=fixsize(n);
else
{
allocpages=n;
}
assert(self && offset >= 0 && offset < self->size);//是否合法
node_size = 1;
index = offset + self->size - 1;
nr_free+=allocpages;//更新空闲页的数量
struct Page* p;
self[index].longest = allocpages;
for(i=0;i<allocpages;i++)//回收已分配的页
{
p=le2page(le,page_link);
p->flags=0;
p->property=1;
SetPageProperty(p);
le=list_next(le);
}
while (index) {//向上合并,修改先祖节点的记录值
index = PARENT(index);
node_size *= 2;
left_longest = self[LEFT_LEAF(index)].longest;
right_longest = self[RIGHT_LEAF(index)].longest;
if (left_longest + right_longest == node_size)
self[index].longest = node_size;
else
self[index].longest = MAX(left_longest, right_longest);
}
for(i=pos;i<nr_block-1;i++)//清除此次的分配记录
{
rec[i]=rec[i+1];
}
nr_block--;//更新分配块数的值
}
static size_t
buddy_nr_free_pages(void) {
return nr_free;
}
//以下是一个测试函数
static void
buddy_check(void) {
struct Page *p0, *A, *B,*C,*D;
p0 = A = B = C = D =NULL;
assert((p0 = alloc_page()) != NULL);
assert((A = alloc_page()) != NULL);
assert((B = alloc_page()) != NULL);
assert(p0 != A && p0 != B && A != B);
assert(page_ref(p0) == 0 && page_ref(A) == 0 && page_ref(B) == 0);
free_page(p0);
free_page(A);
free_page(B);
A=alloc_pages(500);
B=alloc_pages(500);
cprintf("A %p\n",A);
cprintf("B %p\n",B);
free_pages(A,250);
free_pages(B,500);
free_pages(A+250,250);
p0=alloc_pages(1024);
cprintf("p0 %p\n",p0);
assert(p0 == A);
//以下是根据链接中的样例测试编写的
A=alloc_pages(70);
B=alloc_pages(35);
assert(A+128==B);//检查是否相邻
cprintf("A %p\n",A);
cprintf("B %p\n",B);
C=alloc_pages(80);
assert(A+256==C);//检查C有没有和A重叠
cprintf("C %p\n",C);
free_pages(A,70);//释放A
cprintf("B %p\n",B);
D=alloc_pages(60);
cprintf("D %p\n",D);
assert(B+64==D);//检查B,D是否相邻
free_pages(B,35);
cprintf("D %p\n",D);
free_pages(D,60);
cprintf("C %p\n",C);
free_pages(C,80);
free_pages(p0,1000);//全部释放
}
const struct pmm_manager buddy_pmm_manager = {
.name = "buddy_pmm_manager",
.init = buddy_init,
.init_memmap = buddy_init_memmap,
.alloc_pages = buddy_alloc_pages,
.free_pages = buddy_free_pages,
.nr_free_pages = buddy_nr_free_pages,
.check = buddy_check,
};
扩展练习Challenge:任意大小的内存单元slub分配算法(需要编程)
slub算法,实现两层架构的高效内存单元分配,第一层是基于页大小的内存分配,第二层是在第一层基础上实现基于任意大小的内存分配。可简化实现,能够体现其主体思想即可。
参考linux的slub分配算法/,在ucore中实现slub分配算法。要求有比较充分的测试用例说明实现的正确性,需要有设计文档。
前面主要分析了以页为最小单位进行内存分配的伙伴管理算法,这对于内核对内存的管理比较简单,同时较大程度上避免了内存碎片的问题。而实际上对内存的申请却不是每次都申请一个页面的,通常是不规则的,大小不一的,并且远小于一个内存页面的大小,此外更可能会频繁地申请释放这些内存。
明显每次分配小于一个页面的都统一分配一个页面的空间是过于浪费且不切实际的,因此必须充分利用未被使用的空闲空间,同时也要避免过多地访问操作页面分配。基于该问题的考虑,内核需要一个缓冲池对小块内存进行有效的管理起来,于是就有了slab内存分配算法。每次小块内存的分配优先来自于该内存分配器,小块内存的释放也是先缓存至该内存分配器,留作下次申请时进行分配,避免了频繁分配和释放小块内存所带来的额外负载。而这些被管理的小块内存在管理算法中被视之为“对象”。
Slab内存分配算法是最先出现的;后来被改进适用于嵌入式设备,以满足内存较少的情况下的使用,该改进后的算法占用资源极少,其被称之为slob内存分配算法;再后来由于slab内存分配算法的管理结构较大且设计复杂,再一次被改进简化,而该简化改进的算法称之为slub内存分配算法。当前linux内核中对该三种算法是都提供的,但是在编译内核的时候仅可以选择其一进行编译,鉴于slub比slab分配算法更为简洁和便于调试,在linux 2.6.22版本中,slub分配算法替代了slab内存管理算法的代码。此外,虽然该三种算法的实现存在差异,但是其对外提供的API接口都是一样的。
Slub分配管理中,每个CPU都有自己的缓存管理,也就是kmem_cache_cpu数据结构管理;而每个node节点也有自己的缓存管理,也就是kmem_cache_node数据结构管理。
对象分配时:
1、 当前CPU缓存有满足申请要求的对象时,将会首先从kmem_cache_cpu的空闲链表freelist将对象分配出去;
2、 如果对象不够时,将会向伙伴管理算法中申请内存页面,申请来的页面将会先填充到node节点中,然后从node节点取出对象到CPU的缓存空闲链表中;
3、 如果原来申请的node节点A的对象,现在改为申请node节点B的,那么将会把node节点A的对象释放后再申请。
对象释放时:
1、 会先将对象释放到CPU上面,如果释放的对象恰好与CPU的缓存来自相同的页面,则直接添加到列表中;
2、 如果释放的对象不是当前CPU缓存的页面,则会吧当前的CPU的缓存对象放到node上面,然后再把该对象释放到本地的cache中。
为了避免过多的空闲对象缓存在管理框架中,slub设置了阀值,如果空闲对象个数达到了一个峰值,将会把当前缓存释放到node节点中,当node节点也过了阀值,将会把node节点的对象释放到伙伴管理算法中。