欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Nachos实习——Lab1线程机制实习报告

程序员文章站 2022-05-12 12:36:38
...

Nachos实习——Lab1线程机制实习报告

文章目录

内容一:总体概述

本次Lab针对的内容是实现线程最基本的数据结构——进程控制块(PCB)和线程的调度机制,同时扩展实现时间片轮转调度算法。

内容二:任务完成情况

Exercise 1 Exercise 2 Exercise 3 Exercise 4 Exercise 5 Exercise 6 Challenge
完成情况 Y Y Y Y Y Y Y

内容三:具体完成Exercise情况

Exercise 1 调研

调研Linux或Windows中进程控制块(PCB)的基本实现方式,理解与Nachos的异同。调研Linux或Windows中采用的进程/线程调度算法。

我的调研选择了Linux-2.6.0版本阅读

1、Linux进程控制块(PCB)

通过百度了解到Linux的进程控制块由一个task_struct的结构体定义。存放在文件/linux-2.6.0/include/linux/sched.h中,里面包含了众多信息,其中比较重要的信息:

  • 标识符:主要包括进程标识符、用户标识符、组标识符、备份用户标识符、文件系统用户标识符等。

  • 进程状态:进程状态是进城调度和交换的依据。

  • 进程调度信息:这些包含进程优先级以及进程的类别等

  • 进程通信相关的信息:Linux支持多种不同形式的通信机制。它支持典型的Unix通信机制:信号、管道,也支持System V通信机制:共享内存、信号量和消息队列

  • 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

  • 上下文环境:进程执行时处理器的寄存器中的数据

  • 其他:还有其他一些必要信息

2、Linux进程控制块与Nachos的异同

Linux的进程控制块包含了比较详细的进程信息,而Nachos相对于Linux系统的PCB实现就要简单很多。

Nachos在Thread 类中实现了操作系统的线程控制块。Thread 线程控制块较Linux PCB 为简单的多,它没有线程标识 (pid)、实际用户标识 (uid)等和线程操作不是非常有联系的部分。Nachos仅仅定义了四个线程控制块的变量stackTop and stack(表示当前进程所占的栈顶和栈底)、machineState( 保留未在CPU上运行的进程的寄存器状态)、status(表示当前进程的状态)以及一些最基本的对线程操作的函数,如Fork()、Sleep()等。

除此之外,Nachos线程的总数目没有限制,线程的调度比较简单,而且没有实现线程的父子关系等。

3、Linux采用的进程/线程调度算法

Linux内核的三种调度策略 :

  • SCHED_OTHER 分时调度策略,(默认的)

  • SCHED_FIFO实时调度策略,先到先服务

  • SCHED_RR实时调度策略,时间片轮转

Linux内核将进程分成两个级别:普通进程和实时进程。实时进程将得到优先调用,实时进程根据实时优先级决定调度权值,分时进程则通过nice和counter值决定权值,nice越小,counter越大,被调度的概率越大,也就是曾经使用了cpu最少的进程将会得到优先调度。

SHCED_RR和SCHED_FIFO的不同:

当采用SHCED_RR策略的进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平。

SCHED_FIFO一旦占用cpu则一直运行。一直运行直到有 更高优先级任务到达或自己放弃 。

Exercise 2 源代码阅读

仔细阅读下列源代码,理解Nachos现有的线程机制。

  • code/threads/main.cc和code/threads/threadtest.cc

  • code/threads/thread.h和code/threads/thread.cc

Nachos现有的线程机制与Linux的进程机制比较相似,只是相对比较简单。

Nachos的线程有四种状态:

  • JUST_CREATED
  • RUNNING
  • READY
  • BLOCKED

和四个主要的方法Thread()Fork()Finish()Yield()Sleep()

  • Thread():构造函数,初始化一个新的Thread。
  • Fork(VoidFunctionPtr func,int arg):func,新线程运行的函数;分配一块固定大小的内存作为线程的堆栈,在栈顶放入 ThreadRoot 的地址。
  • Finish():并不是直接收回线程的数据结构和堆栈,因为我们仍在这个堆栈上运行这个线程。做法是将threadToBeDestroyed的值设为当前线程,使得Scheduler的Run()可以调用销毁程序,当我们这个程序退出上下文时,将其销毁。
  • Yield():用于本线程放弃处理机。
  • Sleep():可以使当前线程转入阻塞态,并放弃 CPU, 直到被另一个线程唤醒,把它放回就绪线程队列

下面对每个文件进行了简单的说明:

1、code/threads/main.cc

该模块是整个 Nachos 系统的入口,它分析了 Nachos 的命令行参数,根据不同的选项进行不同功能的初始化设置。

2、code/threads/threadtest.cc

这是一个简单的线程实验的测试用例。用于指导我们如何对线程的修改进行测试的。

  • testnum:测试号,对应相应的测试函数。
  • SimpleThread():一个5次循环的程序,每次循环中都让出CPU,让其他就绪的线程执行。
  • ThreadTest1():一个测试方法,创建两个线程,让他们都执行SimpleThread()方法,使这两个线程可以交替执行。
  • ThreadTest():可以看做一个总控程序,根据main函数传过来testnum参数值来执行不同的测试程序。例如,当testnum==1时,就执行ThreadTest1()。

3、code/threads/thread.h

用于管理线程的数据结构。如线程控制块、线程的基本方法都在这个文件中被定义。

4、code/threads/thread.cc

实现了用于管理线程事务的具体方法。主要有四种操作:Fork 、Finish、Yield、Seelp。

Exercise 3 扩展线程的数据结构

增加“用户ID、线程ID”两个数据成员,并在Nachos现有的线程管理机制中增加对这两个数据成员的维护机制。

设计思路:

  • 对于用户ID:现有的Nachos代码不支持多用户,所以我直接将用户ID设置为固定值1000。
  • 对于线程ID:结合Exercise 4,我声明了一个容量为128的数组,初始值全0。每个元素的取值为0或1。0表示该数组下标没有作为线程的ID分配出去,1表示已分配。每次创建线程的时候都遍历一次数组,把第一个不为0的下标分配给线程作为ID。如果全为1,则返回线程ID为-1,表示不创建线程

1、threads/threads.h

在threads/threads.h中添加变量和方法

class Thread {
    private:
    		int tid;    //添加线程ID
   		 	int uid;    //添加用户ID
    public:
    		int getTid();    //获取线程ID
    		int getUid();    //获取用户ID
    		void setUid();   //设置用户ID
}

2、threads/system.h

由于系统的全局变量都声明和定义在system.h或system.cc中,我们也将数组定义在这里

 #define MAX_THREAD_NUM 128						//根据Exercise 4将系统可容纳的线程数量设置为128
extern int tid_flag[MAX_THREAD_NUM];	//用来标识该ID是否被分配

注意:上面使用了extern定义tid_flag数组,是因为我们事先需要在system.cc文件中定义和初始化

3、threads/system.cc

在Initialize()函数中初始化数组

int tid_flag[MAX_THREAD_NUM];
void
Initialize(int argc, char **argv)
{
    for (int i = 0; i < MAX_THREAD_NUM; i++) {  // 初始化线程数组
        tid_flag[i] = 0;
}

4、threads/threads.cc

实现threads.h中声明的方法,已经分配线程ID(在构造函数中分配),同时还需要在析构函数中释放分配了的线程id

Thread::Thread(char* threadName)
{
    int i = 0;
    for(i = 0; i < MAX_THREAD_NUM; i++)
    {
        if(tid_flag[i] == 0)
            break;
    }
    if(i < MAX_THREAD_NUM)
    {
        tid = i;
        tid_flag[i] = 1;
    }
    else
    tid = -1;
}
void
Thread::setUid()
{
    uid = 1000;
}

int
Thread::getUid()
{
    return uid;
}

int
Thread::getTid()
{
    return tid;
}
Thread::~Thread()
{
    tid_flag[this->tid] = 0; // Lab1 Execise3:释放线程标识
}

5、threads/threadtest.cc

仿照threadtest.cc中原有的测试函数,添加新的测试函数,同时将新的测试函数添加至ThreadTest()中,作为情况2

void
Lab1Exercise3Thread(int which)//仿照原有的SimpleThread()函数
{
    int num;
    for (num = 0; num < 5; num++) {
    printf("*** thread %d (uid=%d, tid=%d) looped %d times\n", which, currentThread->getUid(), currentThread->getTid(), num);
        currentThread->Yield();
    }
}
void
Lab1Exercise3()//仿照原有的ThreadTest1()函数
{
    DEBUG('t', "Entering Lab1Exercise3");
    const int max_threads = 5;
    for (int i = 0; i < max_threads; i++) {
        // Generate a Thread object
        Thread *t = new Thread("forked thread");
        t->setUid(); // set uid
        // Define a new thread's function and its parameter
        t->Fork(Lab1Exercise3Thread, (void*)t->getTid());
    }
    Lab1Exercise3Thread(0);
}

注意函数Lab1Exercise3()必须添加在ThreadTest()之前,或者事先进行声明,否则会报错。

6、测试结果截图

Nachos实习——Lab1线程机制实习报告

Exercise 4 增加全局线程管理机制

在Nachos中增加对线程数量的限制,使得Nachos中最多能够同时存在128个线程;

仿照Linux中PS命令,增加一个功能TS(Threads Status),能够显示当前系统中所有线程的信息和状态。

1、增加对线程数量的限制

设计思路:在Exercise 3中已经进行了说明。通过声明一个容量为128的全局数组来实现。

1.1、threads/threads.cc
Thread::Thread(char* threadName)
{
    if (tid==-1) {
        printf("Reach maximum threads number %d, unable to allocate!!\n", MAX_THREAD_NUM);
    }
    ASSERT(tid!=-1);//如果不能创建线程,则终止创建
}
1.2、threads/threadtest.cc

同上仿照threadtest.cc中原有的测试函数,添加新的测试函数,同时将新的测试函数添加至ThreadTest()中,作为情况3

void
Lab1Exercise4_1()
{
    DEBUG('t', "Entering Lab1Exercise4_1");
    const int max_threads = 130;
    for (int i = 0; i < max_threads; i++) {
        Thread *t = new Thread("forked thread");
        printf("*** thread name %s (tid=%d)\n", t->getName(), t->getTid());
    }
}
void ThreadTest()
{
    case 3:
    Lab1Exercise4_1();
    break;
}

同上注意函数Lab1Exercise4_1()必须添加在ThreadTest()之前,或者事先进行声明,否则会报错

1.3、测试结果截图

Nachos实习——Lab1线程机制实习报告

2、增加一个功能TS

设计思路:这里的实现思路和上一个有点相似。上一个实验定义了一个容量为128的int型数组去为线程分配id,所以在这个实验中我们也可以类似的定义一个容量为128的Thread类型的数组去存储已经生成的线程的相关信息,然后通过调用thread类的相关方法就可以打印出线程的信息。

2.1、threads/system.h

声明一个Thread类的数组

extern Thread* tid_p[MAX_THREAD_NUM];
2.2、threads/system.h

初始化Thread类的数组

Thread* tid_p[MAX_THREAD_NUM];
void
Initialize(int argc, char **argv)
{
    for (int i = 0; i < MAX_THREAD_NUM; i++) {  // 初始化线程变量
        tid_flag[i] = 0;
        tid_p[i]=NULL;
    }
}
2.3、threads/thread.h

由于Status是一个枚举类型,要输出其字符串型的内容有些困难,通过添加一公共方法getThreadStatus()来将变量以字符串类型输出

ThreadStatus getThreadStatus() { return (status); } 
2.4、threads/thread.cc

类似于上一个实验,在构造函数中对tid_p数组赋值

if(i < MAX_THREAD_NUM)
{
		tid = i;
    tid_flag[i] = 1;
    tid_p[i]=this;
}
2.5、threads/threadtest.cc

(1)添加TS函数,用来展示当前线程的状态

void
TS()
{
    DEBUG('t', "Entering TS");

    const char* TStoString[] = {"JUST_CREATED", "RUNNING", "READY", "BLOCKED"};

    printf("UID\tTID\tNAME\tSTATUS\n");//打印进程表
    for (int i = 0; i < MAX_THREAD_NUM; i++) { // check pid flag
        if (tid_flag[i]) {
            printf("%d\t%d\t%s\t%s\n", tid_p[i]->getUid(), tid_p[i]->getTid(), tid_p[i]->getName(), TStoString[tid_p[i]->getThreadStatus()]);
        }
    }
}

(2)添加CurrentThreadFunc函数,用来制定当前线程所处状态

void
CurrentThreadFunc(int which)
{
    scheduler->Print();//打印出当前就绪队列里的线程
    printf("\n");
    printf("*** current thread (uid=%d, tid=%d, name=%s)==>", currentThread->getUid(), currentThread->getTid(), currentThread->getName());
    IntStatus oldLevel; 
    switch (which)
    {
        case 0:
            printf("Yield\n");
            currentThread->Yield();
            break;
        case 1:
            printf("Sleep\n");
            oldLevel = interrupt->SetLevel(IntOff); 
            currentThread->Sleep();
            (void) interrupt->SetLevel(oldLevel);   
            break;
        case 2:
            printf("Finish\n");
            currentThread->Finish();
            break;
        default:
            printf("Yield\n");
            currentThread->Yield();
            break;
    }
}

(4)添加测试函数

void
Lab1Exercise4_2()
{
    DEBUG('t', "Entering Lab1Exercise4_2");
    Thread *t1 = new Thread("fork 1");
    Thread *t2 = new Thread("fork 2");
    Thread *t3 = new Thread("fork 3");
    Thread *t4 = new Thread("fork 4");
    Thread *t5 = new Thread("fork 5");
    Thread *t6 = new Thread("fork 6");
		t1->Fork(CurrentThreadFunc, (void*)t1->getTid());
    t2->Fork(CurrentThreadFunc, (void*)t2->getTid());
    t3->Fork(CurrentThreadFunc, (void*)t3->getTid());
    t4->Fork(CurrentThreadFunc, (void*)t4->getTid());
    t5->Fork(CurrentThreadFunc, (void*)t5->getTid());
    t6->Fork(CurrentThreadFunc, (void*)t6->getTid());

    Thread *t7 = new Thread("fork 7");
    CurrentThreadFunc(0);//main进程
    printf("--- Calling TS command ---\n");
    TS();
}
2.6、threads/main.cc

添加TS命令行参数

switch (argv[0][1]) {
      case 'q':
        testnum = atoi(argv[1]);
        argCount++;
        break;
      case 't':
      	testnum=4;
      	break;
2.7、测试结果截图

Nachos实习——Lab1线程机制实习报告

Exercise 5 源代码阅读

仔细阅读下列源代码,理解Nachos现有的线程调度算法。

  • code/threads/scheduler.h和code/threads/scheduler.cc

  • code/threads/switch.s

  • code/machine/timer.h和code/machine/timer.cc

1、threads/scheduler.h和threads/scheduler.cc

Schedule类维护一个就绪进程队列,当一个进程可以占用处理机时,就可以调用ReadyToRun成员函数把这个进程放入就绪进程队列,并把进程状态改成就绪态.FindNextToRun函数根据调度策略,取出下一个应运行的进程,并把这个进程从就绪进程队列中删除.如果就绪进程队列为空,则此函数返回空(NULL).现有的调度策略是先进先出策略(FIFO),取出下一个进程之后调用Run方法运行。下面阶段介绍了Run函数的运行过程。

1.如果当前进程(由currentThread指明)是用户进程,则保存用户程序状态和用户空间的状态.
2.将新进程的地址赋给currentThread变量,表明当前进程变为新进程了.
3.将新进程的状态改为运行态.
4.调用SWITCH函数切换进程正文.
5.查看有没有已经结束但没有删除的进程,如果有,则将其删除.
6.如果新进程是用户进程,则恢复用户程序状态和用户空间的状态.

2、threads/switch.s

switch.s模拟内容是汇编代码,负责CPU上进程的切换。切换过程中,首先保存当前进程的状态,然后恢复新运行进程的状态,之后切换到新进程的栈空间,开始运行新进程。

3、machine/timer.h和machine/timer.cc

Timer类用以模拟硬件的时间中断。在TimerExired中,会调用TimeOfNextInterrupt,计算出下次时间中断的时间,并将中断插入中断队列中。初始化时会调用TimerExired,然后每次中断处理函数中都会调用一次TimerExired,从而时间系统时间一步步向前走

Exercise 6 线程调度算法扩展

扩展线程调度算法,实现基于优先级的抢占式调度算法。

设计思路:更改Thread类,加入priority成员变量,同时更改初始化函数对其初始化,并完成对应的set和get函数。scheduler中的ReadyToRun负责把线程插入就绪队列,默认是尾插法。而FindNextToRun()是负责找到下一个运行的进程,默认是FIFO,也就是取出就绪队列首部的线程。

为此我们现在要实现的根据优先级来返回,只需将插入readyList队列的方法按照优先级从高到低顺序插入SortedInsert,那么插入时会维护队列中的Thread按照优先级排序,每次依旧从头取出第一个,即为优先级最高的队列(我的优先级是数越小优先级越高)。

上述思想实现了优先级调度。而抢占式则是:当有新线程的优先级高于当前线程是,则当前线程主动放弃CPU让给新的线程,具体实现只需改动ReadyToRun函数和Yield 函数即可。

1、threads/thread.h

class Thread {
    private:
        int priority;                       
    public:
  			Thread(char* debugName, int p);//构造函数重载
   		  Thread(char* debugName);  
        int getPriority() { return (priority); }  //获取优先级
        void setPriority(int p) { priority = p; } //设置优先级
}

2、threads/thread.cc

Thread::Thread(char* threadName, int p)
    : priority( p )
{
		//与原来的构造函数一样
}
void
Thread::Yield ()
{
    /*nextThread = scheduler->FindNextToRun();//原来的实现
    if (nextThread != NULL) {
      scheduler->ReadyToRun(this);
   		scheduler->Run(nextThread);
    }*/
    scheduler->ReadyToRun(this);//这里调换顺序是为了实现抢占式调度,需要结合ReadyToRun()。先把this线程加入就绪队列
  															//this线程的优先级有可能大于当前线程,则实现抢占。
    nextThread = scheduler->FindNextToRun();
    if (nextThread != NULL) {
    	scheduler->Run(nextThread);
    }
}

3、threads/scheduler.cc

void
Scheduler::ReadyToRun (Thread *thread)
{
    readyList->SortedInsert((void *)thread, thread->getPriority()); 
    if(thread!=currentThread && thread->getPriority() < currentThread->getPriority())
        currentThread->Yield();
}

4、threads/threadtest.cc

void
SimpleThread(int which)
{
    for (int num = 0; num < 5; num++) {
       printf("*** current thread (uid=%d, tid=%d, pri=%d name=%s) looped %d times\n", currentThread->getUid(), currentThread->getTid(), currentThread->getPriority(), currentThread->getName(),num);
        //currentThread->Yield();//注释掉,因为不再需要线程主动放弃,而是抢占式。
    }
}
void
p2(int which)
{
    for (int num = 0; num < 5; num++) {
        printf("*** current thread (uid=%d, tid=%d, pri=%d name=%s) looped %d times\n", currentThread->getUid(), currentThread->getTid(), currentThread->getPriority(), currentThread->getName(),num);
        if(num==2){
            printf("\ncreate thread(name=t3,pri=5)\n");
            Thread *t3 = new Thread("t3",5);
            t3->Fork(SimpleThread,1);
        }
    }
}
void
p1(int which)
{
    for (int num = 0; num < 5; num++) {
        printf("*** current thread (uid=%d, tid=%d, pri=%d name=%s) looped %d times\n", currentThread->getUid(), currentThread->getTid(), currentThread->getPriority(), currentThread->getName(),num);
        if(num==3){
            printf("\ncreate thread(name=t2,pri=2)\n");
            Thread *t2 = new Thread("t2",2);
            t2->Fork(p2,1);
        }
    }
}
void
Lab1Exercise6()
{
    Thread *t1 = new Thread("t1",7);
    t1->Fork(p1,1);
}

测试说明:在Lab1Exercise6()中,只生成了一个线程,调用了p1()。在p1()中有五次循环输出,其中第4次的时候生成一个优先级更高的t2。并调度执行p2()函数。p2()也是五次循环,在第3次循环的时候生成一个优先级在两个线程中间的t3线程。t3执行SimpleThread()方法,也是循环输出五次。所以优先级抢占式调度执行的结果应该是t1执行到第三次的时候被t2线程抢占,t2顺利执行结束,然后执行t3,最后在执行t1剩下的1次循环.(记得把Lab1Exercise6作为case7加入ThreadTest()中)

5、测试结果截图

Nachos实习——Lab1线程机制实习报告

Challenge 线程调度算法扩展(至少实现一种算法)

可实现“时间片轮转算法”、“多级队列反馈调度算法”,或将Linux或Windows采用的调度算法应用到Nachos上。

实现时间片轮转算法设计思路:Nachos作为一个分时操作系统,本身已经具备了轮转调度的功能,只要在运行程序的时候加上“-rs”, 就可以实现按照random的时间片长度进行线程轮转调度的效果。但是它是一个随机性质的时间片。而我们现在要做的就是实现一个固定时间片的调度算法。

原来的分时调度主要是体现在函数:thread/system.cc文件中的以下代码:

else if (!strcmp(*argv, "-rs")) {
	    ASSERT(argc > 1);
	    RandomInit(atoi(*(argv + 1)));	// initialize pseudo-random
	    randomYield = TRUE;
	    argCount = 2;
	}
if (randomYield)				
	timer = new Timer(TimerInterruptHandler, 0, randomYield);

通过在命令行输入-rs来运行代码设置randomYield为True,调用TimerInterruptHandler从而进行分时调度。为此我们可以仿照系统的时间,设计自己的时间片轮转调度。

在开始实现之前有几个系统参数需要解释一下:

#define TimerTicks 100 // (average) time between timer interrupt 
int totalTicks; // Total time running Nachos of class Statistics
int systemTicks;	 	// Time spent executing system code
int userTicks;       	// Time spent executing user code
#define SystemTick 	10

上面四个变量是系统已经定义好的,前两个分别为时间片、系统运行时间。

1、threads/scheduler.h

在scheduler类中添加一个变量lastSwitchTick,用来记录上一次进行上下文切换的时间

class Scheduler {
  public:
    int lastSwitchTick; 
}

2、threads/system.cc

在该文件中模仿原来的分时调度TimerInterruptHandler()函数添加时间片调度函数RRHandler

static void RRHandler(int dummy)//dummy我也不知道啥用,TimerInterruptHandler()函数中也有
{
    int timeDuration = stats->totalTicks - scheduler->lastSwitchTick;//线程运行时间
    printf("\nTimer interrupt with duration: %d", timeDuration);
    if (timeDuration >= TimerTicks) {//判断线程运行时间是否超过时间片
        if (interrupt->getStatus() != IdleMode) { // IdleMode模式相当于就绪队列为空
            printf(" (Determine to Context switch)\n");//实现进程切换
            interrupt->YieldOnReturn();//设置yieldOnReturn标志来实现进程切换
            scheduler->lastSwitchTick = stats->totalTicks; // 更新进程切换时间
        } else {
            printf(" (readyList is Empty)\n");
        }
    } else {
        printf("\n");}}
//模仿分时调度**时间片轮转调度
bool roundRobin = FALSE; 
if (!strcmp(*argv, "-rr")) { 
    ASSERT(argc > 1);
    roundRobin = TRUE;
    argCount = 2;
}
if (roundRobin)
    timer = new Timer(RRHandler, 0, FALSE);

3、threads/threadtest.cc

void
ThreadWithTicks(int rTime)//rTime代表线程需要运行多少个时间片
{
    int num;
    for (num = 0; num < rTime * SystemTick; num++) {//rTime * SystemTick表示线程需要多少次OneTick()运行结束
        printf("*** thread%d looped %d times (stats->totalTicks: %d)\n", rTime, num+1, stats->totalTicks);
        interrupt->OneTick(); // 让系统时间前进,一次前进10个ticks,也就是系统定义的SystemTick,相当于十分之一个时间片
    }
    currentThread->Finish();
}
void
Lab1ChallengeRR()
{
    DEBUG('t', "Entering Lab1ChallengeRR");
    printf("\nSystem initial ticks:\tsystem=%d, total=%d\n", stats->systemTicks, stats->totalTicks);
    Thread *t1 = new Thread("3");//线程的名字代表线程需要运行多少个时间片
    Thread *t2 = new Thread("2");
    Thread *t3 = new Thread("1");

    printf("\nAfter new Thread ticks:\tsystem=%d, total=%d\n", stats->systemTicks, stats->totalTicks);
    t1->Fork(ThreadWithTicks, (void*)3);
    t2->Fork(ThreadWithTicks, (void*)2);
    t3->Fork(ThreadWithTicks, (void*)1);
    printf("\nAfter 3 fork() ticks:\tsystem=%d, total=%d\n\n", stats->systemTicks, stats->totalTicks);
    scheduler->lastSwitchTick = stats->totalTicks;
    currentThread->Yield(); //因为当前进程是main,所以我们需要切换来进行测试
}

记得把Lab1ChallengeRR()添加做case8

4、测试结果截图

Nachos实习——Lab1线程机制实习报告

5、测试结果解释

首先线程1,2,3在每个时间片结束之后都进行切换了。其中线程1运行2个时间片即10个SystemTick,线程1运行2个时间片即20个SystemTick,线程3运行3个时间片即30个SystemTick。通过上图可以看出第一个时间片的前4个SystemTick是进行系统初始化和创建线程。从第一个时间片第5个SystemTick开始,线程3(最先创建)开始运行。然后线程2、再然后线程1。在第七个时间片的第1个SystemTick线程1运行结束。虽然时间片还没有结束,但是必须因为结束了,所以要进行线程切换。其中线程切换将耗时1个SystemTick(这是系统定义的)。所以totalTicks从610直接到了630。

内容四:遇到的困难以及解决方法

困难1:

对C++的语法不太熟悉。大一大二的时候学过c++。但是之后的很长一段时间没接触过了,所以在最开始看的时候对那些指针函数、函数传参、extern、类等语法都不是很情况。又花了不少时间去百度学习。特别是Nachos里面的很多函数还是用到了gcc的拓展语法。这个看得我是云里雾里,包括到现在涉及到gcc语法方面的可能都还有些困难,不过不影响对整体实验的理解。

困难2:

这虽然是个简单的操作系统。但毕竟是个操作系统,里面涉及到的参数比我大学四年写过的都多。不过还好这些代码都有自己的风格。比如全局变量都会在system.h中声明之类。所以自己多摸索再加上看看大佬的博客也就解决了。

困难3:

我在实现TS功能的时候遇到一个很愚蠢的bug。我定义了一个指针数组,但是忘记赋值了。导致我后面运行的时候一直报错。之前没理解这个bug的出处,我还花了老半天去调试解决。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bxoPDUDl-1603540852401)(/Users/jaggerqian/Library/Application Support/typora-user-images/image-20201024190046014.png)]

内容五:收获及感想

整个实验1结束算是很有成就感。因为大学四年没有认真对待过任何一个实验,都是随便了事。这个实验累计花了我21个小时的时间。不单单只是做实验,还拓展的了解了很多知识。比如我简单的学习了Linux内核的源代码,c++的语法又有了进一步的提升。通过自己实现Nachos的线程控制块和线程调度,对操作系统线程机制又理解得更加透彻了些。

内容六:对课程的意见和建议

这是一门非常值得推荐的课。

内容七:参考文献