ucore lab4

来源:互联网 发布:ubuntu mate 17.10 编辑:程序博客网 时间:2024/05/21 13:54

练习0:填写已有实验

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

练习1:分配并初始化一个进程控制块(需要编码)

说明proc_struct中 struct context context 和 struct trapframe *tf 成员变量含义和在本实验中的作用是啥?

结构体定义:

enum proc_state {              //进程状态    PROC_UNINIT = 0,           //未初始状态     PROC_SLEEPING,             //睡眠(阻塞)状态     PROC_RUNNABLE,             //运行与就绪态     PROC_ZOMBIE,               //僵死状态};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 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;                  };

宏定义:

#define PROC_NAME_LEN               15

alloc()函数的实现:

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;            //进程PID为-1          proc->runs = 0;            //初始化时间片          proc->kstack = 0;          //内核栈地址          proc->need_resched = 0;    //不需要调度          proc->parent = NULL;       //父进程为空          proc->mm = NULL;           //虚拟内存为空          memset(&(proc->context), 0, sizeof(struct                  context));          //初始化上下文          proc->tf = NULL;           //中断帧指针为空          proc->cr3 = boot_cr3;      //页目录为内核页目录表的基址          proc->flags = 0;           //标志位为0          memset(proc->name, 0, PROC_NAME_LEN);//进程名为0    }    return proc;}

总结一下过程:

1 在堆上分配一块内存空间用来存放进程控制块

2 初始化进程控制块内的各个参数

3 返回分配的进程控制块

问题一:struct context context和struct trapframe *tf 成员 变量的含义和作用

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

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

练习2:为新创建的内核线程分配资源(需要编码)

请说明ucore是否做到给每个新fork的线程一个唯一的id?请说明你的分析和理由。

函数定义:

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;}static int  //该函数在本次实验并没有实现copy_mm(uint32_t clone_flags, struct proc_struct *proc) {    assert(current->mm == NULL); //判断当前函数的虚拟内存非空    return 0;}static voidcopy_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);}voidintr_disable(void) { //禁止中断函数    cli(); //禁止中断}static inline bool__intr_save(void) {    if (read_eflags() & FL_IF){ //如果允许屏蔽中断,即IF=1.则中断        intr_disable(); //禁止中断        return 1;    }    return 0;}static inline void__intr_restore(bool flag) { //如果中断被屏蔽,则恢复中断    if (flag) {        intr_enable();  //恢复中断    }}

eflags寄存器

这里写图片描述

宏定义:

#define local_intr_save(x)      \                  do { x = __intr_save(); } while (0)#define FL_IF           0x00000200 #define local_intr_restore(x)   __intr_restore(x);

do_fork()函数的实现:

intdo_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) { //分配进程数大于4096,返回        goto fork_out;  //返回    }    ret = -E_NO_MEM;  //因内存不足而分配失败if ((proc = alloc_proc()) == NULL) { //分配内存失败        goto fork_out; //返回    }    proc->parent = current; //设置父进程名字    if (setup_kstack(proc) != 0) {//分配内核栈        goto bad_fork_cleanup_proc; //返回    }    if (copy_mm(clone_flags, proc) != 0) { //复制父进程内存信息        goto bad_fork_cleanup_kstack; //返回    }    copy_thread(proc, stack, tf); //复制中断帧和上下文信息    bool intr_flag;     local_intr_save(intr_flag);  //屏蔽中断,intr_flag置为1    {        proc->pid = get_pid(); //获取当前进程PID        hash_proc(proc);  //建立hash映射        list_add(&proc_list,&(proc->list_link));//加入进程链表        nr_process ++;  //进程数加一    }    local_intr_restore(intr_flag); //恢复中断    wakeup_proc(proc); //唤醒新进程    ret = proc->pid; //返回当前进程的PIDfork_out:  //已分配进程数大于4096    return ret;bad_fork_cleanup_kstack: //分配内核栈失败    put_kstack(proc);bad_fork_cleanup_proc:     kfree(proc);    goto fork_out;}

问题:ucore是否做到给每个新fork的线程一个唯一的id?

查看get_pid函数

static intget_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;}

从上面的代码可以看出。在使用 fork 或 clone 系统调用时产生的进程均会由内核分配一个新的唯一的PID值。具体来说,就是在分配PID时,设置一个保护锁,暂时不允许中断,这样在就唯一地分配了一个PID。

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

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

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

首先分析schedule函数的源码:

/* 宏定义:   #define le2proc(le, member)         \    to_struct((le), struct proc_struct, member)*/voidschedule(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 运行新进程,允许中断

再分析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函数的源码:

voidproc_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); //允许中断    }}

所以整个函数的执行过程为:

1 屏蔽中断

2 修改esp0,页表项和进行上下文切换

3 允许中断

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

idleproc: ucore: 第一个内核进程,完成内核中各个子系统的初始化,之后立即调度,执行其他进程。

initproc:用于完成实验的功能而调度的内核进程。

问题二:语句A在这里有何作用?请说明理由。

屏蔽中断和打开中断,以免进程切换时其他进程再进行调度。

实验结果:

这里写图片描述

原创粉丝点击