文章目录

  1. 写在前面
  2. 运行环境配置
  3. 最大线程限制
  4. 实现可回收的线程ID机制
  5. 基于优先级的先来先服务调度算法

修改完毕的nachos我已经上传了,需要的话可以点击这里下载,积分不够可以私信我,CSDN设置的积分会自己改,我也没办法。
注意·:在Ubuntu18下能直接使用,其他版本的Ubuntu需要替换除了code外的所有文件,或者可以将链接里的code拷贝到自己的nachos上。

一.写在前面

写这篇博客初衷主要是因为最近在进行操作系统的实验,在网上找了很多教程,但都有些问题,要么就是根本实现不了,要么讲述的不够详细,对于没有面向对象编程基础的小白很不友好。于是就萌发出了写这篇博客的想法,在这篇博客中我会尽可能详细的讲述每一个实现步骤。即使是没有面向对象编程基础的小白,只要认真的跟着步骤来也能成功。如果有幸对您有帮助的话,还望不吝点赞收藏支持一下!

由于是面向小白的,所以在讲述的过程难免有些冗杂,还望谅解,已经有面向对象编程基础的朋友可以直接跳过讲解部分去看具体的实现。
关于nachos,它是一个可修改和跟踪的操作系统教学软件,它给出了一个支持多线程和虚拟内存的操作系统骨架,本篇博客主要介绍了它的线程部分的运行机制,并且进行了一些简单修改,实现了为nachos线程赋予ID号,并加入线程数量限制,在本篇中限制为128。同时在最后实现了基于优先级的先来先服务调度算法

二.运行环境配置

我是在Ubuntu18.04的版本下进行的nachos实验,关于Ubuntu18下安装nachos的方法,可以参考:Ubuntu18下安装nachos
值得注意的是nachos源码已经是被修改过的,建议直接下载使用。否则可能会出现一些未知的错误。当然此次实验不一定要在Ubuntu18下进行,实测Ubuntu版本并不影响实验结果。顺带一提,张老板是个坑。运行环境配置部分可以跳过,当然能配置好更好。

vim的配置

vim是一个很好用的工具,初学者可能会觉得不方便,但当你习惯了之后你一定会深深的爱上它。这里提供了一些简单的vim配置,包括自动缩进,语法高亮括号自动补全等。需要的朋友可以自行取用。

"All system-wide defaults are set in $VIMRUNTIME/debian.vim and sourced by
" the call to :runtime you can find below.  If you wish to change any of those
" settings, you should do it in this file (/etc/vim/vimrc), since debian.vim
" will be overwritten everytime an upgrade of the vim packages is performed.
" It is recommended to make changes after sourcing debian.vim since it alters
" the value of the 'compatible' option." This line should not be removed as it ensures that various options are
" properly set to work with the Vim-related packages available in Debian.
runtime! debian.vim" Uncomment the next line to make Vim more Vi-compatible
" NOTE: debian.vim sets 'nocompatible'.  Setting 'compatible' changes numerous
" options, so any other options should be set AFTER setting 'compatible'.
"set compatible" Vim5 and later versions support syntax highlighting. Uncommenting the next
" line enables syntax highlighting by default.
if has("syntax")syntax onendif" If using a dark background within the editing area and syntax highlighting" turn on this option as well"set background=dark" Uncomment the following to have Vim jump to the last position when" reopening a file"if has("autocmd")"  au BufReadPost * if line("'\"") > 1 && line("'\"") <= line("$") | exe "normal! g'\"" | endif"endif" Uncomment the following to have Vim load indentation rules and plugins" according to the detected filetype."if has("autocmd")"  filetype plugin indent on"endif" The following are commented out as they cause vim to behave a lot" differently from regular Vi. They are highly recommended though."set showcmd      " Show (partial) command in status line."set showmatch        " Show matching brackets."set ignorecase      " Do case insensitive matching"set smartcase      " Do smart case matching"set incsearch        " Incremental search"set autowrite        " Automatically save before commands like :next and :make"set hidden      " Hide buffers when they are abandoned"set mouse=a       " Enable mouse usage (all modes)" Source a global configuration file if availableif filereadable("/etc/vim/vimrc.local")source /etc/vim/vimrc.localendif" 显示行号set nu" 自动缩进set autoindent" c语音的缩进set cindent" 颜色主题colorscheme torte" tab键的宽度set tabstop=4" 统一缩减为4set softtabstop=4set shiftwidth=4" 浅色显示当前行autocmd InsertLeave * se noculautocmd InsertEnter * se cul" 在右下角显示光标位置的状态行set ruler" 光标移动到buffer的顶部和底部时保持3行的距离set scrolloff=5" 滚动光标set cursorline" 历史行数set history=512" 覆盖文件时不备份set nobackup" 侦测文件类型filetype on" 为特定文件类型载入相关缩减文件filetype indent on:inoremap ( ()<ESC>i:inoremap ) <c-r>=ClosePair(')')<CR>:inoremap { {<CR>}<ESC>O:inoremap } <c-r>=ClosePair('}')<CR>:inoremap [ []<ESC>i:inoremap ] <c-r>=ClosePair(']')<CR>:inoremap " ""<ESC>i:inoremap ' ''<ESC>ifunction! ClosePair(char)if getline('.')[col('.') - 1] == a:charreturn "\<Right>"elsereturn a:charendifendfunctionfiletype plugin indent on"打开文件类型检测, 加了这句才可以用智能补全"set completeopt=longest,menu

将以上的代码覆盖原来的vimrc就可以了。vimrc的路径是 /etc/vim/vimrc
当然也可以不配置,直接使用gedit

三.最大线程限制

nachos默认不对线程数进行限制,但在实际的操作系统中,如果不对线程数量进行限制,那极有可能导致整个系统的崩溃。所以在这一部分,我会详细的讲述如何实现将线程数量限制。
首先我们要明白,nachos只是模拟了一个操作系统,在这个系统中,每一个线程其实就是一个线程类定义出来的对象。不明白类和对象的意义的话,可以简单的将类理解为一个更高级的结构体(结构体内只有数据成员,而类里面可以有函数成员,也可以叫成员方法),而对象就是通过这个类定义出来的一个变量。
每一个类都有两个特殊的成员函数,他们分别是构造函数和析构函数,用来初始化对象和在对象的生命周期结束时用来做一些清理工作。
构造函数名与类名相同,析构函数名则是~+类名的形式。构造函数与析构函数都不能由用户调用,它只能由系统在定义对象是自动调用。显而易见,我们要对进程数量限制就要从构造函数入手,在构造一个线程之前先判断当前线程数量是否已经超过了限制,如果超过了就阻止创建。而析构函数的作用则是在一个线程生命周期结束时,将当前线程数量减一。
在nachos中,Thread类的的构造函数会对一个类的对象进行初始化,例如赋予线程的名字。我们要做的就是修改构造函数的内容,使之在每次调用时都判断当前的线程数量是否已经超过了限制,如果是,就打断创建并报错,此外析构函数也要做修改。

第一步.重载构造函数

首先在thread.h里定义一个宏变量MaxThreadNum,顾名思义,这个变量就是我们要限制的线程数。建议写在class Thread之前。同时定义一个枚举变量类型ThreadLevel,作用后面会讲。

#define MaxThreadNum 128
enum ThreadLevel{KERNEL,USER}; //注意大小写

找到Thread类的声明,在public:后增加一个静态变量ThreadNum用于记录当前线程的数量。同时增加一个ThreadLevel类型的变量threadlevel用于记录线程级别(属于内核线程还是用户线程)。并且将重载构造函数声明

class Thread {private:// 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 pointervoid *machineState[MachineStateSize];  // all registers except for stackToppublic:Thread(char* debugName);        // initialize a Thread ~Thread();              // deallocate a Thread// NOTE -- thread being deleted// must not be running when delete // is called//以下是修改部分static int ThreadNum;ThreadLevel threadlevel;//定义了一个名为threadlevel的枚举变量,它的值只能是KERNEL或者USERThread(char* debugName,ThreadLevel threadlevel);//重载了构造函数//当只有一个参数时调用原来的构造函数//当有两个参数时调用重载后的构造函数//以上是修改部分// basic thread operationsvoid Fork(VoidFunctionPtr func, void *arg);// Make thread run (*func)(arg)void Yield();       // Relinquish the CPU if any // other thread is runnablevoid Sleep(bool finishing); // Put the thread to sleep and // relinquish the processorvoid Begin();       // Startup code for the thread  void Finish();          // The thread is done executingvoid CheckOverflow();       // Check if thread stack has overflowedvoid setStatus(ThreadStatus st) { status = st; }char* getName() { return (name); }void Print() { cout << name; }void SelfTest();        // test whether thread impl is workingprivate:// some of the private data for this class is listed aboveint *stack;         // Bottom of the stack // NULL if this is the main thread// (If NULL, don't deallocate stack)ThreadStatus status;    // ready, running or blockedchar* name;void StackAllocate(VoidFunctionPtr func, void *arg);// Allocate a stack for thread.// Used internally by Fork()// A thread running a user program actually has *two* sets of CPU registers --
// one for its state while executing user code, one for its state
// while executing kernel code.int userRegisters[NumTotalRegs];    // user-level CPU register statepublic:void SaveUserState();       // save user-level register statevoid RestoreUserState();        // restore user-level register stateAddrSpace *space;           // User code this thread is running.
};

接下来修改原来的构造函数,主要目的是为了将内核线程的threadlevel赋值为KERNEL
在thread.cc里找到Thread类的构造函数,新增一句

threadlevel = KERNEL;

全文如下:

Thread::Thread(char* threadName)
{threadlevel =KERNEL;name = threadName;stackTop = NULL;stack = NULL;status = JUST_CREATED;for (int i = 0; i < MachineStateSize; i++) {machineState[i] = NULL;     // not strictly necessary, since// new thread ignores contents // of machine registers}   space = NULL;
}

接下来正式开始重载构造函数(关于重载,其实就是使几个同名的函数在参数个数不同,或者参数类型不同的时候实现的功能不同),在thread.cc里写入如下部分,建议写在原构造函数下方,这里我写在了第50行。注意:这里还将线程数初始化为0,因为它是一个静态变量,如果不进行初始化会出错。

int Thread::ThreadNum = 0;//将线程数初始化为0
Thread::Thread(char* threadName,ThreadLevel threadlevel)
{if(++ThreadNum>128){cout<<"创建线程失败,最大线程为128!!"<<endl;ASSERT(++ThreadNum<=128);  //断言,当ThreadNum大于128时打断程序的执行并报错}else{threadlevel =USER;//将线程级别设置为USERname = threadName;stackTop = NULL;stack = NULL;status = JUST_CREATED;for (int i = 0; i < MachineStateSize; i++) {machineState[i] = NULL;     // not strictly necessary, since// new thread ignores contents // of machine registers}   space = NULL;}
}

到此进程限制的修改就算完成了,接下来修改析构函数。在thread.cc的第63行找到它,修改成下面的这个样子:

Thread::~Thread()
{DEBUG(dbgThread, "Deleting thread: " << name);ThreadNum--;ASSERT(this != kernel->currentThread);if (stack != NULL)DeallocBoundedArray((char *) stack, StackSize * sizeof(int));
}

第二步.修改测试函数

在Thread类中定义了一个成员函数SelfTest,用于测试线程,实际上当内核启动,运行

./nachos -K

时,就是在运行SelfTest,具体怎么运行的暂时按下不表。我们只要知道,这个函数就是用来测试线程的就可以。
我们此时要做的就是修改测试线程,使用它来测试我们的修改结果是否正确。打开thread.cc在文件最末尾的位置找到测试函数。原来是这样:

void
Thread::SelfTest()
{DEBUG(dbgThread, "Entering Thread::SelfTest");Thread *t = new Thread("forked thread");t->Fork((VoidFunctionPtr) SimpleThread, (void *) 1);kernel->currentThread->Yield();SimpleThread(0);
}

将原来的注释掉,修改成这样:

void
Thread::SelfTest()
{DEBUG(dbgThread, "Entering Thread::SelfTest");/* Thread *t = new Thread("forked thread");t->Fork((VoidFunctionPtr) SimpleThread, (void *) 1);kernel->currentThread->Yield();SimpleThread(0);*/Thread *t[130];//定义了一个大小为130的指针数组for(int i=0;i<130;i++){t[i] = new Thread("create thread",USER);//进入循环,将数组中的每一个指针指向一个Thread类的对象,也就是一个线程cout<<"create thread : "<<i<<"成功!"<<endl;}
}

最后进入Nachos-4.1/code/build.linux 目录执行

make

再执行

./nachos -K

就可以看到执行结果了

可以看到,当创建完了0到127个线程后,输出了报错信息。说明进程限制成功了!

四.实现可回收的线程ID机制

前言

我们知道,在一个操作系统中,每一个进程都有它独特的PID,它是独一无二的,就相当于人的身份证号一样,不存在两个人身份证号相同,也不存在有两个进程共用一个ID号的情况。ID号是这个进程的标识符,通过这个ID号我们可以知道关于这个进程的信息,使调整进程优先级,KILL进程之类的命令成为可能,并且在一个进程的生命周期结束后,应该可以将它的ID号回收,留给之后创建的进程使用。这样就避免了ID号越编越长的情况,在实际的操作系统中也是这样的机制。
而在nachos中,使用了线程来模拟进程,在此次实验中,可以不用区分得这么严格,就简单的认为nachos里的线程就是进程就可以了,以下统称为线程。(进程和线程的关系,就像鱼缸和鱼缸里的鱼一样,进程是线程的容器,一个进程里可以有多个线程)但是nachos本身并没有为每一个线程赋予ID号,因此在这一部分,笔者将会带着大家实现一个简单的线程ID机制。

问题

细心的读者可能已经注意到,我们在上一部分的内容中有这么一句

cout<<"create thread : "<<i<<"成功!"<<endl;

它的作用是输出一句提示,告诉我们线程已经创建成功。同时输出了循环控制变量i,于是就有读者会想,直接将i作为线程ID不可以吗?为什么还要大费周章的做这么多功夫。这种想法不无道理,但是我们之前已经提到过,在一个进程的生命周期结束后,应该有一种机制可以回收它的ID号,显然,如果简单地将循环控制变量作为它的ID号,是无法做到的。同时还有一个问题,如果是循环之外创建的线程又应该怎么为它赋ID呢,这些都无法解决。因此,就有了这一部分的内容。

解决办法

其实,解决办法很简单:我们只需要定义一个数组,将它的全部成员置为0,每当创建线程的时候,就从零开始遍历这个数组,当找到第x号数组成员为0时,就将x作为线程ID赋给这个线程,同时将数组的第x号成员置1;当线程的生命周期结束时,就将其重新置0,这样就解决了前面提到的问题。

具体实现

在thread.h里,之前定义最大进程限制的下方,增加如下内容

#define MaxThreadNum 128
#define MAXTHREADID 130 //最大线程ID为130
static int ThreadIdArrary[MAXTHREADID]={0};

以上就是数组的定义以及初始化,而后在Thread类体里增加一个整形变量作为线程的ID号。

//以下是修改部分static int ThreadNum;//当前线程数量ThreadLevel threadlevel;//线程级别Thread(char* debugName,ThreadLevel threadlevel);//重载构造函数int ThreadId;        //线程ID                                                                                                                    //以上是修改部分

修改构造函数,注意,此时有两个构造函数(之前我们重载了一个),所以两个都需要修改。因为nachos自己的线程调用的都是原来的构造函数,而我们自己创建的线程调用的是重载过的构造函数,所以在原来的构造函数里将线程ID默认设置为-1,而在重载的构造函数里采用遍历数组的方式获得ID。这样就可以区分内核线程和用户线程了。实际上内核线程不止一个,应该给他们不同的ID号,但这不在本篇讨论内容之内,笔者将这里留白,有兴趣的朋友可以试着解决这个问题,并不难。

修改原来的构造函数:


Thread::Thread(char* threadName)
{threadlevel =KERNEL;//将线程级别设置未KERNELThreadId=-1;//内核线程ID默认为-1name = threadName;stackTop = NULL;stack = NULL;status = JUST_CREATED;for (int i = 0; i < MachineStateSize; i++) {machineState[i] = NULL;     // not strictly necessary, since// new thread ignores contents // of machine registers}space = NULL;
}

修改重载的构造函数:

Thread::Thread(char* threadName,ThreadLevel threadlevel)
{if(++ThreadNum>128){cout<<"创建线程失败,最大线程为128!!"<<endl;ASSERT(++ThreadNum<=128);  //断言,当ThreadNum大于128时打断程序的执行并报错}else{threadlevel =USER;//将线程级别设置为userfor(int i=0;i<MAXTHREADID;i++){if( ThreadIdArrary[i]==0){ThreadId =i ;ThreadIdArrary[i]=1;break;}}name = threadName;stackTop = NULL;stack = NULL;status = JUST_CREATED;for (int i = 0; i < MachineStateSize; i++) {                                                                                             machineState[i] = NULL;     // not strictly necessary, since// new thread ignores contents // of machine registers}space = NULL;}
}

修改析构函数:


Thread::~Thread()
{DEBUG(dbgThread, "Deleting thread: " << name);ThreadNum--;//当前线程数减一ThreadIdArrrary[ThreadId]=0; //将数组的成员重置为0,实现ID回收ASSERT(this != kernel->currentThread);if (stack != NULL)DeallocBoundedArray((char *) stack, StackSize * sizeof(int));
}

修改测试函数,输出线程ID号:

void Thread::SelfTest()
{DEBUG(dbgThread, "Entering Thread::SelfTest");/* Thread *t = new Thread("forked thread");t->Fork((VoidFunctionPtr) SimpleThread, (void *) 1);kernel->currentThread->Yield();SimpleThread(0);*/Thread *t[130];//定义一个大小为130的指针数组for(int i=0;i<130;i++){t[i] = new Thread("create thread",USER);//使指针指向线程cout<<"create thread : "<<i<<"成功!"<<"ThreadId is "<<t[i]->ThreadId<<endl;                                                             }
}

五.基于优先级的FCFS调度算法

1.从main函数讲起
2. 算法实现
3. 思考与反思

ps:第一部分主要面对没有基础的小白,把整个运行的流程讲了一遍,实际上如果只想实现基于优先级的调度算法的话并不需要了解得这么清楚。可以直接跳过前面几个部分,直接看最后的实现。

一.从main函数讲起

前面提到过,nachos其实是模仿了一个操作系统,所以本质上它还是一个c++程序,只是比较复杂;既然是一个c++程序,那就会有它的主函数,一切的一切都是从main开始的。
当我们运行

./nachos -K

实际上就是以"k"为参数运行了nachos这个函数,现在,以-K为线索,让我们从main.cc开始一场奇妙之旅。

打开main.cc,路径为 /Nachos-4.1/code/threads/main.cc
找到-K,在213行

    else if (strcmp(argv[i], "-K") == 0) {                                      threadTestFlag = TRUE;}

这一句做了一个比较,当参数为-K时,将threadTestFlag置为TRUE,此时我们得到了一个新的线索 threadTestFlag 顾名思义,这是线程测试状态;继续往下,找到threadTestFlag下一次出现的地方,在276行

 if (threadTestFlag) {kernel->ThreadSelfTest();  // test threads and synchronizatio}

可以看到当threadTestFlag为TRUE时,它会运行kernel指向的ThreadSelfTest函数。kernel其实也是一个对象,它是由Kernel这个类定义出来的,ThreadSelfTest
是它的一个成员函数,所以接下来让我们进入Kernel类的定义中看看这个函数做了什么。

打开Kernel.cc,路径与main.cc相同,可以在137行找到这个函数。

void
Kernel::ThreadSelfTest() {Semaphore *semaphore;SynchList<int> *synchList;LibSelfTest();       // test library routinescurrentThread->SelfTest();   // test thread switching// test semaphore operationsemaphore = new Semaphore("test", 0); semaphore->SelfTest();delete semaphore;// test locks, condition variables// using synchronized listssynchList = new SynchList<int>;synchList->SelfTest(9);                                                      delete synchList;
}

看起来很复杂,但是不用胆怯,也不要心生畏惧;我们的目标很明确,只用关心线程的部分,其他的部分不用过多关注,实际上nachos的代码有十几万行,没有时间也没有必要全部读懂,要学会从复杂的代码中提取有用的信息。
言归正传,可以看到这个函数中有这么一句

  currentThread->SelfTest();   // test thread switching

很明显,这就是我们要找的线索。currentThread意为当前线程,它既然是一个线程,那必然是由Thread定义出来的一个对象,所以接下来应该从thread.cc中继续,看看它做了什么。
打开thread.cc,在文件的末尾可以找到这个函数,实际上这就是我们之前修改过的测试函数。可以看到如下内容:

void
Thread::SelfTest()
{DEBUG(dbgThread, "Entering Thread::SelfTest");/* Thread *t = new Thread("forked thread");t->Fork((VoidFunctionPtr) SimpleThread, (void *) 1);kernel->currentThread->Yield();SimpleThread(0);*/Thread *t[130];for(int i=0;i<130;i++){t[i] = new Thread("create thread",USER);cout<<"create thread : "<<i<<"成功!"<<"ThreadId is "<<t[i]->ThreadId<<endl;    }
}

在前面部分,我们将这个测试函数原来的部分注释掉了,改成了我们自己的写的内容,很明显,要弄懂它做了什么,应该关注被注释掉的部分。接下来一句一句分析。

 DEBUG(dbgThread, "Entering Thread::SelfTest");

此句不在本次讲述范围之内,不用关心。

Thread *t = new Thread("forked thread");

创建一个线程,并定义一个Thread类型的指针指向它。

 t->Fork((VoidFunctionPtr) SimpleThread, (void *) 1);

运行t指向的Fork,可以看到Fork有两个参数,第一个参数是一个函数指针,它可以将一个函数的入口地址传入到Fork内,此时我们将SimpleThread传入。第二个参数是一个任意类型的1,关于(void*)不用过于纠结,只用知道他将1也传入了Fork内就可以。此时t也是一个线程,所以可以知道,fork的具体内容也应该在thread.cc这个文件里。往上翻,在121行。

void
Thread::Fork(VoidFunctionPtr func, void *arg)
{Interrupt *interrupt = kernel->interrupt;Scheduler *scheduler = kernel->scheduler;IntStatus oldLevel;DEBUG(dbgThread, "Forking thread: " << name << " f(a): " << (int) func << " " << arg);StackAllocate(func, arg);oldLevel = interrupt->SetLevel(IntOff);scheduler->ReadyToRun(this);    // ReadyToRun assumes that interrupts // are disabled!                                            (void) interrupt->SetLevel(oldLevel);
}

其他部分不用关心,找到两句关键句。

StackAllocate(func, arg);
scheduler->ReadyToRun(this);

第一句的作用是为当前线程分配堆栈,告诉这个线程应该做什么,func是我们传下来的参数,所以就很明朗了,这一句告诉了这个线程,当它运行时,它实际要运行的就是我们传入的的SimpleThread,具体内容就在SelfTest的上面。如下:

static void
SimpleThread(int which)
{int num;for (num = 0; num < 5; num++) {cout << "*** thread " << which << " looped " << num << " times\n";kernel->currentThread->Yield();}
}

然后就是第二句:

scheduler->ReadyToRun(this);

这句的作用是将当前线程加入就绪队列。
接下来让我们回到测试程序

void
Thread::SelfTest()
{DEBUG(dbgThread, "Entering Thread::SelfTest");/* Thread *t = new Thread("forked thread");t->Fork((VoidFunctionPtr) SimpleThread, (void *) 1);kernel->currentThread->Yield();SimpleThread(0);*/Thread *t[130];for(int i=0;i<130;i++){t[i] = new Thread("create thread",USER);cout<<"create thread : "<<i<<"成功!"<<"ThreadId is "<<t[i]->ThreadId<<endl;}
}

刚刚我们分析到了Fork,接下来我们看下一句。

kernel->currentThread->Yield();

运行当前线程指向的Yield(),然后再运行

SimpleThread(0);

Yield()也是线程这个类的成员函数,具体实现在thread.cc里的234行。如下:

void Thread::Yield ()
{Thread *nextThread;  //定义线程指针,指向下一个线程IntStatus oldLevel = kernel->interrupt->SetLevel(IntOff);//关中断ASSERT(this == kernel->currentThread);//断言,当当前运行的线程不是currentThread时打断运行并报错DEBUG(dbgThread, "Yielding thread: " << name);nextThread = kernel->scheduler->FindNextToRun();//找到下一个要运行的线程并使nextThrad指向它if (nextThread != NULL) {kernel->scheduler->ReadyToRun(this);//将当前线程加入就绪队列kernel->scheduler->Run(nextThread, FALSE);//运行下一个线程}

以上就是Yield的全部内容,通过笔者的注释应该不难看懂。
最后再回到测试程序,发现测试程序也运行了SimpleThread,所以不难想到,当我们运行./nachos -K的时候,出现的结果应该是0,1两个线程在屏幕上交替打印信息。

至此,我们的分析就算是结束了。
回顾这一部分,我们以-K为线索,从main函数一步一步分析到最后运行结果。基本上将nachos的线程部分的运行流程了解了一遍。如果能理解这一部分的内容,那对接下来的算法实现将有很大的帮助。

思路

在nachos中,就绪队列里的线程是按照加入就绪队列的先后顺序排列的,运行也是从头至尾逐个运行(如果没有执行Yield),这是一种很古老的排序方式,虽然简单明了,但会带来很多问题,例如线程饥饿乃至饿死。所以在这一部分,笔者会从调度器开始讲起,最终实现一个基于优先级的FCFS(先来先服务)算法。

在上面的部分中,我们简单讲述了几个Thread类里面的成员函数,知道了他们的作用。这里回顾一下Yield函数

void Thread::Yield ()
{Thread *nextThread;  //定义线程指针,指向下一个线程IntStatus oldLevel = kernel->interrupt->SetLevel(IntOff);//关中断ASSERT(this == kernel->currentThread);//断言,当当前运行的线程不是currentThread时打断运行并报错DEBUG(dbgThread, "Yielding thread: " << name);nextThread = kernel->scheduler->FindNextToRun();//找到下一个要运行的线程并使nextThrad指向它if (nextThread != NULL) {kernel->scheduler->ReadyToRun(this);//将当前线程加入就绪队列kernel->scheduler->Run(nextThread, FALSE);//运行下一个线程}

可以发现,Yield的作用是打断当前线程的运行,让出CPU,并找到下一个要运行的线程将之从就绪队列里取出,最后把当前线程放入就绪队列,运行下一个线程。注意,这里是要先取出下一个要运行的线程,再把当前的线程加入就绪队列,后面需要用到。这么做的结果就是,线程会按照加入的顺序逐个运行,显然不符合我们的预期。要怎么修改呢?这里给出三个可行的方向:

  1. 改变线程加入就绪队列的方式
  2. 改变从就绪队列中取出线程的方式
  3. 将就绪队列按优先级排序

实际上这三个方向都是可行的,但由于篇幅限制,本篇只着重讲述第一种,其余方法留给读者自己去实现,实际上这三种方法都是大同小异的,相信经过这篇文章的讲解,应该不难找出解决方法。

应该做什么
前面讲过,Yield函数有将线程加入就绪队列的作用,所以我们可以从Yield入手,具体来说,是从

  kernel->scheduler->ReadyToRun(this);//将当前线程加入就绪队列

可以看到,这句执行的是ReadyToRun,它的具体内容在scheduler.cc里,如下:

void
Scheduler::ReadyToRun (Thread *thread)
{ASSERT(kernel->interrupt->getLevel() == IntOff);//关中断DEBUG(dbgThread, "Putting thread on ready list: " << thread->getName());thread->setStatus(READY);//将线程状态设置为READY(就绪)readyList->Append(thread);//将线程加入就绪队列的队尾
}

重点就是最后一句,它调用了readyList指向的Append函数,所以我们只要知道Append是怎么实现的,就可以知道线程究竟是以一种什么方式加入的就绪队列(以下简称队列)的。在开始之前需要知道的是,队列里储存的并不是一个一个的线程,而是一个一个的节点,每个节点有两个成员,一个用以储存线程,另一个则是下一个节点的指针,节点和节点相连,就组成了队列。节点和队列的实现让我们看接下来的这一段代码:

template <class T>
class ListElement {public:ListElement(T itm);     // 以item为参数定义了一个队列的节点,这里的item是形参ListElement *next;          // 指向下一个节点的指针T item;                 // 实际储存的线程
};                                                                                                                                                                                                                                                                                                                                                                                                                                                                        template <class T>
class List {public:List();         // initialize the listvirtual ~List();        // de-allocate the listvirtual void Prepend(T item);// 将线程节点加入就绪队列头virtual void Append(T item); // 将线程节点加入就绪队列尾T Front() { return first->item; }// Return first item on list// without removing itT RemoveFront();        // 取出队头的线程void Remove(T item);    // 取出队列中的某一个线程bool IsInList(T item) const;//判断线程是否已经在队列中unsigned int NumInList() { return numInList;};// 队列中的线程数量bool IsEmpty() { return (numInList == 0); };//队列是否为空?void Apply(void (*f)(T)) const;// apply function to all elements in listvirtual void SanityCheck() const;// has this list been corrupted?void SelfTest(T *p, int numEntries);// 测试函数protected:ListElement<T> *first;      // 一个节点类型的指针,指向队列的第一个节点,当队列为空时值为NULLListElement<T> *last;   // 一个节点类型的指针,指向队列的最后一个节点,当队列为空时值为NULLint numInList;      //队列中节点数量friend class ListIterator<T>;//声明了一个友元类ListIterator
};

这里定义了两个类模板,一个是list模板,一个是ListElement模板。什么是类模板呢?类模板和类的关系,其实就像月饼模具和月饼的关系,一个月饼的模具做出来时就将月饼的大概内容给限定了,比如月饼的大小,宽厚等;但是当我们往模具里加入不同的原料时做出的月饼也不尽相同,加五仁做出来的就是五仁月饼,加莲蓉做出来的就是莲蓉月饼。同理,一个类模板可以将类的大致内容给确定,但当我们用类模板定义对象时,可以给他加入不同的“原料”,这样定义出来的对象也不同。那怎么加入原料呢? 注意到有这样一句:

template <class T>

这一句的作用就是告诉系统,我接下来要定义一个模板了。尖括号里的class T不能理解为定义了一个叫T的类,实际上这里的class是虚拟类型名的意思,避免混淆,也可以写成:

template <typename T>

二者等价,关于类模板的知识这里不再过多赘述,暂时只要看懂就可以了。
总得来说,这一部分给出了队列的和队列节点模板的定义,当我们需要实际使用时,只要把T换成我们要加入的“原料”就可以了。
我们看到list这个模板类中定义了Append方法和Prepend方法,分别是将线程加入就绪队列的尾和头。具体内容如下:

template <class T>
void
List<T>::Prepend(T item)
{ListElement<T> *element = new ListElement<T>(item);//以item作为参数定义了一个节点ASSERT(!IsInList(item));if (IsEmpty()) {        // list is emptyfirst = element;//将头指针指向当前节点last = element;//将尾指针指向当前节点} else {            // else put it before firstelement->next = first;//将当前节点的next指向头节点first = element;//将头指针指向当前节点}   numInList++;ASSERT(IsInList(item));//断言,当当前线程不在队列中时将程序运行打断
}template <class T>
void
List<T>::Append(T item)
{ListElement<T> *element = new ListElement<T>(item);ASSERT(!IsInList(item));if (IsEmpty()) {        // list is emptyfirst = element;last = element;} else {            // else put it after lastlast->next = element;last = element;}numInList++;ASSERT(IsInList(item));
}                                                                               

到这里我们应该想到,当我们要加入就绪队列的线程优先级高于队列中所有线程时,可以直接调用Prepend将它加入就绪队列的队头,当我们要加入就绪队列的线程优先级低于队列中所有线程时,可以直接调用Append将它加入就绪队列的队尾。
现在就很明了了,我们只需要新增一个方法,当我们不确定要加入的线程的优先级应该处于就绪队列哪个位置时,就调用这个方法,将它插入就绪队列中的合适部分就可以了,具体的实现可以模仿Append和Prepend。

应该怎么做

在前面的内容里,我们为nachos里的线程加入了ID号,可想而知,如果我们想基于优先级调度线程的话,必然要给线程加入一个属于它的优先级;但是优先级与线程ID不同,优先级使可以是相同的,当两个线程的优先级相同时,调度器会根据他们进入就绪队列的先后进行调度。

第一步 增改构造函数
每一个线程都应该有它的优先级,不但我们自己创建的用户线程有,nachos自带的线程应该也要有,否则调度时就会出错。所以我们要修改的其实是两个构造函数。
打开thread.h,在类体中声明一个整形变量priority作为线程的优先级,注意不能声明为静态变量,否则会出错。

    //以下是修改部分    static int ThreadNum;ThreadLevel threadlevel;Thread(char* debugName,ThreadLevel threadlevel);int ThreadId;//线程IDint priority;//优先级//以上是修改部分

全文如下:

class Thread {private:// 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 pointervoid *machineState[MachineStateSize];  // all registers except for stackToppublic:Thread(char* debugName);        // initialize a Thread ~Thread();              // deallocate a Thread// NOTE -- thread being deleted// must not be running when delete // is called//以下是修改部分static int ThreadNum;ThreadLevel threadlevel;Thread(char* debugName,ThreadLevel threadlevel);int ThreadId;//线程IDint priority;//优先级//以上是修改部分// basic thread operationsvoid Fork(VoidFunctionPtr func, void *arg);// Make thread run (*func)(arg)void Yield();       // Relinquish the CPU if any // other thread is runnablevoid Sleep(bool finishing); // Put the thread to sleep and // relinquish the processorvoid Begin();       // Startup code for the thread  void Finish();          // The thread is done executingvoid CheckOverflow();       // Check if thread stack has overflowedvoid setStatus(ThreadStatus st) { status = st; }char* getName() { return (name); }void Print() { cout << name; }void SelfTest();        // test whether thread impl is workingprivate:// some of the private data for this class is listed aboveint *stack;         // Bottom of the stack // NULL if this is the main thread// (If NULL, don't deallocate stack)ThreadStatus status;    // ready, running or blockedchar* name;void StackAllocate(VoidFunctionPtr func, void *arg);// Allocate a stack for thread.// Used internally by Fork()// A thread running a user program actually has *two* sets of CPU registers --
// one for its state while executing user code, one for its state
// while executing kernel code.int userRegisters[NumTotalRegs];    // user-level CPU register statepublic:void SaveUserState();       // save user-level register statevoid RestoreUserState();        // restore user-level register stateAddrSpace *space;           // User code this thread is running.
};

打开thread.h修改原来的构造函数:

Thread::Thread(char* threadName)
{threadlevel =KERNEL;ThreadId=-1;priority=3;//内核线程默认优先级为3(最低)name = threadName;stackTop = NULL;stack = NULL;status = JUST_CREATED;for (int i = 0; i < MachineStateSize; i++) {machineState[i] = NULL;     // not strictly necessary, since// new thread ignores contents // of machine registers}   space = NULL;
}

接下来修改析构函数和我们重载的构造函数


Thread::~Thread()
{DEBUG(dbgThread, "Deleting thread: " << name);ThreadNum--;ThreadIdArrary[ThreadId]=0; //将数组的成员重置为0,实现ID回收ASSERT(this != kernel->currentThread);if (stack != NULL)DeallocBoundedArray((char *) stack, StackSize * sizeof(int));
}Thread::Thread(char* threadName,ThreadLevel threadlevel)
{if(++ThreadNum>128){cout<<"创建线程失败,最大线程为128!!"<<endl;ASSERT(++ThreadNum<=128);  //断言,当ThreadNum大于128时打断程序的执行并报错}threadlevel =USER;//将线程级别设置为userfor(int i=0;i<MAXTHREADID;i++){if(ThreadIdArrary[i]==0){ThreadId =i ;ThreadIdArrary[i]=1;srand(i);priority = rand()%3;//随机生成0-2之间的数字作为优先级,默认0最高break;}}name = threadName;stackTop = NULL;stack = NULL;status = JUST_CREATED;for (int i = 0; i < MachineStateSize; i++) {machineState[i] = NULL;     // not strictly necessary, since}                // new thread ignores contents // of machine registers   space = NULL;
}

在这里我使用rand函数将优先级设置为了[0,2]的随机数,但由于没有使用srand播种,随机数种子是固定,所以这里其实是一个伪随机,这是因为我之前使用时间作为随机数种子的时候发现会导致生成的随机数都是相同的,这可能是因为linux系统的限制,同样的语法在windows下可以完美运行。所以之前的随机并没有达到预期的效果。如果想解决这个问题的话,可以在
修改重载构造函数:thread.h里包含一个头文件"time.h",然后在构造函数里将循环控制变量i和时间联合作为随机数种子,由于每次循环的i都不相同,所以随机数种子也就不同,这样就能实现真正的随机生成优先级了。优先级是否随机不影响调度

优先级设置完毕,接下来进入list.h声明一个Insert方法,用于将线程节点根据优先级插入队列中,并在list.cc实现它。

template <class T>
class List {public:List();         // initialize the listvirtual ~List();        // de-allocate the listvirtual void Prepend(T item);// Put item at the beginning of the listvirtual void Append(T item); // Put item at the end of the listT Front() { return first->item; }// Return first item on list// without removing itvoid Insert(T item,int priority);//声明insert函数T RemoveFront();        // Take item off the front of the listvoid Remove(T item);    // Remove specific item from listbool IsInList(T item) const;// is the item in the list?unsigned int NumInList() { return numInList;};// how many items in the list?bool IsEmpty() { return (numInList == 0); };// is the list empty? void Apply(void (*f)(T)) const; // apply function to all elements in listvirtual void SanityCheck() const;   // has this list been corrupted?void SelfTest(T *p, int numEntries);// verify module is workingprotected:ListElement<T> *first;      // Head of the list, NULL if list is emptyListElement<T> *last;   // Last element of listint numInList;      // number of elements in listfriend class ListIterator<T>;
};

进入list.cc实现它:

template <class T>
void
List<T>::Insert(T item, int priority)//将一个线程的指针和优先级作为参数
{ListElement<T> *element = new ListElement<T>(item);//以item(实际运行中是一个线程的指针)作为参数生成一个节点ASSERT(!IsInList(item));//断言,当要加入的线程已经在队列中时就打断运行,并输出报错信息/*判断,当队列为空时,直接将线程加入队列,这里其实可以调用Append函数,或者Prepend函数,因为此时队列为空,所以实际上加入队头和加入队尾效果是一样的*/if(IsEmpty()){first = element;last = element;}else{ /*如果队列不为空,则按照优先级将线程加入队列中合适的位置ListElement<T> *tmp = first;//定义一个节点指针tmp指向头节点ListElement<T> *preTmp = NULL;//定义一个节点指针NULL,用来遍历队列while(tmp != NULL)//tmp不为空时就一直执行{/*比较优先级,如果当前要加入的线程的优先级比tem指向线程的优先级要高就将当前线程加入tem指向的节点之前,否则就一直tem下移一直到tem为NULL时退出循环*/if(priority < tmp->item->priority)//注意这里是小于,因为默认数字越小优先级越高{element->next = tmp;//将生成的节点接在temp之前if(preTmp == NULL)//当就绪队列中没有线程的优先级高于当前要加入的线程时first = element;elsepreTmp->next = element;//将生成的节点接在tem的前一个节点之后numInList++;break;}//preTmp和temp下移preTmp = tmp;tmp = tmp->next;}if(preTmp == last){Append(item);                                                                                                                                                                                                                                            }}
}

进入schduler.cc,修改ReadyToRun函数。

void
Scheduler::ReadyToRun (Thread *thread)
{ASSERT(kernel->interrupt->getLevel() == IntOff);DEBUG(dbgThread, "Putting thread on ready list: " << thread->getName());thread->setStatus(READY);                                                                                                                if(readyList->IsEmpty()){readyList->Append(thread);return;      //大括号和return不能少,否则会出错                                        }   readyList->Insert(thread, thread->priority);//当队列不为空时,调用我们自己写的Inset函数}

修改测试函数,加入如下部分:

Thread *t[8];for(int i=0;i<8;i++){   t[i] = new Thread("creat thread!");cout<<"threadid is "<<t[i]->ThreadId<<" priority is "<<t[i]->priority<<endl;t[i]->Fork((VoidFunctionPtr)SImpleThread,(void *)t[i]);}   

修改后的测试函数:

void
Thread::SelfTest()
{DEBUG(dbgThread, "Entering Thread::SelfTest");/*  Thread *t = new Thread("forked thread");t->Fork((VoidFunctionPtr) SimpleThread, (void *) 1);kernel->currentThread->Yield();SimpleThread(0);Thread *t[130];for(int i=0;i<130;i++){t[i] = new Thread("create thread",USER);cout<<"create thread : "<<i<<"成功!"<<"ThreadId is "<<t[i]->ThreadId<<" priority is"<<t[i]->priority<<endl;}*/Thread *t[8];for(int i=0;i<8;i++){   t[i] = new Thread("creat thread!",USER);cout<<"threadid is "<<t[i]->ThreadId<<" priority is "<<t[i]->priority<<endl;t[i]->Fork((VoidFunctionPtr)SimpleThread,(void *)t[i]);}   }

修改SimpleThread函数:

static void SimpleThread(Thread *t)
{int num;for(num = 0;num < 2; num++){cout << "*** thread id:"<<t->ThreadId<<" priority is " << t->priority << " looped " << num << " times"<<endl;kernel->currentThread->Yield();}
}

保存退出,执行

make

而后运行:

./nahcos -K

可以看到:


如上图所示,我们创建了8个线程,优先级为0的有2、3、6、7,优先级为1的有0、1、4、5。因为优先级0高于1,按照优先级的先来先服务算法,他们的执行顺序应该是:
2->3->6->7->2->3->6->7->0->1->4->5->0->1->4->5
事实上也是基本符合的,细心的朋友可能已经注意到,执行的顺序实际上是:
2->3->6->7->2->6->7->3->0->1->4->5->0->1->4->5
是我们的算法出错了吗?其实并没有,实际上是因为操作系统的调度是实时的,分给每一个线程的时间片是有限的,主线程生成线程并将之加入就绪队列的过程是可以被打断的,并且我们创建的线程运行也是可以被打断的,这就导致了上面创建的8个线程加入就绪队列的顺序与创建顺序不完全相同。但是大体来看,线程的运行还是按照优先级进行的。

写在后面

这篇博客主要是面对没有面向对象编程基础的小白(比如龙哥),所以讲述的略显啰嗦,而且由于笔者水平有限,过程中难免出现纰漏甚至错误,也欢迎批评指正。如果对您有所帮助,必将深感荣幸。

对于基于优先级的调度算法部分,由于时间有限,内容可能不够详尽,如果有遗漏或者不理解的地方,欢迎私信,一起交流,共同进步!

本文完

Nachos进程数量限制128、ID号分配以及基于优先级的调度算法详解相关推荐

  1. 微信公众号图文消息添加word附件教程详解

    微信公众号图文消息添加word附件教程详解 我们都知道创建一个微信公众号,在公众号中发布一些文章是非常简单的,但公众号添加附件下载的功能却被限制,如今可以使用小程序"微附件"进行在 ...

  2. 【Css】css中class之间>(大于号)、~(波浪号)、 (空格)、,(逗号)、+(加号)详解(转载,笔记用)

    css中">"(大于号)."~"(波浪号)." "(空格).","(逗号)."+"(加号)详解 ...

  3. (王道408考研操作系统)第二章进程管理-第二节4:调度算法详解2(RR、HPF和MFQ)

    文章目录 一:时间片轮转调度算法(RR) 二:优先级调度算法(HPF) 三:多级反馈队列调度算法(MFQ) 总结 进程调度算法也称为CPU调度算法,操作系统内存在着多种调度算法,有的调度算法适用于作业 ...

  4. (王道408考研操作系统)第二章进程管理-第二节6、7:调度算法详解2(RR、HPF和MFQ)

    文章目录 一:时间片轮转调度算法(RR) 二:优先级调度算法(HPF) 三:多级反馈队列调度算法(MFQ) 总结 进程调度算法也称为CPU调度算法,操作系统内存在着多种调度算法,有的调度算法适用于作业 ...

  5. 线程和进程/阻塞和挂起以及那些sleep,wait()和notify()方法详解

    线程与进程的阻塞 线程阻塞 线程在运行的过程中因为某些原因而发生阻塞,阻塞状态的线程的特点是:该线程放弃CPU的使用,暂停运行,只有等到导致阻塞的原因消除之后才回复运行,或者是被其他的线程中断,该线程 ...

  6. git id 切分支 按_Git分支本地操作详解

    原文链接:http://www.jianshu.com/p/c05231e6a65a 引言 在上一节中我们对Git的常用本地操作的命令进行详解,而本节要讲解的是Git的分支, 在讲解之前补充两点概念性 ...

  7. log4j 打印线程号配置_Log4J日志整合及配置详解

    一.Log4j简介 Log4j有三个主要的组件:Loggers(记录器),Appenders (输出源)和Layouts(布局).这里可简单理解为日志类别,日志要输出的地方和日志以何种形式输出.综合使 ...

  8. redis 槽点重新分配 集群_Redis群集部署详解

    博文大纲: 一.Redis群集相关概念 二.部署Redis群集 1.部署环境 2.配置Redis实例 3.配置node06主机的多Redis实例 4.主机node01安装配置ruby的运行环境,便于管 ...

  9. python进程socket通信_Python Socket TCP双端聊天功能实现过程详解

    SOCKET编程 socket(套接字):是一个网络通信的端点,能实现不同主机的进程通信, -通过IP+端口定位对方并发送消息的通信机制 分为UDP和TCP 客户端Client: 发起访问的一-方 服 ...

最新文章

  1. 并发编程的那些事。(二)
  2. 详解ABBYY FineReader 12扫描亮度设置
  3. vue --- [全家桶]vue-router
  4. python3字符串处理,高效切片
  5. MarckDown学习
  6. 牛客网———二叉树遍历
  7. 光线跟踪的几种常见求交运算
  8. 网页导出pdf不完整_今天才知道!Word、Excel、PDF格式还能随意转换,20秒即可实现...
  9. Mybatis 常见知识点问题
  10. Windows10与Vmware配置Windowsserver2003共享磁盘
  11. 工作中那些有用的工具
  12. JNI返回复杂对象之中的一个
  13. Linux解压缩.tar.bz2
  14. Python Flask微信公众号开发
  15. FPGA学习任意波函数信号发生器的设计(基于quartus II13.0)
  16. [乐意黎]2016中级会计师考试《财务管理》真题及答案-第一批(9.10-9.11)
  17. 我的世界服务器玩家在线指令,我的世界指令大全:管理自己或者其他玩家及管理服务器指令...
  18. 用HTML+CSS做一个简单好看的环保网页
  19. 关于VB提示ByRef参数类型不符的分析
  20. 关于EJB,为什么用EJB?为什么不用EJB?

热门文章

  1. python爬b站评论_一个简单的爬取b站up下所有视频的所有评论信息的爬虫
  2. GB28181开发(二) pjsip库SDP协议扩展
  3. 什么是 CDP 和 LLDP?
  4. 基于FPGA的数字钟 ——最终实现
  5. Docker知识点整理
  6. ubuntu更新源及添加方法
  7. 消息称勒索软件可逃避PC防御、Office漏洞补丁能被攻击者绕过|12月24日全球网络安全热点
  8. Flex布局:Flex布局
  9. 完成清除工作,可以Destory窗口标志
  10. Ubuntu 20.04 LTS 关闭 Swap 分区