实验目的
- 了解内核线程创建/执行的管理过程
- 了解内核线程的切换和基本调度过程
实验内容
实验2/3完成了物理和虚拟内存管理,这给创建内核线程(内核线程是一种特殊的进程)打下了提供内存管理的基础。当一个程序加载到内存中运行时,首先通过ucore OS的内存管理子系统分配合适的空间,然后就需要考虑如何分时使用CPU来“并发”执行多个程序,让每个运行的程序(这里用线程或进程表示)“感到”它们各自拥有“自己”的CPU。
本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:
- 内核线程只运行在内核态
- 用户进程会在在用户态和内核态交替运行
- 所有内核线程共用ucore内核内存空间,不需为每个内核线程维护单独的内存空间
- 而用户进程需要维护各自的用户内存空间
练习
练习0:填写已有实验
本实验依赖实验1/2/3。请把你做的实验1/2/3的代码填入本实验中代码中有“LAB1”,“LAB2”,“LAB3”的注释相应部分。
打开meld软件,将lab3文件夹和lab4文件夹进行比较
如图,将lab3中带有星号的文件右键copy to right即可
经过比较,将以下五个文件复制到lab4即可:
- default_pmm.c
- pmm.c
- swap_fifo.c
- vmm.c
- trap.c
练习1:分配并初始化一个进程控制块(需要编码)
alloc_proc函数(位于kern/process/proc.c中)负责分配并返回一个新的struct proc_struct结构,用于存储新建立的内核线程的管理信息。ucore需要对这个结构进行最基本的初始化,你需要完成这个初始化过程。
【提示】在alloc_proc函数的实现中,需要初始化的proc_struct结构中的成员变量至少包括:state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
- 请说明proc_struct中struct context context和struct trapframe *tf成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)
知识准备
首先题目要求完成分配并初始化一个进程控制块,那么我们先了解一下什么是进程,什么是进程控制块。
进程
- 进程是指一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程,其中包括正在运行的一个程序的所有状态信息。
- 进程是程序的执行,有核心态/用户态,是一个状态变化的过程
- 进程的组成包括程序、数据块和进程控制块PCB。
进程控制块
- 进程控制块是操作系统管理控制进程运行所用的信息集合。操作系统用PCB来描述进程的基本情况以及运行变化的过程。
- PCB是进程存在的唯一标志 ,每个进程都在操作系统中有一个对应的PCB。
- 进程控制块可以通过某个数据结构组织起来(例如链表)。同一状态进程的PCB连接成一个链表,多个状态对应多个不同的链表。各状态的进程形成不同的链表:就绪联链表,阻塞链表等等。
下面分析题目
我们需要实现的alloc_proc()函数它主要定义了一个结构体proc_struct,我们需要初始化这样一个结构体的一个对象并返回它。这个函数的返回语句是“return proc”,其中proc就是这个proc_struct结构体的一个对象。
这里我们需要初始化的一个东西就是proc_struct的一个对象,分配的是一个内核线程的PCB,它通常只是内核中的一小段代码或者函数,没有用户空间。而由于在操作系统启动后,已经对整个核心内存空间进行了管理,所以内核中的所有线程都不需要再建立各自的页表,只需共享这个核心虚拟空间就可以访问整个物理内存了。
proc_struct结构体如下(kern/process/proc.h):
struct proc_struct {//进程控制块
enum proc_state state; // 进程状态
int pid; // 进程ID
int runs; // 运行时间
uintptr_t kstack; // 内核栈位置
volatile bool need_resched; // 是否需要调度
struct proc_struct *parent; // 父进程
struct mm_struct *mm; // 进程的虚拟内存
struct context context; // 进程上下文
struct trapframe *tf; // 当前中断帧的指针
uintptr_t cr3; // 当前页表地址
uint32_t flags; // 进程
char name[PROC_NAME_LEN + 1]; // 进程名字
list_entry_t list_link; // 进程链表
list_entry_t hash_link;
};
结合实验指导书来分析PCB参数的含义:
1)state:进程所处的状态,这个在proc.h的第11行——15行有定义,具体如下:
PROC_UNINIT //未初始状态
PROC_SLEEPING //睡眠(阻塞)状态
PROC_RUNNABLE //运行与就绪态
PROC_ZOMBIE //僵尸状态
2)pid:进程id号
3)runs:进程运行时间,初始化应该为0
4)kstack:记录了分配给该进程/线程的内核桟的位置。这里记录的是分配给该进程在内存中的栈位置,相关操作应该和lab2和lab3有关。
5)need_resched:是否需要调度,目前实验未到这一步,暂时不管。
6)parent:用户进程的父进程,这是一个指针变量,记录它的父进程是谁。在所有进程中,只有一个进程没有父进程,就是内核创建的第一个内核线程idleproc。
7)mm:负责管理进程的虚拟内存,其实就是内存管理的信息,包括内存映射列表、页表指针等。mm成员变量在lab3中用于虚存管理。但在实际OS中,内核线程常驻内存,不需要考虑替换页问题,mm应该和lab2和lab3有关。
8)context:进程的上下文,用于进程切换。使用Switch.S汇编文件中的定义。
9)tf:中断帧的指针,指导书上说,它是中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore内核允许嵌套中断。
10)cr3:记录了当前使用的页表的地址。
11)name[PROC_NAME_LEN + 1]:这是内核线程(进程)的名称。
完成alloc_proc函数
根据提示我们需要初始化一些变量,具体的代码如下:
static struct proc_struct *alloc_proc(void) {
struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
if (proc != NULL) {
proc->state = PROC_UNINIT; //设置进程为未初始化状态
proc->pid = -1; //未初始化的的进程id为-1
proc->runs = 0; //初始化运行时间为0
proc->kstack = 0; //内存栈的地址,默认从0开始
proc->need_resched = 0; //是否需要调度设为不需要
proc->parent = NULL; //父节点设为空,即当前没有父进程
proc->mm = NULL; //当前未分配内存,虚拟内存设为空
memset(&(proc->context), 0, sizeof(struct context));//用memset非常方便将context(上下文)变量中的所有成员变量置为0
proc->tf = NULL; //当前没有中断帧指针,中断帧指针置为空
proc->cr3 = boot_cr3; //页目录设为内核页目录表的基址
proc->flags = 0; //标志位
memset(proc->name, 0, PROC_NAME_LEN);//进程名为0
}
return proc;
}
整个分配初始化函数的运行过程为:
- 在堆上分配一块内存空间用来存放进程控制块
- 初始化进程控制块内的各个参数
- 返回分配的进程控制块
回答问题:
请说明proc_struct中struct context context和struct trapframe *tf成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)
struct context context:
struct context { // 保存的上下文寄存器,注意没有eax寄存器和段寄存器
uint32_t eip;
uint32_t esp;
uint32_t ebx;
uint32_t ecx;
uint32_t edx;
uint32_t esi;
uint32_t edi;
uint32_t ebp;
};
可以看到,proc_struct中的context用于进程的上下文,用于进程切换。在 uCore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈以及上下文等)。使用 context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。具体切换过程定义在switch.S中。
反汇编switch_to函数:
.text
.globl switch_to
switch_to: # switch_to(from, to)
# save from's registers
movl 4(%esp), %eax # 获取当前进程的context结构地址
popl 0(%eax) # 将eip保存至当前进程的context结构
movl %esp, 4(%eax) # 将esp保存至当前进程的context结构
movl %ebx, 8(%eax) # 将ebx保存至当前进程的context结构
movl %ecx, 12(%eax) # 将ecx保存至当前进程的context结构
movl %edx, 16(%eax) # 将edx保存至当前进程的context结构
movl %esi, 20(%eax) # 将esi保存至当前进程的context结构
movl %edi, 24(%eax) # 将edi保存至当前进程的context结构
movl %ebp, 28(%eax) # 将ebp保存至当前进程的context结构
# restore to's registers
movl 4(%esp), %eax # 获取下一个进程的context结构地址
# 需要注意的是,其地址不是8(%esp),因为之前已经pop过一次栈。
movl 28(%eax), %ebp # 恢复ebp至下一个进程的context结构
movl 24(%eax), %edi # 恢复edi至下一个进程的context结构
movl 20(%eax), %esi # 恢复esi至下一个进程的context结构
movl 16(%eax), %edx # 恢复edx至下一个进程的context结构
movl 12(%eax), %ecx # 恢复ecx至下一个进程的context结构
movl 8(%eax), %ebx # 恢复ebx至下一个进程的context结构
movl 4(%eax), %esp # 恢复esp至下一个进程的context结构
pushl 0(%eax) # 插入下一个进程的eip,以便于ret到下个进程的代码位置。
ret
struct trapframe *tf
当前中断帧的指针。当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。tf变量的作用在于在构造出了新的线程的时候,如果要将控制权交给这个线程,是使用中断返回的方式进行的,因此需要构造出一个伪造的中断返回现场,使得可以正确地将控制权转交给新的线程。
练习2:为新创建的内核线程分配资源(需要编码)
创建一个内核线程需要分配和设置好很多资源。kernel_thread函数通过调用do_fork函数完成具体内核线程的创建工作。do_kernel函数会调用alloc_proc函数来分配并初始化一个进程控制块,但alloc_proc只是找到了一小块内存用以记录进程的必要信息,并没有实际分配这些资源。ucore一般通过do_fork实际创建新的内核线程。do_fork的作用是,创建当前内核线程的一个副本,它们的执行上下文、代码、数据都一样,但是存储位置不同。在这个过程中,需要给新内核线程分配资源,并且复制原进程的状态。你需要完成在kern/process/proc.c中的do_fork函数中的处理过程。它的大致执行步骤包括:
- 调用alloc_proc,首先获得一块用户信息块。
- 为进程分配一个内核栈。
- 复制原进程的内存管理信息到新进程(但内核线程不必做此事)
- 复制原进程上下文到新进程
- 将新进程添加到进程列表
- 唤醒新进程
- 返回新进程号
请在实验报告中简要说明你的设计实现过程。请回答如下问题:
- 请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。
准备知识:
根据注释我们了解几个会用到的函数及其含义:
//创建一个proc并初始化所有成员变量
void alloc_proc(void)
//为一个内核线程分配物理页
static int setup_kstack(struct proc_struct *proc)
//暂时未看出其用处,可能是之后的lab会用到
static int copy_mm(uint32_t clone_flags, struct proc_struct *proc)
//复制原进程上下文到新进程
static void copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf)
//返回一个pid
static int get_pid(void)
//将proc加入到hash_list
static void hash_proc(struct proc_struct *proc)
// 唤醒该线程,即将该线程的状态设置为可以运行
void wakeup_proc(struct proc_struct *proc);
接下来我们按步骤完成do_fork函数
1.调用alloc_proc()函数申请内存块,如果失败,直接返回处理
if ((proc = alloc_proc()) == NULL)
goto fork_out;
2.将子进程的父节点设置为当前进程,代表当前进程的变量current已经在全局定义(第76行)
proc->parent = current;
3.调用setup_stack()函数为进程分配一个内核栈
if (setup_kstack(proc) != 0)
goto bad_fork_cleanup_proc;
4.调用copy_mm()函数复制父进程的内存信息到子进程
首先来看copy函数如下:
copy_mm(uint32_t clone_flags, struct proc_struct *proc) { //253行
assert(current->mm == NULL);
/* do nothing in this project */
return 0;
}
进程proc复制是否共享当前进程current,是根据clone_flags来决定的,如果是clone_flags & CLONE_VM(为真),那么就可以拷贝。这个函数里面似乎没有做任何事情,仅仅是确定了一下current当前进程的虚拟内存是否为空,那么具体的操作,只需要传入它所需要的clone_flag就可以,其余事情不需要我们去做,代码实现如下:
if (copy_mm(clone_flags, proc) != 0)
goto bad_fork_cleanup_kstack;
5.调用copy_thread()函数复制父进程的中断帧和上下文信息
观察相应的函数:
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf)
需要传入的三个参数,第一个是比较熟悉,练习一中已经实现的PCB模块proc结构体的对象,第二个参数,是一个栈,判断的依据是它的数据类型,在练习一中的PCB模块中,为栈定义的数据类型就是uintptr_t,第三个参数也很熟悉,它是练习一PCB中的中断帧的指针。
因此代码为:
copy_thread(proc, stack, tf);
6.将新进程添加到进程的(hash)列表中
hash_proc: add proc into proc hash_list,意思是调用这个函数可以将当前的新进程添加到进程的哈希列表中,分析hash函数的特点,直接调用hash(proc)即可:
hash_proc(struct proc_struct *proc) {
list_add(hash_list + pid_hashfn(proc->pid), &(proc->hash_link));
}
函数的实现如下:
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc); //建立映射
nr_process ++; //进程数加1
list_add(&proc_list, &(proc->list_link));//将进程加入到进程的链表中
}
local_intr_restore(intr_flag);
7.唤醒子进程,直接调用wakeup_proc函数:
wakeup_proc(proc);
8.返回子进程的pid
ret = proc->pid;
完成do_fork函数:
由上述的分析可以得do_fork函数的完整代码为:
int
do_fork(uint32_t clone_flags, uintptr_t stack, struct trapframe *tf) {
int ret = -E_NO_FREE_PROC;
struct proc_struct *proc;
if (nr_process >= MAX_PROCESS) {
goto fork_out;
}
ret = -E_NO_MEM;
//1:调用alloc_proc()函数申请内存块,如果失败,直接返回处理
if ((proc = alloc_proc()) == NULL) {
goto fork_out;
}
//2.将子进程的父节点设置为当前进程
proc->parent = current;
//3.调用setup_stack()函数为进程分配一个内核栈
if (setup_kstack(proc) != 0) {
goto bad_fork_cleanup_proc;
}
//4.调用copy_mm()函数复制父进程的内存信息到子进程
if (copy_mm(clone_flags, proc) != 0) {
goto bad_fork_cleanup_kstack;
}
//5.调用copy_thread()函数复制父进程的中断帧和上下文信息
copy_thread(proc, stack, tf);
//6.将新进程添加到进程的hash列表中
bool intr_flag;
local_intr_save(intr_flag);
{
proc->pid = get_pid();
hash_proc(proc); //建立映射
nr_process ++; //进程数加1
list_add(&proc_list, &(proc->list_link));//将进程加入到进程的链表中
}
local_intr_restore(intr_flag);
// 7.一切就绪,唤醒子进程
wakeup_proc(proc);
// 8.返回子进程的pid
ret = proc->pid;
fork_out:
return ret;
bad_fork_cleanup_kstack:
put_kstack(proc);
bad_fork_cleanup_proc:
kfree(proc);
goto fork_out;
}
回答问题:
- 请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。
uCore中,每个新fork的线程都存在唯一的一个ID,理由如下:
- 在函数get_pid中,如果静态成员last_pid小于next_safe,则当前分配的last_pid一定是安全的,即唯一的PID。
- 但如果last_pid大于等于next_safe,或者last_pid的值超过MAX_PID,则当前的last_pid就不一定是唯一的PID,此时就需要遍历proc_list,重新对last_pid和next_safe进行设置,为下一次的get_pid调用打下基础。
- 之所以在该函数中维护一个合法的PID的区间,是为了优化时间效率。如果简单的暴力搜索,则需要搜索大部分PID和所有的线程,这会使该算法的时间消耗很大,因此使用PID区间来优化算法。
get_pid代码如下:
// get_pid - alloc a unique pid for process
static int
get_pid(void) {
//实际上,之前定义了MAX_PID=2*MAX_PROCESS,意味着ID的总数目是大于PROCESS的总数目的
//因此不会出现部分PROCESS无ID可分的情况
static_assert(MAX_PID > MAX_PROCESS);
struct proc_struct *proc;
list_entry_t *list = &proc_list, *le;
//next_safe和last_pid两个变量,这里需要注意! 它们是static全局变量!!!
static int next_safe = MAX_PID, last_pid = MAX_PID;
//++last_pid>-MAX_PID,说明pid以及分到尽头,需要从头再来
if (++ last_pid >= MAX_PID)
{
last_pid = 1;
goto inside;
}
if (last_pid >= next_safe)
{
inside:
next_safe = MAX_PID;
repeat:
//le等于线程的链表头
le = list;
//遍历一遍链表
//循环扫描每一个当前进程:当一个现有的进程号和last_pid相等时,则将last_pid+1;
//当现有的进程号大于last_pid时,这意味着在已经扫描的进程中
//[last_pid,min(next_safe, proc->pid)] 这段进程号尚未被占用,继续扫描。
while ((le = list_next(le)) != list)
{
proc = le2proc(le, list_link);
//如果proc的pid与last_pid相等,则将last_pid加1
//当然,如果last_pid>=MAX_PID,then 将其变为1
//确保了没有一个进程的pid与last_pid重合
if (proc->pid == last_pid)
{
if (++ last_pid >= next_safe)
{
if (last_pid >= MAX_PID)
{
last_pid = 1;
}
next_safe = MAX_PID;
goto repeat;
}
}
//last_pid<pid<next_safe,确保最后能够找到这么一个满足条件的区间,获得合法的pid;
else if (proc->pid > last_pid && next_safe > proc->pid)
{
next_safe = proc->pid;
}
}
}
return last_pid;
}
练习3:阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。(无编码工作)
请在实验报告中简要说明你对proc_run函数的分析。并回答如下问题:
- 在本实验的执行过程中,创建且运行了几个内核线程?
- 语句
local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在这里有何作用?请说明理由
完成代码编写后,编译并运行代码:make qemu
最开始,kern_init函数调用了proc_init函数,proc_init启动创建内核线程的步骤,首先当前的执行上下文(从kern_init 启动至今)就可以看成是uCore内核(也可看做是内核进程)中的一个内核线程的上下文。为此,uCore通过给当前执行的上下文分配一个进程控制块以及对它进行相应初始化,将其打造成第0个内核线程 – idleproc。
uCore中,内核的第一个进程idleproc会执行cpu_idle函数,并从中调用schedule函数,准备开始调度进程。
void cpu_idle(void) {
while (1)
if (current->need_resched)
schedule();
}
schedule函数会先清除调度标志,并从当前进程在链表中的位置开始,遍历进程控制块,直到找出处于就绪状态的进程。
之后执行proc_run函数,将环境切换至该进程的上下文并继续执行。
void
schedule(void) {
bool intr_flag; //定义中断变量
list_entry_t *le, *last; //当前list,下一list
struct proc_struct *next = NULL; //下一进程
//关闭中断
local_intr_save(intr_flag);
{
current->need_resched = 0;
//last是否是idle进程(第一个创建的进程),如果是,则从表头开始搜索 否则获取下一链表
last = (current == idleproc) ? &proc_list : &(current->list_link);
le = last;
//循环找到可调度的进程
do
{
if ((le = list_next(le)) != &proc_list)
{
//获取下一进程
next = le2proc(le, list_link);
//找到一个可以调度的进程,break
if (next->state == PROC_RUNNABLE)
break;
}
} while (le != last);
//如果没有找到可调度的进程
if (next == NULL || next->state != PROC_RUNNABLE)
{
next = idleproc;
}
next->runs ++; //运行次数加一
//运行新进程,调用proc_run函数
if (next != current)
{
proc_run(next);
}
}
//恢复中断
local_intr_restore(intr_flag);
}
schedule函数的执行逻辑:
- 设置当前内核线程current->need_resched为0
- 在proc_list队列中查找下一个处于“就绪”态的线程或进程next
- 找到这样的进程后,就调用proc_run函数,保存当前进程current的执行现场(进程上下文),恢复新进程的执行现场,完成进程切换
即schedule 函数通过查找 proc_list 进程队列,在这里只能找到一个处于就绪态的 initproc 内核线程。于是通过 proc_run和进一步的 switch_to 函数完成两个执行现场的切换。
理解proc_run函数
// proc_run - make process "proc" running on cpu
// NOTE: before call switch_to, should load base addr of "proc"'s new PDT
void proc_run(struct proc_struct *proc)
{ //判断一下要调度的进程是不是当前进程
if (proc != current)
{
bool intr_flag;
struct proc_struct *prev = current, *next = proc;
// 关闭中断
local_intr_save(intr_flag);
{
//当前进程设为待调度的进程
current = proc;
//加载待调度进程的内核栈基地址
load_esp0(next->kstack + KSTACKSIZE);
//将当前的cr3寄存器改为需要运行进程的页目录表
lcr3(next->cr3);
//进行上下文切换,保存原线程的寄存器并恢复待调度线程的寄存器
switch_to(&(prev->context), &(next->context));
}
//恢复中断
local_intr_restore(intr_flag);
}
}
具体执行过程如下:
- 让 current 指向 next 内核线程 initproc;
- 设置任务状态段 ts 中特权态 0 下的栈顶指针 esp0 为 next 内核线程 initproc 的内核栈的栈顶,即 next->kstack + KSTACKSIZE ;
- 设置 CR3 寄存器的值为 next 内核线程 initproc 的页目录表起始地址 next->cr3,这实际上是完成进程间的页表切换;
- 由 switch_to函数完成具体的两个线程的执行现场切换,即切换各个寄存器,当 switch_to 函数执行完“ret”指令后,就切换到 initproc 执行了。
而switch_to函数在练习2已经详细分析过了主要就是保存之前进程的相关寄存器值,恢复现在进程的相关寄存器的值。
由此可以很容易理解 proc_run 函数。
回答问题
Q1.在本实验的执行过程中,创建且运行了几个内核线程?
两个,分别是idleproc和initproc。
- idleproc:第一个内核进程,完成内核中各个子系统的初始化,之后立即调度,执行其他进程。
- initproc:用于完成实验的功能而调度的内核进程。
Q2.语句local_intr_save(intr_flag);....local_intr_restore(intr_flag);
在这里有何作用?请说明理由
这两句代码的作用分别是阻塞中断和解除中断的阻塞。
这两句的配合,使得这两句代码之间的代码块形成原子操作,可以使得某些关键的代码不会被打断,从而避免引起一些未预料到的错误,避免条件竞争。
以进程切换为例,在proc_run中,当刚设置好current指针为下一个进程,但还未完全将控制权转移时,如果该过程突然被一个中断所打断,则中断处理例程的执行可能会引发异常,因为current指针指向的进程与实际使用的进程资源不一致。
完成练习1,2,3后,运行make qemu
查看运行结果
运行make grade
查看成绩:
此处建议不要直接复制answer文件里的答案直接进行查看成绩。因为评分会多一个check slab的分数,满分是100分。而slab算法需在challenge实现,所以还是一个文件一个文件复制比较好。
扩展练习Challenge:实现支持任意大小的内存分配算法
这不是本实验的内容,其实是上一次实验内存的扩展,但考虑到现在的slab算法比较复杂,有必要实现一个比较简单的任意大小内存分配算法。可参考本实验中的slab如何调用基于页的内存分配算法(注意,不是要你关注slab的具体实现)来实现first-fit/best-fit/worst-fit/buddy等支持任意大小的内存分配算法。。
此处要求我们借鉴slab算法实现其他内存分配算法,这里我们实现buddy算法:
buddy.h的实现:
#ifndef __KERN_MM_BUDDY_PMM_H__
#define __KERN_MM_BUDDY_PMM_H__
#include <pmm.h>
extern const struct pmm_manager buddy_pmm_manager;
#endif /* ! __KERN_MM_DEFAULT_PMM_H__ */
将pmm.c中的 init_pmm_manager 函数中的pmm_manager 值修改为 buddy_pmm_manager ,即:
static void
init_pmm_manager(void) {
pmm_manager = &buddy_pmm_manager;
cprintf("memory management: %s\n", pmm_manager->name);
pmm_manager->init();
}
同时完成buddy.c代码:
#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)))//x是不是2的幂
#define MAX(a, b) ((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) {//找到大于等于所需内存的2的倍数
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,
};
运行make qemu
获得运行结果:
可以看到,成功实现buddy算法
实验总结
本次实验主要针对内核线程的管理,所有内核线程直接使用共同的ucore内核内存空间,而用户进程需要维护各自的用户内存空间。以及了解到了进程切换的相关细节操作,更加深一步的了解了操作系统。