操作系统lab4实验报告

实验四:内核线程管理


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

本次实验将首先接触的是内核线程的管理。内核线程是一种特殊的进程,内核线程与用户进程的区别有两个:

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

实验目的:

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

实验内容:


练习0:

本次实验依赖之前lab1,lab2以及lab3的内容,用meld工具对比,复制文件即可,这里不再过多赘述。

练习1:分配并初始化一个进程控制块

alloc_proc函数(位于kern/process/proc.c中)负责分配并返回一个新的struct proc_struct结构,用于存储新建立的内核线程的管理信息。ucore需要对这个结构进行最基本的初始化,完成这个初始化过程。

首先我们在proc.h中查看进程的数据结构proc_struct

操作系统lab4实验报告_第1张图片下面对参数进行简单的讲解:

(1) mm:内存管理的信息,包括内存映射列表、页表指针等。
(2) state:进程所处的状态。
(3) parent:用户进程的父进程(创建它的进程)。
(4) kstack:记录了分配给该进程/线程的内核桟的位置。
(5) need_resched:是否需要调度
(6) context:进程的上下文,用于进程切换
(7) tf:中断帧的指针
(8) cr3: cr3 保存页表的物理地址

设计实现:

需要初始化的proc_struct结构中的成员变量至少包括:state/pid/runs/kstack/need_resched/parent/mm/context/tf/cr3/flags/name。

对除了具有特殊值的几个成员变量以外,其余均置0;
根据提示,补全代码部分如下:

操作系统lab4实验报告_第2张图片

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

①context:进程的上下文,用于进程切换。起到的作用就是保存了现场。在 ucore中,所有的进程在内核中也是相对独立的,因此context 保存寄存器的目的就在于在内核态中能够进行上下文之间的切换。实际利用context进行上下文切换的函数是在kern/process/switch.S中定义switch_to。

② tf:中断帧的指针,总是指向内核栈的某个位置:当进程从用户空间跳到内核空间时,中断帧记录了进程在被中断前的状态。当内核需要跳回用户空间时,需要调整中断帧以恢复让进程继续执行的各寄存器值。除此之外,ucore内核允许嵌套中断。因此为了保证嵌套中断发生时tf 总是能够指向当前的tf,ucore 在内核栈上维护了 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,首先获得一块用户信息块。
  • 为进程分配一个内核栈。
  • 复制原进程的内存管理信息到新进程(但内核线程不必做此事)
  • 复制原进程上下文到新进程
  • 将新进程添加到进程列表
  • 唤醒新进程
  • 返回新进程号

设计实现:

在本次练习中,主要需要实现的代码位于proc.c的do_fork函数中,该函数的语义为为内核线程创建新的线程控制块,并且对控制块中的每个成员变量进行正确的设置,使得之后可以正确切换到对应的线程中执行;接下来将结合具体的代码来说明本次练习的具体实现过程:
操作系统lab4实验报告_第3张图片
首先,申请内存块,如果失败,直接返回处理,并将子进程的父节点设置为当前进程 。
接着为进程分配一个内核栈。
复制父进程的内存信息到子进程
复制父进程相关寄存器信息(上下文)
将新进程添加到进程列表(此过程需要加保护锁)
建立散列映射方便查找
将进程链节点加入进程列表
之后把进程数+1
等一切准备就绪,唤醒子进程
最后设置返回的子进程号

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

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

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

接下来对proc_run函数进行分析:

首先注意到在本实验框架中,唯一调用到这个函数是在线程调度器的schedule函数中,也就是可以推测proc_run的语义就是将当前的CPU的控制权交给指定的线程;
接下来结合代码分析函数的内部构成:

操作系统lab4实验报告_第4张图片可以看到proc_run中首先进行了TSS以及cr3寄存器的设置,然后调用到了swtich_to函数来切换线程,根据上文中对switch_to函数的分析可以知道,在调用该函数之后,首先会恢复要运行的线程的上下文,然后由于恢复的上下文中已经将返回地址(copy_thread函数中完成)修改成了forkret函数的地址(如果这个线程是第一运行的话,否则就是切换到这个线程被切换出来的地址),也就是会跳转到这个函数,最后进一步跳转到了__trapsret函数,调用iret最终将控制权切换到新的线程;

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

总共创建了两个内核线程,分别为:

  • idleproc: 最初的内核线程,在完成新的内核线程的创建以及各种初始化工作之后,进入死循环,用于调度其他线程;
  • initproc: 被创建用于打印"Hello World"的线程;

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

该语句的左右是关闭中断,使得在这个语句块内的内容不会被中断打断,这就使得某些关键的代码不会被打断,从而不会一起不必要的错误;

实验结果:

执行make qemu,make grade后结果如下图:
操作系统lab4实验报告_第5张图片操作系统lab4实验报告_第6张图片实验结果符合预期。

实验中涉及的知识点列举

在本次实验中设计到的知识点有:

线程控制块的概念以及组成;
切换不同线程的方法;

对应到的OS中的知识点有:

对内核线程的管理;
对内核线程之间的切换;

这两者之间的关系为,前者为后者在OS中的具体实现提供了基础;
实验中未涉及的知识点列举

在本次实验中未涉及的知识点有:

OS的启动过程;
OS中对物理、虚拟内存的管理;
OS中对用户进程的管理;
OS中对线程/进程的调度;

总结

本次实验主要针对内核线程的管理,所有内核线程直接使用共同的ucore内核内存空间,而用户进程需要维护各自的用户内存空间。以及了解到了进程切换的相关细节操作,更加深一步的了解了操作系统。

challenge

对比first-bit/best-fit/worst-fit/slab以及buddy这几种算法的特点,无需编程

一、首次适应算法(First Fit):该算法从空闲分区链首开始查找,直至找到一个能满足其大小要求的空闲分区为止。然后再按 照作业的大小,从该分区中划出一块内存分配给请求者,余下的空闲分区仍留在空闲分区链 中。
特点: 该算法倾向于使用内存中低地址部分的空闲区,在高地址部分的空闲区很少被利用,从而保留了高地址部分的大空闲 区。显然为以后到达的大作业分配大的内存空间创造了条件。
缺点:低地址部分不断被划分,留下许多难以利用、很小的空闲区,而每次查找又都从低地址部分开始,会增加查找的开销。
二、循环首次适应算法(Next Fit):该算法是由首次适应算法演变而成的。在为进程分配内存空间时,不再每次从链首开始查 找,直至找到一个能满足要求的空闲分区,并从中划出一块来分给作业。
特点:使内存中的空闲分区分布的更为均匀,减少了查找时的系统开销。
缺点:缺乏大的空闲分区,从而导致不能装入大型作业。
三、最佳适应算法(Best Fit):该算法总是把既能满足要求,又是最小的空闲分区分配给作业。为了加速查找,该算法要求将 所有的空闲区按其大小排序后,以递增顺序形成一个空白链。这样每次找到的第一个满足要求的空闲区,必然是最优的。孤立 地看,该算法似乎是最优的,但事实上并不一定。因为每次分配后剩余的空间一定是最小的,在存储器中将留下许多难以利用 的小空闲区。同时每次分配后必须重新排序,这也带来了一定的开销。
特点:每次分配给文件的都是最合适该文件大小的分区。
缺点:内存中留下许多难以利用的小的空闲区。
四、最坏适应算法(Worst Fit):该算法按大小递减的顺序形成空闲区链,分配时直接从空闲区链的第一个空闲区中分配(不能 满足需要则不分配)。很显然,如果第一个空闲分区不能满足,那么再没有空闲分区能满足需要。这种分配方法初看起来不太 合理,但它也有很强的直观吸引力:在大空闲区中放入程序后,剩下的空闲区常常也很大,于是还能装下一个较大的新程序。
最坏适应算法与最佳适应算法的排序正好相反,它的队列指针总是指向最大的空闲区,在进行分配时,总是从最大的空闲 区开始查寻。
该算法克服了最佳适应算法留下的许多小的碎片的不足,但保留大的空闲区的可能性减小了,而且空闲区回收也和最佳适 应算法一样复杂。
特点:给文件分配分区后剩下的空闲区不至于太小,产生碎片的几率最小,对中小型文件分配分区操作有利。
缺点:使存储器中缺乏大的空闲区,对大型文件的分区分配不利。
五、伙伴算法
伙伴算法会浪费大量的内存,(如果需要大小为9的内存块必须分配大小为16的内存块).而优点也是明显的,分配和合并算法都很简单易行.但是,当分配和回收较快的时候,例如分配大小为9的内存块,此时分配16,然后又回收,即合并伙伴内存块,这样会造成不必要的cpu浪费,应该设置链表中内存块的低潮个数,即当链表中内存块个数小于某个值的时候,并不合并伙伴内存块,只要当高于低潮个数的时候才合并.
六、slab算法
采用buddy算法,解决了外碎片问题,这种方法适合大块内存请求,不适合小内存区请求,与传统的内存管理模式相比, slab缓存分配器提供了很多优点。首先,内核通常依赖于对小对象的分配,它们会在系统生命周期内进行无数次分配。slab缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。slab分配器还支持通用对象的初始化,从而避免了为同一目而对一个对象重复进行初始化。最后,slab分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓存行,从而提高缓存的利用率并获得更好的性能。

你可能感兴趣的:(操作系统lab4实验报告)