ucore step by step
tutorial
tutorial
  • 欢迎来到ucore step-by-step的世界
  • lab0: 预备起
    • 溯源: ucore的历史
    • 概览: 指导书的结构
    • 开搞: 实验环境搭建
  • lab0.5: 比麻雀更小的麻雀(最小可执行内核)
    • 内存布局,OpenSBI,elf和bin
    • 链接脚本和入口点
    • "真正的"入口点
    • 从SBI到stdio
    • Just make it
    • 项目组成和执行流
  • lab1: 断, 都可以断
    • 掉进兔子洞(中断入口点)
    • 中断处理程序
    • 滴答滴答(时钟中断)
    • 项目组成和执行流
  • lab2: 物理内存和页表
    • 内核初始映射
    • 物理内存管理
    • 页面分配算法
    • 项目组成和执行流
  • lab3: 缺页异常和页面置换
    • 扫清外围
    • 使用多级页表
    • 页面置换机制
    • FIFO置换算法
    • 项目组成和执行流
  • lab4: 进程管理
    • 进程与线程
    • 相关数据结构
    • 进程模块初始化
    • 进程切换
    • 项目组成和执行流
  • lab5: 用户程序
    • 用户程序
    • system call!
    • 中断处理
    • 项目组成和执行流
  • lab6: 进程调度
    • 再次认识进程切换
    • 调度算法框架
    • 项目组成和执行流
  • lab7: 同步互斥
    • 同步互斥的基本概念
    • 信号量
    • 条件变量与管程
    • 项目组成和执行流
  • lab8: 文件系统
    • 文件系统抽象层VFS
    • 硬盘文件系统SFS
    • 设备即文件
    • 从zhong duan 到 zhong duan
    • 项目组成和执行流
  • 练习题
    • lab1
    • lab2
    • lab3
    • lab4
    • lab5
    • lab6
    • lab7
    • lab8
  • 附录
    • makefile
由 GitBook 提供支持
在本页

这有帮助吗?

  1. lab4: 进程管理

进程切换

在这一节中我们来看一看进程切换的全过程。

在ucore完成初始化后,ucore内核就没事做了,于是进入了“idle”的状态(这也是我们给第0个进程起名叫idle的原因)。这是,它会陷入死循环,不断检查自己是否需要调度:

void
cpu_idle(void) {
    while (1) {
        if (current->need_resched) {
            schedule();
            ......

因为我们之前在初始化中把idle进程的need_resched设为了1,所以其总会调用schedule函数来检查是否有进程可以调度。我们已经初始化了另外一个内核进程,所以调度器发现了这个进程,并且准备调度到这个进程。

我们实现的schedule函数非常的简单:当需要调度的时候,把当前的进程放在队尾,从队列中取出第一个可以运行的进程,切换到它运行。这就是FIFO调度算法。schedule函数会调用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;
            lcr3(next->cr3);
            switch_to(&(prev->context), &(next->context));
        }
        local_intr_restore(intr_flag);
    }
}

函数中主要进行了三个操作:

  1. 将当前运行的进程设置为要切换过去的进程

  2. 将页表换成新进程的页表

  3. 使用switch_to切换到新进程

switch_to函数如下:

.text
# void switch_to(struct proc_struct* from, struct proc_struct* to)
.globl switch_to
switch_to:
    # save from's registers
    STORE ra, 0*REGBYTES(a0)
    STORE sp, 1*REGBYTES(a0)
    STORE s0, 2*REGBYTES(a0)
    STORE s1, 3*REGBYTES(a0)
    STORE s2, 4*REGBYTES(a0)
    STORE s3, 5*REGBYTES(a0)
    STORE s4, 6*REGBYTES(a0)
    STORE s5, 7*REGBYTES(a0)
    STORE s6, 8*REGBYTES(a0)
    STORE s7, 9*REGBYTES(a0)
    STORE s8, 10*REGBYTES(a0)
    STORE s9, 11*REGBYTES(a0)
    STORE s10, 12*REGBYTES(a0)
    STORE s11, 13*REGBYTES(a0)

    # restore to's registers
    LOAD ra, 0*REGBYTES(a1)
    LOAD sp, 1*REGBYTES(a1)
    LOAD s0, 2*REGBYTES(a1)
    LOAD s1, 3*REGBYTES(a1)
    LOAD s2, 4*REGBYTES(a1)
    LOAD s3, 5*REGBYTES(a1)
    LOAD s4, 6*REGBYTES(a1)
    LOAD s5, 7*REGBYTES(a1)
    LOAD s6, 8*REGBYTES(a1)
    LOAD s7, 9*REGBYTES(a1)
    LOAD s8, 10*REGBYTES(a1)
    LOAD s9, 11*REGBYTES(a1)
    LOAD s10, 12*REGBYTES(a1)
    LOAD s11, 13*REGBYTES(a1)

    ret

可以看出来这段代码就是将需要保存的寄存器进行保存和调换。在之前我们也已经谈到过了,这里只需要调换被调用者保存寄存器即可。由于我们在初始化时把上下文的ra寄存器设定成了forkret函数的入口,所以这里会返回到forkret函数,进一步进入到forkrets。forkrets函数很短:

    .globl forkrets
forkrets:
    # set stack to this new process's trapframe
    move sp, a0
    j __trapret

这里把传进来的参数,也就是进程的中断帧放在了sp,这样在__trapret中就可以直接从中断帧里面恢复所有的寄存器啦!我们在初始化的时候对于中断帧做了一点手脚,epc寄存器指向的是kernel_thread_entry,s0寄存器里放的是新进程要执行的函数,s1寄存器里放的是传给函数的参数。在kernel_thread_entry函数中:

.text
.globl kernel_thread_entry
kernel_thread_entry:        # void kernel_thread(void)
    move a0, s1
    jalr s0

    jal do_exit

我们把参数放在了a0寄存器,并跳转到s0执行我们指定的函数!这样,一个进程的初始化就完成了。至此,我们实现了基本的进程管理,并且成功创建并切换到了我们的第一个内核进程。

上一页进程模块初始化下一页项目组成和执行流

最后更新于5年前

这有帮助吗?

lab4的代码可以在找到

这里