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之后父子进程轮转协作,运行SimpleThread
    SimpleThread(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 argument
    char filename[50];
    int addr = machine->ReadRegister(4);
    int i = 0;
    do
    {
        // read filname from mainMemory
        machine->ReadMem(addr + i, 1, (int *)&filename[i]);
    } while (filename[i++] != '\0');
    printf("Exec(%s)\n", filename);

    // 为halt.noff创建相应的进程以及相应的核心线程
    // 将该进程映射至新建的核心线程上执行begin
    DEBUG('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 spaceID
    machine->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 registers
	    currentThread->pcb->space->SaveState();
    }
#endif
    
    oldThread->CheckOverflow();		    // check if the old thread
					    // had an undetected stack overflow

    currentThread = nextThread;		    // switch to the next thread
    currentThread->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 space
        currentThread->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 eax
        movl    4(%esp),%eax            # move pointer to t1 into eax
        movl    %ebx,_EBX(%eax)         # save registers
        movl    %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 pointer
        movl    _eax_save,%ebx          # get the saved value of eax
        movl    %ebx,_EAX(%eax)         # store it
        movl    0(%esp),%ebx            # get return address from stack into ebx
        movl    %ebx,_PC(%eax)          # save it into the pc storage

        movl    8(%esp),%eax            # move pointer to t2 into eax

        ......
        movl    _eax_save,%eax

        ret

#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 addresses
    stackTop = 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 stackTop

  public:

结合下面宏定义,了解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   %ebp
        movl    %esp,%ebp
        pushl   InitialArg
        call    StartupPC
        call    InitialPC
        call    WhenDonePC

        # NOT REACHED
        movl    %ebp,%esp
        popl    %ebp
        ret

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个过程。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/16271.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySQL学习笔记第七天

第07章单行函数 2. 数值函数 2.4 指数函数、对数函数 函数用法POW(x,y)&#xff0c;POWER(X,Y)返回x的y次方EXP(X)返回e的x次方&#xff0c;其中e是一个常数&#xff0c;2.718281828459045LN(X)&#xff0c;LOG(X)返回以e为底的X的对数&#xff0c;当x<0时&#xff0c;返…

Linux Ansible任务控制(循环判断、处理程序、失败任务)

目录 Ansible的Loop循环 简单的Loop循环 数组列表方式的Loop循环 字典方式的Loop循环 基于外部变量的Loop循环 Ansible的When判断 通过魔法变量、事实变量作为条件 通过剧本执行结果的变量来作为条件 Ansible处理程序 Ansible处理失败任务 处理失败任务ignore_error…

Stable Diffusion 本地部署教程不完全指南

ChatGPT免费体验入口网址 http://chat.xutongbao.top 参考链接&#xff1a; ERROR: Could not find a version that satisfies the requirement torch1.7.0 ERROR: No matching……_congcongiii的博客-CSDN博客 下载链接&#xff1a; https://download.pytorch.org/whl/cu11…

KeepChatGPT插件-提效神器,解决ChatGPT报错!

KeepChatGPT插件-提效神器&#xff0c;解决ChatGPT报错&#xff01; 文章目录 KeepChatGPT插件-提效神器&#xff0c;解决ChatGPT报错&#xff01;一、错误提示关于 为何会出现大规模地网络错误二、解决方案三、安装步骤电脑端使用1.Chrome浏览器安装2.Firefox浏览器安装 四、使…

医院信息系统HIS源码——接口技术:RESTful API + WebSocket + WebService

云HIS系统采用SaaS软件应用服务模式&#xff0c;提供软件应用服务多租户机制&#xff0c;实现一中心部署多机构使用。相对传统HIS单机构应用模式&#xff0c;它可灵活应对区域医疗、医疗集团、医联体、连锁诊所、单体医院等应用场景&#xff0c;并提升区域内应用的标准化与规范…

LangChain 2 ONgDB:大模型+知识图谱实现领域知识问答

LangChain 2 ONgDB&#xff1a;大模型知识图谱实现领域知识问答 LangChain 2 ONgDB&#xff1a;大模型知识图谱实现领域知识问答系统截图LangChain代理流程 Here’s the table of contents: LangChain 2 ONgDB&#xff1a;大模型知识图谱实现领域知识问答 LangChain 是一种 LL…

asp.net+sqlserver旅游网站zjy99A2

1&#xff0e;系统登录&#xff1a;系统登录是用户访问系统的路口&#xff0c;设计了系统登录界面&#xff0c;包括用户名、密码和验证码&#xff0c;然后对登录进来的用户判断身份信息&#xff0c;判断是管理员用户还是普通用户。 2&#xff0e;系统用户管理&#xff1a;不管是…

【校招VIP】IT职位校招简历千万不要用两栏的模板,另外,告诉你个陷阱:越个性机会越少

前两天在简历指导的直播里&#xff0c;发现了不应该出现的一种简历格式问题。 有的同学喜欢用那种竖栏两栏的简历模板。 我们建议研发岗的校招&#xff0c;简历不要这么去写。 因为两栏的话&#xff0c;实际上有一个很大的问题。 因为简历上需要写项目经历&#xff0c;需要写…

从Pandas快速切换到Polars :数据的ETL和查询

对于我们日常的数据清理、预处理和分析方面的大多数任务&#xff0c;Pandas已经绰绰有余。但是当数据量变得非常大时&#xff0c;它的性能开始下降。 我们以前的两篇文章来测试Pandas 1.5.3、polar和Pandas 2.0.0之间的性能了&#xff0c;Polars 正好可以解决大数据量是处理的…

国内直接使用的ChatGTP

ChatGTP都能做一些什么事&#xff1a; 回答问题&#xff1a;我可以通过自然语言处理技术来回答用户的问题&#xff0c;提供有用的信息和解决方案。 聊天互动&#xff1a;我可以和用户聊天互动&#xff0c;倾听对话和提供支持。 搜索&#xff1a;我可以搜索互联网和已知的数据…

【Java笔试强训 19】

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 一、选择题 二、编程题 &#x1f525;汽水瓶 …

「Bug」解决办法:Could not switchto this profil,无法使用节点的解决方法,彻底解决

♥️作者&#xff1a;白日参商 &#x1f935;‍♂️个人主页&#xff1a;白日参商主页 ♥️坚持分析平时学习到的项目以及学习到的软件开发知识&#xff0c;和大家一起努力呀&#xff01;&#xff01;&#xff01; &#x1f388;&#x1f388;加油&#xff01; 加油&#xff01…

【五一创作】Scratch资料袋

Scratch软件是免费的、免费的、免费的。任何需要花钱才能下载Scratch软件的全是骗子。 1、什么是Scratch Scratch是麻省理工学院的“终身幼儿园团队”开发的一种图形化编程工具。是面向青少年的一款模块化&#xff0c;积木化、可视化的编程语言。 什么是模块化、积木化&…

1. 安装Open vSwitch环境

1. 安装Open vSwitch环境 1 配置基础环境。 在VMware Workstation软件中创建一个虚拟机VM1&#xff0c;配置2张网卡&#xff0c;虚拟机VM1配置如图4-3所示。将网卡ens33地址配置为192.168.1.131/24&#xff0c;网卡ens34地址配置为192.168.2.131/24。 图4-3 VM1虚拟机配置 2…

好家伙,9:00面试,9:06就出来了,问的实在是太...

从外包出来&#xff0c;没想到死在另一家厂子 自从加入这家公司&#xff0c;每天都在加班&#xff0c;钱倒是给的不少&#xff0c;所以也就忍了。没想到2月一纸通知&#xff0c;所有人不许加班&#xff0c;薪资直降30%&#xff0c;顿时有吃不起饭的赶脚。 好在有个兄弟内推我去…

为什么在马云成功前就有那么多影像留下来?

马云创业的各个阶段&#xff0c;都有意无意得到媒体的推波助澜&#xff0c;不光是影像&#xff0c;还留下了很多相关的文字报道。站在当时的角度&#xff0c;马云或许并不总是以一种成功人士的身份出现&#xff0c;但即便如此&#xff0c;他做事情也足够新潮、足够前卫、或者足…

【Spring】三大依赖注入(@Autowired,Setter,构造方法)

目录 一、属性注入&#xff08;Autowired&#xff09; 1.1 优点分析 1.2 缺点分析 1.2.1 无法实现final修饰的变量注入。 1.2.2 兼容性不好 1.2.3 &#xff08;可能违背&#xff09;设计原则问题 1.2.4 代码举例&#xff1a; 1.2.5 出现循环依赖该怎么办&#xff1f; …

初识MySQL数据库——“MySQL数据库”

各位CSDN的uu们你们好呀&#xff0c;小雅兰好久没有更文啦&#xff0c;确实是心有余而力不足&#xff0c;最近学习的内容太难了&#xff0c;这篇博客又是小雅兰的新专栏啦&#xff0c;主要介绍的是一些MySQL数据库的知识点&#xff0c;下面&#xff0c;让我们进入初识MySQL数据…

【Java入门合集】第二章Java语言基础(二)

【Java入门合集】第二章Java语言基础&#xff08;二&#xff09; 博主&#xff1a;命运之光 专栏JAVA入门 学习目标 掌握变量、常量、表达式的概念&#xff0c;数据类型及变量的定义方法&#xff1b; 掌握常用运算符的使用&#xff1b; 掌握程序的顺序结构、选择结构和循环结构…

计算机是如何工作的

目录 一、冯诺依曼体系&#xff1a; 二、操作系统 三、进程 3.1 PCB&#xff08;进程控制块&#xff09;— 描述进程属性的结构体 3.2 CPU分配 — 进程调度 3.3 内存分配 — 虚拟地址 3.4 进程间通信 一、冯诺依曼体系&#xff1a; CPU中央处理器&#xff08;运算器控制…