当前位置: 首页 > news >正文

Nachos系统的上下文切换

在这里插入图片描述

Fork调用创建进程

在实验1中通过gdb调试初步熟悉了Nahcos上下文切换的基本流程,但这个过程还不够清晰,通过源码阅读进一步了解这个过程。
在实验1中通过执行ThreadtestFork创建子进程,并传入SimpleThread执行currentThread->Yield()进行进程切换:

//----------------------------------------------------------------------
// SimpleThread
// 	Loop 5 times, yielding the CPU to another ready thread 
//	each iteration.
//
//	"which" is simply a number identifying the thread, for debugging
//	purposes.
//----------------------------------------------------------------------void
SimpleThread(_int which)
{int num;for (num = 0; num < 5; num++) {printf("*** thread %d looped %d times\n", (int) which, num);currentThread->Yield(); // 模拟时间片用完,Yield进入就绪队列}
}//----------------------------------------------------------------------
// ThreadTest
// 	Set up a ping-pong between two threads, by forking a thread 
//	to call SimpleThread, and then calling SimpleThread ourselves.
//----------------------------------------------------------------------void
ThreadTest()
{DEBUG('t', "Entering SimpleTest");Thread *t = new Thread("forked thread");t->Fork(SimpleThread, 1);   // fork之后父子进程轮转协作,运行SimpleThreadSimpleThread(0);
}

在实验8中通过系统调用Exec执行Fork进行子进程的创建,传入StartProcess,与实验1不同,这个Fork是为用户程序创建进程,需要进行地址空间的拷贝,调用InitRegisters初始化CPU寄存器,调用RestoreState加载页表(在处理SC_Exec时已经分配好了用户程序的地址空间),并调用machine::run()执行。

case SC_Exec:
{printf("Execute system call of Exec()\n");// read argumentchar filename[50];int addr = machine->ReadRegister(4);int i = 0;do{// read filname from mainMemorymachine->ReadMem(addr + i, 1, (int *)&filename[i]);} while (filename[i++] != '\0');printf("Exec(%s)\n", filename);// 为halt.noff创建相应的进程以及相应的核心线程// 将该进程映射至新建的核心线程上执行beginDEBUG('x', "thread:%s\tExec(%s):\n", currentThread->getName(), filename);// if (filename[0] == 'l' && filename[1] == 's') // ls// {//     DEBUG('x', "thread:%s\tFile(s) on Nachos DISK:\n", currentThread->getName());//     fileSystem->List();//     machine->WriteRegister(2, 127); ////     AdvancePC();//     return;// }// OpenFile *executable = fileSystem->OpenTest(filename);OpenFile *executable = fileSystem->Open(filename);AddrSpace *space;if (executable == NULL){printf("Unable to open file %s\n", filename);// return;ASSERT(false);}space = new AddrSpace(executable);// printf("u:Fork\n");Thread *thread = new Thread(filename);thread->Fork(StartProcess, space->getSpaceId());// end// return spaceIDmachine->WriteRegister(2, space->getSpaceId());AdvancePC();currentThread->Yield();delete executable;break;
}

Yield调度线程执行

但是Fork出的子程序并不是立即执行的!进程的调度由Scheduler负责,Fork前会将该进程加入到就绪队列中。如果我们不用任何进程调度命令,bar程序在执行完系统调用之后,会继续执行该程序剩下的指令,并不会跳转到halt程序执行,因此需要在系统调用返回前调用currentThread->Yield,切换执行halt进程,这样才是完整的Exec系统调用。

  nextThread = scheduler->FindNextToRun();if (nextThread != NULL) {scheduler->ReadyToRun(this);scheduler->Run(nextThread);}

scheduler::run

Yield调用scheduler::run执行:

void
Scheduler::Run (Thread *nextThread)
{   // 运行一个新线程Thread *oldThread = currentThread;#ifdef USER_PROGRAM			// ignore until running user programs // 1. 将当前CPU寄存器的内容保存到旧进程的用户寄存器中// 2. 保存用户页表if (currentThread->pcb->space != NULL) {	// if this thread is a user program,currentThread->SaveUserState(); // save the user's CPU registerscurrentThread->pcb->space->SaveState();}
#endifoldThread->CheckOverflow();		    // check if the old thread// had an undetected stack overflowcurrentThread = nextThread;		    // switch to the next threadcurrentThread->setStatus(RUNNING);      // nextThread is now running// 将线程指针指向新线程DEBUG('t', "Switching from thread \"%s\" to thread \"%s\"\n",oldThread->getName(), nextThread->getName());// This is a machine-dependent assembly language routine defined // in switch.s.  You may have to think// a bit to figure out what happens after this, both from the point// of view of the thread and from the perspective of the "outside world".// 从线程和外部世界的视角SWITCH(oldThread, nextThread);  // 此时寄存器(非通用数据寄存器,可能是PC,SP等状态寄存器)的状态还未保存和更新DEBUG('t', "Now in thread \"%s\"\n", currentThread->getName());// If the old thread gave up the processor because it was finishing,// we need to delete its carcass.  Note we cannot delete the thread// before now (for example, in Thread::Finish()), because up to this// point, we were still running on the old thread's stack!if (threadToBeDestroyed != NULL) {delete threadToBeDestroyed;     // 回收旧线程的堆栈threadToBeDestroyed = NULL;}#ifdef USER_PROGRAM// 1. 如果运行用户进程,需要将当前进程的用户寄存器内存加载到CPU寄存器中// 2. 并且加载用户页表if (currentThread->pcb->space != NULL) {		// if there is an address spacecurrentThread->RestoreUserState();     // to restore, do it.currentThread->pcb->space->RestoreState();}
#endif
}

①首尾两部分是用户寄存器和CPU寄存器之间的操作,将40个CPU寄存器的状态保存至用户寄存器,将新进程的用户寄存器加载至CPU寄存器,并加载用户程序页表。
②检查栈溢出。
③将currentThread指向新进程,将其状态设置为RUNNING
④调用汇编程序SWITCH,实验1中此处的注释是该程序保存并更新寄存器的状态,可是这不是在①部分code做的事情?
⑤回收进程堆栈。

SWITCH初步探索

结合源码进行分析:

#ifdef HOST_i386
/* void SWITCH( thread *t1, thread *t2 )
**
** on entry, stack looks like this:
**      8(esp)  ->              thread *t2
**      4(esp)  ->              thread *t1
**       (esp)  ->              return address
**
** we push the current eax on the stack so that we can use it as
** a pointer to t1, this decrements esp by 4, so when we use it
** to reference stuff on the stack, we add 4 to the offset.
*/.comm   _eax_save,4.globl  _SWITCH
_SWITCH:movl    %eax,_eax_save          # save the value of eaxmovl    4(%esp),%eax            # move pointer to t1 into eaxmovl    %ebx,_EBX(%eax)         # save registersmovl    %ecx,_ECX(%eax)movl    %edx,_EDX(%eax)movl    %esi,_ESI(%eax)movl    %edi,_EDI(%eax)movl    %ebp,_EBP(%eax)movl    %esp,_ESP(%eax)         # save stack pointermovl    _eax_save,%ebx          # get the saved value of eaxmovl    %ebx,_EAX(%eax)         # store itmovl    0(%esp),%ebx            # get return address from stack into ebxmovl    %ebx,_PC(%eax)          # save it into the pc storagemovl    8(%esp),%eax            # move pointer to t2 into eax......movl    _eax_save,%eaxret#endif // HOST_i386

的确是寄存器的保存与加载,但这个寄存器和前面的CPU、用户寄存器有什么不同?实际上它们对应0-32(Step=4)之间数字。

#define _ESP     0
#define _EAX     4
#define _EBX     8
#define _ECX     12
#define _EDX     16
#define _EBP     20
#define _ESI     24
#define _EDI     28
#define _PC      32

StackAllocate开辟堆栈存储进程信息

关注Thread::StackAllocate,将无关部分删除。可看到该函数在StartProcess中也被调用,用于为进程创建堆栈,是的,Nachos的堆栈并不是在内存中进行创建,内存中仅仅存放了noff文件加载进来的内容。
调用AllocBoundedArray分配了StackSize 的堆栈空间,之后将stack+StackSize-4的位置设置为栈顶stackTop,之后在stack位置处设置了一个标志STACK_FENCEPOST,用于检查堆栈溢出(②),当该标志被修改,代表堆栈溢出了。也就是stack维护栈底,stackTop维护栈顶。

ASSERT((unsigned int)*stack == STACK_FENCEPOST);
void
Thread::StackAllocate (VoidFunctionPtr func, _int arg)
{   // 栈分配stack = (int *) AllocBoundedArray(StackSize * sizeof(_int));// i386 & MIPS & SPARC & ALPHA stack works from high addresses to low addressesstackTop = stack + StackSize - 4;	// -4 to be on the safe side!*stack = STACK_FENCEPOST;    machineState[PCState] = (_int) ThreadRoot;machineState[StartupPCState] = (_int) InterruptEnable;machineState[InitialPCState] = (_int) func;machineState[InitialArgState] = arg;machineState[WhenDonePCState] = (_int) ThreadFinish;
}

注意到Thread类将stackTopmachineState的定义提前了,而SWITCH的参数为Thread*,其操作的也是0偏移量为32以内的内存单元,因此SWITCH操作的“寄存器”就是stackTopmachineState,以及Linux系统的寄存器%ebx,%eax,%esp等等。

class Thread {private:  // 特意提前,保证SWITCH例程正常// NOTE: DO NOT CHANGE the order of these first two members.// THEY MUST be in this position for SWITCH to work.int* stackTop;			 // the current stack pointer_int machineState[MachineStateSize];  // all registers except for stackToppublic:

结合下面宏定义,了解machineState中各个寄存器存储内容的含义。

#define PCState         (_PC/4-1)
#define FPState         (_EBP/4-1)
#define InitialPCState  (_ESI/4-1)
#define InitialArgState (_EDX/4-1)
#define WhenDonePCState (_EDI/4-1)
#define StartupPCState  (_ECX/4-1)

ThreadRoot维护进程的生命周期

那么machineState的“寄存器”存储了哪些信息?有什么作用?SWITCH之后Linux系统PC保存的应该是ThreadRoot的起始地址,因为4(%esp)SWITCH的返回地址,它被设置为ThreadRoot的起始地址。这个函数模拟了一个进程的“一生”,初始化,执行,到终结。下面这几个调用函数的地址在StackAllocate最末尾被存储在machineState中,并在SWITCH中装载到Linux系统的寄存器中。

#define InitialPC       %esi
#define InitialArg      %edx
#define WhenDonePC      %edi
#define StartupPC       %ecx
        .text.align  2.globl  _ThreadRoot/* void ThreadRoot( void )
**
** expects the following registers to be initialized:
**      eax     points to startup function (interrupt enable)
**      edx     contains inital argument to thread function
**      esi     points to thread function
**      edi     point to Thread::Finish()
*/
_ThreadRoot:pushl   %ebpmovl    %esp,%ebppushl   InitialArgcall    StartupPCcall    InitialPCcall    WhenDonePC# NOT REACHEDmovl    %ebp,%esppopl    %ebpret

SWITCH更新Linux寄存器状态

现在可以回答最开始的问题了,SWITCH程序保存和更新的是Linux系统寄存器的内容,将其保存到machinState寄存器中。不同于Nachos的用户寄存器和CPU寄存器,这两者是在执行用户程序时才会使用到,用于保存用户指令和机器指令的结果和状态,前者是Nachos得以在Linux机器上运行的基础,这也是为什么它必须要通过汇编代码实现的原因,要操纵Linux系统的寄存器。

回收进程堆栈

scheduler::run最后的语句,回收线程堆栈,这时当前线程仍然在执行,它回收的肯定不是当前线程的堆栈!实际上是已经结束的线程,执行Thread::Finish,会将threadToBeDestroyed指向自己。而Finish是在进程终结之时执行,ThreadRootcall WhenDonePC就是执行Finish。It all makes sense!

总结

之前在阅读scheduler::run的时候,有一段SWITCH注释让我很迷惑,“You may have to think a bit to figure out what happens after this, both from the point of view of the thread and from the perspective of the “outside world”.”,这个从外部世界去看是什么意思?现在,我也许有了答案。Nachos终归是运行在Linux上的操作系统,用了很多设计Timer,Scheduler,Interrupt模拟了machine,也就是硬件,但是归根结底,它只是运行在Linux系统上的一个进程,Linux系统的PC寄存器保存着Nachos“程序”正在执行的指令地址,为了实现Nachos系统的进程切换,在Linux的一个进程中的一个部分(Nachos进程)跳转到另一个部分,必须修改Linux系统的PC寄存器以及其他寄存器。为了操作Linux系统的寄存器,也就是实际物理硬件的寄存器,必须通过汇编程序进行,因此有了两个SWITCHThreadRoot两个汇编方法调用。在Nachos上下文切换中,前者更新Linux寄存器的内容,后者管理进程(除了在Initialize方法中创建的第一个线程main)的生命周期,通过Linux寄存器调用进程初始化StartupPC,运行InitialPC,终结WhenDonePC的3个过程。

相关文章:

Nachos系统的上下文切换

Fork调用创建进程 在实验1中通过gdb调试初步熟悉了Nahcos上下文切换的基本流程&#xff0c;但这个过程还不够清晰&#xff0c;通过源码阅读进一步了解这个过程。 在实验1中通过执行Threadtest&#xff0c;Fork创建子进程&#xff0c;并传入SimpleThread执行currentThread->…...

streamx平台部署

一. streamx介绍 StreamPark 总体组件栈架构如下&#xff0c; 由 streampark-core 和 streampark-console 两个大的部分组成 , streampark-console 是一个非常重要的模块, 定位是一个综合实时数据平台&#xff0c;流式数仓平台, 低代码 ( Low Code ), Flink & Spark 任务托…...

css中的background属性

文章目录 一&#xff1a;background-repeat二&#xff1a;background-position三&#xff1a;background缩写方式三&#xff1a;background-size四&#xff1a;background-origin五&#xff1a;background-clip 在日常前端开发中&#xff0c;经常需要进行背景或背景图的处理。但…...

代码评审平台Gerrit安装配置方法介绍

Gerrit是一款开源免费的基于 web 的代码审查工具&#xff0c;是基于 Git 的版本控制系统。在代码入库之前对开发人员的提交进行审阅&#xff0c;检视通过的代码才能提交入库。本文记录如何安装部署gerrit平台。 目录 Gerrit简介环境准备1. 安装Java2. 安装Git3. 安装nginx4. 安…...

一篇文章解决Mysql8

基于尚硅谷的Mysql8.0视频&#xff0c;修修改改。提取了一些精炼的内容。 首先需要在数据库内引入一张表。链接地址如下。 链接&#xff1a;https://pan.baidu.com/s/1DD83on3J1a2INI7vrqPe4A 提取码&#xff1a;68jy 会进行持续更新。。 1. Mysql目录结构 Mysql的目录结构…...

【Python】【进阶篇】6、Django视图函数

目录 6、Django视图函数1. 第一个视图函数1&#xff09;HttpResponse视图响应类型2&#xff09;视图函数参数request3&#xff09;return视图响应 2. 视图函数执行过程 6、Django视图函数 视图是 MTV 设计模式中的 V 层&#xff0c;它是实现业务逻辑的关键层&#xff0c;可以用…...

Latex常用符号和功能记录

公式下括号 \underbrace & \overbrace \begin{equation} \underbrace{L_1L_2}_{loss ~ 1} \overbrace{L_3L_4}^{loss ~ 2} \end{equation}L L 1 L 2 ⏟ l o s s 1 L 3 L 4 ⏞ l o s s 2 L \underbrace{L_1L_2}_{loss ~ 1} \overbrace{L_3L_4}^{loss ~ 2} Lloss 1…...

MySQL高级篇——索引的创建与设计原则

导航&#xff1a; 【黑马Java笔记踩坑汇总】JavaSEJavaWebSSMSpringBoot瑞吉外卖SpringCloud黑马旅游谷粒商城学成在线牛客面试题 目录 一、索引的分类与使用 1.1 索引的分类 1.1.1. 普通索引 1.1.2. 唯一性索引 1.1.3. 主键索引&#xff08;唯一非空&#xff09; 1.1.4…...

王一茗: “大数据能力提升项目”与我的成长之路 | 提升之路系列(三)

导读 为了发挥清华大学多学科优势&#xff0c;搭建跨学科交叉融合平台&#xff0c;创新跨学科交叉培养模式&#xff0c;培养具有大数据思维和应用创新的“π”型人才&#xff0c;由清华大学研究生院、清华大学大数据研究中心及相关院系共同设计组织的“清华大学大数据能力提升项…...

MySQL:数据库的基本操作

MySQL是一个客户端服务器结构的程序, 一.关系型数据库 关系型数据库是一个结构化的数据库&#xff0c;创建在关系模型&#xff08;二维表格模型&#xff09;基础上&#xff0c;一般面向于记录。 主流的关系型数据库包括 Oracle、MySQL、SQL Server、Microsoft Access、DB2 等. …...

银行系统【GUI/Swing+MySQL】(Java课设)

系统类型 Swing窗口类型Mysql数据库存储数据 使用范围 适合作为Java课设&#xff01;&#xff01;&#xff01; 部署环境 jdk1.8Mysql8.0Idea或eclipsejdbc 运行效果 本系统源码地址&#xff1a;​​​​​​​https://download.csdn.net/download/qq_50954361/87708777 …...

【社区图书馆】-《科技服务与价值链》总结

【为什么研究价值链】 价值链及价值链协同体系是现代产业集群的核心枢纽&#xff0c;是推进城市群及产业集群化、服务化、生态化发展的纽带。因而推进价值链协同&#xff0c;创新发展价值链协同业务科技资源体系&#xff0c;既是科技服务业创新的重要方向&#xff0c;也是重塑生…...

工具链和其他-异步模块加载

目录 CMD/AMD Asynchronous Module Definition(AMD异步模块定义&#xff0c;语法风格) Common Module Definition ES6/CommonJS CommonJS ES6 Module 加载器示例 总结 cmd和amd的区别 现在有哪些异步加载方式 整体结构 编程&#xff1a;commonjs es6 module (有可能解…...

第一次使用R语言

在R语言中&#xff0c;“<-”符号与“”意义一样。另一种奇怪的R语言的等号表示方法&#xff0c;是以“->”表示&#xff0c;但是用得少。 有些计算机语言&#xff0c;变量在使用前要先定义&#xff0c;R语言则不需先定义&#xff0c;可在程序中直接设定使用。 若在Con…...

《语文教学通讯》栏目 收稿范围

《语文教学通讯》创刊于1978年&#xff0c;是由山西师范大学主管&#xff0c;山西师大教育科技传媒集团主办的期刊。历年被人民大学书报资料中心转载、复印的篇幅数量均居同类报刊之首。国内刊号&#xff1a;CN 14-1017/G4&#xff0c;国际刊号&#xff1a;ISSN 1004-6097&…...

Towards Principled Disentanglement for Domain Generalization

本文用大量的理论论述了基于解纠缠约束优化的域泛化问题。 这篇文章认为以往的文章在解决域泛化问题时所用的方法都是non-trivial的&#xff0c;也就是说没有作严格的证明&#xff0c;是不可解释的&#xff0c;而本文用到大量的定理和推论证明了方法的有效性。 动机 因为域泛…...

计算机网络学习02

1、TCP 与 UDP 的区别&#xff1f; 是否面向连接 &#xff1a; UDP 在传送数据之前不需要先建立连接。而 TCP 提供面向连接的服务&#xff0c;在传送数据之前必须先建立连接&#xff0c;数据传送结束后要释放连接。是否是可靠传输&#xff1a; 远地主机在收到 UDP 报文后&…...

网络交换机端口管理工具

如今&#xff0c;企业或组织级网络使用数百个交换机端口作为其 IT 基础架构的一部分来实现网络连接。这使得交换机端口管理成为日常网络管理任务的一部分。传统上&#xff0c;网络管理员必须依靠手动网络交换机端口管理技术来跟踪交换机及其端口连接状态。这种手动任务弊大于利…...

redis五大命令kv设计建议内存淘汰

什么是redis&#xff1f;主要作用&#xff1f; redis(remote dictionary server)远程字典服务&#xff1a;是一个开源的使用ANSI C语言编写&#xff0c;支持网络、可基于内存可持久化的日志型、key-value数据库&#xff0c;并提供多种语言的api redis的数据存在内存中&#xff…...

如何真正认识 Linux 系统结构?这篇文章告诉你

Linux 系统一般有 4 个主要部分&#xff1a;内核、shell、文件系统和应用程序。内核、shell 和文件系统一起形成了基本的操作系统结构&#xff0c;它们使得用户可以运行程序、管理文件并使用系统。 Linux内核 内核是操作系统的核心&#xff0c;具有很多最基本功能&#xff0c;…...

【移动端网页布局】流式布局案例 ① ( 视口标签设置 | CSS 样式文件设置 | 布局宽度设置 | 设置最大宽度 | 设置最小宽度 )

文章目录 一、视口标签设置二、CSS 样式文件设置三、布局宽度设置1、设置布局宽度2、设置布局最大宽度3、设置布局最小宽度4、查看网页最大最小宽度5、布局宽度设置 四、代码示例1、主界面标签2、CSS 布局设置 一、视口标签设置 参考 【移动端网页布局】移动端网页布局基础概念…...

力扣---LeetCode88. 合并两个有序数组

文章目录 前言88. 合并两个有序数组链接&#xff1a;方法一&#xff1a;三指针(后插)1.2 代码&#xff1a;1.2 流程图&#xff1a;方法二&#xff1a;开辟新空间2.1 代码&#xff1a;2.2 流程图&#xff1a;2.3 注意&#xff1a; 总结 前言 “或许你并不熠熠生辉甚至有点木讷但…...

H7-TOOL的CANFD Trace全解析功能制作完成,历时一个月(2023-04-28)

为了完成这个功能&#xff0c;差不多耗费了一个月时间&#xff0c;精神状态基本已经被磨平了。 当前已经支持&#xff1a; 1、LUA小程序控制&#xff0c;使用灵活。 2、采用SWD接口直接访问目标板芯片的CANFD外设寄存器和CANFD RAM区实现&#xff0c;支持USB&#xff0c;以太网…...

探析Android中的四类性能优化

作者&#xff1a;Yj家的孺子牛 流畅性优化 主线程模型 了解 Android 的流畅性优化之前&#xff0c;我们需要先了解Android的线程结构。在 Android 中&#xff0c;有一个主线程模型&#xff0c;其中所有的绘制以及交互都是在主线程中进行的&#xff0c;所以&#xff0c;当我们…...

ubuntu18.04 安装编译zlmediakit

参考http://www.cherrylord.cn/archives/zlmediakit 1、获取代码 #国内用户推荐从同步镜像网站gitee下载 git clone --depth 1 https://gitee.com/xia-chu/ZLMediaKit cd ZLMediaKit #千万不要忘记执行这句命令 git submodule update --init#国内用户推荐…...

C++ -5- 内存管理

文章目录 C语言和C内存管理的区别示例1. C/C 中程序内存区域划分2. C中动态内存管理3.operator new 与 operator delete 函数4.new 和 delete 的实现原理5.定位new表达式 C语言和C内存管理的区别示例 //C语言&#xff1a; struct SListNode {int data;struct SListNode* next; …...

(Linux)在Ubuntu系统中添加新用户并授予root权限

向Ubuntu系统中添加新用户并为其授予root权限的步骤如下: 打开终端Terminal 输入命令: sudo su - 以 root 身份登录. 注: sudo su : 切换root身份, 不携带当前用户环境变量 sudo su - : 切换root身份, 携带当前用户环境变量 输入命令: adduser username 向Ubuntu系统中添…...

AttributeError: ‘ChatGLMModel‘ object has no attribute ‘prefix_encoder‘

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…...

Huggingface的GenerationConfig 中的top_k与top_p详细解读

Huggingface的GenerationConfig 中的top_k与top_p详细解读 Top_kTop_p联合共用 Top_k top-k是指只保留概率最高的前k个单词&#xff0c;然后基于剩余单词的概率进行归一化&#xff0c;从中随机抽取一个单词作为最终输出。这种方法可以限制输出序列的长度&#xff0c;并仍然保持…...

学生信息管理系统简易版(文件读写操作)

功能模块 具体功能如下&#xff1a; 添加学生信息修改学生信息&#xff08;按学号&#xff09;排序&#xff08;分别按总分升序、降序、以及按姓名升序&#xff09;查找学生&#xff08;按学号&#xff09;删除学生查看所有学生信息 数据结构体设计 本表设计一个学生信息的结…...