Nachos实习——Lab1线程机制实习报告
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、测试结果截图
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、测试结果截图
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、测试结果截图
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、测试结果截图
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、测试结果截图
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的线程控制块和线程调度,对操作系统线程机制又理解得更加透彻了些。
内容六:对课程的意见和建议
这是一门非常值得推荐的课。
内容七:参考文献
-
[1] Nachos中文教程
-
[2] 线程机制实习报告
-
[3] 线程调度实验报告
-
[4] Nachos实验
-
[5] Nachos LAB1实习报告