Nachos系统的上下文切换

news2024/11/23 11:55:55

在这里插入图片描述

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.coloradmin.cn/o/472920.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vulnhub靶场之Wayne Manor

1.信息收集 探测存活主机&#xff0c;发现192.168.239.174存活 对主机进行端口扫描&#xff0c;发现存在端口21、22、80&#xff1b;且21端口状态是filtered。 打开浏览器访问http://192.168.239.174&#xff0c;未发现可疑&#xff0c;进行目录扫描只发现robots.txt。 什…

集合详解之(七)泛型

文章目录 &#x1f412;个人主页&#x1f3c5;JavaSE系列专栏&#x1f4d6;前言&#xff1a;&#x1f380;泛型的由来--参数化类型&#x1fa80;元组的介绍 ( int...数组名x )&#x1fa84;类型通配符&#x1f387;使用泛型的注意事项&#x1f3c5;子类继承泛型类【两种情况】 …

Transformers 发展一览

动动发财的小手&#xff0c;点个赞吧&#xff01; Transformers 研究概览 1. 介绍 近年来&#xff0c;深度学习的研究步伐显着加快&#xff0c;因此越来越难以跟上所有最新发展。尽管如此&#xff0c;有一个特定的研究方向因其在自然语言处理、计算机视觉和音频处理等多个领域取…

CLion开发工具 | 04 - CLion内置工具和插件

专栏介绍 一、CLion内置工具 1. SSH终端工具 填写ssh远程连接信息&#xff1a; 连接后在Terminal栏即可使用&#xff1a; 2. HTTP请求 填写http请求内容&#xff0c;并发起请求&#xff0c;方便的一批&#xff1a; 二、插件 1. 管理已安装的插件 2. 简体中文插件 3. 主题…

React 环境搭建,并打包到服务器

一. 安装node.js brew install node 二. 创建react app npx create-react-app my-app cd my-app npm start 默认使用3000端口&#xff0c;本地3000端口被占用&#xff0c;修改/node_modules/react-scripts/scripts/start.js中的端口号 // 这是start.js部分源码 const DEFAU…

【前端客栈】使用CSS实现畅销书排行榜页面

&#x1f4ec;&#x1f4eb;hello&#xff0c;各位小伙伴们&#xff0c;我是小浪。大家都知道&#xff0c;我最近是在更新各大厂的软件测试开发的面试真题&#xff0c;也是得到了很大的反馈和好评&#xff0c;几位小伙伴也是成功找到了测开的实习&#xff0c;非常不错。如果能前…

移除链表元素

☃️个人主页&#xff1a;fighting小泽 &#x1f338;作者简介&#xff1a;目前正在学习C语言和数据结构 &#x1f33c;博客专栏&#xff1a;leetcode练习题 &#x1f3f5;️欢迎关注&#xff1a;评论&#x1f44a;&#x1f3fb;点赞&#x1f44d;&#x1f3fb;留言&#x1f4a…

ChatGPT 和 Elasticsearch:OpenAI 遇见私有数据(二)

在之前的文章 “ChatGPT 和 Elasticsearch&#xff1a;OpenAI 遇见私有数据&#xff08;二&#xff09;” 中&#xff0c;我们详细描述了如何结合 ChatGPT 及 Elasticsearch 来进行搜索。它使用了如下的架构&#xff1a; 在今天的文章中&#xff0c;我们来详细描述实现这个的详…

BatchNormalization 介绍

1 为何要用BatchNormalization 为了让深层网络更容易训练&#xff0c;有两种方法&#xff1a; 使用更好的优化器&#xff1a;如 SDGMomentun等&#xff1b; 改变网络结构&#xff0c;比如加入BN层&#xff0c;处理网络数据&#xff0c;让网络数据服从标准的高斯分布&#xff0…

人群计数传统方法:object detection, regression-based

数据标注方式&#xff1a; &#xff08;1&#xff09;人很少、人很大的时候用bounding box&#xff0c;把人从头到脚都框进长方形方框内&#xff0c;这个方框只用记录三个点的坐标&#xff0c;左下、左上、右下&#xff1b;测试集预测的时候&#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…

(有假币,因子个数)笔试强训

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE初阶 目录 文章目录 一、选择1 二、选择2 二、[编程题]有假币 三、[编程题]因子个数 一、选择1 在使用锁保证线程安全时&#xff0c;可能会出现活跃度失败的情况&#xff0c;活跃度失败主要…

【DataGrip】手把手教你使用可视化数据库管理工具DataGrip(附数据库驱动无法下载解决办法)

博主简介&#xff1a;努力学习的大一在校计算机专业学生&#xff0c;热爱学习和创作。目前在学习和分享&#xff1a;数据结构、Go&#xff0c;Java等相关知识。博主主页&#xff1a; 是瑶瑶子啦所属专栏: Mysql从入门到精通近期目标&#xff1a;写好专栏的每一篇文章 目录 一、…

Android 面试笔记总结,建议吸收一下灵气~

android消息机制 消息机制指Handler、Looper、MessageQueue、Message之间如何工作的。 handler是用来处理消息和接收消息的中间者&#xff0c;handler的创建会伴随着handler中产生looper和MessageQueue&#xff0c;handler依赖于looper&#xff0c;looper依赖于MessageQueue&a…

大学生体质测试管理系统~java

摘要 大学生体质测试管理系统提供给用户一个简单方便体质测试管理信息&#xff0c;通过留言区互动更方便。本系统采用了B/S体系的结构&#xff0c;使用了java技术以及MYSQL作为后台数据库进行开发。系统主要分为系统管理员、教师和用户三个部分&#xff0c;系统管理员主要功能…

[论文笔记]C^3F,MCNN:图片人群计数模型

(万能代码)CommissarMa/Crowd_counting_from_scratch 代码&#xff1a;https://github.com/CommissarMa/Crowd_counting_from_scratch (万能代码)C^3 Framework开源人群计数框架 科普中文博文&#xff1a;https://zhuanlan.zhihu.com/p/65650998 框架网址&#xff1a;https…

[oeasy]python0141_自制模块_module_reusability_复用性

自制包内容 回忆上次内容 上次导入了外部的py文件 import my_module 导入一个自己定义的模块 可以使用my_module中的变量 不能 直接使用 my_module.py文件中的变量只要加my_module.作为前缀就可以 直接导入导入变量、函数 from my_module import pi 可以导入my_module.pi 并…

面试必问的Java 线程池原理及最佳实践

1. 概述 1.1 线程池是什么 线程池&#xff08;Thread Pool&#xff09;是一种基于池化思想管理线程的工具&#xff0c;经常出现在多线程服务器中&#xff0c;如MySQL。 创建线程本身开销大&#xff0c;反复创建并销毁&#xff0c;过多的占用内存。所以有大量线程创建考虑使用…

【Python_Opencv图像处理框架】直方图与傅里叶变换

写在前面 本篇文章是opencv学习的第五篇文章&#xff0c;主要讲解了直方图与傅里叶变换的有关操作&#xff0c;作为初学者&#xff0c;我尽己所能&#xff0c;但仍会存在疏漏的地方&#xff0c;希望各位看官不吝指正&#x1f970; 写在中间 一、直方图 &#xff08; 1 &…

软件企业利用ChatGPT的正确姿势

先来看一下现在市场环境 ChatGPT作为现象级产品横空出世之后&#xff0c;极大地带动了大语言模型产业和生成式AI&#xff08;AIGC&#xff09;产业的蓬勃发展。海外市场上&#xff0c;OpenAI、微软、谷歌、Meta等巨头动作频频。中国市场更是风起云涌&#xff0c;百度、阿里、华…