multiplexing

xv6的进程调度通过两个机制实现:程序通过调用sleep/wakeup进行的自主切换 和 定时中断驱动的强制切换

要进行对用户进程透明的方式进行进程切换,就需要进行上下文切换,而为了维护切换过程中的不变量(invariant),还需要在适当的时候加锁

xv6中上下文切换如图:

用户进程trap到该进程的内核线程(内核态),然后进行上下文切换(这里的切换,和前面的切换,区别在于,后者是通过函数调用switch函数,而前者不是,所以后者切换时只需要保存callee-saved register和ra,sp,而前者需要保存全部的寄存器)到cpu的调度器线程,调度器线程选出下一个要运行的进程,上下文切换到该进程的内核栈,然后trap return到该进程的用户线程,这样,完成了一个进程到另一个进程的切换

code:context switching

上下文切换需要保存原进程的寄存器(内存不用管,因为不同进程的内存不会重叠,他们映射到不同区域),我们需要保存的是callee-saved寄存器,以及返回地址ra,栈顶指针sp,根据riscv的函数调用约定,他们是:

// Saved registers for kernel context switches.
struct context {uint64 ra;uint64 sp;// callee-saveduint64 s0;uint64 s1;uint64 s2;uint64 s3;uint64 s4;uint64 s5;uint64 s6;uint64 s7;uint64 s8;uint64 s9;uint64 s10;uint64 s11;
};

真正执行保存恢复寄存器的是switch.S,他有两个参数,分别指向旧进程context和新进程context(保存在a0,a1里)

swtch:sd ra, 0(a0)sd sp, 8(a0)sd s0, 16(a0)sd s1, 24(a0)sd s2, 32(a0)sd s3, 40(a0)sd s4, 48(a0)sd s5, 56(a0)sd s6, 64(a0)sd s7, 72(a0)sd s8, 80(a0)sd s9, 88(a0)sd s10, 96(a0)sd s11, 104(a0)ld ra, 0(a1)ld sp, 8(a1)ld s0, 16(a1)ld s1, 24(a1)ld s2, 32(a1)ld s3, 40(a1)ld s4, 48(a1)ld s5, 56(a1)ld s6, 64(a1)ld s7, 72(a1)ld s8, 80(a1)ld s9, 88(a1)ld s10, 96(a1)ld s11, 104(a1)ret

switch.S的内容很简单,即根据结构体不同成员变量的偏移量(都是uint64,所以都是相隔8个字节)进行保存,加载

值得注意的是,最后面的ret,它将ra的内容设置为pc,注意这里的ra是要切换到的进程之前执行switch的时候保存的ra(可以看到基地址分别时a0和a1,对应第一第二个函数参数),也就是此次执行完switch之后,不是接着当前位置的下一行运行,而是另一处switch的下一行运行,举xv6的做法为例说明:

xv6中只有两处调用switch:

void
sched(void)
{....swtch(&p->context, &mycpu()->scheduler);....
}
void
scheduler(void)
{...swtch(&c->scheduler, &p->context);...
}

可以看出这里没有两个用户进程之间的直接切换,只有用户进程和调度器线程之间的切换

xv6中要主动让出cpu的进程都是通过调用exit/sleep/yield,间接调用sched,从而实现切换到调度器线程,再由调度器线程选出并切换到一个runnable

此次执行完switch之后,不是接着当前位置的下一行运行,而是另一处switch的下一行运行

再回答这个问题:在sched中调用switch,switch完成并返回后,接着在scheduler中调用switch的下一行运行,然后遍历所有进程,找到一个runnable,再switch到该runnable进程,而这次switch完成后,则是从该进程context中ra指向的位置开始运行(不一定是sched中的switch,对于一个被fork产生的进程,他的ra被设置为forkret,所以他被调度之后,是去执行forkret函数)

code:scheduling

scheduler运行的方式很简单:选一个runnable,然后switch

本节主要关注上面提到过的invariant:

  • 如果一个进程的状态为RUNNING,那么定时中断导致的yield必须能够安全的让他让出cpu,这意味着cpu寄存器此时必须保存该进程的上下文(否则switch就无法保存该进程的上下文),并且cpu->proc(即当前cpu的当前进程)必须指向他
  • 如果一个进程状态为RUNNABLE,那么scheduler必须能够安全的调度,运行它,这意味着,该进程的context(指该结构体)必须保存了该进程让出cpu时的上下文,并且没有cpu在该进程的栈上运行,cpu->proc没有指向它

但在一些情况下,这些invariant是不成立的:比如,调用yield之后,进程状态被改为了RUNNABLE,但是它的上下文没有保存在context中,cpu->proc还指向它,cpu仍然在该栈上运行,因此,scheduler此时不能安全的调度它,为此,yield在修改状态为RUNNABLE之前,必须先获取该进程的锁,这样,调度器线程就就不能切换到它

同样,还有scheduler中,设置状态为RUNNING,那么直到运行该进程之前,都要持有锁

TODO:是怎么做到在yield中获取锁,在scheduler中释放的?两边的p对应的是一个进程吗?为什么?

update:对应lec里确实说p指向同一个进程

再更新:

get到了,因为scheduler在找到一个runnable之后,将c->proc设为该进程,然后switch到该进程,该进程运行一段时间后,主动或者被动放弃cpu,在switch之前他会获取当前进程也就是自己的锁,这时候是通过访问c->proc得到当前进程p,而这个p就是之前scheduler调用它时设置c->proc的值,所以它再switch到scheduler时,p仍然指向的是它,switch到调度器线程之后,就能恰好释放掉这个进程的锁
至于为什么要这么做,是因为这段时间中invariant是不成立的,所以需要一直持有锁

code:mycpu和myproc

因为环境是多核,所以无法用一个全局指针指向当前进程,而要为每个cpu维护一个指针,指向该cpu的当前进程,但要找到当前进程,我们首先要找到当前cpu的信息,我们利用每个cpu的tp寄存器,保存当前cpu的id

但即使如此,要确保cpu的tp永远保存cpu的id,也有一些复杂,因为用户进程可能会修改tp寄存器:

首先在start中,为每个cpu的tp寄存器赋值,设置为对应的cpuid。usertrapret将tp的值保存在该进程的trapframe中,在userret中,将trapframe中的tp加载到tp寄存器,重新trap到内核态时,又将tp保存到trapframe(?) 编译器保证不使用tp(但是用户进程可能修改tp?

调用cpuid时,可能因为定时中断,导致进程放弃当前cpu,然后又被另一个cpu的调度器线程选中运行,从而cpuid返回的值不在正确(?

为了避免这个问题,调用cpuid和mycpu时,需要禁用中断

但对于myproc来说,它返回p时,已经启用了中断,因为,即使运行它的cpu改变了,当前proc仍然是它,没有错

sleep和wakeup

sleep和wakeup可以让我们自主的进行进程切换

考虑下面PV操作的实现:

假设只有一个生产者(P),一个消费者,他们在不同的cpu上工作,假设这个实现可以正常工作(编译器没有激进优化)

在P中,当count为0时,一直在循环,如果V很少执行,那么P将长期轮循,很浪费cpu资源,我们需要让P在等待过程中让出cpu,在条件满足后重新开始运行

假设我们改变实现为:

(其中sleep让出cpu,进程进入SLEEPING状态,设置进程属性chan为s。wakeup遍历所有进程,找到所以处于SLEEPING并且chan为s的,将其唤醒(改变状态为RUNNABLE))

此时,虽然P确实能在不满足条件时让出cpu,但是,在并行环境中,如果P刚执行完212行,发现count为0,还没执行213行,而V此时执行完205行,wakeup发现没有进程处于SLEEPING并且chan为s(P还没sleep),所以没有唤醒任何进程,直接返回。但是P在之后执行sleep,发生了所谓lost wakeup的现象,此时除非V再执行一次,否则p将持续睡眠,尽管此时count非0

发生这种现象,是因为invariant:p仅在count为0时sleep ,被V打破了

和之前一样,要保持invariant,可以使用锁:我们可以利用锁,将检查count和sleep作为一个原子操作

但是这样也带来了问题,因为如果检查出coun为0,那么sleep,而sleep之后仍然持有锁,而V无法获取到锁, 也就无法wakeup

因此我们需要修改sleep的接口,让sleep在成功休眠(状态为SLEEPING)之后释放锁:

这样,V就能获取锁,增加count后发现处于sleeping的P,并唤醒

但注意,我们必须将改变p的状态为SLEEPING和释放锁这两者作为一个原子操作:如果我们先释放锁,仍然可能会遭遇lost wakeup

如果先修改状态为SLEEPING 然后释放锁 然后调用sched呢?TODO

code:sleep 和 wakeup

xv6中sleep和wakeup的实现:

sleep:释放传入的condition lock,获取进程锁 , 然后设置chan,chan一般是参与到等待的数据结构的地址,设置状态为SLEEPING,然后调用sched

wakeup:遍历所有进程,获取进程锁,判断是否为SLEEPING并且chan是否匹配,如果是,那么修改状态,并且释放锁

之前提到,获取condition lock,是为了防止V在不正确的时候调用wakeup,因为wakeup要唤醒进程,需要获取进程锁,所以一旦sleep持有进程锁,他就可以释放condition lock了,这样也不会lost wakeup

(sleep的进程锁何时释放?如之前所说,在scheduler中,释放)

有时候,多个sleeping进程的chan相同,一次wakeup会全部唤醒他们,首先运行的会获取condition lock,然后“消耗”掉条件,那么其他进程可能发现无法获取锁(假如首先运行的进程还没有释放锁),或者能获取锁,但是发现条件不满足了,对他们来说,这个唤醒是没意义的,会重新陷入睡眠(因此,sleep总是在一个检查条件的循环中被调用)

如下例:

int
piperead(struct pipe *pi, uint64 addr, int n)
{...acquire(&pi->lock);while(pi->nread == pi->nwrite && pi->writeopen){  //DOC: pipe-emptyif(myproc()->killed){release(&pi->lock);return -1;}sleep(&pi->nread, &pi->lock); //DOC: piperead-sleep}...
}

(其中sleep被唤醒后,即sched返回后,会重新获取condition lock)

所以,即使两对sleep/wakeup使用了同样的chan也不会有大问题,虽然其中之一可能被另一对里的wakeup唤醒,但是因为唤醒之后仍然不满足条件,因此他会再次sleep

sleep/wakeup的组合很轻量(不需要创建额外的数据结构),也提供了一层indirection,调用者不需要知道自己在和哪个进程打交道(chan是他们的纽带)

code:pipes

如上面所提到的,xv6对pipe的实现应用到了sleep和wakeup,虽然要更复杂一点

struct pipe {struct spinlock lock;char data[PIPESIZE];uint nread;     // number of bytes readuint nwrite;    // number of bytes writtenint readopen;   // read fd is still openint writeopen;  // write fd is still open
};

nwrite和nread分别记录已经写/读的字节总数

data是一个环形缓冲区,写了最后一个后,又从第一个写起,因此,如果缓冲区为空,那么nread==nwrite

如果缓冲区满,那么nread==nwrite-len,我们读取缓冲区字符时,也要将下标modlen,比如data[nread%PIPESIZE]

假设piperead和pipewrite运行在不同cpu上,他们在读/写之前都要获取pipe的锁,假设现在是write持有锁,那么他在缓冲区写入数据,当缓冲区满,它wakeup reader,(wakeup找到对应进程,获取进程锁,然后设为runnable,但是此时reader还没有condition lock,因此还不能开始读)然后sleep(sleep中会释放condition lock,因为此时nwrite>nread,reader开始读).reader读完之后wakeup writer

本实现中pipe对读和写使用了不同的chan,这样如果有大量的reader/writer等待同一个pipe会更有效率(TODO WHY?

(本节只介绍了pipe中与调度有关的,实际上pipe的分配和释放也有很有意思,和fs有点关系,TODO

Code:wait,exit,kill

孩子exit的时候,父进程可能已经在wait中sleep了,也可能还在做其他事,如果是后一种情况,那么等到父进程调用wait,一定能看到已经exit的孩子。exit记录孩子退出状态是通过设置状态为zombie,保持这个状态直到父进程的wait将其改为UNUSED。如果父进程比子进程先退出,父进程就将就把自己的子进程全部交给init进程,而init一直在调用wait(init.c),这样,每个进程都由父进程来清理

实现时主要难点在于父进程wait和子进程exit之间,以及exit和exit之间的race和死锁 TODO

wait使用calling进程的锁作为condition lock,以避免丢失唤醒(?)

回忆什么是丢失唤醒:指一个进程发现条件不满足(没有ZOMBIE的子进程),准备sleep,但是在sleep之前,另一个进程满足了这个条件,并wakeup,wakeup时,前一个进程还没有sleep,所以wakeup无功而返,此时前一个进程再sleep,就没有接收到这次唤醒,这就是丢失唤醒

再看为什么这么做可以防止丢失唤醒:wait一开始就获取了进程锁,那么exit获取进程锁时就会spin,直到wait进入sleep并释放该锁

当然这里因为wait调用sleep的condition lock就是进程锁,所以并没有一开始就释放进程锁,而是在scheduler里才由调度器释放

此时exit终于可以获取wait的进程锁,然后唤醒,此时wait已经是SLEEPING,所以不会丢失唤醒

wait的操作逻辑:遍历所有的进程,如果是它的子进程,并且是ZOMBIE,(如果需要,设置wait status为该子进程的退出状态),释放该进程资源,返回pid

如果没有ZOMBIE的子进程,那么进入sleep

注意:

  • 这里wait会持有两个锁:首先是自己的进程锁,然后是子进程的进程锁,xv6约定必须先获取父进程,再获取子进程的锁,避免死锁
  • wait在查看其他进程的parent时,没有持有该进程的锁,这是因为如果那个进程是wait的父进程,那么就违反了上面的约定,可能导致死锁。此外,能改变进程parent属性的,只有他的父进程,如果wait是它的父进程,那么除非当前进程改变他,否则就不会改变(TODO 后面这什么意思?应该是指,如果wait这个进程,是它的父进程,那么除了当前进程之外的进程也不会改变它,如果wait不是它的父进程,那么wait只会查看这个属性,不会有影响?)

exit记录了退出状态(xstate),释放了一些资源(打开的文件,cwd的inode),将全部子进程交给init进程,唤醒父进程,标记自己为ZOMBIE,永久让出cpu。当设置自己状态为ZOMBIE和唤醒父进程时,必须持有父进程的锁,以防止丢失唤醒

为什么?为了保证,做这些事的时候父进程没有持有锁,注意wait中,持有锁的区域是check到sleep之间,如果子进程不获取锁,而是直接wakeup,就像上面说的,可能父进程check发现没有ZOMBIE,准备sleep,但是还没有修改为SLEEPING,那么子进程的wakeup无功而返,父进程再去sleep,就不会接受到这一次的唤醒,所谓丢失唤醒

获取父进程的锁之后,还需要获取自己的锁,因为此时子进程状态为ZOMBIE,但是还没有让出CPU(调用sched),不能被父进程释放。

exit使用了一个专门的唤醒函数,wakeup1来唤醒父进程,他只唤醒处于SLEEPING的父进程

这里是先调用wakeup1,再设置子进程状态为ZOMBIE,但是并不要紧,因为在释放子进程的锁(scheduler中)之前,父进程无法查看子进程的状态

exit让一个进程终止自己,kill让其终止另一进程,xv6对kill的实现很轻量:仅仅是设置其killed字段为1,如果该进程为SLEEPING,就唤醒她。当该进程重新进入内核态(如定时中断),就会因为killed为1,而调用exit

kill中会唤醒被sleep的进程,但是如之前所说,xv6中sleep总是放在循环中,如果被唤醒时没有满足条件,就会再次循环,进入sleep

所以大部分sleep循环都会检查killed是否为1,如果是,就退出循环

但不是所有都会检查killed,比如virtio_disk_rw:因为一个磁盘操作可能是一系列写入中的一个,为了让文件系统处于一个正确的状态,这些写操作是都需要的。一个正在等待磁盘io的进程被kill,要到完成当前系统调用然后usertrap看到killed为1,才会调用exit TODO

Begin_op里也没检查killed?

即使是检查了killed,也可能lost wakeup:比如,刚检查killed,为0,那么准备sleep,此时设置killed为1,并且wakeup,但是该进程还没有sleep,wakeup无功而返,那么直到victim(被kill的进程)再次被唤醒,他都将保持sleeping

real world

xv6的调度策略是round robin,时间片轮转,真正的os会有更复杂的调度策略,比如之前ostep看到的多级反馈什么的

xv6 risc-v scheduler 笔记相关推荐

  1. 计组学习笔记2(RISC v版)

    指令集解释 (规定:R[r]表示通用寄存器r的内容,M[addr]表示存储单元addr的内容,SEXT[imm]表示对imm进行符号扩展,ZEXT[imm]表示对imm进行零扩展) 整数运算类 -U型 ...

  2. RISC V (RV32+RV64) 架构 整体介绍

    文章目录 riscv 市场 芯片介绍 软件介绍 开发板介绍 PC介绍 riscv 架构 编程模型(指令集/寄存器/ABI/SBI) 运行状态 指令集 寄存器 riscv32和riscv64两者的区别 ...

  3. 年轻人的第一篇V语言笔记

    V语言极限学习 我听说V语言看文档半小时就能完全掌握????以我的智商一小时掌握不了我就给各位科普一下广告法??? 宇宙惯例hello world // first v code fn main(){ ...

  4. GoKit3(V)学习笔记02_自定义产品数据点

    跟着Gokit3使用说明书的教程顺利地让设备跑了起来,家里一下子热闹了起来,玛丽玛丽的声音此起披伏,多多儿还不会讲话,也跟着maaa地叫着.这是GoKit3给生活带来的快乐. 言归正传,这篇开始讲述G ...

  5. Windows任务计划程序Task Scheduler笔记

    微软文档居然搜不到了 Windows任务计划程序已经存在许多年了,原来在微软的TechNet上有详细的操作介绍的,现在发现网站改版,原来的介绍居然搜索不到了,微软的平台上出现这种事情,也是比较吃惊了. ...

  6. 安装Ubuntu RISC V toolchain失败(网速、git配置原因)

    git获取大容量工程出错:RPC failed: curl GnuTLS recv error : Decryption has failed. error: RPC failed; curl 56 ...

  7. JS之数据类型v(** v**)v个人笔记

    <body> <!-- 单词记忆 argument:实参 assignment:赋值 instance:实例 1.JS中的数据类型分为以下类型 *值类型(基本类型)*String:可 ...

  8. XV6实验(2020)

    XV6实验记录(2020) 环境搭建 参考连接 Lab guidance (mit.edu) 6.S081 / Fall 2020 (mit.edu) xv6 book中文版 Lab1:Xv6 and ...

  9. RISC-V 指令学习笔记(基于CH32V103)

    文章目录 RISC-V 指令学习笔记(基于CH32V103) 一.指令结构分类 二.寄存器功能 三.加载存储指令 四.算数运算指令 五.移位指令 六.逻辑操作指令 七.跳转指令 7.1 条件跳转 7. ...

最新文章

  1. CodeForces - 1497D Genius(dp)
  2. 数据结构实验之栈二:一般算术表达式转换成后缀式
  3. SDN精华问答 | SDN的核心技术是什么?
  4. 基于Windows下python3.4.1IDLE常用快捷键小结
  5. Hadoop 实践(一) 环境搭建
  6. .NET : 如何理解字符串和它的字节表现形式
  7. 2017-10-08 前端日报
  8. 20145206《Java程序设计》实验五Java网络编程及安全
  9. 【从Northwind学习数据库】数据更新
  10. 第106章 Caché 函数大全 $ZF(-4),$ZF(-5),$ZF(-6) 函数
  11. spring boot打包本地idea跑能行,上线jar包跑不行 解决
  12. JavaWeb-仿小米商城(1) 项目启动
  13. python3socket非阻塞在linux里无效_Linux Socket - 内核非阻塞功能
  14. 软件测试b s环境如何配置,B/S架构测试环境搭建_SQLServer篇(Win32系统)
  15. 工具的服务端口已关闭。要使用命令行调用工具,请打开工具 - 设置 - 安全设置,将服务端口开启。
  16. Cocos Creator 3.2 中实现2D地图3D人物45度角RPG游戏效果笔记(摄像机设置方案)
  17. java宠物之王-龙灵传说,《宠物之王-龙灵传说》的流程攻略(上)
  18. 聚观早报 | 科技巨头组建元宇宙组织,苹果缺席;推特董事会批准马斯克收购交易​;TikTok调整欧盟用户相关权利
  19. 甲骨文发布2018第一季度财报 总收入92亿美元
  20. 浪潮信息做pc服务器,浪潮信息:高性能AI服务器将成为智算中心生产算

热门文章

  1. 个人项目需求和分析------日程管理APP
  2. 高中数学知识那些和计算机有关系,高中数学教学手段有哪些
  3. ChatGPT大流行的思考-设想篇
  4. 【face-api.js】前端实现,人脸捕获、表情识别、年龄性别识别、人脸比对、视频人脸追踪、摄像头人物识别
  5. Android Vlc播放器加载rtsp及http地址视频
  6. HFSS(ANSYS Electronics)和ADS(Advanced Design system)联合仿真
  7. 关于IP SLA及与EEM联动的探讨(转)
  8. 可以动态添加、模糊搜索的单选下拉框插件formSelects
  9. Python软件编程等级考试三级——20210620
  10. c++怎么调用java_Java 和 C++之间互相调用