前言

lab4要求我们实现一个内核线程的初始化和调度

正文

Part0

同样是利用Clion的compare功能直接将lab1,2,3的代码贴到lab4中

Part1 分配并初始化一个进程控制块

我们先看下进程控制块PCB的结构

依次介绍各个参数的含义

  • state:进程状态

  • pid:进程id

  • runs:进程运行时间

  • kstack:进程内核栈

  • need_resched:是否需要调度

  • parent:父进程

  • mm:进程内存控制块,即lab3中控制虚拟内存的结构体,lab4中没有怎么涉及

  • context:进程上下文环境,即一些寄存器

  • tf:中断帧指针,用来存储进程的中断前的状态,因为ucore可以嵌套,所以在进程esp位置后维护了中断链

  • cr3:指向一级页表,也就是页目录

  • flags:进程标志位

  • name:进程的名字

  • list_link:进程用一个双向链表来存储

  • hash_link:当进程很多的时候遍历双向链表效率肯定会很慢,所以维护了一个hash链表用来寻找对应的进程

然后我们要实现的alloc_page()函数要求分配一个proc_struct结构,就是简单的初始化一些数据,代码如下

Part2 为新创建的内核线程分配资源

do_fork过程:

1.分配并初始化进程控制块(alloc_proc 函数);
2.分配并初始化内核栈(setup_stack 函数);
3.根据 clone_flag标志复制或共享进程内存管理结构(copy_mm 函数);
4.设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文
(copy_thread函数);
5.把设置好的进程控制块放入hash_list 和proc_list 两个全局进程链表中;
6.自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
7.设置返回码为子进程的 id号。

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

首先proc_init()中初始化好了idleproc进程,并分配好了initproc的内存堆栈等环境,并设置了进程idleproc的need_resched位为1表示需要被调度,然后在cpu_idle()中一直循环看need_resched是否为1,然后调用schedule函数

local_intr_save,local_intr_restore两个分别是用来屏蔽中断和使能中断,然后在主要部分是首先设置当前进程的need_resched为0,然后在proc_list中寻找第一个state为PROC_RUNNABLE的,没找到就重新指向idleproc,run++,如果找到就调用proc_run函数进行切换。

然后具体看下proc_run,同样先屏蔽中断,然后load_esp0设置任务状态段ts的esp0指针指向next proc的内核栈顶,这个主要是为了保存中断信息,当出现特权切换的时候(从特权态0<-->特权态3,或从特权态3<-->特权态3),正确定位处于特权态0时进程的内核栈的栈顶,而这个栈顶其实放了一个trapframe结构的内存空间,当中断结束时会根据这个保存信息恢复到中断前的状态。

lcr3用来切换页表,将cr3寄存器的值替换为next proc的cr3值,但是因为在lab4时idleproc和initproc共用一个内核页表boot_cr3,所以这里其实是无效的。

最后switch_to用来切换两个进程的context

保存前一个进程的执行现场,前两条汇编指令(如下所示)保存了进程在返回switch_to函数后的指令地址到context.eip中

在接下来的7条汇编指令完成了保存前一个进程的其他7个寄存器到context中的相应成员变量中。至此前一个进程的执行现场保存完毕。再往后是恢复向一个进程的执行现场。

最后的pushl 0(%eax)其实把 context 中保存的下一个进程要执行的指令地址 context.eip 放到了堆栈顶,这样接下来执行最后一条指令“ret”时,会把栈顶的内容赋值给 EIP 寄存器,这样就切换到下一个进程执行了,即当前进程已经是下一个进程了,从而完成了进程的切换。

initproc初始化时设置了initproc->context.eip = (uintptr_t)forkret,这样,当执行switch_to函数并返回后,initproc将执行其实际上的执行入口地址forkret。

forkrets函数首先把esp指向当前进程的中断帧,从_trapret开始执行到iret前,esp指向了current->tf.tf_eip,而如果此时执行的是initproc,则current->tf.tf_eip=kernel_thread_entry,kernel_thread_entry函数

call ebx调用fn函数即init_main即打印字符。

流程总结

从kern/init/init.c中来看

  1. pmm_init()

    (1) 初始化物理内存管理器。
    (2) 初始化空闲页,主要是初始化物理页的 Page 数据结构,以及建立页目录表和页表。
    (3) 初始化 boot_cr3 使之指向了 ucore 内核虚拟空间的页目录表首地址,即页目录的起始物理地址。
    (4) 初始化第一个页表 boot_pgdir。
    (5) 初始化了GDT,即全局描述符表。

  2. pic_init()

    初始化8259A中断控制器

  3. idt_init()

    初始化IDT,即中断描述符表

  4. vmm_init()

    主要就是实验了一个 do_pgfault()函数达到页错误异常处理功能,以及虚拟内存相关的 mm,vma 结构数据的创建/销毁/查找/插入等函数

  5. proc_init()

    这个函数启动了创建内核线程的步骤,完成了 idleproc 内核线程和 initproc 内核线程的创建或复制工作,分配好了内存堆栈空间等信息。

  6. ide_init()

    完成对用于页换入换出的硬盘(简称 swap 硬盘)的初始化工作

  7. swap_init()

    swap_init() 函数首先建立完成页面替换过程的主要功能模块,即 swap_manager ,其中包含了页面置换算法的实现

  8. clock_init()

    时钟初始化

  9. intr_enable()

    开启中断

  10. cpu_idle()

    用来从idleproc切换到initproc

然后我们来看proc_init()

建立hash表,alloc_proc分配空间kmalloc,设置pid,state,kstack,need_resched,kstack直接使用内核栈,need_resched表示需要被调度,然后kernel_thread进行init proc的复制

创建tf指针保存中断信息并传递给do_fork函数,先设置代码段和数据段,指明initproc开始执行的地址tf_eip为kernel_thread_entry,然后看下这个函数

push %edx将fn函数的参数压栈,call *%ebx调用fn函数,push %eax将结果保存在eax,最后调用do_exit函数但是lab4没有涉及do_exit

然后看下do_fork,这个是主要创建复制进程的函数

  1. 分配并初始化进程控制块(alloc_proc函数);
  2. 分配并初始化内核栈(setup_stack函数);
  3. 根据clone_flag标志复制或共享进程内存管理结构(copy_mm函数);
  4. 设置进程在内核(将来也包括用户态)正常运行和调度所需的中断帧和执行上下文(copy_thread函数);
  5. 把设置好的进程控制块放入hash_list和proc_list两个全局进程链表中;
  6. 自此,进程已经准备好执行了,把进程状态设置为“就绪”态;
  7. 设置返回码为子进程的id号。

主要看copy_thread函数

先在新建的进程中分配用来存储中断帧的栈空间,拷贝在kernel_thread中创建的tf到新进程中断帧栈中,设置好esp栈顶指针和eflag位置,eflag表示允许中断,即ucore允许嵌套中断,然后设置init proc的context,当context设置好,ucore切换到底init proc时就需要根据context来执行context.eip存储上次中断之后执行的下一个命令,esp为中断后的栈,但是init proc没有执行过,所以这就是第一次执行的命令和堆栈地址,init proc的执行函数为forkret(处理do_fork函数返回的工作)

esp指向当前进程的中断帧,然后这个中断帧就是在kernel_thread函数中声明的tf.tf_eip = (uint32_t) kernel_thread_entry;当调用kernle_thread_entry就是调用fn,即init main

然后我们继续看,do_fork返回initproc的pid一路往上传递会proc_init函数,最后设置好name等一些不重要的信息,proc_init()结束

kern_init中调用cpu_idle就是我们part3完成的这里不再赘述。

总结

低估了难度和我的懒度。。。多花了两天才搞定的,我好菜啊,挣扎中。