win7下ubuntu的硬盘安装之前一篇文章已有介绍,不再详述。在获得nachos的源码包之后,解压到/usr/local目录下,就可以发现在3.4中nachos一共有下面几个主要的文件夹:
1.bin 这里主要放的是交叉编译要用到的一些东西,.noff文件就是可以运行在nachos环境下的用户程序,这里由于第一阶段主要是threads的学习,这个可以先不管。
2.filesys 顾名思义,文件系统。
3.machine 这个文件夹的东西很有意思,是整个nachos最有意思的部分,也就是模拟硬件,其他部分只不过是os原理的具体实现
4.network 网络部分
5.test 这里是一个测试,在halt.c中测试系统的Halt()函数
6.threads 这里是系统线程的实现,下面会介绍关于它的两个很有意思问题
7.userprog 这里放的是要跑在nachos上的用户程序
8.vm 这里就一个makefile,可以打开看,主要是对一些关键代码的组织编译。
最后就是几个Makefile文件,也是编排编译整个代码结构的。
由于3.4版本中,各个模块可以单独的运行,所以我们对于首先关注的threads这个模块,就可以进行单独编译,这里由于编译器版本的问题,可能需要对某些代码文件进行修改,否则会出现很多编译错误,网上有这方面介绍,但是不是很多,而我自己之前也修改的是4.1版本的nachos,目前手里的代码是老师给的,错误已经被修改了,可以直接make,有需要的朋友可以留邮箱。
进入到threads目录下,执行make,就会生成一个nachos的可执行文件,然后点击运行,控制台下就会出现两个线程交互打印的结果,然后输出一些参数,包括总的时钟周期,系统的时钟周期和用户的时钟周期,这里就算是环境基本上搭起来了。
第一个问题:整个nachos是个怎么样的逻辑?即哪些东西是跑在我们的linux系统上的,哪些东西是跑在nachos的?
下面这个图是nachos咋linux上的层次结构:
我们可以这么理解,nachos本身就是一个c++程序而已,本质上和helloword没啥区别,可能就是代码行多一些,模块复杂一些,但是在linux看来,他们是一样的东西,而我们所要做的也就是修改一个c++程序,不管这个c++程序有多复杂,它还是一个c++程序,并且在threads中尽管它模拟了多线程,但是nachos本身还是一个单线程的程序,弄清这点可以帮我们更好的认识下面的问题。所以说,除了交叉编译之后的.noff文件,其他的文件都是linux上的c++程序,当然为了实现线程交换还使用了汇编,而.noff是第二部分用户程序的内容,这里先不细说。
第二个问题:我们在threads文件夹下编译的nachos是怎么模拟的多线程?
要解释这个问题,最好也是最简单的办法就是读代码。下面我按顺序贴上这第一个程序的执行流程,这里推荐kscope或者在windows下用sourceInsight 阅读代码。
1.main.cc的main函数
main(int argc, char **argv) { int argCount; // the number of arguments // for a particular command DEBUG('t', "Entering main"); (void) Initialize(argc, argv);
这里我们可以看到在main中首先调用了Initialize,参数不重要,因为事实上第一个程序和参数还没关系,如果使用si,则鼠标选择Initialize上的时候,下面的副代码框就会显示这个函数的实现部分,kscope则需要查找一下,这个函数在system.cc中:
2.system.cc的Initialize函数
DebugInit(debugArgs); // initialize DEBUG messages stats = new Statistics(); // collect statistics interrupt = new Interrupt; // start up interrupt handling scheduler = new Scheduler(); // initialize the ready queue if (randomYield) // start the timer (if needed) timer = new Timer(TimerInterruptHandler, 0, randomYield); threadToBeDestroyed = NULL; // We didn't explicitly allocate the current thread we are running in. // But if it ever tries to give up the CPU, we better have a Thread // object to save its state. currentThread = new Thread("main"); currentThread->setStatus(RUNNING); interrupt->Enable();
之前还有很多的设置代码我没有贴出,在这个程序中都不是重点,在这里可以看到,程序新建了一些用于控制的对象,比如中断和调度器,最后还新建了一个名字为main的thread对象,并设置为正在运行,这里不要把它想成一个真的线程,它就是一个自定义的数据结构。那么现在我们可以知道,我们已经有一个假设的线程了,由于构造函数中只是定义一些成员变量,这里我就不再贴了。这个函数到这里就返回了,下面继续到main中去:
3.main.cc的main函数
#ifdef THREADS ThreadTest(); #endif
不用细说,直接去找这个函数的定义,在ThreadTest.cc中
4.TheadTest.cc的ThreadThest函数
void ThreadTest() { DEBUG('t', "Entering SimpleTest"); Thread *t = new Thread("forked thread"); t->Fork(SimpleThread, 1); SimpleThread(0); }
Debug不管,这里又新建了一个Thread对象,也就是说我们已经有了两个Thread对象,一个叫main,一个叫forked thread。
并且给forked thread传了一个函数指针和参数,由于整个程序是一个单线程的顺序执行,所以下面就去看看fork函数看了什么。
5.Thread.cc的Fork函数
Thread::Fork(VoidFunctionPtr func, int arg) { DEBUG('t', "Forking thread /"%s/" with func = 0x%x, arg = %d/n", name, (int) func, arg); StackAllocate(func, arg); IntStatus oldLevel = interrupt->SetLevel(IntOff); scheduler->ReadyToRun(this); // ReadyToRun assumes that interrupts // are disabled! (void) interrupt->SetLevel(oldLevel); }
这里有两个关键的函数,一个是StackAllocate,一个是ReadyToRun,继续顺藤摸瓜
6.Thread.cc的StackAllocate函数
stack = (int *) AllocBoundedArray(StackSize * sizeof(int)); #ifdef HOST_SNAKE // HP stack works from low addresses to high addresses stackTop = stack + 16; // HP requires 64-byte frame marker stack[StackSize - 1] = STACK_FENCEPOST; #else // i386 & MIPS & SPARC stack works from high addresses to low addresses #ifdef HOST_SPARC // SPARC stack must contains at least 1 activation record to start with. stackTop = stack + StackSize - 96; #else // HOST_MIPS || HOST_i386 stackTop = stack + StackSize - 4; // -4 to be on the safe side! #ifdef HOST_i386 // the 80386 passes the return address on the stack. In order for // SWITCH() to go to ThreadRoot when we switch to this thread, the // return addres used in SWITCH() must be the starting address of // ThreadRoot. *(--stackTop) = (int)ThreadRoot; #endif #endif // HOST_SPARC *stack = STACK_FENCEPOST; #endif // HOST_SNAKE machineState[PCState] = (int)ThreadRoot; // eagle : ThreadRoot is implemented in switch.s machineState[StartupPCState] = (int) InterruptEnable; machineState[InitialPCState] = (int) func; machineState[InitialArgState] = arg; machineState[WhenDonePCState] = (int) ThreadFinish;
可以看到这里主要是对线程的栈进行初始化,AllocBoundedArray完成的就是这个功能,而在最下面,可以看到几个关键的函数地址都被存到了相应的数组中,这个数组就是用来存放模拟寄存器数据的,其中ThreadRoot是第一个要执行的函数,所谓的PC计数器嘛,而ThreadFinish是程序最后要执行的东西,这两个函数是在涉及到汇编,以后有机会再说吧。
7.Scheduler.cc的ReadyToRun函数
void Scheduler::ReadyToRun (Thread *thread) { DEBUG('t', "Putting thread %s on ready list./n", thread->getName()); thread->setStatus(READY); readyList->Append((void *)thread); }
这里可以看到,我们将forked thread设置为Ready,并添加到了等待队列中了,再次声明:这些都是普通的对象,没有涉及到linux上真正的多线程。到这里fork的过程结束,进入了SimpleThead这个函数。
8.TheadTest.cc的SimpleThread函数
void SimpleThread(int which) { int num; for (num = 0; num < 5; num++) { printf("*** thread %d looped %d times/n", which, num); currentThread->Yield(); } }
关键的地方就是Yield函数,其他的都很好理解,这里肯定已经打印了0线程,并且currentThead指向的叫main的对象,是他调用了Yield函数。
9.Thread.cc的Yield函数
void Thread::Yield () { Thread *nextThread; IntStatus oldLevel = interrupt->SetLevel(IntOff); ASSERT(this == currentThread); DEBUG('t', "Yielding thread /"%s/"/n", getName()); nextThread = scheduler->FindNextToRun(); if (nextThread != NULL) { scheduler->ReadyToRun(this); scheduler->Run(nextThread); } (void) interrupt->SetLevel(oldLevel); }
这里的关键是scheduler的FindNextToRun函数和Run函数,ReadToRun上面已经介绍,就是将当前Thread对象添加到一个等待队列中。
10.Scheduler.cc的FindNextToRun函数
Thread * Scheduler::FindNextToRun () { return (Thread *)readyList->Remove(); }
这个,不解释!
11.Scheduler.cc的Run函数
void Scheduler::Run (Thread *nextThread) { Thread *oldThread = currentThread; #ifdef USER_PROGRAM // ignore until running user programs if (currentThread->space != NULL) { // if this thread is a user program, currentThread->SaveUserState(); // save the user's CPU registers currentThread->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);
这段中的最关键的是最后一行SWITCH,这个函数实现了两个线程的切换,主要是保存的各种函数指针和参数的上CPU和下CPU,这些是我们保存之前对象和运行另一个对象所必须要知道的信息,在这个具体的例子中,就是保存main的信息并调出forked thread的要函数指针和参数,这样在接下来就会打印thread1.而同时又会调用Yield函数,这时候和上面同理,不过又开始执行main这个thread对象的东西,通过这种方式模拟了多线程。所以说SWITCH是精华部分,之后有机会好好研究一下。由于是main先开始running,所以最后也是main这个Thread先跳出这个TheadTest,返回main函数
12.main.cc的main函数
currentThread->Finish(); // NOTE: if the procedure "main" // returns, then the program "nachos" // will exit (as any other normal program // would). But there may be other // threads on the ready list. We switch // to those threads by saying that the // "main" thread is finished, preventing // it from returning. return(0); // Not reached...
这段注释比代码多,一言以蔽之,就是这里不会返回,最后那个return也不会执行,因为nachos是一个单线程的程序,所以先去看看finish中做了些什么。
13.Thread.cc的Finish函数
void Thread::Finish () { (void) interrupt->SetLevel(IntOff); ASSERT(this == currentThread); DEBUG('t', "Finishing thread /"%s/"/n", getName()); threadToBeDestroyed = currentThread; Sleep(); // invokes SWITCH // not reached }
这里就是把当前的currentThread指向的对象赋给了threadToBeDestroyed,然后sleep,并没有任何删除动作。
14.Thread.cc的Sleep函数
void Thread::Sleep () { Thread *nextThread; ASSERT(this == currentThread); ASSERT(interrupt->getLevel() == IntOff); DEBUG('t', "Sleeping thread /"%s/"/n", getName()); status = BLOCKED; while ((nextThread = scheduler->FindNextToRun()) == NULL) interrupt->Idle(); // no one to run, wait for an interrupt scheduler->Run(nextThread); // returns when we've been signalled }
这里非常有意思,可以多写几个打印语句看下执行流程。第一次我们sleep的时候,main的状态设置为阻塞,然后去等待队列找下一个要执行的线程,很明显,返回的是forked thread,而它这时才从simpleTest真的出来,然后和main一样finish,sleep,然后又到这里,这时候等待队列为空,就会执行interrupt的Idle()函数。
15.Interrupt.cc的Idle函数
void Interrupt::Idle() { DEBUG('i', "Machine idling; checking for interrupts./n"); status = IdleMode; if (CheckIfDue(TRUE)) { // check for any pending interrupts while (CheckIfDue(FALSE)) // check for any other pending ; // interrupts yieldOnReturn = FALSE; // since there's nothing in the // ready queue, the yield is automatic status = SystemMode; return; // return in case there's now // a runnable thread } // if there are no pending interrupts, and nothing is on the ready // queue, it is time to stop. If the console or the network is // operating, there are *always* pending interrupts, so this code // is not reached. Instead, the halt must be invoked by the user program. DEBUG('i', "Machine idle. No interrupts to do./n"); printf("No threads ready or runnable, and no pending interrupts./n"); printf("Assuming the program completed./n"); Halt();
这里就会打印很多我们在控制台可以看到的信息,然后执行Halt函数
16.Interrupt.cc的Halt函数
void Interrupt::Halt() { printf("Machine halting!/n/n"); stats->Print(); Cleanup(); // Never returns. }
打印各种参数,即用了多少时钟周期,当然这个周期也是nachos自己定义的,关于这个,以后有机会说。
17.System.cc的CleanUp函数
void Cleanup() { printf("/nCleaning up.../n"); #ifdef NETWORK delete postOffice; #endif #ifdef USER_PROGRAM delete machine; #endif #ifdef FILESYS_NEEDED delete fileSystem; #endif #ifdef FILESYS delete synchDisk; #endif delete timer; delete scheduler; delete interrupt; Exit(0); }
到此,全部的控制台的信息,我们都找到了归宿,这就是整个的流程:一个单线程的nachos模拟了多线程的运行。
最后说一句:Nachos的代码风格非常好,注释也十分详细,十分便于学习os的原理,读起来往往使人意犹未尽啊。