ucore lab4

UCORE实验4

实验目的

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

实验内容

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

本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:
内核线程只运行在内核态,用户进程会在用户态和内核态交替运行。
所有内核线程共用ucore内核内存空间,不需为每个内核线程维护单独的内存空间,而用户进程需要维护各自的用户内存空间。

相关原理介绍可看附录B:【原理】进程/线程的属性与特征解析。
(在ucore的调度和执行管理中,并没有区分线程和进程。且由于ucore内核中的所有内核线程共享一个内核地址空间和其他资源,所以这些内核线程从属于同一个唯一的内核进程,即ucore内核本身。)

练习0:填写已有实验

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

分析

使用meld修改即可。

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

分析

相关背景知识:
线程和进程:
拥有资源所有权的单位通常仍称作进程,对资源的管理成为进程管理;把指令执行流的单位称为线程,对线程的管理就是线程调度和线程分派。在一个进程中,可能有一个或多个线程,每个线程有线程执行状态(运行、就绪、等待等),保存上次运行时的线程上下文、线程的执行栈等。考虑到CPU有不同的特权模式,参照进程的分类,线程又可进一步细化为用户线程和内核线程。

进程控制块:
进程控制块是操作系统管理控制进程运行所用的信息集合。操作系统用PCB来描述进程的基本情况以及运行变化的过程。PCB是进程存在的唯一标志 ,每个进程都在操作系统中有一个对应的PCB。进程控制块可以通过某个数据结构组织起来(例如链表)。同一状态进程的PCB连接成一个链表,多个状态对应多个不同的链表。各状态的进程形成不同的链表,如就绪联链表,阻塞链表等等。
在实验四中,进程管理信息用struct proc_struct表示,在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:内存管理的信息,包括内存映射列表、页表指针等。mm成员变量在lab3中用于虚存管理。但在实际OS中,内核线程常驻内存,不需要考虑swap page问题,在lab5中涉及到了用户进程,才考虑进程用户内存空间的swap page问题,mm才会发挥作用。所以在lab4中mm对于内核线程就没有用了,这样内核线程的proc_struct的成员变量* mm=0是合理的。mm里有个很重要的项pgdir,记录的是该进程使用的一级页表的物理地址。由于*mm=NULL,所以在proc_struct数据结构中需要有一个代替pgdir项来记录页表起始地址,这就是proc_struct数据结构中的cr3成员变量。
● state:进程所处的状态,分四种,PROC_UNINIT(未初始化),PROC_SLEEPING(休眠),PROC_RUNNABLE(运行就绪),PROC_ZOMBIE(僵尸进程,等待父进程回收资源)。
● parent:用户进程的父进程(创建它的进程)。在所有进程中,只有一个进程没有父进程,就是内核创建的第一个内核线程idleproc。内核根据这个父子关系建立一个树形结构,用于维护一些特殊的操作,例如确定某个进程是否可以对另外一个进程进行某种操作等等。
● context:进程的上下文,用于进程切换(参见switch.S)。在 uCore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈以及上下文等等)。使用 context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中定义switch_to。
● tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore内核允许嵌套中断。因此为了保证嵌套中断发生时tf 总是能够指向当前的trapframe,uCore 在内核栈上维护了 tf 的链,可以参考trap.c::trap函数做进一步的了解。
● cr3: cr3 保存页表的物理地址,目的就是进程切换的时候方便直接使用cr3实现页表切换,避免每次都根据 mm 来计算 cr3。mm数据结构是用来实现用户空间的虚存管理的,但是内核线程没有用户空间,它执行的只是内核中的一小段代码(通常是一小段函数),所以它没有mm 结构,也就是NULL。当某个进程是一个普通用户态进程的时候,PCB 中的 cr3 就是 mm 中页表(pgdir)的物理地址;而当它是内核线程的时候,cr3 等于boot_cr3。而boot_cr3指向了uCore启动时建立好的内核虚拟空间的页目录表首地址。
● kstack: 每个线程都有一个内核栈,并且位于内核地址空间的不同位置。对于内核线程,该栈就是运行时的程序使用的栈;而对于普通进程,该栈是发生特权级改变的时候使保存被打断的硬件信息用的栈。uCore在创建进程时分配了 2 个连续的物理页(参见memlayout.h中KSTACKSIZE的定义)作为内核栈的空间。这个栈很小,所以内核中的代码应该尽可能的紧凑,并且避免在栈上分配大的数据结构,以免栈溢出,导致系统崩溃。kstack记录了分配给该进程/线程的内核栈的位置。主要作用有以下几点:首先,当内核准备从一个进程切换到另一个的时候,需要根据kstack 的值正确的设置好 tss (可以回顾一下在实验一中讲述的 tss 在中断处理过程中的作用),以便在进程切换以后再发生中断时能够使用正确的栈。其次,内核栈位于内核地址空间,并且是不共享的(每个线程都拥有自己的内核栈),因此不受到 mm 的管理,当进程退出的时候,内核能够根据 kstack 的值快速定位栈的位置并进行回收。uCore 的这种内核栈的设计借鉴的是 linux 的方法(但由于内存管理实现的差异,它实现的远不如 linux 的灵活),它使得每个线程的内核栈在不同的位置,这样从某种程度上方便调试,但同时也使得内核对栈溢出变得十分不敏感,因为一旦发生溢出,它极可能污染内核中其它的数据使得内核崩溃。如果能够通过页表,将所有进程的内核栈映射到固定的地址上去,能够避免这种问题,但又会使得进程切换过程中对栈的修改变得相当繁琐。

为了管理系统中所有的进程控制块,uCore维护了如下全局变量(位于kern/process/proc.c):
● static struct proc *current:当前占用CPU且处于“运行”状态进程控制块指针。通常这个变量是只读的,只有在进程切换的时候才进行修改,并且整个切换和修改过程需要保证操作的原子性,目前至少需要屏蔽中断。可以参考 switch_to 的实现。
● 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将链接入这个链表中。

1.alloc_proc函数的实现

根据背景知识和注释进行完善:

// alloc_proc - alloc a proc_struct and init all fields of proc_struct
static struct proc_struct *
alloc_proc(void) {
    struct proc_struct *proc = kmalloc(sizeof(struct proc_struct));
    if (proc != NULL) {
    //LAB4:EXERCISE1 202008010404 
    proc->state = PROC_UNINIT; //设置进程为“初始”态
    proc->pid = -1; //设置进程pid的未初始化值
    proc->runs = 0; //进程运行时间为0
    proc->kstack = 0; //进程的内核栈为0,即没有内核栈
    proc->need_resched = 0; //1为进程占据CPU,0为进程释放CPU
    proc->parent = NULL; //进程的父进程
    proc->mm = NULL; //lab4中mm对于内核线程就没有用
    memset(&(proc->context), 0, sizeof(struct context)); //初始化进程上下文
    proc->tf = NULL; //中断帧的指针为NULL
    proc->cr3 = boot_cr3; //使用内核页目录表的基址
    proc->flags = 0; //进程标志为0
    memset(proc->name, 0, PROC_NAME_LEN); //初始化进程名称
    }
    return proc;
}
2.请说明proc_struct中struct context context和struct trapframe *tf成员变量含义和在本实验中的作用是什么

struct context context:进程的上下文,用于进程切换(参见switch.S)。在uCore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈以及上下文等等),使用 context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。

struct context {
    uint32_t eip;
    uint32_t esp;
    uint32_t ebx;
    uint32_t ecx;
    uint32_t edx;
    uint32_t esi;
    uint32_t edi;
    uint32_t ebp;
};

struct trapframe *tf:中断帧的指针,总是指向内核栈的某个位置。当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。

struct trapframe {
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;
    uint32_t tf_trapno;
    /* below here defined by x86 hardware */
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    /* below here only when crossing rings, such as from user to kernel */
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
} __attribute__((packed));

以kernel_thread函数为例,讲述二者在本实验中的作用:

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地址
    return do_fork(clone_flags | CLONE_VM, 0, &tf);
}

kernel_thread是创建内核线程的函数,采用了局部变量tf来放置保存内核线程的临时中断帧,并把中断帧的指针传递给do_fork函数,而do_fork函数会调用copy_thread函数来在新创建的进程内核栈上专门给进程的中断帧分配一块空间。

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; //拷贝在kernel_thread函数建立的临时中断帧的初始值
    proc->tf->tf_regs.reg_eax = 0;
    //设置子进程/线程执行完do_fork后的返回值
    proc->tf->tf_esp = esp; //设置中断帧中的栈指针esp
    proc->tf->tf_eflags |= FL_IF; //使能中断

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

设置好中断帧后,最后就是设置initproc的进程上下文(process context,也称执行现场)了。只有设置好执行现场后,一旦uCore调度器选择了initproc执行,就需要根据initproc->context中保存的执行现场来恢复initproc的执行。这里设置了initproc的执行现场中主要的两个信息:上次停止执行时的下一条指令地址context.eip和上次停止执行时的堆栈地址context.esp。其实initproc还没有执行过,所以这其实就是initproc实际执行的第一条指令地址和堆栈指针。可以看出,由于initproc的中断帧占用了实际给initproc分配的栈空间的顶部,所以initproc就只能把栈顶指针context.esp设置在initproc的中断帧的起始位置。根据context.eip的赋值,可以知道initproc实际开始执行的地方在forkret函数(是forkrets的包装函数,forkrets主要功能是从current->tf中恢复上下文,跳转至current->tf->tf_eip,也就是kernel_thread_entry,即该函数是完成do_fork函数返回的处理工作)处。

总而言之,proc->context.eip = (uintptr_t)forkret会使新进程正确的从中断处理例程中返回。而proc->context.esp = (uintptr_t)proc_struct->tf,在中断返回时,使新进程恢复保存的trapframe信息至各个寄存器中,然后开始执行用户代码。可见,tf与context共同用于进程的状态保存与恢复。

练习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?请说明你的分析和理由。

分析
1.根据注释完善do_fork函数

我们先来细化一下do_fork函数的运行过程:
①分配并初始化进程控制块(alloc_proc函数)。
②分配并初始化内核栈(setup_stack函数)。
③根据clone_flag标志复制或共享进程内存管理结构(copy_mm函数)。
④设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread函数)。
⑤把设置好的进程控制块放入hash_list和proc_list两个全局进程链表中。
⑥自此,进程已经准备好执行了,把进程状态设置为“就绪”态。
⑦设置返回码为子进程的id号。

do_fork中涉及到的MACROs,Functions,DEFINEs:
MACROs or Functions:
alloc_proc: create a proc struct and init fields (lab4:exercise1)
创建一个proc结构和初始字段。
setup_kstack: alloc pages with size KSTACKPAGE as process kernel stack
分配大小为KSTACKPAGE的页面作为进程的内核栈。
copy_mm:process “proc” duplicate OR share process “current” 's mm according clone_flags,if clone_flags & CLONE_VM, then “share” ; else “duplicate”
根据clone_flags来决定当前进程复制还是共享当前的mm(内存管理的信息),如果clone_flags & CLONE_VM为1,则共享mm,反之复制。
copy_thread:setup the trapframe on the process’s kernel stack top and setup the kernel entry point and stack of process
在进程的内核栈顶建立栈帧,并设置进程的内核入口和进程的栈。
hash_proc:dd proc into proc hash_list
复制进程到进程哈希链表中。
get_pid:alloc a unique pid for process
为进程分配一个唯一的pid。
wakeup_proc:set proc->state = PROC_RUNNABLE
将进程状态设为就绪。
VARIABLES:
proc_list:the process set’s list
进程集合的链表。(同状态进程的PCB会连接成一个链表)
nr_process:the number of process set
进程集合的数目。

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;
    //LAB4:EXERCISE2 202008010404
    //首先分配并初始化一个PCB,如果分配失败则跳转到fork_out
    if ((proc = alloc_proc()) == NULL) {
        goto fork_out;
    }
    //fork后必然会有父进程,因此设置子进程的父进程
    proc->parent = current;
    //分配并初始化内核栈
    if (setup_kstack(proc) != 0) {
        goto bad_fork_cleanup_proc;
    }
    //根据clone_flag标志复制或共享进程内存管理结构
    if (copy_mm(clone_flags, proc) != 0) {
        goto bad_fork_cleanup_kstack;
    }
    //设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文
    copy_thread(proc, stack, tf);
    //将子进程的PCB添加到hash_list和proc_list两个全局进程链表中
    bool intr_flag;
    local_intr_save(intr_flag);//屏蔽中断,以免进程切换时其他进程在进行调度
    {
        proc->pid = get_pid();
        hash_proc(proc);
        list_add(&proc_list, &(proc->list_link));
        nr_process ++;
    }
    local_intr_restore(intr_flag);//打开中断
    //设置新的子进程可执行
    wakeup_proc(proc);
    //返回子进程的pid
    ret = proc->pid;
fork_out:
    return ret;

bad_fork_cleanup_kstack:
    put_kstack(proc);
bad_fork_cleanup_proc:
    kfree(proc);
    goto fork_out;
}
2.请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。

由于获取pid是由get_pid完成的,我们来查看get_pid:

// get_pid - alloc a unique pid for process
static int
get_pid(void) {
    static_assert(MAX_PID > MAX_PROCESS);
    struct proc_struct *proc;
    list_entry_t *list = &proc_list, *le;
    static int next_safe = MAX_PID, last_pid = MAX_PID;
    if (++ last_pid >= MAX_PID) {
        last_pid = 1;
        goto inside;
    }
    if (last_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;
            }
        }
    }
    return last_pid;
}

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

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

请在实验报告中简要说明你对proc_run函数的分析。并回答如下问题:
在本实验的执行过程中,创建且运行了几个内核线程?
语句local_intr_save(intr_flag);…local_intr_restore(intr_flag);在这里有何作用?请说明理由

完成代码编写后,编译并运行代码:make qemu
如果可以得到如 附录A所示的显示内容(仅供参考,不是标准答案输出),则基本正确。

分析
1.进程切换与proc_run函数分析

(1)第一个内核线程
在uCore执行完proc_init函数后,就创建好了两个内核线程:idleproc和initproc。首先当前的执行上下文(从kern_init 启动至今)就可以看成是uCore内核(也可看做是内核进程)中的一个内核线程的上下文。为此,uCore通过给当前执行的上下文分配一个进程控制块以及对它进行相应初始化,将其打造成第0个内核线程 – idleproc。

(2)进程调度
创建idleproc后,它会执行cpu_idle函数,并从中调用schedule函数,准备开始调度进程。

void cpu_idle(void) {
    while (1) //不断自旋等到有进程可以调度
        if (current->need_resched)
            schedule();
}

schedule函数是一个最简单的FIFO调度器的核心,其执行逻辑如下:
①设置当前内核线程current->need_resched为0。
②在proc_list队列中查找下一个处于“就绪”态的线程或进程next;。
③找到这样的进程后,就调用proc_run函数,保存当前进程current的执行现场(进程上下文),恢复新进程的执行现场,完成进程切换。
同样的,注意在进程调度的时候不能被中断,不然会造成进程间抢夺CPU。

schedule函数如下:

void
schedule(void) {
    bool intr_flag;
    list_entry_t *le, *last;
    struct proc_struct *next = NULL; //清除调度标志
    local_intr_save(intr_flag); //关闭中断
    {
        current->need_resched = 0; //令当前进程处于不用调度的状态
        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;
                }
            }
        } while (le != last);
        if (next == NULL || next->state != PROC_RUNNABLE) { //如果没有找到,则next指回idleproc
            next = idleproc;
        }
        next->runs ++; //找到需要调度的进程数+1
        if (next != current) {
            proc_run(next); //执行proc_run,让next与current进行上下文交换
        }
    }
    local_intr_restore(intr_flag);
}

(3)进程切换
至此,新的进程next就开始执行了。由于在proc10中只有两个内核线程,且idleproc要让出CPU给initproc执行,我们可以看到schedule函数通过查找proc_list进程队列,只能找到一个处于“就绪”态的initproc内核线程。并通过proc_run和进一步的switch_to函数完成两个执行现场的切换,具体流程如下:
①让current指向next内核线程initproc。
②设置任务状态段ts中特权态0下的栈顶指针esp0为next内核线程initproc的内核栈的栈顶,即next->kstack + KSTACKSIZE。
(这么设置的意义是,建立好内核线程或将来用户线程在执行特权态切换(从特权态0<–>特权态3,或从特权态3<–>特权态3)时能够正确定位处于特权态0时进程的内核栈的栈顶,而这个栈顶其实放了一个trapframe结构的内存空间。)
③设置CR3寄存器的值为next内核线程initproc的页目录表起始地址next->cr3,这实际上是完成进程间的页表切换。
④由switch_to函数完成具体的两个线程的执行现场切换,即切换各个寄存器,当switch_to函数执行完“ret”指令后,就切换到initproc执行了。

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); //设置ring0的内核栈地址
            lcr3(next->cr3); //重新加载cr3寄存器,使页目录表更新为新进程的页目录表
            switch_to(&(prev->context), &(next->context)); //上下文切换,把当前进程的当前各寄存器的值保存在其proc_struct结构体的context变量中,再把要切换到的进程的proc_struct结构体的context变量加载到各寄存器。
        }
        local_intr_restore(intr_flag); //开启中断
    }
}

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
2.在本实验的执行过程中,创建且运行了几个内核线程?

不难看出创建了两个线程,idleproc和initproc。

3.语句local_intr_save(intr_flag);…local_intr_restore(intr_flag);在这里有何作用?请说明理由

这两句代码的作用分别是阻塞中断和解除中断的阻塞,这使得这两句代码之间的代码块形成原子操作,让某些关键的代码不会被打断,从而避免引起一些未预料到的错误。
以进程切换为例,在proc_run中,当刚设置好current指针为下一个进程,但还未完全将控制权转移时,如果该过程突然被一个中断所打断,则中断处理例程的执行可能会引发异常,因为current指针指向的进程与实际使用的进程资源不一致。

最后运行结果如下:
屏幕截图 2022-05-15 202727
屏幕截图 2022-05-15 204353

实验心得

lab2和lab3完成了对内存的虚拟化,但整个控制流还是一条线串行执行。lab4将在此基础上进行CPU的虚拟化,即让ucore实现分时共享CPU,实现多条控制流能够并发执行。从某种程度上,我们可以把控制流看作是一个内核线程。本实验让我更加了解进程的创建,调度,上下文切换等相关内容:本实验中,kern_init函数完成虚拟内存的初始化工作后,就调用了proc_init函数,这个函数完成了idleproc内核线程的创建。第0个内核线程idleproc主要工作是完成内核中各个子系统的初始化,然后就通过执行cpu_idle函数开始过退休生活了,但还有其他工作要做,因此就通过调用kernel_thread函数创建了一个内核线程。除调用kernel_thread函数外,还调用了do_fork函数和copy_thread函数来一起创建内核线程initproc。随后idleproc内核线程让出CPU,让schedule将initproc内核线程调度运行,涉及到了进程的切换,而Initprocde的主体函数很简单就是输出一段字符串,然后就返回到kernel_tread_entry函数,并进一步调用do_exit执行退出操作了。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值