天天看點

nachos 程序管理,跟蹤main.cc 過程分析

1.從main.cc的注釋中可以看出,main.cc該函數做的主要工作是:

Bootstrap code to initialize the operating system kernel.

Allows direct calls into internal operating system functions,

to simplify debugging and testing.  In practice, the

bootstrap code would just initialize data structures,

and start a user program to print the login prompt.

Most of this file is not needed until later assignments.

一個用來初始化作業系統核心的引導程式,允許直接調用内部方法來簡化調試和測試。

實際上,這個引導程式僅進行了資料的初始化和啟動一個使用者程式來顯示登入提示。

許多内容隻有在配置設定之後才是需要的。

(比如說線上程管理中,我們隻定義了THREAD,這樣檔案系統等相關資訊就不會被執行)

2.在我們執行 ./nachos 之後可以增加很多參數,每個參數都代表了不同的含義,下面是可以增加的參數和參數的具體含義

// Usage: nachos -d <debugflags> -rs <random seed #>

//        -s -x <nachos file> -c <consoleIn> <consoleOut>

//        -f -cp <unix file> <nachos file>

//        -p <nachos file> -r <nachos file> -l -D -t

//              -n <network reliability> -m <machine id>

//              -o <other machine id>

//              -z

以上是所有可以使用的參數值

//    -d causes certain debugging messages to be printed (cf. utility.h)

//    -rs causes Yield to occur at random (but repeatable) spots

//    -z prints the copyright message

-d  與調試資訊相關,如果執行時增加參數 -d -t 則是列印出和thread相關的調試資訊,如果執行時增加參數 -d + 則列印出所有調試資訊。否則不列印。

-d  所跟的參數代表了需要列印哪些相關參數(具體見utility.h)

-rs 在指定的範圍内随機觸發線程的Yield方法,讓出cpu,這個範圍就是-rs 之後跟的參數(個人了解,不一定對)

-z  列印版本資訊

之後都是與 USER_PROGRAM,FILESYS,NETWORK相關的内容,現在先不考慮

3.

#ifdef THREADS

extern int testnum;

#endif

由于現在分析的是線程管理過程,我們在Makefile中隻定義了THREADS

DEFINES = -DTHREADS

是以将會定義參數testnum,用來記錄測試的線程數。

在額外函數中,我們隻調用了如下函數,該函數在threadtest.cc 檔案中,執行一個線程測試。

extern void ThreadTest(void)

4.進入主函數main

int argCount;

該變量用來獲得執行參數中表示完整意義的參數的個數,比如-rs 之後必須跟随一個參數 <random seed #> ,否則沒有實際意義,是以它的argCount為2.

比如-z 之後不需要跟随與之相關的參數,是以它的argCount為1.

5.

DEBUG('t', "Entering main");

調用utility.cc中的DEBUG方法,列印調試資訊

如果執行時添加參數-d -t 則會在終端中列印出 DEBUG('t', "*");中的第二個參數的内容

否則不列印

6.

(void) Initialize(argc, argv);

調用system.cc中的Initialize(argc, argv)方法進行初始化,進入system.cc,檢視具體實作,忽略于threads無關的過程:

void Initialize(int argc, char **argv)

{

    int argCount;

    char* debugArgs = "";    //用來記錄-d 之後的參數值    

    bool randomYield = FALSE;

    ……

    //判斷指令參數,每循環一次,argc減1,argv往後挪一個

    for (argc--, argv++; argc > 0; argc -= argCount, argv += argCount) {

    argCount = 1;

    if (!strcmp(*argv, "-d")) {    //如果有參數等于-d

        if (argc == 1)        //并且該參數為最後一個,預設為-d +

        debugArgs = "+";    // turn on all debug flags 打開所有的調試資訊

        else {

            debugArgs = *(argv + 1); //debug的值為-d之後的參數值

            argCount = 2;        

        }

    } else if (!strcmp(*argv, "-rs")) {    //如果參數等于-rs,在一個範圍内随機調用yield ,線程讓出cpu

        ASSERT(argc > 1);            //斷言 argc 必定大于1,-rs之後必須跟随一個參數

        RandomInit(atoi(*(argv + 1)));    // initialize pseudo-random

                        // number generator

        randomYield = TRUE;

        argCount = 2;

    }

……

    }

    DebugInit(debugArgs);            // initialize DEBUG messages

                           //将使用者輸入的-d之後的值付給utility中的enableflags

    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)    如果有-rs指令則啟動一個新的計時器

    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.

   //這段話的意思是說,我們并不明确需要配置設定一個目前線程,但是如果所有線程都打算讓出cpu,

   //我們最好能有一個線程來保持這個狀态。也就是說讓cpu一直在運作中

    currentThread = new Thread("main");        //目前的線程為main    

    currentThread->setStatus(RUNNING);        //設定線程狀态為running

    interrupt->Enable();            //使中斷可用

    CallOnUserAbort(Cleanup);            //使用者點選ctrl+c手動中斷 if user hits ctl-C

}

7.繼續檢視main.cc中的内容

#ifdef THREADS

    //循環判斷傳入的參數

    for (argc--, argv++; argc > 0; argc -= argCount, argv += argCount) {

      argCount = 1;

      switch (argv[0][1]) {        //判斷參數中的第二個字元,比如執行./nachos cq 4 即設定測試線程數為4個

      case 'q':                //如果等于q

        testnum = atoi(argv[1]);    //将q後的參數為測試線程數

        argCount++;

        break;

      default:                //否則,線程數預設為1

        testnum = 1;            

        break;

      }

    }

    ThreadTest();            //在作業系統上運作測試線程

#endif

執行./nachos cq 4,将會列印如下内容:

No test specified.

這是因為系統隻為我們寫好了測試數為1的測試内容,如果指定數目,則需要我們自定義測試線程類。

8.ThreadTest();

最後調用線程測試方法,該方法在threadtest.cc中:

void

ThreadTest()

{

    switch (testnum) {    //判斷傳入的測試線程數

    case 1:

    ThreadTest1();

    break;

    default:        //不為1的話就列印

    printf("No test specified.\n");

    break;

    }

}

如上所示,隻定義了testnum為1時的情況,我們可以在switch中添加幾個case,就可以定義我們自己的測試方法。

當測試線程數為1時調用ThreadTest1()方法:

void

ThreadTest1()

{

    DEBUG('t', "Entering ThreadTest1");

    Thread *t = new Thread("forked thread");    //聲明一個新線程

    t->Fork(SimpleThread, 1);            //線程建立執行,辨別為1

    SimpleThread(0);                //目前線程辨別為0

}

首先我們建立一個新的線程,将會調用thread.cc中的構造方法,在該方法中初始化線程名等相關資訊,此時線程的狀态為JUST_CREATED

(nachos線程一共包含4個狀态JUST_CREATED, RUNNING, READY, BLOCKED)

Thread::Thread(char* threadName)

{    

    name = threadName;    //線程名

    stackTop = NULL;

    stack = NULL;

    status = JUST_CREATED; //初始化狀态為剛剛建立

#ifdef USER_PROGRAM

    space = NULL;

#endif

}

然後調用該線程的Fork方法,運作線程。兩個參數分别為新進成需要執行的方法名以及傳入該方法的參數。

void 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);        //執行完之後,中斷恢複為原來的狀态

}    

在Fork方法中又調用了StackAllocate(func, arg)方法,該方法的主要工作是為不同型号的機器配置設定不同的棧空間,和棧頂位置

machineState是除了棧頂以外的其他寄存器,我們将相關資訊寫到寄存器中,包括func 和 arg,線程即會運作該func

該方法的注釋中說到,該方法時用來配置設定和初始化線程堆棧,這個棧中的一幀儲存了線程根,使得線程可以中斷,執行,調用方法和結束finish

//----------------------------------------------------------------------

// Thread::StackAllocate

//    Allocate and initialize an execution stack.  The stack is

//    initialized with an initial stack frame for ThreadRoot, which:

//        enables interrupts

//        calls (*func)(arg)

//        calls Thread::Finish

//

//    "func" is the procedure to be forked

//    "arg" is the parameter to be passed to the procedure

//----------------------------------------------------------------------

最後再讓我們回到ThreadTest1()方法

void

ThreadTest1()

{

    DEBUG('t', "Entering ThreadTest1");

    Thread *t = new Thread("forked thread");    //聲明一個新線程

    t->Fork(SimpleThread, 1);            //線程建立執行,辨別為1

    SimpleThread(0);                //目前線程辨別為0

}

主線程首先建立了一個新線程,并讓新線程調用SimpleThread方法。

然後主線程自己調用SimpleThread方法,傳入的參數為目前執行該方法的線程辨別。目前線程為0,新建立的線程為1

void

SimpleThread(int which)

{

    int num;

    for (num = 0; num < 5; num++) {    //每運作一次目前線程就讓出cpu,讓另外一個線程繼續執行

    printf("*** thread %d looped %d times\n", which, num);

        currentThread->Yield();

    }

}

在該方法中,每個線程需要循環執行5此列印操作,每執行一次列印,目前線程就執行Yield()方法,讓另一個線程執行。

來回切換直到循環結束。

void

Thread::Yield ()

{

    Thread *nextThread;                    

    IntStatus oldLevel = interrupt->SetLevel(IntOff);    //中斷關閉

    ASSERT(this == currentThread);            //斷言執行Yield的線程一定是目前線程

    DEBUG('t', "Yielding thread \"%s\"\n", getName());

    nextThread = scheduler->FindNextToRun();        //利用排程器找到下一個需要執行的線程

    if (nextThread != NULL) {                //如果下一個線程不為空

    scheduler->ReadyToRun(this);            //将該線程的狀态設定為就緒狀态

    scheduler->Run(nextThread);            //排程器運作下一個線程

    }

    (void) interrupt->SetLevel(oldLevel);        //中斷狀态恢複

}