【已弃用】HNU-操作系统OS-实验Lab4

OS_Lab4_Experimental report

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

计科 210X wolf (学号 202108010XXX)

在这里插入图片描述

实验目的

  • 了解内核线程创建/执行的管理过程
  • 了解内核线程的切换和基本调度过程

实验内容

lab2/3完成了物理和虚拟内存管理,这给创建内核线程(内核线程是一种特殊的进程)打下了提供内存管理的基础。当一个程序加载到内存中运行时,首先通过ucore OS的内存管理子系统分配合适的空间,然后就需要考虑如何分时使用CPU来“并发”执行多个程序,让每个运行的程序(这里用线程或进程表示)“感到”它们各自拥有“自己”的CPU。

本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:

  • 内核线程只运行在内核态
  • 用户进程会在在用户态和内核态交替运行
  • 所有内核线程共用ucore内核内存空间,不需为每个内核线程维护单独的内存空间
  • 而用户进程需要维护各自的用户内存空间

相关原理介绍可看附录B:【原理】进程/线程的属性与特征解析。

练习0:填写已有实验

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

使用meld将实验1/2/3的代码中相应的部分填入实验四中的代码中:

使用meld工具可以比较方便地查看Lab4与Lab3的差异,由于Lab1、Lab2已经是被Lab3兼容了,所以不需要再做考虑。

其中,需要修改的部分为:

  • 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 contextstruct trapframe *tf成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)

1.内核线程及管理

内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:

  • 内核线程只运行在内核态,用户进程会在在用户态和内核态交替运行;
  • 所有内核线程直接使用共同的ucore内核内存空间,不需为每个内核线程维护单独的内存空间,而用户进程需要拥有各自的内存空间。

把内核线程看作轻量级的进程,对内核线程的管理和对进程的管理是一样的。对进程的管理是通过进程控制块结构实现的,将所有的进程控制块通过链表链接在一起,形成进程控制块链表,对进程的管理和调度就通过从链表中查找对应的进程控制块来完成。

2.进程控制块

保存进程信息的进程控制块结构的定义在kern/process/proc.h中定义如下:

struct proc_struct {
    enum proc_state state; 		// Process state
    int pid; 					// Process ID
    int runs; 					// the running times of Proces
    uintptr_t kstack; 			// Process kernel stack
    volatile bool need_resched; // need to be rescheduled to release CPU?
    struct proc_struct *parent; // the parent process
    struct mm_struct *mm; 		// Process's memory management field
    struct context context; 	// Switch here to run process
    struct trapframe *tf; 		// Trap frame for current interrupt
    uintptr_t cr3; 				// the base addr of Page Directroy Table(PDT)
    uint32_t flags; 			// Process flag
    char name[PROC_NAME_LEN + 1]; // Process name
    list_entry_t list_link; 	// Process link list
    list_entry_t hash_link; 	// Process hash list
};

  • mm:在Lab3中,该结构用于内存管理。在对内核线程管理时,由于内核线程不需要考虑换入换出,该结构不需要使用,因此设置为NULL。唯一需要使用的是mm中的页目录地址,保存在cr3变量中。

  • state:进程状态,有以下几种

    • PROC_UNINIT:未初始化
    • PROC_SLEEPING:睡眠状态
    • PROC_RUNNABLE:可运行(可能正在运行)
    • PROC_ZOMBIE:等待回收
  • parent:父进程

  • context:进程上下文,用于进程切换

  • tf:中断帧指针,用于中断后恢复进程状态

  • cr3:页目录的物理地址,用于进程切换时快速找到页表位置

  • kstack:线程所使用的内核栈

  • list_link:所有进程控制块链接形成的链表的节点

  • hash_link:所有进程控制块有一个根据pid建立的哈希表,hash_link是该链表的节点

为了管理系统中的所有进程控制块,ucore还维护了以下全局变量:

  • static struct proc *current:当前占用CPU且处于“运行”状态进程控制块指针。通常这个变量是只读的,只有在进程切换的时候才进行修改,并且整个切换和修改过程需要保证操作的原子性,需要屏蔽中断。
  • static struct proc *initproc:本实验中,指向一个内核线程。本实验以后,此指针将指向第一个用户态进程。
  • static list_entry_t hash_list[HASH_LIST_SIZE]:所有进程控制块的哈希表,proc_struct中的成员变量hash_link将基于pid链接入这个哈希表中。
  • list_entry_t proc_list:所有进程控制块的双向线性列表,proc_struct中的成员变量list_link将链接入这个链表中。

★3.分配并初始化一个进程控制块

内核线程创建之前,需要先创建一个进程控制块管理保存进程信息。alloc_proc函数负责分配创建一个proc_struct结构,并进行基本的初始化。此时仅是创建了进程块,内核线程本身还没有创建。这是练习一需要完成的部分,具体的实现如下:

//进程状态信息
enum proc_state {
    // 未初始化
    PROC_UNINIT = 0,  // uninitialized
    // 休眠、阻塞状态
    PROC_SLEEPING,    // sleeping
    // 可运行、就绪状态
    PROC_RUNNABLE,    // runnable(maybe running)
    // 僵尸状态(几乎已经终止,等待父进程回收其所占资源)
    PROC_ZOMBIE,      // almost dead, and wait parent proc to reclaim his resource
};

// alloc_proc -负责创建并初始化一个新的proc_struct结构存储内核线程信息
static struct proc_struct *
alloc_proc(void) 
{
    //为创建的线程申请空间
    struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
    if (proc != NULL) 
    {
    //LAB4:EXERCISE1 YOUR CODE
    //因为没有分配物理页,故将线程状态初始为初始状态
     proc->state=PROC_UNINIT;
     proc->pid=-1; //id初始化为-1
     proc->runs=0; //运行时间为0
     proc->kstack=0; 
     proc->need_resched=0; //不需要释放CPU,因为还没有分配
     proc->parent=NULL;  //当前没有父进程,初始为null
     proc->mm=NULL;     //当前未分配内存,初始为null
     //用memset非常方便将context变量中的所有成员变量置为0
     //避免了一一赋值的麻烦。。
     memset(&(proc -> context), 0, sizeof(struct context)); 
     proc->tf=NULL;   		//当前没有中断帧,初始为null
     proc->cr3=boot_cr3;    //内核线程,cr3 等于boot_cr3
     proc->flags=0;
     memset(proc -> name, 0, PROC_NAME_LEN);
    }
    return proc;
}

▲问题:请说明proc_struct中struct context context和struct trapframe *tf成员变量含义和在本实验中的作用为?

(1)简单来说:
  • context保存了进程的上下文信息,即各个寄存器的值,用于进程切换时恢复上下文。
  • tf是中断帧的指针,指向中断帧。中断帧记录了进程被中断前的信息,除寄存器外还有中断号,错误码等信息,用于中断处理后进程状态的恢复。

发生中断时,首先从TSS中找到进程内核栈的指针切换到内核栈,然后在内核栈顶建立trapframe,进入内核态。当中断服务例程运行结束,从中断返回时,再从trapframe恢复寄存器的值,并切换回用户态。用户程序在用户态通过系统调用进入内核态,以及在内核态新创建的进程,都通过tf指向的中断帧恢复寄存器的值,从而回到用户态继续运行。

(2)详细的解题过程

根据提示我们查看相关的代码(通过查找定义tf以及context的函数):

首先我们找到了kernel_thread函数和copy_thread函数,可知该函数对tf进行了设置,并对context的esp和eip进行了设置(具体设置过程在代码注释中给出):

/*
kernel_thread函数采用了局部变量tf来放置保存内核线程的临时中断帧,并把中断帧的指针传递给do_fork函数,而do_fork函数会调用copy_thread函数来在新创建的进程内核栈上专门给进程的中断帧分配一块空间
*/
int kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
    struct trapframe tf;
    memset(&tf, 0, sizeof(struct trapframe));
    //kernel_cs和kernel_ds表示内核线程的代码段和数据段在内核中
    tf.tf_cs = KERNEL_CS;
    tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;
    //fn指实际的线程入口地址
    tf.tf_regs.reg_ebx = (uint32_t)fn;
    tf.tf_regs.reg_edx = (uint32_t)arg;
    //kernel_thread_entry用于完成一些初始化工作
    tf.tf_eip = (uint32_t)kernel_thread_entry;
    return do_fork(clone_flags | CLONE_VM, 0, &tf);
}
static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) 
{
    //将tf进行初始化
    proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
    *(proc->tf) = *tf;
    proc->tf->tf_regs.reg_eax = 0;
    //设置tf的esp,表示中断栈的信息
    proc->tf->tf_esp = esp;
    proc->tf->tf_eflags |= FL_IF;
    //对context进行设置
    //forkret主要对返回的中断处理,基本可以认为是一个中断处理并恢复
    proc->context.eip = (uintptr_t)forkret;
    proc->context.esp = (uintptr_t)(proc->tf);
}

通过上述函数并结合switch.S中对context的操作,将各种寄存器的值保存到context中。我们可以知道context是与上下文切换相关的,而tf则与中断的处理相关。

(3)具体回答:

context作用:

进程的上下文,用于进程切换。主要保存了前一个进程的现场(各个寄存器的状态)。在uCore中,所有的进程在内核中也是相对独立的。使用context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中定义switch_to。

tf作用:

中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore内核允许嵌套中断。因此为了保证嵌套中断发生时tf 总是能够指向当前的trapframe,uCore 在内核栈上维护了 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()函数的实现大致步骤包括七步,然后根据注释大致实现过程如下:

①调用alloc_proc()函数申请内存块,如果失败,直接返回处理。

alloc_proc()函数在练习一中实现过,如果分配进程PCB失败,也就是说,进程一开始就是NULL,那么就会被if(proc!=NULL)判定为否,那么就不会分配初始化资源,连初始化资源都没有了,那么就会返回NULL。

②调用setup_kstack()函数为进程分配一个内核栈。

从下面此函数代码中可以看到,如果页不为空的时候,会return 0,也就是说分配内核栈成功了(这样推测的根据在于,最后一个return -E_NO_MEM,大概推测就是一个初始化的或者错误的状态,因为在这个函数最开始不需要实现的部分,这个值就赋值给了ret),那么就会返回0,否则返回一个奇怪的东西。因此,我们调用该函数分配一个内核栈空间,并判断是否分配成功。

static int
setup_kstack(struct proc_struct *proc) {
    struct Page *page = alloc_pages(KSTACKPAGE);
    if (page != NULL) {
        proc->kstack = (uintptr_t)page2kva(page);
        return 0;
    }
    return -E_NO_MEM;
}

③调用copy_mm()函数,复制父进程的内存信息到子进程。

对于这个函数可以看到,进程proc复制还是共享当前进程current,是根据clone_flags来决定的,如果是clone_flags & CLONE_VM(为真),那么就可以拷贝。这个函数里面似乎没有做任何事情,仅仅是确定了一下current当前进程的虚拟内存是否为空,那么具体的操作,只需要传入它所需要的clone_flag就可以,其余事情不需要我们去做。

static int
copy_mm(uint32_t clone_flags, struct proc_struct *proc) {
    assert(current->mm == NULL);
    /* do nothing in this project */
    return 0;
}

④调用copy_thread()函数复制父进程的中断帧和上下文信息。

copy_thread()函数需要传入的三个参数,第一个是比较熟悉,练习一中已经实现的PCB模块proc结构体的对象,第二个参数,是一个栈,判断的依据是它的数据类型,在练习一中的PCB模块中,为栈定义的数据类型就是uintptr_t,第三个参数也很熟悉,它是练习一PCB中的中断帧的指针。

static void
copy_thread(struct proc_struct *proc, uintptr_t esp, struct trapframe *tf) {
    proc->tf = (struct trapframe *)(proc->kstack + KSTACKSIZE) - 1;
    *(proc->tf) = *tf;
    proc->tf->tf_regs.reg_eax = 0;
    proc->tf->tf_esp = esp;
    proc->tf->tf_eflags |= FL_IF;

    proc->context.eip = (uintptr_t)forkret;
    proc->context.esp = (uintptr_t)(proc->tf);
}

⑤将新进程添加到进程的(hash)列表中。

调用hash_proc这个函数可以将当前的新进程添加到进程的哈希列表中,分析hash函数的特点,直接调用hash(proc)即可。

hash_proc(struct proc_struct *proc) {
    list_add(hash_list + pid_hashfn(proc->pid), &(proc->hash_link));
}

⑥唤醒新进程。

wakeup_proc(proc);

⑦返回新进程pid。

ret = proc->pid;

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调用打下基础。

ucore通过调用get_pid函数分配pid,我们可以对get_pid函数进行分析。

static int
get_pid(void) {
    static_assert(MAX_PID > MAX_PROCESS);//分配pid前,get_pid会先确认可用进程号大于最大进程数。
    struct proc_struct *proc;
    list_entry_t *list = &proc_list, *le;
    static int next_safe = MAX_PID, last_pid = MAX_PID;
    //该函数定义了两个静态全局变量,next_safe最初被设置为最大进程号,last_pid最初设置为1,
    //[last_pid,next_safe]就是合法的pid区间。
    if (++ last_pid >= MAX_PID) {
        //如果last_pid++在这个区间内,就可以直接返回last_pid作为新分配的进程号。
        last_pid = 1;
        goto inside;
    }
    //区间合法性判断
    if (last_pid >= next_safe) {
        //如果last_pid>=next_safe,就将next_safe设置为MAX_PID,遍历链表确保last_pid和已有进程的pid不相同,并更新next_safe。
    inside:
        next_safe = MAX_PID;
    repeat:
        le = list;
        //遍历进程链表
        while ((le = list_next(le)) != list) {
            proc = le2proc(le, list_link);
            if (proc->pid == last_pid) {
                if (++ last_pid >= next_safe) {
                    if (last_pid >= MAX_PID) {
                        last_pid = 1;
                    }
                    next_safe = MAX_PID;
                    goto repeat;			//区间不合法,重新遍历链表
                }
            }
            else if (proc->pid > last_pid && next_safe > proc->pid) {
                next_safe = proc->pid;		//更新next_safe	
            }
        }
    }
    return last_pid;
}

维护last_pid到next_safe这个区间将可用的pid范围缩小,以提高了分配的效率,如果区间不合法,也会重新更新区间,并排除和已有进程进程号相同的情况,因此最终产生的进程的pid是唯一的。但是需要注意的是进程链表是全局变量,如果有另一个进程get_pid后还没有把进程加入链表,调度到了当前进程,而当前进程又需要遍历链表排除进程号相同的情况,就可能产生错误,因此要在get_pid和将进程加入链表的位置添加互斥。保证互斥的方法为在do_fork中分配进程号和进程加入进程链表的部分关中断,避免进程调度。

练习3:阅读代码,理解 proc_run 函数和它调用的函数如何完成进程切换的。(无编码工作)

请在实验报告中简要说明你对proc_run函数的分析。并回答如下问题:

  • 在本实验的执行过程中,创建且运行了几个内核线程?
  • 语句local_intr_save(intr_flag);....local_intr_restore(intr_flag);在这里有何作用?请说明理由

根据实验指导书,uCore中,内核的第一个进程idleproc会执行cpu_idle函数,并从中调用schedule函数,准备开始调度进程,完成进程调度和进程切换。

void cpu_idle(void) {
    while (1)
        if (current->need_resched)
            schedule();
}

对schedule函数代码进行分析:

/* 宏定义:
   #define le2proc(le, member)         \
    to_struct((le), struct proc_struct, member)*/
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);//获取下一进程
                if (next->state == PROC_RUNNABLE) {
                    break; //找到一个可以调度的进程,break
                }
            }
        } while (le != last); //循环查找整个链表
        if (next == NULL || next->state != PROC_RUNNABLE) {
            next = idleproc; //未找到可以调度的进程
        }
        next->runs ++; //运行次数加一
        if (next != current) {
            proc_run(next); //运行新进程,调用proc_run函数
        }
    }
    local_intr_restore(intr_flag); //允许中断
}

可以看到ucore实现的是FIFO调度算法:

1 调度开始时,先屏蔽中断。

2 在进程链表中,查找第一个可以被调度的程序

3 运行新进程,允许中断

★注意到这里的调度也有不允许中断的操作

chedule函数会先清除调度标志,并从当前进程在链表中的位置开始,遍历进程控制块,直到找出处于就绪状态的进程。

之后执行proc_run函数,将环境切换至该进程的上下文并继续执行。

提到上下文切换,就需要使用switch_to函数:

switch_to:                      # switch_to(from, to)
    # save from's registers
    movl 4(%esp), %eax          #保存from的首地址
    popl 0(%eax)                #将返回值保存到context的eip
    movl %esp, 4(%eax)          #保存esp的值到context的esp
    movl %ebx, 8(%eax)          #保存ebx的值到context的ebx
    movl %ecx, 12(%eax)         #保存ecx的值到context的ecx
    movl %edx, 16(%eax)         #保存edx的值到context的edx
    movl %esi, 20(%eax)         #保存esi的值到context的esi
    movl %edi, 24(%eax)         #保存edi的值到context的edi
    movl %ebp, 28(%eax)         #保存ebp的值到context的ebp

    # restore to's registers
    movl 4(%esp), %eax          #保存to的首地址到eax
    movl 28(%eax), %ebp         #保存context的ebp到ebp寄存器
    movl 24(%eax), %edi         #保存context的ebp到ebp寄存器
    movl 20(%eax), %esi         #保存context的esi到esi寄存器
    movl 16(%eax), %edx         #保存context的edx到edx寄存器
    movl 12(%eax), %ecx         #保存context的ecx到ecx寄存器
    movl 8(%eax), %ebx          #保存context的ebx到ebx寄存器
    movl 4(%eax), %esp          #保存context的esp到esp寄存器
    pushl 0(%eax)               #将context的eip压入栈中
    ret

所以switch_to函数主要完成的是进程的上下文切换,先保存当前寄存器的值,然后再将下一进程的上下文信息保存到对于寄存器中。

//proc_run函数
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);//修改esp
            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 执行了

▲问题一:在本实验的执行过程中,创建且运行了几个内核线程?

两个,分别是idleproc和initproc。

  • idleproc:第一个内核进程,完成内核中各个子系统的初始化,之后立即调度,执行其他进程。
  • initproc:用于完成实验的功能而调度的内核进程。

idlepro是0号内核线程。kern_init调用了proc_init,在proc_init中会创建该线程。该线程的need_resched设置为1,运行cpu_idle函数,总是要求调度器切换到其他线程。

//proc_init中创建idle_proc
	if ((idleproc = alloc_proc()) == NULL) {
        panic("cannot alloc idleproc.\n");
    }
	//线程初始化
    idleproc->pid = 0;								//0号线程
    idleproc->state = PROC_RUNNABLE;				//设置为可运行
    idleproc->kstack = (uintptr_t)bootstack;		//启动后的内核栈被设置为该线程的内核栈
    idleproc->need_resched = 1;						
    set_proc_name(idleproc, "idle");
    nr_process ++;
    current = idleproc;
//kern_init最后会运行该内核线程,调度到其他线程
void cpu_idle(void) {
    while (1) {
        if (current->need_resched) {
            schedule();
        }
    }
}

initproc是第1号线程,未来所有的进程都是由该线程fork产生的。init_proc也是在proc_init中创建的,通过调用kernel_thread创建,该线程运行init_main并输出字符串。

//init_proc的创建
    int pid = kernel_thread(init_main, "Hello world!!", 0);
    if (pid <= 0) {
        panic("create init_main failed.\n");
    }
    initproc = find_proc(pid);
    set_proc_name(initproc, "init");

kernel_thread中定义了一个trapframe结构,然后将该结构传入do_fork完成线程的建立。

int kernel_thread(int (*fn)(void *), void *arg, uint32_t clone_flags) {
    struct trapframe tf;
    memset(&tf, 0, sizeof(struct trapframe));
    tf.tf_cs = KERNEL_CS;
    tf.tf_ds = tf.tf_es = tf.tf_ss = KERNEL_DS;			//使用内核的代码和数据段
    tf.tf_regs.reg_ebx = (uint32_t)fn;					//函数地址
    tf.tf_regs.reg_edx = (uint32_t)arg;					//参数
    tf.tf_eip = (uint32_t)kernel_thread_entry;			//kernel_thread_entry中将进入ebx指定的函数执行
    return do_fork(clone_flags | CLONE_VM, 0, &tf);
}

该线程创建完成后,proc_init也完成了工作,返回到kern_init,kern_init会运行idle_proc的cpu_idle,进行进程调度,从而切换运行init_proc。切换线程是调度器schedule函数完成的,该函数会在进程链表中寻找一个就绪的进程,调用proc_run切换到改进程。proc_run会进行上下文切换,而在do_fork中调用的copy_thread函数中,将context.eip设置为了forkret,进程切换完成后从forkret开始运行。forkret实际上是forkrets,forkrets会从当前进程的trapframe恢复上下文,然后跳转到设置好的kernel_thread_entry。

▲问题二:语句local_intr_save(intr_flag);....local_intr_restore(intr_flag);在这里有何作用?请说明理由。

原子化的操作,用于哈希表写入的过程中。

作用分别是屏蔽中断和打开中断,以免进程切换时其他进程再进行调度。也就是保护进程切换不会被中断,以免进程切换时其他进程再进行调度,相当于互斥锁。之前在第六步添加进程到列表的时候也需要有这个操作,是因为进程进入列表的时候,可能会发生一系列的调度事件,比如我们所熟知的抢断等,加上这么一个保护机制可以确保进程执行不被打乱。

代码准确性核验

完成代码编写后,编译并运行代码:make qemu

如果可以得到如 附录A所示的显示内容(仅供参考,不是标准答案输出),则基本正确。

输出结果如下,发现与参考答案大致相同。

wolf@wolf-VB:~/桌面/wolf/os_kernel_lab-master/labcodes/lab4$ make qemu
+ cc kern/init/entry.S
+ cc kern/init/init.c
kern/init/init.c: In function ‘kern_init’:
kern/init/init.c:32:5: warning: implicit declaration of function ‘grade_backtrace’ [-Wimplicit-function-declaration]
   32 |     grade_backtrace();
      |     ^~~~~~~~~~~~~~~
kern/init/init.c: In function ‘grade_backtrace2’:
kern/init/init.c:57:5: warning: implicit declaration of function ‘mon_backtrace’; did you mean ‘grade_backtrace2’? [-Wimplicit-function-declaration]
   57 |     mon_backtrace(0, NULL, NULL);
      |     ^~~~~~~~~~~~~
      |     grade_backtrace2
kern/init/init.c: At top level:
kern/init/init.c:71:1: warning: conflicting types for ‘grade_backtrace’
   71 | grade_backtrace(void) {
      | ^~~~~~~~~~~~~~~
kern/init/init.c:32:5: note: previous implicit declaration of ‘grade_backtrace’ was here
   32 |     grade_backtrace();
      |     ^~~~~~~~~~~~~~~
kern/init/init.c:104:1: warning: ‘lab1_switch_test’ defined but not used [-Wunused-function]
  104 | lab1_switch_test(void) {
      | ^~~~~~~~~~~~~~~~
+ cc kern/libs/stdio.c
+ cc kern/libs/rb_tree.c
+ cc kern/libs/readline.c
+ cc kern/debug/panic.c
kern/debug/panic.c: In function ‘__panic’:
kern/debug/panic.c:27:5: warning: implicit declaration of function ‘print_stackframe’; did you mean ‘print_trapframe’? [-Wimplicit-function-declaration]
   27 |     print_stackframe();
      |     ^~~~~~~~~~~~~~~~
      |     print_trapframe
+ cc kern/debug/kdebug.c
+ cc kern/debug/kmonitor.c
+ cc kern/driver/ide.c
+ cc kern/driver/clock.c
+ cc kern/driver/console.c
+ cc kern/driver/picirq.c
+ cc kern/driver/intr.c
+ cc kern/trap/trap.c
kern/trap/trap.c: In function ‘print_trapframe’:
kern/trap/trap.c:108:16: warning: taking address of packed member of ‘struct trapframe’ may result in an unaligned pointer value [-Waddress-of-packed-member]
  108 |     print_regs(&tf->tf_regs);
      |                ^~~~~~~~~~~~
+ cc kern/trap/vectors.S
+ cc kern/trap/trapentry.S
+ cc kern/mm/pmm.c
kern/mm/pmm.c:279:1: warning: ‘boot_alloc_page’ defined but not used [-Wunused-function]
  279 | boot_alloc_page(void) {
      | ^~~~~~~~~~~~~~~
+ cc kern/mm/swap_fifo.c
+ cc kern/mm/vmm.c
kern/mm/vmm.c: In function ‘check_vmm’:
kern/mm/vmm.c:165:12: warning: unused variable ‘nr_free_pages_store’ [-Wunused-variable]
  165 |     size_t nr_free_pages_store = nr_free_pages();
      |            ^~~~~~~~~~~~~~~~~~~
kern/mm/vmm.c: In function ‘check_vma_struct’:
kern/mm/vmm.c:177:12: warning: unused variable ‘nr_free_pages_store’ [-Wunused-variable]
  177 |     size_t nr_free_pages_store = nr_free_pages();
      |            ^~~~~~~~~~~~~~~~~~~
+ cc kern/mm/kmalloc.c
kern/mm/kmalloc.c: In function ‘__slob_free_pages’:
kern/mm/kmalloc.c:93:23: warning: passing argument 1 of ‘kva2page’ makes pointer from integer without a cast [-Wint-conversion]
   93 |   free_pages(kva2page(kva), 1 << order);
      |                       ^~~
      |                       |
      |                       long unsigned int
In file included from kern/mm/kmalloc.c:7:
kern/mm/pmm.h:106:16: note: expected ‘void *’ but argument is of type ‘long unsigned int’
  106 | kva2page(void *kva) {
      |          ~~~~~~^~~
+ cc kern/mm/swap.c
+ cc kern/mm/default_pmm.c
+ cc kern/fs/swapfs.c
+ cc kern/process/entry.S
+ cc kern/process/switch.S
+ cc kern/process/proc.c
+ cc kern/schedule/sched.c
+ cc libs/string.c
+ cc libs/printfmt.c
+ cc libs/hash.c
+ cc libs/rand.c
+ ld bin/kernel
+ cc boot/bootasm.S
+ cc boot/bootmain.c
+ cc tools/sign.c
+ ld bin/bootblock
'obj/bootblock.out' size: 446 bytes
build 512 bytes boot sector: 'bin/bootblock' success!
记录了10000+0 的读入
记录了10000+0 的写出
5120000字节(5.1 MB,4.9 MiB)已复制,0.0149466 s,343 MB/s
记录了1+0 的读入
记录了1+0 的写出
512字节已复制,7.141e-05 s,7.2 MB/s
记录了409+1 的读入
记录了409+1 的写出
209440字节(209 kB,205 KiB)已复制,0.000446185 s,469 MB/s
记录了128+0 的读入
记录了128+0 的写出
134217728字节(134 MB,128 MiB)已复制,0.125741 s,1.1 GB/s
WARNING: Image format was not specified for 'bin/ucore.img' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
WARNING: Image format was not specified for 'bin/swap.img' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
(THU.CST) os is loading ...

Special kernel symbols:
  entry  0xc0100036 (phys)
  etext  0xc010ab8d (phys)
  edata  0xc012e000 (phys)
  end    0xc0131158 (phys)
Kernel executable memory footprint: 197KB
ebp:0xc012af48 eip:0xc0101e9d args:0x00010094 0x00010094 0xc012af78 0xc01000d0 
    kern/debug/kdebug.c:308: print_stackframe+25
ebp:0xc012af58 eip:0xc01021b1 args:0x00000000 0x00000000 0x00000000 0xc012afc8 
    kern/debug/kmonitor.c:129: mon_backtrace+14
ebp:0xc012af78 eip:0xc01000d0 args:0x00000000 0xc012afa0 0xffff0000 0xc012afa4 
    kern/init/init.c:57: grade_backtrace2+23
ebp:0xc012af98 eip:0xc01000f6 args:0x00000000 0xffff0000 0xc012afc4 0x0000002a 
    kern/init/init.c:62: grade_backtrace1+31
ebp:0xc012afb8 eip:0xc0100117 args:0x00000000 0xc0100036 0xffff0000 0xc0100079 
    kern/init/init.c:67: grade_backtrace0+23
ebp:0xc012afd8 eip:0xc010013c args:0x00000000 0x00000000 0x00000000 0xc010aba0 
    kern/init/init.c:72: grade_backtrace+30
ebp:0xc012aff8 eip:0xc0100086 args:0xc010aff8 0xc010b000 0xc010212e 0xc010b01f 
    kern/init/init.c:32: kern_init+79
memory management: default_pmm_manager
e820map:
  memory: 0009fc00, [00000000, 0009fbff], type = 1.
  memory: 00000400, [0009fc00, 0009ffff], type = 2.
  memory: 00010000, [000f0000, 000fffff], type = 2.
  memory: 07ee0000, [00100000, 07fdffff], type = 1.
  memory: 00020000, [07fe0000, 07ffffff], type = 2.
  memory: 00040000, [fffc0000, ffffffff], type = 2.
check_alloc_page() succeeded!
check_pgdir() succeeded!
check_boot_pgdir() succeeded!
-------------------- BEGIN --------------------
PDE(0e0) c0000000-f8000000 38000000 urw
  |-- PTE(38000) c0000000-f8000000 38000000 -rw
PDE(001) fac00000-fb000000 00400000 -rw
  |-- PTE(000e0) faf00000-fafe0000 000e0000 urw
  |-- PTE(00001) fafeb000-fafec000 00001000 -rw
--------------------- END ---------------------
use SLOB allocator
check_slab() success
kmalloc_init() succeeded!
check_vma_struct() succeeded!
page fault at 0x00000100: K/W [no page found].
check_pgfault() succeeded!
check_vmm() succeeded.
ide 0:      10000(sectors), 'QEMU HARDDISK'.
ide 1:     262144(sectors), 'QEMU HARDDISK'.
SWAP: manager = fifo swap manager
BEGIN check_swap: count 1, total 31916
setup Page Table for vaddr 0X1000, so alloc a page
setup Page Table vaddr 0~4MB OVER!
set up init env for check_swap begin!
page fault at 0x00001000: K/W [no page found].
page fault at 0x00002000: K/W [no page found].
page fault at 0x00003000: K/W [no page found].
page fault at 0x00004000: K/W [no page found].
set up init env for check_swap over!
write Virt Page c in fifo_check_swap
write Virt Page a in fifo_check_swap
write Virt Page d in fifo_check_swap
write Virt Page b in fifo_check_swap
write Virt Page e in fifo_check_swap
page fault at 0x00005000: K/W [no page found].
swap_out: i 0, store page in vaddr 0x1000 to disk swap entry 2
write Virt Page b in fifo_check_swap
write Virt Page a in fifo_check_swap
page fault at 0x00001000: K/W [no page found].
swap_out: i 0, store page in vaddr 0x2000 to disk swap entry 3
swap_in: load disk swap entry 2 with swap_page in vadr 0x1000
write Virt Page b in fifo_check_swap
page fault at 0x00002000: K/W [no page found].
swap_out: i 0, store page in vaddr 0x3000 to disk swap entry 4
swap_in: load disk swap entry 3 with swap_page in vadr 0x2000
write Virt Page c in fifo_check_swap
page fault at 0x00003000: K/W [no page found].
swap_out: i 0, store page in vaddr 0x4000 to disk swap entry 5
swap_in: load disk swap entry 4 with swap_page in vadr 0x3000
write Virt Page d in fifo_check_swap
page fault at 0x00004000: K/W [no page found].
swap_out: i 0, store page in vaddr 0x5000 to disk swap entry 6
swap_in: load disk swap entry 5 with swap_page in vadr 0x4000
write Virt Page e in fifo_check_swap
page fault at 0x00005000: K/W [no page found].
swap_out: i 0, store page in vaddr 0x1000 to disk swap entry 2
swap_in: load disk swap entry 6 with swap_page in vadr 0x5000
write Virt Page a in fifo_check_swap
page fault at 0x00001000: K/R [no page found].
swap_out: i 0, store page in vaddr 0x2000 to disk swap entry 3
swap_in: load disk swap entry 2 with swap_page in vadr 0x1000
count is 0, total is 5
check_swap() succeeded!
++ setup timer interrupts
this initproc, pid = 1, name = "init"
To U: "Hello world!!".
To U: "en.., Bye, Bye. :)"
kernel panic at kern/process/proc.c:354:
    process exit!!.

stack trackback:
ebp:0xc0333fa8 eip:0xc0101e9d args:0xc010962a 0xc0131044 0xc03310c0 0xc0333fdc 
    kern/debug/kdebug.c:308: print_stackframe+25
ebp:0xc0333fc8 eip:0xc010185e args:0xc010cdf5 0x00000162 0xc010ce09 0xc0131044 
    kern/debug/panic.c:27: __panic+111
ebp:0xc0333fe8 eip:0xc0109bfd args:0x00000000 0xc010ce88 0x00000000 0x00000010 
    kern/process/proc.c:354: do_exit+32
Welcome to the kernel debug monitor!!
Type 'help' for a list of commands.

使用make grade查看分数

可以看到几乎正确,这里的slab项未拿全分数是因为这是下一项扩展练习需要用到的。

扩展练习Challenge:实现支持任意大小的内存分配算法

这不是本实验的内容,其实是上一次实验内存的扩展,但考虑到现在的slab算法比较复杂,有必要实现一个比较简单的任意大小内存分配算法。可参考本实验中的slab如何调用基于页的内存分配算法(注意,不是要你关注slab的具体实现)来实现first-fit/best-fit/worst-fit/buddy等支持任意大小的内存分配算法。

1.对比first-bit/best-fit/worst-fit/slab以及buddy这几种算法的特点

  • 首次适应算法(First Fit):该算法从空闲分区链首开始查找,直至找到一个能满足其大小要求的空闲分区为止。然后再按 照作业的大小,从该分区中划出一块内存分配给请求者,余下的空闲分区仍留在空闲分区链 中。
    • 特点: 该算法倾向于使用内存中低地址部分的空闲区,在高地址部分的空闲区很少被利用,从而保留了高地址部分的大空闲 区。显然为以后到达的大作业分配大的内存空间创造了条件。
    • 缺点:低地址部分不断被划分,留下许多难以利用、很小的空闲区,而每次查找又都从低地址部分开始,会增加查找的开销。
  • 循环首次适应算法(Next Fit):该算法是由首次适应算法演变而成的。在为进程分配内存空间时,不再每次从链首开始查 找,直至找到一个能满足要求的空闲分区,并从中划出一块来分给作业。
    • 特点:使内存中的空闲分区分布的更为均匀,减少了查找时的系统开销。
    • 缺点:缺乏大的空闲分区,从而导致不能装入大型作业。
  • 最佳适应算法(Best Fit):该算法总是把既能满足要求,又是最小的空闲分区分配给作业。为了加速查找,该算法要求将 所有的空闲区按其大小排序后,以递增顺序形成一个空白链。这样每次找到的第一个满足要求的空闲区,必然是最优的。孤立 地看,该算法似乎是最优的,但事实上并不一定。因为每次分配后剩余的空间一定是最小的,在存储器中将留下许多难以利用 的小空闲区。同时每次分配后必须重新排序,这也带来了一定的开销。
    • 特点:每次分配给文件的都是最合适该文件大小的分区。
    • 缺点:内存中留下许多难以利用的小的空闲区。
  • 最坏适应算法(Worst Fit):该算法按大小递减的顺序形成空闲区链,分配时直接从空闲区链的第一个空闲区中分配(不能 满足需要则不分配)。很显然,如果第一个空闲分区不能满足,那么再没有空闲分区能满足需要。这种分配方法初看起来不太 合理,但它也有很强的直观吸引力:在大空闲区中放入程序后,剩下的空闲区常常也很大,于是还能装下一个较大的新程序。
    • 最坏适应算法与最佳适应算法的排序正好相反,它的队列指针总是指向最大的空闲区,在进行分配时,总是从最大的空闲 区开始查寻。该算法克服了最佳适应算法留下的许多小的碎片的不足,但保留大的空闲区的可能性减小了,而且空闲区回收也和最佳适 应算法一样复杂。
    • 特点:给文件分配分区后剩下的空闲区不至于太小,产生碎片的几率最小,对中小型文件分配分区操作有利。
    • 缺点:使存储器中缺乏大的空闲区,对大型文件的分区分配不利。
  • 伙伴算法
    • 伙伴算法会浪费大量的内存,(如果需要大小为9的内存块必须分配大小为16的内存块).而优点也是明显的,分配和合并算法都很简单易行.但是,当分配和回收较快的时候,例如分配大小为9的内存块,此时分配16,然后又回收,即合并伙伴内存块,这样会造成不必要的cpu浪费,应该设置链表中内存块的低潮个数,即当链表中内存块个数小于某个值的时候,并不合并伙伴内存块,只要当高于低潮个数的时候才合并。
  • slab算法
    • 采用buddy算法,解决了外碎片问题,这种方法适合大块内存请求,不适合小内存区请求,与传统的内存管理模式相比, slab缓存分配器提供了很多优点。首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。slab分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象重复进行初始化。最后,slab分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。

2.Slub算法详解

什么是slab缓存池呢?我的解释是使用struct kmem_cache结构描述的一段内存就称作一个slab缓存池。一个slab缓存池就像是一箱牛奶,一箱牛奶中有很多瓶牛奶,每瓶牛奶就是一个object。分配内存的时候,就相当于从牛奶箱中拿一瓶。总有拿完的一天。当箱子空的时候,你就需要去超市再买一箱回来。超市就相当于partial链表,超市存储着很多箱牛奶。如果超市也卖完了,自然就要从厂家进货,然后出售给你。厂家就相当于伙伴系统。

在这里插入图片描述
【这一部分非常推荐看后面参考文献列出的部分,讲解的十分精彩!】

(1) per cpu freelist

针对每一个cpu都会分配一个struct kmem_cache_cpu的结构体。可以称作是本地缓存池。当内存申请的时候,优先从本地cpu缓存池申请。在分配初期,本地缓存池为空,自然要从伙伴系统分配一定页数的内存。内核会为每一个物理页帧创建一个struct page的结构体。kmem_cacche_cpu中page就会指向正在使用的slab的页帧。freelist成员指向第一个可用内存obj首地址。处于正在使用的slab的struct page结构体中的freelist会置成NULL,因为没有其他地方使用。struct page结构体中inuse代表已经使用的obj数量。这地方有个很有意思的地方,在刚从伙伴系统分配的slab的inuse在分配初期就置成obj的总数,在分配obj的时候并不会改变。你是不是觉得很奇怪,既然表示已经使用obj的数量,为什么一直是obj的总数呢?你想想,slab中的对象总有分配完的时候,那个时候就直接脱离kmem_cache_cpu了。此时的inuse不就名副其实了嘛!对于full slab就像图的右下角,就像无人看管的孩子,没有任何链表来管理。

(2) per cpu partial

当图中右下角full slab释放obj的时候,首先就会将slab挂入per cpu partial链表管理。通过struct page中next成员形成单链表。per cpu partial链表指向的第一个page中会存放一些特殊的数据。例如:pobjects存储着per cpu partial链表中所有slab可供分配obj的总数,如图所示。当然还有一个图中没有体现的pages成员存储per cpu partial链表中所有slab缓存池的个数。pobjects到底有什么用呢?我们从full slab中释放一个obj就添加到per cpu partial链表,总不能无限制的添加吧!因此,每次添加的时候都会判断当前的pobjects是否大于kmem_cache的cpu_partial成员,如果大于,那么就会将此时per cpu partial链表中所有的slab移送到kmem_cache_node的partial链表,然后再将刚刚释放obj的slab插入到per cpu partial链表。如果不大于,则更新pobjects和pages成员,并将slab插入到per cpu partial链表。

(3) per node partial

per node partia链表类似per cpu partial,区别是node中的slab是所有cpu共享的,而per cpu是每个cpu独占的。假如现在的slab布局如上图所示。假如现在如红色箭头指向的obj将会释放,那么就是一个empty slab,此时判断kmem_cache_node的nr_partial是否大于kmem_cache的min_partial,如果大于则会释放该slab的内存。

3.实现Slub算法

通过少量的修改,即可使用实验2扩展练习实现的 Slab 算法。

  • 初始化 Slub 算法:在初始化物理内存最后初始化 Slub ;
void pmm_init(void) {
	...
    kmem_int();
}

  • 在 vmm.c 中使用 Slub 算法:

为了使用Slub算法,需要声明仓库的指针。

struct kmem_cache_t *vma_cache = NULL;
struct kmem_cache_t *mm_cache = NULL;

在虚拟内存初始化时创建仓库。

void vmm_init(void) {
    mm_cache = kmem_cache_create("mm", sizeof(struct mm_struct), NULL, NULL);
    vma_cache = kmem_cache_create("vma", sizeof(struct vma_struct), NULL, NULL);
	...
}

在 mm_create 和 vma_create 中使用 Slub 算法。

struct mm_struct *mm_create(void) {
    struct mm_struct *mm = kmem_cache_alloc(mm_cache);
	...
}

struct vma_struct *vma_create(uintptr_t vm_start, uintptr_t vm_end, uint32_t vm_flags) {
    struct vma_struct *vma = kmem_cache_alloc(vma_cache);
	...
}

在 mm_destroy 中释放内存。

void
mm_destroy(struct mm_struct *mm) {
	...
    while ((le = list_next(list)) != list) {
		...
        kmem_cache_free(mm_cache, le2vma(le, list_link));  //kfree vma        
    }
    kmem_cache_free(mm_cache, mm); //kfree mm
	...
}

  • 在 proc.c 中使用 Slub 算法:

声明仓库指针。

struct kmem_cache_t *proc_cache = NULL;

在初始化函数中创建仓库。

void proc_init(void) {
 	...
    proc_cache = kmem_cache_create("proc", sizeof(struct proc_struct), NULL, NULL);
  	...
}

在 alloc_proc 中使用 Slub 算法。

static struct proc_struct *alloc_proc(void) {
    struct proc_struct *proc = kmem_cache_alloc(proc_cache);
  	...
}

本实验没有涉及进程结束后 PCB 回收,不须要回收内存。

实验总结

本实验主要是内核线程创建与切换的具体实现。在ucore中,首先创建idle_proc这个第0号内核线程,然后调用kernel_thread建立init_proc第1号内核线程,最后回到kern_init执行idle_proc线程,idle_proc总是调度到其他线程。线程具体的创建是由do_fork完成的,do_fork调用alloc_proc等函数,完成进程控制块的创建,内核栈和pid的分配,父进程上下文和中断帧的复制,还会进行一些设置,如将上下文的eip设置为fork_ret,在trapframe中将返回值设置为0等。创建完毕后返回pid,当调度器调度该线程时,调度器调用proc_run完成上下文切换后就会执行fork_ret,恢复中断帧,从而开始执行指定的程序。

重要知识点

  • 内核线程和用户进程的区别

  • 进程控制块

  • 内核线程的创建

  • 内核线程资源分配

  • 进程(线程)切换的过程

  • 进程控制块

  • 进程状态

  • 进程挂起

  • 用户线程与内核线程

  • 线程与进程的比较

参考文献

lab4实验的进行以及知识点的理解参考了

  • https://blog.csdn.net/Aaron503/article/details/130453791?spm=1001.2014.3001.5501

  • https://blog.csdn.net/sfadjlha/article/details/124859514?spm=1001.2014.3001.5502

对比first-bit/best-fit/worst-fit/slab以及buddy这几种算法的特点参考了

  • https://blog.csdn.net/weixin_42637204/article/details/90968731

实现slub算法部分【强烈推荐】

  • http://www.noobyard.com/article/p-edwelvki-cr.html
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 申请内存、释放内存和查看内存使用情况。这些操作将通过系统调用来实现。内存管理器的主要功能是管理系统中的内存分配和释放,确保内存使用的高效性和安全性。虽然内存管理器还没有完成,但开发人员正在努力工作,以便在不久的将来发布完整的版本。 ### 回答2: 内存分配、内存释放和内存查找。内存分配可以让程序在运行时获取需要的内存空间,内存释放则是将不再需要的内存空间还给操作系统,以便重新分配给其他程序使用。内存查找则是检查某个内存地址是否已经被分配。这些功能对于操作系统运行和程序开发都具有很大的重要性。 内存管理器的实现主要考虑了几个因素。首先是内存的物理组织形式。操作系统需要能够识别内存的物理组织形式,才能够有效地进行内存管理。其次是内存的分配和释放。操作系统需要能够根据不同的需求分配不同大小的内存空间,并且能够及时地释放内存空间以提高系统的运行效率。还有一个重要的因素是内存的安全性。操作系统需要确保不同程序之间的内存空间互相隔离,避免出现内存泄露或者内存破坏等问题。 对于内存管理器的实现方式,目前有很多种不同的方法。其中,最常见的是分页内存管理方式。这种方式将内存空间分成若干个大小相等的页面,每个页面都有自己的地址和内存状态,操作系统只需要记录每个页面的状态和分配情况即可。当程序需要内存空间时,内存管理器会寻找空闲的页面,并将其分配给程序使用。当程序不再需要这些内存空间时,内存管理器会将其释放,并将页面标记为未分配状态。 总之,内存管理器是一个操作系统非常重要的组件,对于系统的性能和稳定性都有很大的影响。hnu-os的内存管理器的实现将进一步完善,以满足更多的需求。 ### 回答3: 内存分配、内存释放和内存查看。但是,由于内存管理器的实现需要许多复杂的算法和微妙的技巧,因此开发人员需要花费更多的时间来完成它。 内存管理器是操作系统中最重要的组件之一,因为它确保操作系统能够正确地使用计算机的内存。内存管理器的主要任务是将物理内存地址映射到虚拟内存地址,并跟踪哪些内存块已经被分配、哪些是空闲的,以及哪些正在使用。 在操作系统中,内存被分割成多个块。当一个程序需要内存时,内存管理器会分配一个或多个块来满足程序的要求。当程序完成它的工作并将内存释放时,内存管理器会将这些块标记为空闲状态,以备后续程序再次使用。 内存管理器还需要处理许多其他事项,例如内存碎片化和内存保护。内存碎片化是指当程序释放一些内存块时,内存管理器会留下一些无法使用的小空间,这些空间可能过小,无法再分配给其他程序。内存保护是指内存管理器可以防止程序访问不属于它的内存区域,从而保护操作系统和其他程序的稳定性和安全性。 总之,内存管理器对于计算机系统的稳定性和性能至关重要。虽然它可能需要一些时间来完成,但一旦实现,它将使操作系统功能更加完整且能够更好地支持更复杂的任务和应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值