操作系统实验:Lab4 内核线程管理

清华大学操作系统Lab4实验报告
课程主页:http://os.cs.tsinghua.edu.cn/oscourse/OS2018spring
实验指导书:https://chyyuu.gitbooks.io/ucore_os_docs/content/
github:https://github.com/chyyuu/ucore_os_lab

实验目的

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

实验内容

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

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

练习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 2015011345
        proc -> state = PROC_UNINIT;
        proc -> pid = -1;
        proc -> cr3 = boot_cr3;  // 内核线程,页表使用boot_cr3
        proc -> mm = NULL;
        proc -> runs = 0;
        proc -> kstack = 0;
        proc -> need_resched = 0;
        proc -> parent = NULL;
        memset(&(proc -> context), 0, sizeof(struct context));
        proc -> tf = NULL;
        proc -> flags = 0;
        memset(proc -> name, 0, PROC_NAME_LEN);
    }
    return proc;
}
请说明proc_struct中struct context contextstruct trapframe *tf成员变量含义和在本实验中的作用。
  • proc_struct中的context:进程的上下文,用于进程切换(switch.S中)。在uCore中,所有的进程在内核中也是相对独立的(例如独立的内核堆栈和上下文等等)。使用context保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中的switch_to

  • proc_struct中的tf:中断帧的指针。总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,uCore内核允许嵌套中断。因此为了保证嵌套中断发生时tf总是能够指向当前的trapframe,uCore在内核栈上维护了tf的链,在trap.c::trap中。

练习2:为新创建的内核线程分配资源

/* do_fork -     parent process for a new child process
 * @clone_flags: used to guide how to clone the child process
 * @stack:       the parent's user stack pointer. if stack==0, It means to fork a kernel thread.
 * @tf:          the trapframe info, which will be copied to child process's proc->tf
 */
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. call alloc_proc to allocate a proc_struct
    proc = alloc_proc();
    if (proc == NULL) {
        goto fork_out;
    }
    proc -> parent = current;
//    2. call setup_kstack to allocate a kernel stack for child process
    int kstack_success = setup_kstack(proc);
    if (kstack_success != 0) {
        goto bad_fork_cleanup_proc;
    }
//    3. call copy_mm to dup OR share mm according clone_flag
    int copy_success = copy_mm(clone_flags, proc);
    if (copy_success != 0) {
        goto bad_fork_cleanup_kstack;
    }
//    4. call copy_thread to setup tf & context in proc_struct
    copy_thread(proc, stack, tf);

    bool intr_flag;
    local_intr_save(intr_flag);
    proc -> pid = get_pid();
//    5. insert proc_struct into hash_list && proc_list
    hash_proc(proc);
    list_add(&proc_list, &(proc -> list_link));
    nr_process++;
    local_intr_restore(intr_flag);
//    6. call wakeup_proc to make the new child process RUNNABLE
    wakeup_proc(proc);
//    7. set ret vaule using child proc's 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?请说明你的分析和理由。

可以保证每个线程的id唯一。具体可见proc.c::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;
}

首先,第一句assert可以保证进程数一定不会多于可以分配的进程标识号的数目。
接下来,函数将扫描所有的进程,找到一个当前没被使用的进程号,存储在last_pid中,作为新进程的进程号。具体来说,循环扫描每一个当前进程:当一个现有的进程号和last_pid相等时,则将last_pid+1;当现有的进程号大于last_pid时,这意味着在已经扫描的进程中[last_pid, min(next_safe, proc->pid)]这段进程号尚未被占用,继续扫描。
这样可以保证返回的新进程号一定没有被占用,即具有唯一的id。

练习3:阅读代码,理解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);
            lcr3(next->cr3);
// 保存原线程的寄存器并恢复待调度线程的寄存器
            switch_to(&(prev->context), &(next->context));
        }
// 恢复中断
        local_intr_restore(intr_flag);
    }
}

保存寄存器和恢复待调度进程的寄存器部分代码在switch_to中,如下:

switch_to:                      # switch_to(from, to)

    # save from's registers
    movl 4(%esp), %eax          # eax points to from
    popl 0(%eax)                # save eip !popl
    movl %esp, 4(%eax)          # save esp::context of from
    movl %ebx, 8(%eax)          # save ebx::context of from
    movl %ecx, 12(%eax)         # save ecx::context of from
    movl %edx, 16(%eax)         # save edx::context of from
    movl %esi, 20(%eax)         # save esi::context of from
    movl %edi, 24(%eax)         # save edi::context of from
    movl %ebp, 28(%eax)         # save ebp::context of from

    # restore to's registers
    movl 4(%esp), %eax          # not 8(%esp): popped return address already
                                # eax now points to to
    movl 28(%eax), %ebp         # restore ebp::context of to
    movl 24(%eax), %edi         # restore edi::context of to
    movl 20(%eax), %esi         # restore esi::context of to
    movl 16(%eax), %edx         # restore edx::context of to
    movl 12(%eax), %ecx         # restore ecx::context of to
    movl 8(%eax), %ebx          # restore ebx::context of to
    movl 4(%eax), %esp          # restore esp::context of to

    pushl 0(%eax)               # push eip

    ret
本实验在执行过程中,创建且运行了几个内核线程

共两个线程:

  • idleproc:ucore的第一个内核线程,完成内核中各个子系统的初始化,之后立即调度,执行其他进程。
  • initproc:“hello world”线程。
语句 local_intr_save(intr_flag);....local_intr_restore(intr_flag);在这里有何作用?请说明理由。

在进程调度开始前关中断,在结束进程调度后开中断。这是为了防止在进程调度过程中产生中断导致进程调度的嵌套。

覆盖的知识点

  • 内核线程的创建和切换。

与参考答案的区别

  • 练习1:只写了一部分初始化,参考答案后发现没有写完整。
  • 练习2:没有使用local_intr_save(intr_flag);....local_intr_restore(intr_flag);,后参考答案后不上。

总结

感谢期中考试让我有机会从头开始搞清楚了各种原理和ucore的前几个lab的很多实现细节。
尽管这次在实验开始前观看了mooc并认真阅读了实验指导书,但是在实现过程中还是有很多问题,有待进一步研究。

你可能感兴趣的:(操作系统实验:Lab4 内核线程管理)