《ucore lab4》实验报告

资源

  1. ucore在线实验指导书
  2. 我的ucore实验代码

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

解答

我的设计实现过程

alloc_proc函数主要是初始化进程控制块,亦即初始化proc_struct结构体的各成员变量。

  • state:进程所处的状态。由于分配进程控制块时,进程还处于创建阶段,因此设置其状态的PROC_UNINIT,表示尚未完成初始化。
  • pid:先设置pid为无效值-1,用户调完alloc_proc函数后再根据实际情况设置pid。
  • cr3:设置为前面已经创建好的页目录表boot_pgdir的物理地址。注意是物理地址,实际编码时应写成PADDR(boot_pgdir)。
  • need_resched:标记是否需要调度其他进程。初始化为0,表示不需调度其他进程。
  • kstack:内核栈地址,先初始化为0,后续根据需要来设置
  • tf:中断帧,先初始化为NULL,后续根据需要来设置

回答问题:context和tf的含义及作用是什么

  1. context是进程上下文,即进程执行时各寄存器的取值。用于进程切换时保存进程上下文比如本实验中,当idle进程被CPU切换出去时,可以将idle进程上下文保存在其proc_struct结构体的context成员中,这样当CPU运行完init进程,再次运行idle进程时,能够恢复现场,继续执行。
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;
};
  1. 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));
  1. trap_frame与context的区别是什么?

    • 从内容上看,trap_frame包含了context的信息,除此之外,trap_frame还保存有段寄存器、中断号、错误码err和状态寄存器eflags等信息。
    • 从作用时机来看,context主要用于进程切换时保存进程上下文,trap_frame主要用于发生中断或异常时保存进程状态。
    • 当进程进行系统调用或发生中断时,会发生特权级转换,这时也会切换栈,因此需要保存栈信息(包括ss和esp)到trap_frame,但不需要更新context。
  2. trap_frame与context在创建进程时所起的作用:
    • 当创建一个新进程时,我们先分配一个进程控制块proc,并设置好其中的tf及context变量;
    • 然后,当调度器schedule调度到该进程时,首先进行上下文切换,这里关键的两个上下文信息是context.eip和context.esp,前者提供新进程的起始入口,后者保存新进程的trap_frame地址。
    • 上下文切换完毕后,CPU会跳转到新进程的起始入口。在新进程的起始入口中,根据trap_frame信息设置通用寄存器和段寄存器的值,并执行真正的处理函数。可见,tf与context共同用于进程的状态保存与恢复。
    • 综上,由上下文切换到执行新进程的处理函数fn,中间经历了多次函数调用:forkret() -> forkrets(current->tf) -> __trapret -> kernel_thread_entry -> init_main.

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

解答

我的设计实现过程

根据注释提供的步骤,很容易完成do_fork函数的实现。这里需要注意的是:如果前面的步骤失败,比如alloc_proc分配进程控制块失败或建立内核栈失败,那么需要释放已申请的资源。

回答问题:ucore是否为每个新fork的线程提供唯一的pid?

首先,本实验不提供线程释放的功能,意味着pid只分配不回收。当fork的线程总数小于MAX_PID时,每个线程的pid是唯一的。当fork的线程总数大于MAX_PID时,后面fork的线程的pid可能与前面的线程重复(暂不确定)。

注:get_pid函数没完全看懂,next_safe的含义不理解?

代码修改

对照答案时,发现自己的代码有几个优化的地方:

  1. 没有设置proc->parent,应将其设置为current
  2. 由于do_fork已经设置了标签,setup_kstack执行失败后直接跳转到bad_fork_cleanup_proc即可,copy_mm失败后直接跳转到bad_fork_cleanup_kstack即可。
  3. copy_thread的第二个输入参数esp应该使用do_fork的第二个输入参数stack。
  4. 将当前进程插入到proc_list和hash_list时需要去使能中断。(为什么?)
  5. 我是将proc插入到proc_list的末尾,而答案是插入到proc_list的开头。为何?是不是因为插入到开头的话,schedule选择要执行的线程时会快些?

我的代码:

    if (NULL == (proc = alloc_proc())) {
        goto fork_out;
    }

    if (0 != setup_kstack(proc)) {
        kfree(proc);
        goto fork_out;
    }

    if (0 != copy_mm(clone_flags, proc)) {
        kfree((void *)proc->kstack);
        kfree(proc);
        goto fork_out;
    }

    proc->pid = get_pid();

    int esp = 0;
    asm volatile ("movl %%esp, %0" : "=r" (esp));

    copy_thread(proc, esp, tf);

    list_add_before(&proc_list, &proc->list_link);

    hash_proc(proc);

    wakeup_proc(proc);

    nr_process++;

答案的代码:

    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);
    {
        proc->pid = get_pid();
        hash_proc(proc);
        list_add(&proc_list, &(proc->list_link));
        nr_process ++;
    }
    local_intr_restore(intr_flag);

    wakeup_proc(proc);

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

题目

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

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

解答

分析proc_run函数

  1. 首先判断要切换到的进程是不是当前进程,若是则不需进行任何处理。
  2. 调用local_intr_save和local_intr_restore函数去使能中断,避免在进程切换过程中出现中断。(疑问:进程切换过程中处理中断会有什么问题?)
  3. 更新current进程为proc
  4. 更新任务状态段的esp0的值(疑问:为什么更新esp0?)
  5. 重新加载cr3寄存器,使页目录表更新为新进程的页目录表
  6. 上下文切换,把当前进程的当前各寄存器的值保存在其proc_struct结构体的context变量中,再把要切换到的进程的proc_struct结构体的context变量加载到各寄存器。
  7. 完成上下文切换后,CPU会根据eip寄存器的值找到下一条指令的地址并执行。根据copy_thread函数可知eip寄存器指向forkret函数,forkret函数的实现为forkrets(current->tf);
  8. forkrets函数的实现如下。首先是把输入变量current->tf复制给%esp,此时栈上保存了tf的值,亦即各寄存器的值。然后在trapret函数中使用popal和popl指令将栈上的内容逐一赋值给相应寄存器。最后执行iret,把栈顶的数据(也就是tf_eip、tf_cs和tf_eflags)依次赋值给eip、cs和eflags寄存器。
.globl __trapret
__trapret:
    # restore registers from stack
    popal

    # restore %ds, %es, %fs and %gs
    popl %gs
    popl %fs
    popl %es
    popl %ds

    # get rid of the trap number and error code
    addl $0x8, %esp
    iret

.globl forkrets
forkrets:
    # set stack to this new process's trapframe
    movl 4(%esp), %esp
    jmp __trapret
  1. 根据kernel_thread函数,可知tf_eip指向kernel_thread_entry,其函数实现如下所示。由于kernel_thread函数中把要执行的函数地址fn保存在ebx寄存器,把输入参数保存到edx寄存器,因此kernel_thread_entry函数先通过pushl %edx将输入参数压栈,然后通过call *%ebx调用函数fn。
.globl kernel_thread_entry
kernel_thread_entry:        # void kernel_thread(void)

    pushl %edx              # push arg
    call *%ebx              # call fn

    pushl %eax              # save the return value of fn(arg)
    call do_exit            # call do_exit to terminate current thread
  1. 根据proc_init函数,可知调用kernel_thread时,输入的fn函数即init_main,输入参数为"Hello world!!"。init_main函数的功能是打印输入字符串及其他内容,其实现如下所示。
init_main(void *arg) {
    cprintf("this initproc, pid = %d, name = \"%s\"\n", current->pid, get_proc_name(current));
    cprintf("To U: \"%s\".\n", (const char *)arg);
    cprintf("To U: \"en.., Bye, Bye. :)\"\n");
    return 0;
}

回答问题1:本实验创建且运行了几个内核线程

答:本实验创建且运行了两个内核线程,分别是idle和init线程。

回答问题2:local_intr_save和local_intr_restore的作用

答:避免在进程切换过程中处理中断。

扩展练习Challenge:实现支持任意大小的内存分配算法(待完成)

这不是本实验的内容,其实是上一次实验内存的扩展,但考虑到现在的slab算法比较复杂,有必要实现一个比较简单的任意大小内存分配算法。可参考本实验中的slab如何调用基于页的内存分配算法(注意,不是要你关注slab的具体实现) 来实现first-fit/best-fit/worst-fit/buddy等支持任意大小的内存分配算法。

【注意】 下面是相关的Linux实现文档,供参考
- SLOB
- SLAB

原文地址:https://www.cnblogs.com/wuhualong/p/ucore_lab4_report.html

时间: 2024-10-30 19:16:29

《ucore lab4》实验报告的相关文章

《MIT 6.828 Lab1: Booting a PC》实验报告

实验内容 熟悉x86汇编语言.QEMU x86仿真器.PC开机引导流程 测试6.828 内核的启动加载器(boot loader) 研究6.828 内核的初始化模板(JOS) 实验题目 Exercise 1:阅读汇编语言资料 Exercise 1. Familiarize yourself with the assembly language materials available on the 6.828 reference page. You don't have to read them

MIT 6.828 学习笔记6 Lab4实验报告

Lab4实验报告 开始之前,为了弄懂 mpconfig 与 lapic,可能需要阅读 Intel processor manual 和 MP Specification,相关资源可以在课程中找到 Execrise 1 Implement mmio_map_region in kern/pmap.c. // mmio_map_region() uintptr_t ret = base; size = ROUNDUP(size, PGSIZE); base = base + size; if (ba

MIT 6.828 学习笔记5 Lab3实验报告

Lab3 实验报告 Exercise 1 Modify mem_init() in kern/pmap.c to allocate and map the envs array. // mem_int() // 第一处 envs = (struct Env *) boot_alloc(NENV * sizeof(struct Env)); memset(pages, 0, NENV * sizeof(struct Env)); // 第二处 boot_map_region(kern_pgdir,

MIT 6.828 JOS学习笔记2. Lab 1 Part 1.2: The kernel

Lab 1 Part 1: PC bootstrap 我们继续~ PC机的物理地址空间 这一节我们将深入的探究到底PC是如何启动的.首先我们看一下通常一个PC的物理地址空间是如何布局的:                           这张图仅仅展示了内存空间的一部分. 第一代PC处理器是16位字长的Intel 8088处理器,这类处理器只能访问1MB的地址空间,即0x00000000~0x000FFFFF.但是这1MB也不是用户都能利用到的,只有低640KB(0x00000000~0x00

操作系统ucore lab1实验报告

操作系统lab1实验报告 [练习1] 理解通过 make 生成执行文件的过程.(要求在报告中写出对下述问题的回答) 在此练习中,大家需要通过阅读代码来了解: 1. 操作系统镜像文件 ucore.img 是如何一步一步生成的?(需要比较详细地解释 Makefile 中 每一条相关命令和命令参数的含义,以及说明命令导致的结果) 2. 一个被系统认为是符合规范的硬盘主引导扇区的特征是什么? [练习1.1] 1.生成ucore.img需要kernel和bootblock 生成ucore.img的代码如下

PC平台逆向破解实验报告(待补充)

PC平台逆向破解实验报告(待补充) 实践目标 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串. 该程序同时包含另一个代码片段,getShell,会返回一个可用Shell.正常情况下这个代码是不会被运行的.我们实践的目标就是想办法运行这个代码片段.我们将学习两种方法运行这个代码片段,然后学习如何注入运行任何Shellcode. 实践内容 手工修改可执行文件,改变程序执行流程,直接跳转到getShell

ST:Lab1实验报告(测试判断三角形边长)

Lab 1 实验报告 一.   实验任务 下载Junit(4.12), Hamcrest(1.3),并在Eclipse上添加这来那个jar包 在Eclipse上安装Eclemma,用来显示测试的覆盖率. 写一个java程序,来判断三角形的形状,并且Junit来对这个程序进行测试. a)       判断三角形问题的描述: 判断三角形的函数传入三个形参int a, int b, int c 来代表三角形的三个边.并且计算并判断三角形是等边三角形.等腰三角形以及三边都不等的三角形. 二.   实验过

二次实验报告:使用Packet Tracer分析应用层协议

个人信息 郑兰艳 201821121064 计算1813 1 实验目的 熟练使用Packet Tracer工具.分析抓到的应用层协议数据包,深入理解应用层协议,包括语法.语义.时序. 2 实验内容 使用Packet Tracer,正确配置网络参数,抓取应用层协议的数据包并分析,协议包含DNS.FTP, DHCP, stmp, pop3.步骤包含: 建立网络拓扑结构 配置参数 抓包 分析数据包 3. 实验报告 (1)建立网络拓扑结构 说明:将一台PC主机与一台服务器连接在一起,建立网络拓扑结构 3

第四次实验报告

北京电子科技学院(BESTI) 实验报告 课程:信息安全系统设计基础   班级:1353 姓名:王剑桥.李雪琦           学号:20135316.20135309 成绩: 指导教师:娄嘉鹏  实验日期:2015.12.01 实验密级:   预习程度:  实验时间:15:30~18:00 仪器组次:  必修/选修:  实验序号:4 实验名称:外设驱动程序设计 实验目的与要求: 1.掌握实时系统应用和驱动程序的编写2.选择某个接口电路 实验仪器: 名称 型号 数量 嵌入式开发平台 UP-N

java第二次实验报告

课程:Java实验   班级:201352     姓名:黄坤  学号:2015226 成绩:           指导教师:娄佳鹏     实验日期:15.05.05 实验密级:         预习程度:       实验时间: 仪器组次:         必修/选修:选修    实验序号:2 实验名称:java面向对象程序设计 实验目的与要求: 1. 初步掌握单元测试和TDD 2. 理解并掌握面向对象三要素:封装.继承.多态 3. 初步掌握UML建模 4. 熟悉S.O.L.I.D原则 5.