Soyi L9 2020-04-28 15:00:17 lab4 :内核线程
179 0

“操作系统实验-基于uCore OS(厦门大学)”实验报告

lab4 :内核线程

实验报告 —— lab4

姓名:宋亦婷  

学号:22920172204201

实验目的

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

实验内容

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

实验练习

练习0:填写已有实验

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

  • ~/lab3/kern/kdebug.c 部分;

  • ~/lab3/kern/trap.c 部分;

  • ~/lab3/kern/mm/default_pmm.c部分;

  • ~/lab3/kern/mm/pcc.m部分;

  • ~/lab3/kern/mm/swap_fifo.c部分;

  • ~/lab3/kern/mm/vmm.c部分。

    可以使用一些工具直接进行文件对比,例如可以用Eclipse,直接使用Compare With将lab3中的代码搬入lab4。

练习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。

实验如下:

#Step1:查看所需要填写部分的内容:

图片描述

#Step2:查看proc.h中定义了进程控制块的结构体 proc_struct:

图片描述

#Step3:初始化进程控制块如下:

// 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;        //未初始化进程id=-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;
}
copy

回答以下问题:

  • 请说明proc_struct中struct context contextstruct trapframe *tf成员变量含义和在本实验中的作用是啥?(提示通过看代码和编程调试可以判断出来)

  • struct context context:进程的上下文,用于进行的切换。又因为在系统中,所有的进程在内核中也是相对独立的,即使用context保存寄存器可以使得在内核态中进行上下文间的切换。

  • struct trapframe *tf:中断帧的指针,用于保存当进程用用户空间跳到内核空间时,前一个被中断或异常打断的进程的状态信息;当内核空间需要跳回用户空间时,需调整中断帧以恢复让进程继续执行的各寄存器值。在实验系统中,内核允许嵌套中断,即为了保证嵌套中断发生时 *tf 能指向当前的trapftame,系统在内核栈上维护了 *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,首先获得一块用户信息块。
  • 为进程分配一个内核栈。
  • 复制原进程的内存管理信息到新进程(但内核线程不必做此事)
  • 复制原进程上下文到新进程
  • 将新进程添加到进程列表
  • 唤醒新进程
  • 返回新进程号

实验如下:

#Step1:查看kernel_thread()部分:

图片描述

#Step2:查看该部分注释有:

图片描述

对注释进行分析:

1.分配并初始化进程控制块。

2.分配并初始化内核栈。

3.根据* clone_flag标志复制或共享进程内存管理结构。

4.设置进程在内核正常运行和调度所需的中断帧和执行上下文。

5.将设置好的进程控制块放入hash_listproc_list两个全局进程链表中。

6.将进程状态设置为就绪。

7.设置返回码为子进程的id号。

#Step3:根据注释和分析补全代码有:

/* 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;
}
copy

回答以下问题:

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

查看get_pid部分函数:

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;
}
copy

分析有:

  • 第一句assert可以保证进程数一定不会多于可以分配的进程标识号的数目。

  • 函数将扫描所有的进程,找到一个当前没被使用的进程号,存储在last_pid中,作为新进程的进程号。即循环扫描每一个当前进程,当一个现有的进程号和last_pid相等时,则将last_pid+1;当现有的进程号大于last_pid时,这意味着在已经扫描的进程中[last_pid, min(next_safe, proc->pid)]这段进程号尚未被占用,继续扫描。这样可以保证返回的新进程号一定没有被占用,即具有唯一的id。

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

实验要求:

请简要说明你对proc_run函数的分析。

实验如下:

查看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);
            lcr3(next->cr3);
// 保存原线程的寄存器并恢复待调度线程的寄存器
            switch_to(&(prev->context), &(next->context));
        }
// 恢复中断
        local_intr_restore(intr_flag);
   }
}
copy

保存寄存器和恢复待调度进程的寄存器部分代码在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
copy

回答以下问题:

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

    创建了两个线程,分别为:

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

    (2)initproc:“hello world”线程。

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

    该语句为屏蔽中断操作语句,在此作用为:在进程调度开始前关中断,在结束进程调度后开中断,即为了防止在进程调度过程中产生中断导致进程调度的嵌套。

实验总结

本次实验主要了解了内核线程的创建和切换,知道了内核线程直接使用共同的ucore内核内存空间,且内核线程是一种特殊的线程,其与用户线程存在一定的区别。

最新评论
暂无评论~