xv6 磁盘中断流程和启动时调度流程
- 首发公号:Rand_cs
本文讲述 xv6 中的一些细节流程,还有对之前文中遗留的问题做一些补充说明,主要有以下几个问题:
- 一次完整的磁盘中断流程
- 进入调度器后的详细流程
- sched 函数中的条件判断
- scheduler 函数中为什么要周期性关中断
一次完整的磁盘流程
此节讲述完整的磁盘读写流程,读写的流程总体差不多,这里以读为例子,先看“流程图”(看代码时的笔记图)
read
int $T_SYSCALL
sys_read
fileread
readi
bread
bget
iderw
idestart
还是从 A 进程的用户态 read 函数开始:
- A 进程用户态调用 read 读取磁盘上的数据
- read 通过 INT 0x80 软件中断,通过中断门进入内核,此时会关中断(NOTE 这里我以中断门来实现系统调用为例,会关中断,xv6 源代码是以陷阱门实现系统调用,不会关中断)
- 期间多次取锁放锁,进行了多次 pushcli 和 popcli,但总是成对存在,所以目前总体还是处于 0 次 puchcli 状态
- 如果磁盘数据没有缓存,调用 iderw 来读写磁盘
A 进程内核态,iderw 函数:
- acquire(&idelock),获取磁盘锁,pushcli,cpu.IF = 0,1 次pushcli 状态
- 调用 idestart,将要读写的命令,扇区号等信息写进磁盘端口,以此来请求磁盘操作。写磁盘端口是通过 out 指令实现的。向磁盘发送命令后,磁盘就会工作,磁盘完成工作后就会向 cpu 发送中断信号。
- A 进程调用 sleep 等待磁盘操作完成。在 sleep 函数中,获取 ptable.lock,释放 idelock,然后调用 sched 让出
by the way,这里补充说明 sched 函数中的条件检查,之前的文章都一笔带过了:
void sched(void) //让出CPU,重新调度
{
int intena;
struct proc *p = myproc();
if(!holding(&ptable.lock)) // 必须持有 ptable.lock
panic("sched ptable.lock");
if(mycpu()->ncli != 1) // 1 次 pushcli 状态
panic("sched locks");
if(p->state == RUNNING) // 只可能是 SLEEPING、RUNNABLE、ZOMBIE 三种状态之一
panic("sched running");
if(readeflags()&FL_IF) // 此时肯定处于关中断状态(通过中断门进入内核会关中断,1次pushcli状态也应该对应关中断状态)
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&p->context, mycpu()->scheduler);
mycpu()->intena = intena;
}
sched 函数中有 4 个条件检查:
- xv6 是个多 CPU 多任务系统,在 sched 任务调度的时候,需要持有 ptable.lock,不然进程的上下文会发生紊乱,举个简单的例子,A 时间片到了,先将 A 的状态设置为 RUNNABLE,然后调用 sched 让出 CPU,如果此时没有持有 ptable.lock,那么 A 进程便可能在另一个 CPU 上被调度,那么便出现一个进程在两 CPU 上运行的情况,Error
- 在 sched 函数中应当只有 1 次 pushcli 状态,这个条件检查感觉有点难以理解。从实践看代码确实,不管从哪条路径到达 sched 函数,都应该只有 1 次 pushcli,这是获取 ptable.lock 的锁造成的。从个人理解上说,sched 是为了调度进程,是要从 A 进程到 B 进程,那么 A 进程的开关中断(pushcli popcli 次数)不应该带入 B 进程,除了一种情况——调度,那就是 A 进程获取 ptable.lock 但是要在 B 进程中释放 ptable.lock。所以 sched 中应当只有 1 次 pushcli
- 在真正切换进程上下文之前,会首先修改旧进程的状态,在 xv6 中是 SLEEPING、RUNNABLE、ZOMBIE 三种之一
- 在 sched 中理应处于关中断状态,如果是通过中断门进入内核的,那么本身就处于关中断。如果是通过陷阱门进入内核,那么有 1 次 pushcli,也会处于关中断状态
回到磁盘中断,当 A 进程调用 sched 切换到 B 进程,这里假如 B 进程最初是因为时间片到了,调用 yield->sched->swtch 主动让出 CPU 的,则 B 进程的流程如下:
- 回到 B 进程 sched 函数中的 swtch 下一条指令处,然后释放 ptable.lock,此时 cpu.IF = 0,0 次 pushcli 状态,cpu.IF = 0 仍然处于关中断状态 是因为 A 进程通过中断门进入内核关中断造成的
- B 进程经过一些列指令后,最后执行 iret 返回 B 进程的用户态,此时会开中断
- NOTE,这里我们假设 CPU 内部逻辑:每条指令执行后都会检查是否有中断发生,如果有中断发生且开中断的情况下,则去处理中断。再假设,此前的磁盘操作已完成,已经向 CPU 发生了中断信号。但是在此之前 CPU 一直没有去处理中断,是因为在此之前一直处于关中断状态。
- Now,CPU 处于开中断状态,继续执行 B 进程的指令,开中断后的第一条指令执行完成后,检查是否有中断发生,发现有磁盘中断,那么中断 B 进程,通过中断门进入内核(该过程关中断)
- 执行磁盘中断处理程序,也就是执行 insl 指令从 0x1f0 端口将磁盘数据读取到内存,然后唤醒等待该磁盘事件的进程,在我们的例子当中就是 A 进程
- 中断执行完成,iret 返回 B 进程用户态(该过程开中断)
- 继续执行 B 进程的指令
- 时钟中断 B 进程,再次通过中断门进入内核(关中断),发现 B 进程的时间片到了,那么调用 yield->sched->swtch 重新调度进程,这里假设调度到 A 进程
回到 A 进程的内核态,准确来说回到 iderw->sleep->sched->swtch 的下一条指令
- A 进程执行 release(ptable.lock)、acquire(idelock)、release(idelock),此时状态: cpu.IF = 0,0 次 pushcli
- 将磁盘中断获取的数据 cp 到 A 进程内核态
- A 进程层层返回
- 最后 iret 返回 A 进程用户态(开中断)
系统启动进入调度器后的流程
main
userinit //准备好 initcode 进程
mpmain
scheduler // 进入调度器
调度器上下文:
- 第一次进入 scheduler,for 循环找到 RUNNABLE 进程,目前就只有一个 initcode 进程为 RUNNABLE 进程,找到并切换上下文到 initcode 进程
initcode 进程上下文:
- 执行 forkret 函数,因为是第一次执行,会首先执行 iinit 来初始化根文件系统
- 执行 readsb 从磁盘中读取超级块数据,期间会使用 iderw 读写磁盘,具体流程见第一小节。总之,initcode 进程会调用 sleep 函数让出 CPU 来等待磁盘操作
- 在 sleep->sched->swtch 中再次切换上下文到 调度器上下文
调度器上下文:
- 切换到内核页表,然后遍历任务队列,寻找 RUNNABLE 进程,但是目前只有一个且处于 SLEEPING 状态的进程,所以这里调度器会轮询空转,直到磁盘中断处理完成,initcode 进程被唤醒。
- 再次切换上下文到 initcode 进程
initcode 进程上下文
- 回到 forkret->iinit->readsb->bread->iderw->sleep->sched->swtch 的下一条指令处,然后层层返回到 forkret
- 再执行 initlog 恢复日志,这里会涉及到两次磁盘读写,道理同上,不再赘述
- 第 4 次调度到 initcode 进程后,forkret 函数执行完毕,再执行 trapret 函数,其中包含了 iret 指令,至此回到用户态,开始执行 initcode 进程的逻辑
by the way again,这里解释为什么在 scheduler 函数中需要周期性的开中断:
void scheduler(void)
{
for(;;){
sti(); // 周期性开中断
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){ //循环找一个RUNNABLE进程
...
}
}
}
进入调度器上下文有两条路径:
- 系统刚启动进入 scheduler
- sched 函数中 swtch 上下文到调度器
回想前面说的 sched 函数,在它切换到新进程并返回用户态之前理应都处于关中断的状态。而一直处于关中断且没有开中断的话会引发死循环。
举个例子,假设没有周期性的开中断,也就是 scheduler 代码长这样的话:
void scheduler(void)
{
for(;;){
// 遍历进程列表,寻找 RUNNABLE 进程
for(p = ptable.proc; p < &ptable.proc[NPROC]; p++){
...
}
}
}
假如当前系统只有一个进程(shell进程),它需要等待键盘输入而被阻塞(state==SLEEPING),内层循环是找不到 RUNNABLE 进程的,便回到外层循环,外层循环现在相当于什么也不做,便又再次进入内层循环。如此下来死循环。
而加入周期性的开中断后,CPU 便会响应中断。当有键盘输入时,中断当前的调度上下文而进入中断上下文,执行键盘中断处理程序,唤醒 shell 进程,中断处理完成后再回到调度上下文。此时内层循环便能找到一个 RUNNABLE 进程,然后切换到它的上下文执行。
本文就先补充这么多吧,这补充系列的文章是之前做了关于 xv6、nemu 的项目,将 xv6 启动到 nemu 上,这需要对很多地方细扣,对 xv6 的理解又增加了一些,分享出来。
停更这么久啊,一直再忙工作,学习新的东西,时间不是很多,当然也有懒的原因,后面慢慢克服回归吧。OK,那有什么问题欢迎来讨论交流。
- 首发公号:Rand_cs
xv6 磁盘中断流程和启动时调度流程的更多相关文章
- MySQL指定mysqld启动时所加载的配置文件
mysqld.exe --init-file=file_name 以下为配置文件参数优化和中文详解: [client]port = 3306socket = /tmp/mysql.sock [mysq ...
- Linux系统开机启动时的工作原理
Linux系统开机启动时的工作原理也是深入了解Linux系统核心工作原理的一个很好的途径. 启动第一步--加载BIOS 当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至 ...
- gevent调度流程解析
gevent是目前应用非常广泛的网络库,高效的轮询IO库libev加上协程(coroutine),使得gevent的性能非常出色,尤其是在web应用中.本文介绍gevent的调度流程,主要包括geve ...
- linux文件系统启动流程、启动脚本
linux文件系统启动流程.启动脚本 下面是一张Linux启动流程图: 在了解启动流程之前,我们应该先知道系统的几个重要脚本和配置文件,他们对应的路径为: 1. /sbin/init 2. /etc/ ...
- 《转》深入理解Activity启动流程(三)–Activity启动的详细流程1
本文原创作者:Cloud Chou. 出处:本文链接 本系列博客将详细阐述Activity的启动流程,这些博客基于Cm 10.1源码研究. 深入理解Activity启动流程(一)--Activity启 ...
- Kubernetes调度流程与安全(七)
一.Kubernetes中的调度流程 1,介绍 Scheduler 是 k8s 中的调度器,主要的任务是把定义的 Pod 分配到集群的节点上.Scheduler 是作为一个单独的程序运行的,启动之后会 ...
- worker 启动时向 etcd 注册自己的信息,并设置一个带 TTL 的租约,每隔一段时间更新这个 TTL,如果该 worker 挂掉了,这个 TTL 就会 expire 并删除相应的 key。
1.通过etcd中的选主机制,我们实现了服务的高可用.同时利用systemd对etcd本身进行了保活,只要etcd服务所在的机器没有宕机,进程就具备了容灾性. https://mp.weixin.qq ...
- 以TiDB热点问题来谈Region的调度流程
什么是热点问题 说这个话题之前我们先回顾一下TiDB的主要结构和概念. TiDB的核心架构分为TiDB.TiKV.PD三个部分,其中TiKV是一个分布式数据存储引擎用来存储真实的数据,在TiKV中又对 ...
- scheduler源码分析——调度流程
前言 当api-server处理完一个pod的创建请求后,此时可以通过kubectl把pod get出来,但是pod的状态是Pending.在这个Pod能运行在节点上之前,它还需要经过schedule ...
- linux启动时文件系统错误问题
linux开机启动时,文件系统错误(磁盘有坏轨或文件系统出现错误) 原因: 1.系统运行时,突然断电或不正常关机,导致文件系统错误(文件系统错误并不是硬件错误,而是软件数据的问题) ...
随机推荐
- OpenYurt 深度解读:如何构建 Kubernetes 原生云边高效协同网络?
作者 | 郑超 导读:OpenYurt 是阿里巴巴开源的云边协同一体化架构,与同类开源方案相比,OpenYurt 拥有可实现边缘计算全场景覆盖的能力.在之前的一篇文章中,我们介绍了 OpenYurt ...
- 基于 Observable 构建前端防腐策略
简介:To B 业务的生命周期与迭代通常会持续多年,随着产品的迭代与演进,以接口调用为核心的前后端关系会变得非常复杂.在多年迭代后,接口的任何一处修改都可能给产品带来难以预计的问题.在这种情况下,构 ...
- 来电科技:基于 Flink + Hologres 的实时数仓演进之路
简介: 本文将会讲述共享充电宝开创企业来电科技如何基于 Flink + Hologres 构建统一数据服务加速的实时数仓 作者:陈健新,来电科技数据仓库开发工程师,目前专注于负责来电科技大数据平台离线 ...
- 顺丰科技 Hudi on Flink 实时数仓实践
简介: 介绍了顺丰科技数仓的架构,趟过的一些问题.使用 Hudi 来优化整个 job 状态的实践细节,以及未来的一些规划. 本文作者为刘杰,介绍了顺丰科技数仓的架构,趟过的一些问题.使用 Hudi ...
- 2019-8-31-dotnet-线程静态字段
title author date CreateTime categories dotnet 线程静态字段 lindexi 2019-08-31 16:55:58 +0800 2019-06-13 0 ...
- c++图像处理程序编译成.so动态链接库供Java调用(包含jni文件与引用第三方动态链接库opencv)
一.linux编译so文件需要准备的环境 1.安装JDK(注意:不能安装openjdk,因为openjdk没有include目录,编译时需要用到include目录的头文件) 2.安装gcc和g++ ...
- STLINK/V2下载器接线方法
一.ST-LINK ST-LINK产品如下图所示: ST-LINK接口定义如下图所示 ST-LINK与stm32接线 使用SW接法只需要四根线: STM32 ST-LINK VCC(3.3V) TVC ...
- 让 KEPServer 变成一款 Web 组态软件
KEPServerEX是行业领先的连接平台,用于向您的所有应用程序提供单一来源的工业自动化数据.该平台的设计使用户能够通过一个直观的用户界面来连接.管理.监视和控制不同的自动化设备和软件应用程序. ...
- Linux内核之I2C协议
I2C协议标准文档 THE I2C-BUS SPECIFICATION VERSION 2.1 JANUARY 2000: https://www.csd.uoc.gr/~hy428/reading/ ...
- vue首次缓存判断
在向本地缓存存值前,判断localstory是否已经存在某个属性,没有就正常存,有就替换掉 created(){ if(localStorage.getItem("属性名") != ...