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

2018-04-23  本文已影响0人  wenj1997

清华大学操作系统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。
本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:

练习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成员变量含义和在本实验中的作用。

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

共两个线程:

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

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

覆盖的知识点

与参考答案的区别

总结

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

上一篇 下一篇

猜你喜欢

热点阅读