说明

  • 阅读的代码是 xv6-riscv 版本的

概述


从一个用户进程(旧进程)切换到另一个用户进程(新进程)所涉及的步骤:

  • 通过中断机制,从trap机制进入内核线程
  • 调用swtch从该进程进行上下文切换,切换到到调度进程
  • 通过调度进程切换到新进程的内核线程
  • 在通过trap机制返回到新进程用户线程

上面过程就简单阐述,是怎样进行进程切换的
当然,里面会有很多细节,后面会详细介绍

从用户线程到内核线程

这一部分在上一章是有过介绍的,通过中断机制,通过uservec保存寄存器,然后进入usertrap()

usertrap

void
usertrap(void)
{int which_dev = 0;if((r_sstatus() & SSTATUS_SPP) != 0)panic("usertrap: not from user mode");//省略 ··········// give up the CPU if this is a timer interrupt.if(which_dev == 2)yield();usertrapret();
}

其他部分就不过多赘述了(上一篇博客讲的很详细),通过判断中断类型,这是一个定时器中断,就调用yield()函数

从旧进程进入调度进程

yield

// Give up the CPU for one scheduling round.
void
yield(void)
{struct proc *p = myproc();acquire(&p->lock);p->state = RUNNABLE;sched();release(&p->lock);
}

该函数干的事情很简单,先获取该进程的锁,将进程状态设置为RUNNABLE
然后调用sched()函数进行后续操作
题:为什么再设置进程状态前,要先获取该进程的锁呢
因为一旦将进程状态设置为RUNNABLE,调度器就会认为该进程处于等待状态的可能会让他接下来运行,但实际上,该进程还是处于运行状态的
如果这是一个多核CPU的,就可能会发生一个进程被两个CPU同时运行,程序可能会立马崩溃。
所以我们要保证改变进程状态,保存寄存器,载入另一个进程的寄存器这三步是原子的。

sched

void
sched(void)
{int intena;struct proc *p = myproc();if(!holding(&p->lock))panic("sched p->lock");if(mycpu()->noff != 1)panic("sched locks");if(p->state == RUNNING)panic("sched running");if(intr_get())panic("sched interruptible");intena = mycpu()->intena;swtch(&p->context, &mycpu()->context);mycpu()->intena = intena;
}
  • 该函数操作也很简单,进行一系列判断(防御性编程)
  • 将mycpu()->intena保存下来,因为切换进程后,他会被改变
  • 调用swtch切换上下文,该函数完成后就会进行入了调度进程
  • 恢复mycpu()->intena(要等到该进程再次拿到CPU)

swtch

这是一段汇编代码,主要干的工作是将该进程的寄存器保存下来,然后载入调度进程的寄存器


.globl swtch
swtch:# 保存寄存器sd ra, 0(a0)# 省略```````````````````sd s11, 104(a0)# 恢复所要切换进程的寄存器ld ra, 0(a1)# 省略```````````````````ld s11, 104(a1)ret

问题:为什么RISC-V中有32个寄存器,但是swtch函数中只保存并恢复了14个寄存器?
因为swtch函数是从C代码调用的,所以我们知道Caller Saved Register会被C编译器保存在当前的栈上。

该函数完成后,就进入了调度进程

调度

scheduler

void
scheduler(void)
{struct proc *p;struct cpu *c = mycpu();c->proc = 0;for(;;){//通过确保设备可以中断来避免死锁。intr_on();for(p = proc; p < &proc[NPROC]; p++) {acquire(&p->lock);if(p->state == RUNNABLE) {// 切换到所选的进(释放它的锁,然后在跳回我们之前重新获取它)p->state = RUNNING;c->proc = p;swtch(&c->context, &p->context);// 进程目前已完成运行。c->proc = 0;}release(&p->lock);}}
}
  • 进程切换过来,是切换到swtch(&c->context, &p->context)这一行的,后面他会先释放锁
    (避免该进程永远无法被调度,该锁是在yield()函数中获取的)

  • scheduler在进程表上循环查找可运行的进程,该进程具有p->state == RUNNABLE。一旦找到一个进程,它将设置CPU当前进程变量c->proc,将该进程标记为RUNINING,然后调用swtch开始运行它

  • scheduler在一开始要获取进程锁,将RUNNABLE进程转换为RUNNING,在内核线程完全运行之前(在swtch之后,例如在yield中)绝不能释放锁。

调用swtch后就会进入另一个进程了

从调度进程进入新进程

该过程也是在完成swtch后完成切换的
他会从sched函数返回

void
sched(void)
{int intena;struct proc *p = myproc();//、、、、、、、、、、、、、、swtch(&p->context, &mycpu()->context);mycpu()->intena = intena;
}

他会返回到swtch处,然后恢复 mycpu()->intena

从内核线程返回用户线程

这后面就是一步步返回,通过trap机制返回用户层,在上一篇博客讲的很详细,不过多赘述

XV6线程第一次调用swtch函数

allocproc

static struct proc*
allocproc(void)
{struct proc *p;//、、、、、、、、、、省略memset(&p->context, 0, sizeof(p->context));p->context.ra = (uint64)forkret;p->context.sp = p->kstack + PGSIZE;return p;
}

在创建进程时,会设置ra和sp
ra很重要,因为这是进程的第一个switch调用会返回的位置。同时因为进程需要有自己的栈,所以ra和sp都被设置了。这里设置的forkret函数就是进程的第一次调用swtch函数会切换到的“另一个”线程位置。

当调度线程将CPU交给该进程时,该进程会从forkret()函数处返回,执行该函数,

// A fork child's very first scheduling by scheduler()
// will swtch to forkret.
void
forkret(void)
{static int first = 1;// Still holding p->lock from scheduler.release(&myproc()->lock);if (first) {// File system initialization must be run in the context of a// regular process (e.g., because it calls sleep), and thus cannot// be run from main().first = 0;fsinit(ROOTDEV);}usertrapret();
}

从代码中看,它的工作其实就是释放调度器之前获取的锁。函数最后的usertrapret函数其实也是一个假的函数,它会使得程序表现的看起来像是从trap中返回,但是对应的trapframe其实也是假的,这样才能跳到用户的第一个指令中。

xv6源码阅读——进程切换相关推荐

  1. xv6源码阅读——中断与异常

    目录 说明 陷入机制概述 Traps from user space 调用逻辑 ecall uservec usertrap usertrapret userret sret Traps from k ...

  2. xv6源码阅读——文件系统

    说明 阅读的代码是 xv6-riscv 版本的 七层结构 xv6文件系统实现分为七层 文件描述符(File descriptor) 路径名(Pathname) 目录(Directory) 索引结点(I ...

  3. Android源码阅读---init进程

    Android源码阅读-init进程 文章目录 Android源码阅读---init进程 1. 编译命令和进程入口 1. init 进程编译命令 2. main函数流程 2. 主函数处理流程 1. 创 ...

  4. mysql 1260,MYSQL 源码阅读 六

    前期节要 MYSQL源码阅读 一 MYSQL源码阅读 二 MYSQL源码阅读 三 MYSQL 源码阅读 四 MYSQL 源码阅读 五 上次有两个问题没搞明白 1 是 为什么一定要开启调试线程 ? 因为 ...

  5. 源码阅读:AFNetworking(十六)——UIWebView+AFNetworking

    该文章阅读的AFNetworking的版本为3.2.0. 这个分类提供了对请求周期进行控制的方法,包括进度监控.成功和失败的回调. 1.接口文件 1.1.属性 /**网络会话管理者对象*/ @prop ...

  6. webpack源码阅读——npm脚本运行webpack与命令行输入webpack的区别

    原文地址:webpack源码阅读--npm脚本执行webpack与命令行输入webpack执行的区别 如有错误,欢迎指正! webpack是目前被大家广为使用的模块打包器.从命令行输入webpack或 ...

  7. 【Dubbo源码阅读系列】之远程服务调用(上)

    今天打算来讲一讲 Dubbo 服务远程调用.笔者在开始看 Dubbo 远程服务相关源码的时候,看的有点迷糊.后来慢慢明白 Dubbo 远程服务的调用的本质就是动态代理模式的一种实现.本地消费者无须知道 ...

  8. Spark源码学习之IDEA源码阅读环境搭建

    软件准备 (1)Java 1.8 (2)Scala 2.11.12(需要在IDEA中安装) (3)Maven 3.8.2(需要在IDEA中配置) (4)Git 2.33 以上软件需要安装好,并进行环境 ...

  9. Mycat源码篇 : 起步,Mycat源码阅读调试环境搭建

    在研究mycat源码之前必须先把环境搭建好.这篇文章的目标就是搭建mycat源码调试环境.环境主要包括: git jdk maven eclipse mysql 这里假设你知道上面的知识点.我们搭建的 ...

  10. DM 源码阅读系列文章(二)整体架构介绍

    2019独角兽企业重金招聘Python工程师标准>>> 作者:张学程 本文为 DM 源码阅读系列文章的第二篇,第一篇文章 简单介绍了 DM 源码阅读的目的和规划,以及 DM 的源码结 ...

最新文章

  1. Go 学习笔记(12)— 字典map定义、初始化、读取字典、删除字典、清空字典、map 按 key 进行有序遍历
  2. R对数秩检验(log rank test)
  3. (转)access和SQL语句的区别
  4. 洛谷——P2083 找人
  5. Kotlin binding+RecyclerView实现支付宝首页更多、应用编辑界面
  6. [js] 写一个方法,实时验证input输入的值是否满足金额如:3.56(最多只有两位小数且只能数字和小数点)的格式,其它特殊字符禁止输入
  7. Windows Server 2016补丁更新机制
  8. @@Autowired依赖注入先后顺序
  9. java做抽奖小程序_随机抽奖小程序
  10. (短除法)求两个给定正整数的最大公约数和最小公倍数。
  11. 『贪心』阿狸和桃子的游戏
  12. 《今日简史》2018比尔盖茨推荐(pdf, mobi, epub三种格式)
  13. 弗洛伊德篇-梦的解析
  14. 2022年(23届)电子信息/通信工程保研|四非上岸浙大的保研之旅(浙大、中科大、哈工大、东南、南开、西电、成电等)
  15. Your project path contains non-ASCII characters. 解决办法
  16. 通过js实现切换背景颜色
  17. 数据结构与算法(陈越版)第五讲 (树下)树的应用——集合及其运算
  18. python调用百度api接口_python调用百度API
  19. vb6.0 CLng,CInt等的bug及其解决办法
  20. 计算机网络虚电路数据报,计算机网络——网络层-虚电路和数据报网络

热门文章

  1. 使用js一行代码解决上网培训弹窗问题
  2. 一些NLP数据/语料下载
  3. 关于绕开百度文库复制限制的那档子事
  4. 焊接工时简便计算工具_焊接工时定额计算手册.doc
  5. 结构力学计算软件_辽宁网格修复软件价格如何
  6. 修复EXE文件无法打开
  7. Python面试必备!最全面的重点知识汇总,建议收藏!
  8. MTK驱动增加支持扫描显示中文SSID
  9. 我的世界f服务器自定义皮肤,我的世界服务器皮肤指令
  10. 苹果手机计算机怎么放桌面,iOS13系统的苹果手机中如何重新布局桌面应用