当前位置: 首页 > news >正文

HNU-操作系统OS-实验Lab2

OS_Lab2_Experimental report

湖南大学信息科学与工程学院

计科 210X wolf (学号 202108010XXX)

在这里插入图片描述

前言

实验一过后大家做出来了一个可以启动的系统,实验二主要涉及操作系统的物理内存管理。操作系统为了使用内存,还需高效地管理内存资源。在实验二中大家会了解并且自己动手完成一个简单的物理内存管理系统。

实验目的

  • 理解基于段页式内存地址的转换机制
  • 理解页表的建立和使用方法
  • 理解物理内存的管理方法

实验内容

本次实验包含三个部分。

  • 首先了解如何发现系统中的物理内存;
  • 然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;
  • 最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。

本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如针对 cache 的优化等。如果大家有余力,尝试完成扩展练习。

练习要求

为了实现lab2的目标,lab2提供了3个基本练习和2个扩展练习,要求完成实验报告。

对实验报告的要求:

  • 基于markdown格式来完成,以文本方式为主
  • 填写各个基本练习中要求完成的报告内容
  • 完成实验后,请分析ucore_lab中提供的参考答案,并请在实验报告中说明你的实现与参考答案的区别
  • 列出你认为本实验中重要的知识点,以及与对应的OS原理中的知识点,并简要说明你对二者的含义,关系,差异等方面的理解(也可能出现实验中的知识点没有对应的原理知识点)
  • 列出你认为OS原理中很重要,但在实验中没有对应上的知识点

实验执行流程概述

本次实验主要完成ucore内核对物理内存的管理工作。参考ucore总控函数kern_init的代码,可以清楚地看到在调用完成物理内存初始化的pmm_init函数之前和之后,是已有lab1实验的工作.

lab2相对于lab1有两个方面的扩展。

  • 首先,bootloader的工作有增加,在bootloader中,完成了对物理内存资源的探测工作(可进一步参阅附录A和附录B),让ucore kernel在后续执行中能够基于bootloader探测出的物理内存情况进行物理内存管理初始化工作。

  • 其次,bootloader不像lab1那样,直接调用kern_init函数,而是先调用位于lab2/kern/init/entry.S中的kern_entry函数。kern_entry函数的主要任务是为执行kern_init建立一个良好的C语言运行环境(设置堆栈),而且临时建立了一个段映射关系,为之后建立分页机制的过程做一个准备。完成这些工作后,才调用kern_init函数。

kern_init函数在完成一些输出并对lab1实验结果的检查后,将进入物理内存管理初始化的工作,即调用pmm_init函数完成物理内存的管理,这也是我们lab2的内容。接着是执行中断和异常相关的初始化工作,即调用pic_init函数和idt_init函数等,这些工作与lab1的中断异常初始化工作的内容是相同的。

为了完成物理内存管理,这里首先需要探测可用的物理内存资源;了解到物理内存位于什么地方,有多大之后,就以固定页面大小来划分整个物理内存空间,并准备以此为最小内存分配单位来管理整个物理内存,管理在内核运行过程中每页内存,设定其可用状态(free的,used的,还是reserved的),这其实就对应了我们在课本上讲到的连续内存分配概念和原理的具体实现;接着ucore kernel就要建立页表, 启动分页机制,让CPU的MMU把预先建立好的页表中的页表项读入到TLB中,根据页表项描述的虚拟页(Page)与物理页帧(Page Frame)的对应关系完成CPU对内存的读、写和执行操作。这一部分其实就对应了我们在课本上讲到内存映射、页表、多级页表等概念和原理的具体实现。

内存管理相关的总体控制函数是pmm_init函数,它完成的主要工作包括:

  1. 初始化物理内存页管理器框架pmm_manager;
  2. 建立空闲的page链表,这样就可以分配以页(4KB)为单位的空闲内存了;
  3. 检查物理内存页分配算法;
  4. 为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表;
  5. 建立一一映射关系的二级页表;
  6. 使能分页机制;
  7. 从新设置全局段描述符表;
  8. 取消临时二级页表;
  9. 检查页表建立是否正确;
  10. 通过自映射机制完成页表的打印输出(这部分是扩展知识)

另外,主要注意的相关代码内容包括:

  • boot/bootasm.S中探测内存部分(从probe_memory到finish_probe的代码);
  • 管理每个物理页的Page数据结构(在mm/memlayout.h中),这个数据结构也是实现连续物理内存分配算法的关键数据结构,可通过此数据结构来完成空闲块的链接和信息存储,而基于这个数据结构的管理物理页数组起始地址就是全局变量pages,具体初始化此数组的函数位于page_init函数中;
  • 用于实现连续物理内存分配算法的物理内存页管理器框架pmm_manager,这个数据结构定义了实现内存分配算法的关键函数指针,而我们需要完成这些函数的具体实现;
  • 设定二级页表和建立页表项以完成虚实地址映射关系,这与硬件相关,且用到不少内联函数,源代码相对难懂一些。具体完成页表和页表项建立的重要函数是boot_map_segment函数,而get_pte函数是完成虚实映射关键的关键。

练习0:填写已有实验

本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。

提示:可采用diff和patch工具进行半自动的合并(merge),也可用一些图形化的比较/merge工具来手动合并,比如meld,eclipse中的diff/merge工具,understand中的diff/merge工具等。

实际上,Lab1中改动的代码部分有限,因此手动对以下三个文件进行复制:

  • kern/debug/kdebug.c
  • kern/init/init.c
  • kern/trap/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算法是否有进一步的改进空间

阅读default_pmm.c的注释部分

您应该重写函数:default_init,default_init _memmap,default_alloc_pages,default_free_pages

所以我们需要重点关注这几个函数。但在这之前,需要先了解分配内存之前都需要干些什么(怎么初始化)

1.探测系统物理内存布局

当 ucore 被启动之后,最重要的事情就是知道还有多少内存可用,一般来说,获取内存大小的方法有以下两种

  • BIOS 中断调用(只在实模式下完成)
  • 直接探测(只在保护模式下完成)

通过 BIOS 中断获取内存布局有三种方式,都是基于INT 15h中断,分别为88h、e801h、e820h。但是并非在所有情况下这三种方式都能工作。在 Linux kernel 里,采用的方法是依次尝试这三种方法。而在本实验中,我们通过e820h中断获取内存信息。

因为e820h中断必须在实模式下使用,所以我们在 bootloader 进入保护模式之前调用这个 BIOS 中断,并且把 e820 映射结构保存在物理地址0x8000处。具体实现详见boot/bootasm.S,e820map的结构定义则在memlayout.h中。

//memlayout.h
struct e820map {int nr_map;struct {uint64_t addr;uint64_t size;uint32_t type;} __attribute__((packed)) map[E820MAX];
};
//bootasm.S
probe_memory:movl $0, 0x8000            #存放内存映射的位置xorl %ebx, %ebxmovw $0x8004, %di          #0x8004开始存放map
start_probe:movl $0xE820, %eax         #设置int 15h的中断参数   movl $20, %ecx             #内存映射地址描述符的大小movl $SMAP, %edxint $0x15jnc cont                   #CF为0探测成功movw $12345, 0x8000jmp finish_probe
cont:addw $20, %di              #下一个内存映射地址描述符的位置incl 0x8000                #nr_map+1cmpl $0, %ebx              #ebx存放上次中断调用的计数值,判断是否继续进行探测jnz start_probe

总结来说,在实模式下,我们就通过e820H来探测了物理内存有多少可用。

2.以页为单位管理物理内存

在获得可用物理内存范围后,系统需要建立相应的数据结构来管理以物理页(按4KB对齐,且大小为4KB的物理内存单元)为最小单位的整个物理内存,以配合后续涉及的分页管理机制。每个物理页可以用一个 Page数据结构来表示。由于一个物理页需要占用一个Page结构的空间,Page结构在设计时须尽可能小,以减少对内存的占用。Page的定义在kern/mm/memlayout.h中。以页为单位的物理内存分配管理的实现在kern/default_pmm

如下为物理页的Page结构(重要)

//memlayout.h中的Page定义
struct Page {int ref;                        // 引用计数uint32_t flags;                 // 状态标记unsigned int property;          // 在first-fit中表示地址连续的空闲页的个数,空闲块头部才有该属性list_entry_t page_link;         // 双向链表
};
//状态标记(在kern/mm/memlayout.h中)
#define PG_reserved                 0       //表示是否被保留
#define PG_property                 1       //表示是否是空闲块第一页

解读上述成员变量:

  • ref:如果这个页被页表引用了,即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系,就会把Page的ref加1;反之,若页表项取消,即映射关系解除,就会把Page的ref减1。
  • flags:此物理页的状态标记(若为0则表示被保留,不能被分配与释放;若为1,表示free,可以被分配)
  • property:记录某连续内存空闲块的大小(即地址连续的空闲页的个数)
  • page_link:把多个连续内存空闲块链接在一起的双向链表指针,连续内存空闲块利用这个页的成员变量page_link来链接比它地址小和大的其他连续内存空闲块

在初始情况下,这个物理内存的空闲物理页都是连续的,这样就形成了一个大的连续内存空闲块。但随着物理页的分配与释放,这个大的连续内存空闲块会分裂为一系列地址不连续的多个小连续内存空闲块,且每个连续内存空闲块内部的物理页是连续的。那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来,便于分配和释放,为此定义了一个free_area_t数据结构,具体实现如下。

typedef struct {list_entry_t free_list;         // 链表:连接空闲物理页unsigned int nr_free;           // 空闲页个数
} free_area_t;

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

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

可以根据内存布局信息找到最大物理内存地址maxpa计算出页的个数npage = maxpa / PGSIZE

并计算出管理页的Page结构需要的空间sizeof(struct Page) * npage

ucore的结束地址(即.bss段的结束地址,用全局变量end表示)以上的空间空闲,从这个位置开始存放Pages结构,而存放Pages结构的结束的位置以上就是空闲物理内存空间。将空闲的物理内存空间使用init_memmap()函数纳入物理内存管理器,部分代码如下:

//pmm.c中的page_init的部分代码npage = maxpa / PGSIZE;                                    //页数pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);       //Page结构的位置for (i = 0; i < npage; i ++) {SetPageReserved(pages + i);                            //每一个物理页默认标记为保留}//空闲空间起始uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage); for (i = 0; i < memmap->nr_map; i ++) {uint64_t begin = memmap->map[i].addr, end = begin + memmap->map[i].size;if (memmap->map[i].type == E820_ARM) {if (begin < freemem) {begin = freemem;                               //限制空闲地址最小值}if (end > KMEMSIZE) {end = KMEMSIZE;                                //限制空闲地址最大值}if (begin < end) {begin = ROUNDUP(begin, PGSIZE);                //对齐地址end = ROUNDDOWN(end, PGSIZE);if (begin < end) {//将空闲内存块映射纳入内存管理init_memmap(pa2page(begin), (end - begin) / PGSIZE);        }}}}

3.物理内存空间管理的初始化

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

//pmm.h中的pmm_manager定义
struct pmm_manager {const char *name;                                 // 管理器名称void (*init)(void);                               // 初始化管理器void (*init_memmap)(struct Page *base, size_t n); // 设置并初始化可管理的内存空间(初始化page)struct Page *(*alloc_pages)(size_t n);            // 分配n个连续物理页,返回首地址void (*free_pages)(struct Page *base, size_t n);  // 释放自Base起的连续n个物理页size_t (*nr_free_pages)(void);                    // 返回剩余空闲页数void (*check)(void);                              // 用于检测分配释放是否正确
};

init_memmap用于对页内存管理的Page的初始化,init用于初始化已定义好的free_area_t结构的free_area。

在内核初始化的kern_init中会调用pmm_init(),pmm_init()中会调用init_pmm_manager()进行初始化,使用默认的物理内存页管理函数,即使用default_pmm.c中定义的default_init等函数进行内存管理,本实验中需要实现的分配算法可以直接通过修改这些函数进行实现。

//pmm.c中的init_pmm_manager()定义,在pmm_init()中调用,在kernel_init()中初始化
static void
init_pmm_manager(void) {pmm_manager = &default_pmm_manager;             //pmm_manager指向default_pmm_managercprintf("memory management: %s\n", pmm_manager->name);pmm_manager->init();
} 
//default_pmm.c中的default_init()
static void
default_init(void) {list_init(&free_list);                          //初始化链表nr_free = 0;
}
//init_memmap:将page进行打包,放入free_list内
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));                    //检查是否为保留页//property设置为0,只有空闲块的第一页使用该变量,标志位清0p->flags = p->property = 0;                 set_page_ref(p, 0);                         //引用计数为0}base->property = n;                             //设置第一页的propertySetPageProperty(base);nr_free += n;                                   //空闲页+nlist_add(&free_list, &(base->page_link));       //将空闲块加入列表
}

4.first-fit算法的实现

在pmm_manager完成链表的初始化,page_init完成页的初始化后,就可以使用pmm_manager的函数进行内存空间管理了。本练习中使用默认的pmm_manager进行管理,其中的default_init可以直接使用,只需要修改default_init_memmap,default_alloc_pages,default_free_pages这三个就可以实现first-fit算法以及内存空间释放。

使用的函数及宏定义

使用的链表相关操作和宏定义如下。ucore对空闲内存空间管理使用的链表与常见的链表不同,链表只包含了前后节点的信息,而链表包含在Page结构中,这样做是为了实现链表通用性。

★宏定义le2page可以实现通过链表节点获取节点数据。(这个在后面会反复用到)

链表定义和部分相关函数如下:

//双向链表的定义
struct list_entry {struct list_entry *prev, *next;
};
typedef struct list_entry list_entry_t;//返回下一个节点
static inline list_entry_t *
list_next(list_entry_t *listelm) {return listelm->next;
}
//删除当前节点
static inline void
list_del(list_entry_t *listelm) { __list_del(listelm->prev, listelm->next);
}
//前插节点,还有类似的list_add_after
static inline void
list_add_before(list_entry_t *listelm, list_entry_t *elm) {__list_add(elm, listelm->prev, listelm);
}

le2page通过page_link地址减去其相对于Page结构的偏移,实现从链表节点找到对应的数据域。

#define le2page(le, member)                 to_struct((le), struct Page, member)
#define to_struct(ptr, type, member)                               ((type *)((char *)(ptr) - offsetof(type, member))))

还有一些其他函数需要使用,以设置Page的信息。如下。

//pmm.h中定义的设置引用计数的函数
static inline void
set_page_ref(struct Page *page, int val) {page->ref = val;
}
//memlayout.h中的关于页标志位设置的宏定义
#define SetPageReserved(page)       set_bit(PG_reserved, &((page)->flags))
#define ClearPageReserved(page)     clear_bit(PG_reserved, &((page)->flags))
#define PageReserved(page)          test_bit(PG_reserved, &((page)->flags))
#define SetPageProperty(page)       set_bit(PG_property, &((page)->flags))
#define ClearPageProperty(page)     clear_bit(PG_property, &((page)->flags))
#define PageProperty(page)          test_bit(PG_property, &((page)->flags))

default_init函数实现

按照注释解释,该部分代码无需修改,可直接使用

//default_init:
//初始化空闲链表
static void
default_init(void) {list_init(&free_list);nr_free = 0;
}

free_list是全局变量free_area的list_entry_t型成员变量。使用list_init函数后,它的前驱和后继都将指向自己,形成闭环;同时,nr_free是全局变量free_area的整型成员变量,表示空闲列表里所有空闲块可用的页数目,初始化为0。于是,该函数构建了一个只有头节点、没有空闲块的初始空闲列表。

default_init_memmap函数实现

根据注释要求,此函数与原来相比只需要修改最后一行,使用list_add_before即可。

After that, We can use p->page_link to link this page into free_list.(e.g.: list_add_before(&free_list, &(p->page_link)); )

实现如下:

//default_init_memmap:
//初始化内存中的各个Page变量(这些变量连续存放在内存中,起始地址是参数base,总数是n)
static void
default_init_memmap(struct Page *base, size_t n) {
//n为页数,base为起始地址
assert(n > 0);//断言:必须满足n>0,否则警告
struct Page *p = base;
for (; p != base + n; p ++) {
//base+n表示第n+1个页的指针(指针加法),这里使用循环遍历所有page
assert(PageReserved(p));//断言:必须满足若页面被设置为可被分配,否则警告
p->flags = p->property = 0;	//设置为被保留且不可分配(因为已经归base统一管理)
set_page_ref(p, 0);//设置为未被引用
}
//开始构建空闲列表
base->property = n;//初始所有物理页都空闲,拥有n的巨大空闲块
SetPageProperty(base);//将property位设置为1,表示这个空闲块可被分配
nr_free += n;//初始空闲块有完整的n个页大小
list_add_before(&free_list, &(base->page_link));//将base指向的Page的list_entry_t成员page_link加入双向链表,从而将这个Page作为一个结点加入空闲列表
}
//完成此函数后,空闲列表里有两个结点,一个是头节点,另一个是base,它是连续n个Page里的第一个Page,表示初始空闲块有n个页大,而且是可分配的。

default_alloc_pages函数实现

//函数作用:负责分配n个页,返回值是一个Page类型的指针,该指针描述了那个被真实分配的一个空闲区域的首个物理页。
static struct Page *
default_alloc_pages(size_t n) {
assert(n > 0);
if (n > nr_free) {
return NULL;
}//要分配的页数比总空闲页还多,直接返回空,分配失败(供不应求)
struct Page *page = NULL;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {//不停用le指向后继结点的list_entry_t型成员,可以达到遍历的目的,但要注意范围,避免死循环
struct Page *p = le2page(le, page_link);//使用le2page通过链表节点获取节点数据
if (p->property >= n) {//property值说明了此空闲块里含有的页数;循环不断进行,直到发现一个Page的property≥n,说明这个Page象征的空闲块足够大,可供分配
page = p;
break;
}
}
if (page != NULL) {
nr_free -= n;//找到了合适的空闲块,成功分配
ClearPageProperty(page);//可分配,所以可用的空闲页必须减少,同时清除page的有效位
//处理该块中剩余的page
if (page->property > n) {//当前空闲块被分割n个页后还有剩余的可作为新空闲块
struct Page *newnode=NULL; //需要新的空闲块
newnode=page + n; //新的空闲块的指针指向page后的第n个页
newnode->property = page->property - n;//设置新空闲块的空闲页数目
SetPageProperty(newnode);//同时设置它是可分配的
list_add_after(&(page->page_link), &(newnode->page_link));
//接在旧的节点后面,然后删除旧的节点的时候,新的自然就取代了它
list_del(&(page->page_link));
}
else
{ //否则,直接删除就行了
list_del(&(page->page_link));
}
}
return page;
}
//注意:若干Page变量本身是顺序存放的,对应着有序的若干物理页;只有空闲块中的第一个物理页对应的Page会被放入空闲列表并用指针将其le2page成员和空闲列表其他结点的le2page成员连接起来。虽然说“放入了空闲列表”,但各个Page的具体位置并不变化,只是其le2page成员的指针指向发生了变化而已。

default_free_pages函数实现

//函数作用:释放n个页
static void
default_free_pages(struct Page *base, size_t n) {//
assert(n > 0);//断言
struct Page *p = base;
for (; p != base + n; p ++) {
assert(!PageReserved(p) && !PageProperty(p));
//Property只有某个连续空闲块的顶部的页才有,相应的flags里页才会有这个位存在。如果PageReserved成立,说明是保留页,不能释放;如果是PageProperty成立,说明是某个空闲块的第一个页,不能释放。
//总的来说,如果这个页是已经保留的,或者是有property的,就终止。
p->flags = 0;
set_page_ref(p, 0);
}//为了释放,p不断向下移动,把沿途的页的flags全部变为0,设置其ref引用也为0
//总之,for循环把需要释放的p个页的flags和ref全部都设置为0
base->property = n;
SetPageProperty(base);
//把n个页的属性重新设置之后,它们变成一个还没有加入空闲表的空闲块,且第一个页的地址就是参数base。它的property在分配前后都是大于等于n的,现在必须设为n,表示它是一个n个页的空闲块。同时,它的property位被再次设为有效。
list_entry_t *le = list_next(&free_list);//接下来遍历已有空闲列表,试图发现被释放的head有没有可能与已有的空闲块相连
while (le != &free_list) {//While循环会开始遍历当前的双向链表。这个链表挤满了可用空闲块的首页(property不为0)
p = le2page(le, page_link);//用le2page取出双向链表中list_entry_t类所在的Page结构指针
le = list_next(le);
//此时,p所指向的Page是当前双向链表中的某个空闲块的第一个页。base指向要释放的空闲块的第一个页,base->property是要释放的页的个数
if (base + base->property == p) {//表示相连且base在p前面
base->property += p->property;//合并延长
ClearPageProperty(p);//合并后,p不是单独的且不是第一个,被取消掌管property的权力
list_del(&(p->page_link));
}
else if (p + p->property == base) {//表示相连且p在base前面
p->property += base->property;//合并延长
ClearPageProperty(base);//合并后,base不是单独的且不是第一个,被取消掌管property的权力
base = p;
list_del(&(p->page_link));
}
}
nr_free += n;//可用页增加
//将base指向的页的list_entry_t类放入双向链表。
//为。用while找出第一个地址比base自己大的页p;p作为空闲块的第一个页,地址大于base,那么就意味着base对应的页应该放到它的前面。这样一来,双向链表中各个空闲块的排序就是按照地址从小到大的,符合first_fit算法的要求。
le = &free_list;
while ((le=list_next(le)) != &free_list)
{
p = le2page(le, page_link);
if(base<p)
{ break; }
}
list_add_before(le, &(base->page_link));
}

改进空间

  • 时间复杂度改进:用线段树可以实现 alloc 和 free 达到 O(log n) 级别的时间复杂度,不过空间(复杂度虽然还是 O(n) 不变)要增加一倍。
  • 空间改进:在空闲链表开头会产生许多小的空闲块,需要处理。
  • 遍历顺序:可以通过适当提前较大的空闲块达到更好的成功命中效果。

5.其他实现算法概览

1、First Fit(最先匹配算法)

该算法从空闲分区链首开始查找,直至找到一个能满足其大小要求的空闲分区为止。然后再按照作业的大小,从该分区中划出一块内存分配给请求者,余下的空闲分区仍留在空闲分区链中。

  • 优点:该算法倾向于使用内存中低地址部分的空闲区,在高地址部分的空闲区很少被利用,从而保留了高地址部分的大空闲区。显然为以后到达的大作业分配大的内存空间创造了条件。
  • 缺点:低地址部分不断被划分,留下许多难以利用、很小的空闲区,而每次查找又都从低地址部分开始,会增加查找的开销

2、Next Fit(循环首次匹配算法)

该算法是由首次适应算法演变而成的。在为进程分配内存空间时,不再每次从链首开始查找,直至找到一个能满足要求的空闲分区,并从中划出一块来分给作业。

  • 优点:使内存中的空闲分区分布的更为均匀,减少了查找时的系统开销
  • 缺点:缺乏大的空闲分区,从而导致不能装入大型作业。

3、Best Fit(最佳匹配算法)

该算法总是把既能满足要求,又是最小的空闲分区分配给作业。为了加速查找,该算法要求将所有的空闲区按其大小排序后,以递增顺序形成一个空白链。这样每次找到的第一个满足要求的空闲区,必然是最优的。孤立地看,该算法似乎是最优的,但事实上并不一定。因为每次分配后剩余的空间一定是最小的,在存储器中将留下许多难以利用的小空闲区。同时每次分配后必须重新排序,这也带来了一定的开销。

  • 优点:每次分配给文件的都是最合适该文件大小的分区
  • 缺点:内存中留下许多难以利用的小的空闲区

4、Worst Fit(最差匹配算法)

该算法按大小递减的顺序形成空闲区链,分配时直接从空闲区链的第一个空闲区中分配(不能满足需要则不分配)。很显然,如果第一个空闲分区不能满足,那么再没有空闲分区能满足需要。这种分配方法初看起来不太 合理,但它也有很强的直观吸引力:在大空闲区中放入程序后,剩下的空闲区常常也很大,于是还能装下一个较大的新程序。

最坏适应算法与最佳适应算法的排序正好相反,它的队列指针总是指向最大的空闲区,在进行分配时,总是从最大的空闲区开始查寻。该算法克服了最佳适应算法留下的许多小的碎片的不足,但保留大的空闲区的可能性减小了,而且空闲区回收也和最佳适应算法一样复杂。

  • 优点:给文件分配分区后剩下的空闲区不至于太小,产生碎片的几率最小,对中小型文件分配分区操作有利。
  • 缺点:使存储器中缺乏大的空闲区,对大型文件的分区分配不利。

练习2:实现寻找虚拟地址对应的页表项(需要编程)

通过设置页表和对应的页表项,可建立虚拟内存地址和物理内存地址的对应关系。其中的get_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址,如果此二级页表项不存在,则分配一个包含此项的二级页表。本练习需要补全get_pte函数 in kern/mm/pmm.c,实现其功能。请仔细查看和理解get_pte函数中的注释。get_pte函数的调用关系图如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pDhwsbr4-1683099850419)(C:\Users\y\AppData\Roaming\Typora\typora-user-images\image-20230421213911801.png)]

图1 get_pte函数的调用关系图

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 请描述页目录项(Page Directory Entry)和页表项(Page Table Entry)中每个组成部分的含义以及对ucore而言的潜在用处。
  • 如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?

1.段页式管理

在保护模式中,内存地址分成三种:逻辑地址、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。段式管理的映射将逻辑地址转换为线性地址,页式管理的映射将线性地址转换为物理地址。

ucore中段式管理仅为一个过渡,逻辑地址与线性地址相同。而页式管理是通过二级页表实现的,地址的高10位为页目录索引,中间10位为页表索引,低12位为偏移(页对齐,低12位为0)。一级页表的起始物理地址存放在 boot_cr3中。

2.页目录项和页表项的组成

问题一:请描述页目录项(Pag Director Entry)和页表(Page Table Entry)中每个组成部分的含义和以及对ucore而言的潜在用处

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gYoHf19B-1683099850420)(C:\Users\y\AppData\Roaming\Typora\typora-user-images\image-20230422153547635.png)]

(1)页目录项的组成

前20位表示该PDE对应的页表起始位置
第9-11位保留给OS使用
第8位可忽略
第7位用于设置Page大小,0表示4KB
第6位为0
第5位表示该页是否被写过
第4位表示是否需要进行缓存
第3位表示CPU是否可直接写回内存
第2位表示该页是否可被任何特权级访问
第1位表示是否允许读写
第0位为该PDE的存在位

/* page table/directory entry flags */
//在mmu.h文件中,对页目录项各个位的构成有说明如下:
#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_MBZ 0x180 // Bits must be zero
#define PTE_AVAIL 0xE00 // Available for software use
// The PTE_AVAIL bits aren't used
by the kernel or interpreted by the
// hardware, so user processes
are allowed to set them arbitrarily.

(2) 页表项的组成

前20位表示该PTE指向的物理页的物理地址
第9-11位保留给OS使用
第8位表示在 CR3 寄存器更新时无需刷新 TLB 中关于该页的地址
第7位恒为0
第6位表示该页是否被写过
第5位表示是否可被访问
第4位表示是否需要进行缓存
第0-3位与页目录项的0-3位相同

在这里插入图片描述

3.页访问异常的处理

问题二:如果ucore执行过程中访问内存,出现了页访问异常,请问硬件要做哪些事情?
当发生页访问异常时,硬件需要将发生错误的地址存放在cr2寄存器中,向栈中压入EFLAGS,CS,EIP等,如果异常发生在用户态,还需要进行特权级切换,最后根据中断描述符找到中断服务例程,接下来由中断服务例程处理该异常,转交给软件处理。

4.实现get_pte寻找页表项

练习二要求实现get_pte函数,使该函数能够找到传入的线性地址对应的页表项,返回页表项的地址。为了完成该函数,需要了解ucore中页式管理的相关函数及定义。

//PDX(la):la为线性地址,该函数取出线性地址中的页目录项索引
#define PDX(la) ((((uintptr_t)(la)) >> PDXSHIFT) & 0x3FF)
//取出线性地址中的页表项索引
#define PTX(la) ((((uintptr_t)(la)) >> PTXSHIFT) & 0x3FF)
//KADDR(pa):返回物理地址pa对应的虚拟地址
#define KADDR(pa) {...}
//set_page_ref(page,1):设置该页引用次数为1
static inline void
set_page_ref(struct Page *page, int val) {page->ref = val;
}
//page2pa:找到page结构对应的页的物理地址
static inline uintptr_t
page2pa(struct Page *page) {return page2ppn(page) << PGSHIFT;
}
//alloc_page():分配一页
#define alloc_page() alloc_pages(1)
//页目录项、页表项的标志位
#define PTE_P           0x001                   // 存在位
#define PTE_W           0x002                   // 是否可写
#define PTE_U           0x004                   // 用户是否可访问
//页目录项和页表项类型
typedef uintptr_t pte_t;
typedef uintptr_t pde_t;
//以下两个函数用于取出页目录项中的页表地址,取出页表项中的页地址
#define PDE_ADDR(pde)   PTE_ADDR(pde)
#define PTE_ADDR(pte)   ((uintptr_t)(pte) & ~0xFFF)
//补充:
*memset(void*s,char c,size_t n):将s指向的内存区域的前n个字节设置为指定的值c。

需要完成的get_pte函数原型如下

pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create) 

这个函数的作用为:获得二级页表中的页表项pte,并返回此pte的内核虚拟地址以供la使用。如果该pte不存在,根据create参数决定是否为该pte分配一页。

参数含义:

  • pgdir:一级页目录的起始地址,
  • la:需要映射的线性地址
  • create:表示是否可以为页表分配新的页

注释对该函数的步骤说明:

  1. 查找页目录表项目(搜寻二级页表在一级页表里保存的地址)
  2. 查看这样的目录项是否存在
  3. 根据create变量查看有没有必要在不存在的时候创建表,如果需要的话,为二级页表申请分配一个页
  4. 设置页的引用(表示这个物理页被一个进程使用,分配出去了。如果在目录项里查不到对应的二级页表,且需要分配一个页存放二级页表,那么这个被分配的页就需要给当前进程使用,所以要设置它被引用一次。)
  5. 获得页的线性地址
  6. 用memeset清除页的内容
  7. 设置页目录表项的权限
  8. 返回页表项

取出线性地址对应的页表项。

首先在页目录中找到对应的页目录项,并判断是否有效(二级页目录是否存在)。如果不存在则根据create判断是否需要创建新的一页存放页表,如果需要则调用alloc_page分配新的一页,将这一页的地址结合标志位设置为页目录项。最后返回页表项的线性地址,使用PDE_ADDR取出页目录项中保存的页表地址,再加上使用PTX从线性地址取出的页表索引,就找到了页表项的位置,使用KADDR转换为线性地址返回(先将页表地址转换为线性地址再加索引同样可行),注意类型转换。

函数实现:

//函数功能:返回已知线性地址所对应页表项的指针
get_pte(pde_t *pgdir, uintptr_t la, bool create) {
uintptr_t index=PDX(la);//获取目录表的索引
pde_t *pde_ad = &pgdir[index]; //根据索引,获得目录表的表项的地址,这样方便后续修改这个表项的内容//若不存在,则需要创建或直接退出
if(!(*pde_ad & PTE_P)) //先看bit 1位是否为1,即物理页是否存在。如果不存在,根据情况决定是否分配
{
if(create)
{
struct Page *newpage=alloc_page();//分配一个页
set_page_ref(newpage, 1);//设置这个页被引用,因为它是一个存放二级页表的页
uintptr_t pa=page2pa(newpage); //获得这个页的物理地址,用于填写到pde目录项之1中
*pde_ad =(pa & ~0x0FFF)|PTE_U|PTE_W|PTE_P;//重置页目录项
memset(KADDR(pa),0,PGSIZE);//设置这个页为全0,初始化清空
}
else
{
return NULL; //如果不要求新建页,也返回空
}
}//正式开始工作,返回其页表项
uintptr_t pt_pa=PDE_ADDR(*pde_ad);//从页目录项里,获得二级页表的物理地址
pte_t *mypte=KADDR(pt_pa);//将这个二级页表的物理地址转化为虚拟地址,然后赋值给指针
index=PTX(la);//获得在二级页表里的索引
mypte+=index;//增加以指向二级页表里相应的表项
return mypte;//返回这个指向表项的指针
}

练习3:释放某虚地址所在的页并取消对应二级页表项的映射(需要编程)

当释放一个包含某虚地址的物理内存页时,需要让对应此物理内存页的管理数据结构Page做相关的清除处理,使得此物理内存页成为空闲;另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page_remove_pte函数中的注释。为此,需要补全在 kern/mm/pmm.c中的page_remove_pte函数。page_remove_pte函数的调用关系图如下所示:

在这里插入图片描述

图2 page_remove_pte函数的调用关系图

请在实验报告中简要说明你的设计实现过程。请回答如下问题:

  • 数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?
  • 如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事? 鼓励通过编程来具体完成这个问题

1.Page与页目录项和页表项的关系

问题一:数据结构Page的全局变量(其实是一个数组)的每一项与页表中的页目录项和页表项有无对应关系?如果有,其对应关系是啥?

当页目录项与页表项均有效时,有对应关系。每个页目录项记录一个页表的位置,每个页表项则记录一个物理页的位置,而Page变量保存的就是物理页的信息,因此每个有效的页目录项和页表项,都对应了一个page结构,即一个物理页的信息

2.实现虚拟地址与物理地址相等

问题二:如果希望虚拟地址与物理地址相等,则需要如何修改lab2,完成此事?

由附录可知,在lab1中,虚拟地址=线性地址=物理地址,ucore的起始虚拟地址(也即物理地址)从0x100000开始。

而在lab2中建立了从虚拟地址到物理地址的映射,ucore的物理地址仍为0x100000,但虚拟地址变为了0xC0100000,即最终建立的映射为:virt addr = linear addr = phy addr + 0xC0000000。

 //memlayout.h给出了直观的映射关系*     4G ------------------> +---------------------------------+*                            |                                 |*                            |         Empty Memory (*)        |*                            |                                 |*                            +---------------------------------+ 0xFB000000*                            |   Cur. Page Table (Kern, RW)    | RW/-- PTSIZE*     VPT -----------------> +---------------------------------+ 0xFAC00000*                            |        Invalid Memory (*)       | --/--*     KERNTOP -------------> +---------------------------------+ 0xF8000000*                            |                                 |*                            |    Remapped Physical Memory     | RW/-- KMEMSIZE*                            |                                 |*     KERNBASE ------------> +---------------------------------+ 0xC0000000*                            |                                 |*                            |                                 |*                            |                                 |*                            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

只要取消这个映射,就可以实现虚拟地址和物理地址相等,将ld工具形成的ucore的虚拟地址修改为0x100000,就可以取消映射。

ENTRY(kern_entry)
SECTIONS {/* Load the kernel at this address: "." means the current address */. = 0xC0100000;        //修改为0x100000就可以实现虚拟地址=物理地址.text : {*(.text .stub .text.* .gnu.linkonce.t.*)}

还需要在memlayout.h中将KERNBASE即虚拟地址基址设置为0,并关闭entry.S中对页表机制的开启

//memlayout.h中定义的KERNBASE
#define KERNBASE 0x0
//页表机制开启# enable pagingmovl %cr0, %eaxorl $(CR0_PE | CR0_PG | CR0_AM | CR0_WP | CR0_NE | CR0_TS | CR0_EM | CR0_MP), %eaxandl $~(CR0_TS | CR0_EM), %eaxmovl %eax, %cr0        //将这句注释掉

pmm_init中的check_pgdir和check_boot_pgdir都假设kernbase不为0,对线性地址为0的地址进行页表查询等,因此会产生各种错误,可以将这两个函数注释掉

3.实现page_remove_pte释放虚拟页并取消二级映射

本练习中需要完成的是page_remove_pte函数,该函数将传入的虚拟页释放,取消二级页表的映射,并且需清除TLB中对应的项。原型如下:

page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep)

使用的相关函数定义如下:

//pte2page:找到页表项对应的页
struct Page *page pte2page(*ptep)
//free_page:释放页
free_page(page)
//page_ref_dec:引用计数-1
page_ref_dec(page)
//tlb_invalidate:清除TLB
tlb_invalidate(pde_t *pgdir, uintptr_t la) 

对于传入的页表项,首先要判断其是否有效,如果有效,将引用计数-1,当引用计数为0时释放该页,再清0二级页表映射,使用tlb_invalidate清除对应的TLB项。最终实现如下:

static inline void
page_remove_pte(pde_t *pgdir, uintptr_t la, pte_t *ptep) {    //判断页表项是否有效if (*ptep & PTE_P) {                      struct Page *page = pte2page(*ptep);        //找到对应的页page_ref_dec(page);                         //引用计数-1if(page->ref==0) free_page(page);           //引用计数为0释放该页*ptep=0;                                    //清除二级页表映射tlb_invalidate(pgdir,la);                   //修改TLBreturn;}return;
}

结果测试

首先输入make clean,make qemu运行构建操作系统:

在这里插入图片描述

操作系统正常运行,显示检查成功(三个succeeded)

再次输入make clean,并用make grade检查测试结果:

在这里插入图片描述

通过测试。

扩展练习Challenge1:buddy system(伙伴系统)分配算法(需要编程)

Buddy System算法把系统中的可用存储空间划分为存储块(Block)来进行管理, 每个存储块的大小必须是2的n次幂(Pow(2, n)), 即1, 2, 4, 8, 16, 32, 64, 128…

  • 参考伙伴分配器的一个极简实现, 在ucore中实现buddy system分配算法,要求有比较充分的测试用例说明实现的正确性,需要有设计文档。

1.伙伴系统分配算法
伙伴系统是一种采用二分的方式分割和合并空闲空间的内存分配算法。空闲空间被视为2^n的空间,当有内存空间分配请求时,将空闲空间一分为二,直到刚好可以满足请求大小。在空间释放时,分配程序会检查该块同大小的伙伴块是否空闲,如果是则可以进行合并,并继续上溯,直到完成全部可能的合并。

高层节点对应大的块,低层节点对应小的块,在分配和释放中我们就通过这些节点的标记属性来进行块的分离合并。

分配器使用伙伴系统,是通过数组形式的二叉树实现的。二叉树的节点标记相应的内存块是否使用,通过这些标记进行块的分离与合并。二叉树的情况如下图(总大小为16),其中节点数字为在数组中的索引:

在这里插入图片描述

2.伙伴系统的实现
面是对实验指导书中给出的伙伴系统的分析。

首先是数据结构和一些宏定义,主要是伙伴系统定义,计算节点等:

struct buddy2 {unsigned size;//表明物理内存的总单元数unsigned longest[1]; //二叉树的节点标记,表明对应内存块的空闲单位
};
#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)))//x是不是2的幂
#define MAX(a, b) ((a) > (b) ? (a) : (b))//判断a,b大小
#define ALLOC malloc//申请内存
#define FREE free//释放内存

在分配时,分配的空间大小必须满足需要的大小,且为2的幂次方,以下函数用于找到合适的大小:

static unsigned fixsize(unsigned size) {//找到大于等于所需内存的2的倍数size |= size >> 1;size |= size >> 2;size |= size >> 4;size |= size >> 8;size |= size >> 16;return size+1;
}

接下来是分配器的初始化及销毁。初始化传入的参数是需要管理的内存空间大小,且这个大小应该是2的幂次方。在函数中node_size用于计算节点的大小,每次除2,初始化每一个节点。

struct buddy2* buddy2_new( int size ) {//初始化分配器struct buddy2* self;unsigned node_size;//节点所拥有的内存大小int i;if (size < 1 || !IS_POWER_OF_2(size))return NULL;self = (struct buddy2*)ALLOC( 2 * size * sizeof(unsigned));self->size = size;node_size = size * 2;for (i = 0; i < 2 * size - 1; ++i) {if (IS_POWER_OF_2(i+1))node_size /= 2;self->longest[i] = node_size;}return self;
}void buddy2_destroy( struct buddy2* self) {FREE(self);
}

内存分配的实现如下,传入分配器,需要分配的空间大小,首先判断是否可以进行分配,并将空间大小调整为2的幂次方,然后进行分配。分配的过程为遍历寻找合适大小的节点,将找到的节点大小清0表示以被占用,并且需要更新父节点的值,最后返回的值为所分配空间相对于起始位置的偏移。

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 = fixsize(size);if (self->longest[index] < size)//可分配内存不足return -1;//从根节点开始向下寻找合适的节点for(node_size = self->size; node_size != size; node_size /= 2 ) {if (self->longest[LEFT_LEAF(index)] >= size)index = LEFT_LEAF(index);elseindex = RIGHT_LEAF(index);//左子树不满足时选择右子树}self->longest[index] = 0;     //将节点标记为已使用offset = (index + 1) * node_size - self->size;//计算偏移量//更新父节点while (index) {index = PARENT(index);self->longest[index] = MAX(self->longest[LEFT_LEAF(index)], self->longest[RIGHT_LEAF(index)]);}return offset;
}

内存释放时,先自底向上寻找已被分配的空间,将这块空间的大小恢复,接下来就可以匹配其大小相同的空闲块,如果块都为空闲则进行合并。

void buddy2_free(struct buddy2* self, int offset) {unsigned node_size, index = 0;unsigned left_longest, right_longest;//判断请求是否出错assert(self && offset >= 0 && offset < self->size);node_size = 1;index = offset + self->size - 1;//寻找分配过的节点for (; self->longest[index] ; index = PARENT(index)) {node_size *= 2;if (index == 0)                //如果节点不存在return;}self->longest[index] = node_size;//释放空间//合并while (index) {index = PARENT(index);node_size *= 2;left_longest = self->longest[LEFT_LEAF(index)];right_longest = self->longest[RIGHT_LEAF(index)];if (left_longest + right_longest == node_size) //如果可以则合并self->longest[index] = node_size;elseself->longest[index] = MAX(left_longest, right_longest);}
}

以上就是参考资料中给出的伙伴系统的实现(另外还有两个函数分别用于返回当前节点大小和打印内存状态),在ucore中实现伙伴系统的原理相同,但需要对具体的页进行处理分配以及释放,完成对应的buddy.h头文件和buddy.c文件后,修改pmm.c中的init_pmm_manager,将默认使用的分配器修改为伙伴系统分配器就可以在ucore中实现伙伴系统了。

扩展练习Challenge2:任意大小的内存单元slub分配算法(需要编程)

slub算法,实现两层架构的高效内存单元分配,第一层是基于页大小的内存分配,第二层是在第一层基础上实现基于任意大小的内存分配。可简化实现,能够体现其主体思想即可。

  • 参考linux的slub分配算法/,在ucore中实现slub分配算法。要求有比较充分的测试用例说明实现的正确性,需要有设计文档。

SLAB 分配器为每种使用的内核对象建立单独的缓冲区。每种缓冲区由多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象。根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024 个物理内存页框构成。

内核使用 kmem_cache 数据结构管理缓冲区。由于 kmem_cache 自身也是一种内核对象,所以需要一个专门的缓冲区。所有缓冲区的 kmem_cache 控制结构被组织成以 cache_chain 为队列头的一个双向循环队列,同时 cache_cache 全局变量指向kmem_cache 对象缓冲区的 kmem_cache 对象。每个 slab 都需要一个类型为 struct slab 的描述符数据结构管理其状态,同时还需要一个 kmem_bufctl_t(被定义为无符号整数)的结构数组来管理空闲对象。如果对象不超过 1/8 个物理内存页框的大小,那么这些 slab 管理结构直接存放在 slab 的内部,位于分配给 slab 的第一个物理内存页框的起始位置;否则的话,存放在 slab 外部,位于由 kmalloc 分配的通用对象缓冲区中。

slab 中的对象有 2 种状态:已分配或空闲。

为了有效地管理 slab,根据已分配对象的数目,slab 可以有 3 种状态,动态地处于缓冲区相应的队列中:

  1. Full 队列,此时该 slab 中没有空闲对象。
  2. Partial 队列,此时该 slab 中既有已分配的对象,也有空闲对象。
  3. Empty 队列,此时该 slab 中全是空闲对象。

在 SLUB 分配器中,一个 slab 就是一组连续的物理内存页框,被划分成了固定数目的对象。slab 没有额外的空闲对象队列(这与 SLAB 不同),而是重用了空闲对象自身的空间。slab 也没有额外的描述结构,因为 SLUB 分配器在代表物理页框的 page 结构中加入 freelist,inuse 和 slab 的 union 字段,分别代表第一个空闲对象的指针,已分配对象的数目和缓冲区 kmem_cache 结构的指针,所以 slab 的第一个物理页框的 page 结构就可以描述自己。

每个处理器都有一个本地的活动 slab,由 kmem_cache_cpu 结构描述。

以下是slab_alloc的实现

static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, void *addr)
{void **object;struct kmem_cache_cpu *c;unsigned long flags;local_irq_save(flags);c = get_cpu_slab(s, smp_processor_id()); //获取本处理器的 kmem_cache_cpu 数据结构if (unlikely(!c->freelist || !node_match(c, node)))object = __slab_alloc(s, gfpflags, node, addr, c); // 假如当前活动 slab 没有空闲对象,或本处理器所在节点与指定节点不一致,则调用 __slab_alloc 函数else {object = c->freelist; //获得第一个空闲对象的指针,然后更新指针使其指向下一个空闲对象c->freelist = object[c->offset];stat(c, ALLOC_FASTPATH);}local_irq_restore(flags);if (unlikely((gfpflags & __GFP_ZERO) && object))memset(object, 0, c->objsize);return object; //返回对象地址
}

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, void *addr, struct kmem_cache_cpu *c)函数实现

static void *__slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node, void *addr, struct kmem_cache_cpu *c)
{void **object;struct page *new;gfpflags &= ~__GFP_ZERO;if (!c->page) // (a):如果没有本地活动 slab,转到 (f) 步骤获取 slabgoto new_slab;slab_lock(c->page);if (unlikely(!node_match(c, node))) // (b):检查处理器活动 slab 没有空闲对象,转到 (e) 步骤goto another_slab;stat(c, ALLOC_REFILL);load_freelist:object = c->page->freelist;if (unlikely(!object)) // (c):此时活动 slab 尚有空闲对象,将 slab 的空闲对象队列指针复制到 kmem_cache_cpu 结构的 freelist 字段,把 slab 的空闲对象队列指针设置为空,从此以后只从 kmem_cache_cpu 结构的 freelist 字段获得空闲对象队列信息goto another_slab;if (unlikely(SlabDebug(c->page)))goto debug;c->freelist = object[c->offset]; // (d):此时活动 slab 尚有空闲对象,将 slab 的空闲对象队列指针复制到 kmem_cache_cpu 结构的 freelist 字段,把 slab 的空闲对象队列指针设置为空,从此以后只从 kmem_cache_cpu 结构的 freelist 字段获得空闲对象队列信息c->page->inuse = s->objects;c->page->freelist = NULL;c->node = page_to_nid(c->page);unlock_out:slab_unlock(c->page);stat(c, ALLOC_SLOWPATH);return object;another_slab:deactivate_slab(s, c); // (e):取消当前活动 slab,将其加入到所在 NUMA 节点的 Partial 队列中new_slab:new = get_partial(s, gfpflags, node); // (f):优先从指定 NUMA 节点上获得一个 Partial slabif (new) {c->page = new;stat(c, ALLOC_FROM_PARTIAL);goto load_freelist;}if (gfpflags & __GFP_WAIT) // (g):加入 gfpflags 标志置有 __GFP_WAIT,开启中断,故后续创建 slab 操作可以睡眠local_irq_enable();new = new_slab(s, gfpflags, node); // (h):创建一个 slab,并初始化所有对象if (gfpflags & __GFP_WAIT)local_irq_disable();if (new) {c = get_cpu_slab(s, smp_processor_id());stat(c, ALLOC_SLAB);if (c->page) flush_slab(s, c);slab_lock(new);SetSlabFrozen(new);c->page = new;goto load_freelist;}if (!(gfpflags & __GFP_NORETRY) && (s->flags & __PAGE_ALLOC_FALLBACK)) {if (gfpflags & __GFP_WAIT) local_irq_enable();object = kmalloc_large(s->objsize, gfpflags); // (i):如果内存不足,无法创建 slab,调用 kmalloc_large(实际调用物理页框分配器)分配对象if (gfpflags & __GFP_WAIT)local_irq_disable();return object;}return NULL;debug:if (!alloc_debug_processing(s, c->page, object, addr))goto another_slab;c->page->inuse++;c->page->freelist = object[c->offset];c->node = -1;goto unlock_out;
}

以下是slab_free的实现

static __always_inline void slab_free(struct kmem_cache *s, struct page *page, void *x, void *addr)
{void **object = (void *)x;struct kmem_cache_cpu *c;unsigned long flags;local_irq_save(flags);c = get_cpu_slab(s, smp_processor_id());debug_check_no_locks_freed(object, c->objsize);if (likely(page == c->page && c->node >= 0)) { // (a)如果对象属于处理器当前活动的 slab,或处理器所在 NUMA 节点号不为 -1(调试使用的值),将对象放回空闲对象队列object[c->offset] = c->freelist;c->freelist = object;stat(c, FREE_FASTPATH);} else__slab_free (s, page, x, addr, c->offset); // (b)否则调用 __slab_free 函数local_irq_restore(flags);
}

以下是static void __slab_free(struct kmem_cache *s, struct page *page,
void *x, void *addr, unsigned int offset)函数的实现

static void __slab_free(struct kmem_cache *s, struct page *page,
void *x, void *addr, unsigned int offset)
{void *prior;void **object = (void *)x;struct kmem_cache_cpu *c;c = get_cpu_slab(s, raw_smp_processor_id());stat(c, FREE_SLOWPATH);slab_lock(page);if (unlikely(SlabDebug(page)))goto debug;checks_ok:prior = object = page->freelist; // (a)执行本函数表明对象所属 slab 并不是某个活动 slab。保存空闲对象队列的指针,将对象放回此队列,最后把已分配对象数目减一。page->freelist = object;page->inuse--;if (unlikely(SlabFrozen(page))) {stat(c, FREE_FROZEN);goto out_unlock;}if (unlikely(!page->inuse)) // (b)如果已分配对象数为 0,说明 slab 处于 Empty 状态,转到 (d) 步骤。goto slab_empty;if (unlikely(!prior)) { // (c)1. 如果原空闲对象队列的指针为空,说明 slab 原来的状态为 Full,那么现在的状态应该是 Partial,将该 slab 加到所在节点的 Partial 队列中。add_partial(get_node(s, page_to_nid(page)), page, 1);stat(c, FREE_ADD_PARTIAL);}out_unlock:slab_unlock(page);return;slab_empty:if (prior) { // (d)如果 slab 状态转为 Empty,且先前位于节点的 Partial 队列中,则将其剔出并释放所占内存空间。remove_partial(s, page);stat(c, FREE_REMOVE_PARTIAL);}slab_unlock(page);stat(c, FREE_SLAB);discard_slab(s, page);return;debug:if (!free_debug_processing(s, page, x, addr))goto out_unlock;goto checks_ok;
}

参考答案对比

练习1

  1. default_init函数实现未在原基础上做修改;
  2. default_init_memmap函数在原基础上做的修改只是按照注释要求使用了List_add_before而不是list_add,但由于初始构建空闲列表时只有两个结点、顺序无关,所以实质上效果一样。和参考答案一致。
  3. default_alloc_pages函数在原基础上,在找到第一个合适的空闲块并拆分新空闲块的实现上,虽然基本原理与参考一致,但执行思路不同;此外,似乎不如参考答案的实现更加简洁。
  4. default_free_pages函数在原基础上只修改了将空闲块添加到空闲列表时的方式,保证了空闲块按照地址从小到达分步在空闲列表中。采用的循环检查方式和比较方式均不同于答案,而答案中在对空闲块的大小进行比较时明显更加严谨一些,而且能够排除更多的潜在错误情况。

练习2

  • 与参考答案相比,使用的临时变量较多,不是十分简洁;判断是否分配页的时候,不如参考答案精简,对页目录项的赋值处理和返回指针的方式较为繁琐。

练习3

  • 释放部分原本没有考虑到对引用的处理和辨析,所以最后借鉴了参考报告的思路(详见后续参考文献),基本和参考答案一致。

重要知识点和对应原理

实验中的重要知识点

  • 连续内存管理机制
  • 物理内存分配算法具体实现
  • 实现双向链表的数据结构
  • 利用函数指针和结构体近似面向对象功能
  • 段机制与页机制相关数据结构和操作方法
  • 虚拟地址到物理地址的映射和转换

对应的OS原理知识点

  • 连续空间分配算法
  • 分段机制、分页机制和多级页表
  • 虚拟地址空间到物理地址空间的映射关系

二者关系

  • 本实验设计的知识是对OS原理的具体实现,在细节上非常复杂。

未对应的知识点

  • 页交换和页分配机制
  • TLB快速缓存实现方法
  • 页中断处理的详细软件机制
  • 虚存地址空间实现方法
  • 操作系统代码的映射关系和内核栈的具体实现

参考文献

对于slub分配算法参考了https://www.bbsmax.com/A/gAJG6BegzZ/

对于练习二与练习三的理解参考了https://blog.csdn.net/Aaron503/article/details/130189764

对于基础原理的理解部分参考了老师提供的2019级曹书与同学的实验报告

相关文章:

HNU-操作系统OS-实验Lab2

OS_Lab2_Experimental report 湖南大学信息科学与工程学院 计科 210X wolf &#xff08;学号 202108010XXX&#xff09; 前言 实验一过后大家做出来了一个可以启动的系统&#xff0c;实验二主要涉及操作系统的物理内存管理。操作系统为了使用内存&#xff0c;还需高效地管理…...

如何使用HTML和CSS创建有方向感知的按钮

在互联网应用中&#xff0c;按钮是一种常见的控件&#xff0c;用户通过点击按钮来触发相应的操作。考虑到用户体验和交互设计&#xff0c;设计有方向感知的按钮可以使得用户更加易于理解按钮的功能和状态。 在本文中&#xff0c;我们将介绍如何使用HTML和CSS来创建具有方向感知…...

java 线程安全

内部锁 在 Java 中&#xff0c;每个对象都有一个内部锁&#xff0c;也称为监视器锁或对象锁。内部锁是通过在代码块或方法前加上 synchronized 关键字来实现的。当一个线程执行一个带有 synchronized 关键字的方法或代码块时&#xff0c;它必须先获得该对象的内部锁&#xff0…...

移动硬盘修复后文件丢失恢复方法

最近收到很多这样的咨询&#xff1a; 问1&#xff1a;移动硬盘目录损坏用chkdsk修复后&#xff0c;文件被删除&#xff0c;怎么才可以恢復文件&#xff1f; 问2&#xff1a;移动硬盘出错然后修复&#xff0c;然后文件都没有了怎么处理啊&#xff01;&#xff01;&#xff01;&a…...

直线飙升到10万+star的AutoGpt,有多强?帮我写了个网页!

先来感受一下10万的star&#xff0c;到底有多强&#xff01; 从4月2日开始&#xff0c;直线飙升到10万star Auto-GPT是一个实验性的开源应用程序&#xff0c;展示了GPT-4语言模型的功能。这个程序由GPT-4驱动&#xff0c;将LLM“思想”链接在一起&#xff0c;以自主实现您设定的…...

rk3568平台调试typec口实现uvc输出,网络共享等功能

一、修改kernel相关配置 注意&#xff1a;一定要知道主控接线&#xff0c;那个物理口是otg的&#xff0c;然后要找准与之所连接的phy和控制器。然后处理CC1 CC2识别芯片&#xff0c;fusb302。默认sdk自带有驱动&#xff0c;需要配上中断脚和提供VBUS 5V的脚。用来判断角色是DF…...

java基础知识——26.反射

这篇文章我们来讲一下java的代理与反射&#xff0c;这是很重要的一部分内容。 目录 1.什么是反射 2.获取class对象的三种方式 3.反射获取构造方法 4.利用反射来获取成员变量 5.利用反射来获取成员方法 6.反射的作用 7.反射小结 1.什么是反射 首先&#xff0c;我们来看…...

【容器化】Docker 简介和安装

【容器化】Docker 简介和安装 DockerDocker的应用场景Docker 的优点1、快速&#xff0c;一致地交付您的应用程序2、响应式部署和扩展3、在同一硬件上运行更多工作负载 Docker 架构Docker 安装Ubuntu Docker 、Debian Docker 安装使用官方安装脚本自动安装手动安装使用 Shell 脚…...

性能测试场景分析并设计?超细案例讲解,看这篇就够了

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能测试场景&…...

JAVA9新特性

JAVA9新特性 概述 ​ 经过4次推迟&#xff0c;历经曲折的Java9最终在2017年9月21日发布。因为里面加入的模块化系统&#xff0c;在最初设想的时候并没有想过那么复杂&#xff0c;花费的时间超出预估时间。距离java8大约三年时间。 ​ Java 9提供了超过150项新功能特性&#x…...

( 数组和矩阵) 645. 错误的集合 ——【Leetcode每日一题】

❓645. 错误的集合 难度&#xff1a;简单 集合 s 包含从 1 到 n 的整数。不幸的是&#xff0c;因为数据错误&#xff0c;导致集合里面某一个数字复制了成了集合里面的另外一个数字的值&#xff0c;导致集合 丢失了一个数字 并且 有一个数字重复 。 给定一个数组 nums 代表了…...

2023年全国最新道路运输从业人员精选真题及答案63

百分百题库提供道路运输安全员考试试题、道路运输从业人员考试预测题、道路安全员考试真题、道路运输从业人员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 119.在危险货物道路运输过程中&#xff0c;&#xff08;&#x…...

Kettle安装与使用

一、Kettle简介 Kettle最早是一个开源的ETL&#xff08;Extract-Transform-Load的缩写&#xff09;工具&#xff0c;全称为KDE Extraction, Transportation, Transformation and Loading Environment。后来Kettle重命名为Pentaho Data Integration 。它由Java开发&#xff0c;…...

C51 - DS18B20

Thermometer 1> 实验概述2> 硬件设计3> DS18B203.1> 原理框图3.2> 数据格式 4> 单总线&#xff08;1-Wire&#xff09;通讯协议4.1> 初始化&#xff08;复位&#xff09;时序4.2> 写-DS18B20时序4.3> 读-DS18B20时序4.4> 命令 5> 程序设计5.1…...

手把手教你使用vue2搭建微前端micro-app

​ 简述 本文主要讲述新手小白怎么搭建micro-app&#xff0c;几乎是每一步都有截图说明。上手应该很简单。 研究背景 这段时间在网上找了很多有关微前端相关的知识&#xff0c;起初本来是想着先搭建一个single-spa&#xff0c;但是奈何网上能找到的内容都是千篇一律。我也是…...

DDR3(MIG核配置官方demoFPGA代码实现及仿真)

由于直接对 DDR3 进行控制很复杂&#xff0c;因此一般使用 MIG IP 来实现&#xff0c;同时为了更简单地使用 MIG IP&#xff0c;我们采用 AXI4 总线协议进行控制。下面首先介绍 MIG IP 的配置&#xff0c;然后看看官方 demo &#xff08;里面包含一个仿真要用到的 DDR3 模型&am…...

传奇人物《周兴和》书连载之67 不辱神圣的使命

不辱神圣的使命 这里&#xff0c;先前还是一个十分神秘的地方。 外人和车辆要想进入这片区域&#xff0c;那是绝对不允许的。这片区域隐于群山之中&#xff0c;且戒备森严&#xff0c;外人若想进入&#xff0c;那是要经过好几道政治审查和随身检查的。近年来&#xff0c;随着…...

Spring框架中的单例Beans是线程安全的么?

在Spring框架中&#xff0c;单例Beans默认是线程安全的。 当你在Spring框架中声明一个单例Bean并配置为默认的单例作用域时&#xff0c;Spring会确保对该Bean的并发访问是线程安全的。以下是一个简单的代码演示&#xff1a; 假设我们有一个名为 SingletonBean 的单例 Bean 类…...

AI脚本插件开发-链接图自动建立档名-插件制作源码-illustrator插件开发

文章目录 1.illustrator1.1.app.activeDocument1.2.selection2.模块分析3.源码工程4.功能描述5.作者答疑本文主要分析一款插件的源码,链接图自动建立档名,代码一般较长,读者耐心阅读,对于学习插件开发具有不小的帮助。先介绍了一下基础资料,如有不懂的地方,就去这些资料里…...

rust智能指针

智能指针 智能指针虽然也号称指针&#xff0c;但是它是一个复杂的家伙&#xff1a;通过比引用更复杂的数据结构&#xff0c;包含比引用更多的信息&#xff0c;例如元数据&#xff0c;当前长度&#xff0c;最大可用长度等。引用和智能指针的另一个不同在于前者仅仅是借用了数据…...

Git、Gitee、Github、Gitlab区别与联系

Git&#xff1a;本地软件&#xff0c;无需联网即可使用&#xff0c;实现本地代码的管理。 分布式版本控制系统&#xff0c;是一种工具&#xff0c;用于代码的存储和版本控制。 将本地文件通过一定的操作将其同步上传到Github或Gitee Gitee&#xff1a;是一家中…...

接口优化的策略

1.批处理 批量思想&#xff1a;批量操作数据库&#xff0c;这个很好理解&#xff0c;我们在循环插入场景的接口中&#xff0c;可以在批处理执行完成后一次性插入或更新数据库&#xff0c;避免多次IO。 //批量入库 batchInsert();List的安全操作有以下几种方式&#xff1a; 使…...

android 隐藏底部虚拟按键

方法一 滑动屏幕 可重新显示出来 protected void hideBottomUIMenu() { //隐藏虚拟按键&#xff0c;并且全屏 if (Build.VERSION.SDK_INT <11 && Build.VERSION.SDK_INT < 19) { // lower api View v this.getWindow().getDecorView(); v.setSyst…...

基于电流控制的并网逆变器(Simulink)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

learn_C_deep_9 (汇编角度理解return的含义、const 的各种应用场景)

return 关键字 不知道我们大家是否有一个疑惑&#xff1a;我们下载一个大型游戏软件&#xff08;王者荣耀&#xff09;&#xff0c;都要花几个小时去下载&#xff0c;但是一旦我们游戏连输&#xff0c;想要删除这个软件的时候&#xff0c;它仅仅只需要十几秒&#xff0c;这是为…...

基于深度学习的OCR技术

随着数字化时代的到来&#xff0c;图片识别技术越来越受到人们的关注。其中&#xff0c;OCR技术作为图片处理的一个重要分支&#xff0c;可以将扫描的图片进行自动识别和分类&#xff0c;极大地提高了工作效率。本文将介绍有道实况OCR技术的相关内容&#xff0c;帮助读者更好地…...

『python爬虫』09. bs4实战之下载精美壁纸(保姆级图文)

目录 爬取思路代码思路1.拿到主页面的源代码. 然后提取到子页面的链接地址, href2.通过href拿到子页面的内容. 从子页面中找到图片的下载地址 img -> src3.下载图片 3. 完整实现代码总结 欢迎关注 『python爬虫』 专栏&#xff0c;持续更新中 欢迎关注 『python爬虫』 专栏&…...

【Linux学习】多线程——线程控制 | 线程TCB

&#x1f431;作者&#xff1a;一只大喵咪1201 &#x1f431;专栏&#xff1a;《Linux学习》 &#x1f525;格言&#xff1a;你只管努力&#xff0c;剩下的交给时间&#xff01; 线程控制 | 线程TCB &#x1f9f0;线程控制&#x1f3b4;线程创建&#x1f3b4;线程结束&#x1…...

Node 10 接口

接口 简介 接口是什么 接口是 前后端通信的桥梁 简单理解&#xff1a;一个接口就是 服务中的一个路由规则 &#xff0c;根据请求响应结果 接口的英文单词是 API (Application Program Interface)&#xff0c;所以有时也称之为 API 接口 这里的接口指的是『数据接口』&#…...

大型互联网企业大流量高并发电商领域核心项目已上线(完整流程+项目白皮书)

说在前面的话 面对近年来网络的飞速发展&#xff0c;大家已经都习惯了网络购物&#xff0c;从而出现了一些衍生品例如&#xff1a;某宝/某东/拼夕夕等大型网站以及购物APP~ 并且从而导致很多大型互联网企业以及中小厂都需要有完整的项目经验&#xff0c;以及优秀处理超大流量…...

建设银行建湖支行官方网站/seo搜索是什么

下午密码键盘返回了一个校验码&#xff0c;是BYTE类型数组&#xff0c;给上层应用返回最好是字符串方式&#xff0c;怎样原样的将BYTE数组转为string串呢&#xff1f;不多说&#xff0c;开动脑筋上手干&#xff01;&#xff01;&#xff01; BYTE格式的数组bt{08,D7,B4,FB}&…...

专门做实习计算机项目的网站平台/西安网站建设比较好的公司

问题&#xff1a;如何在一张图上面绘制多组散点图和折线图&#xff1f;例如在下面的评分数据中&#xff0c;每轮数据绘制散点图&#xff0c;平均值绘制折线图。 数据&#xff08;excel&#xff09;&#xff1a;营业厅评分数据 做法&#xff1a;R语言ggplot函数 1 library(readx…...

辽宁网站建设/详情页设计

主要软件包&#xff0c; 1. httpd-2.2.6.tar.gz 2. mysql-5.0.45-linux-i686-glibc23.tar.gz ( 这个版本是已编译好的压缩包&#xff0c;解压后稍做配置即可使用 ) 3. php-5.2.5.tar.gz 安装 php 所需的软件包&#xff08;其中 libxml2 是安装 php5 必须的 . &#xff09; 1. l…...

上海网站设计公司有哪些/免费网站推广网站短视频

1、Python常见异常类型&#xff1a; Exception 常规错误的基类 AttributeError 对象没有这个属性 IOError 输入/输出操作失败 IndexError 序列中没有此索引(index) KeyError 映射中没有这个键 NameError 未声明/初始化对象 (没有属性) Syntax…...

手机社区网站模板/手机推广app

Linux程序包管理器(rpm,yum,源码安装)一&#xff0e;RPM程序包管理器debian的软件包管理器为deb&#xff0c;软件包后缀为.dpt。而红帽的程序包管理器为rpm&#xff0c;包后缀为.rpmRPM是redhat发行版专门用来管理Linux软件包的&#xff0c;最早由Red Hat开发出来&#xff0c;后…...

目前做美术的网站以及app/网络营销这个专业怎么样

&#xff08;下面内容均是来源于网友经验&#xff0c;我只是大自然的搬运工&#xff0c;多谢广大网友&#xff09; 原因应该是 &#xff1a;上次关机时未正常关机&#xff1b; 表现症状&#xff1a;QQ初始化失败&#xff0c;很多网页打不开&#xff0c;一些应用打不开&#xff…...