Nachos线程管理
Nachos中的线程是在内核中以一个thread类的对象的方式实现的。线程控制块是以类的数据成员的方式实现。
//thread类源代码--定义
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 pointer int machineState[MachineStateSize]; // all registers except for stackTop public: Thread(char* debugName); // initialize a Thread ~Thread(); // deallocate a Thread // NOTE -- thread being deleted // must not be running when delete // is called // basic thread operations void Fork(VoidFunctionPtr func, int arg); // Make thread run (*func)(arg) void Yield(); // Relinquish the CPU if any // other thread is runnable void Sleep(); // Put the thread to sleep and // relinquish the processor void Finish(); // The thread is done executing void CheckOverflow(); // Check if thread has // overflowed its stack void setStatus(ThreadStatus st) { status = st; } char* getName() { return (name); } void Print() { printf("%s, ", name); } private: // some of the private data for this class is listed above int* stack; // Bottom of the stack // NULL if this is the main thread // (If NULL, don't deallocate stack) ThreadStatus status; // ready, running or blocked char* name; void StackAllocate(VoidFunctionPtr func, int arg); // Allocate a stack for thread. // Used internally by Fork() #ifdef USER_PROGRAM // 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 state public: void SaveUserState(); // save user-level register state void RestoreUserState(); // restore user-level register state AddrSpace *space; // User code this thread is running. #endif };
线程的状态存储在ThreadStatus类型的status数据成员中。ThreadStatus定义如下:
enum ThreadStatus { JUST_CREATED, RUNNING, READY, BLOCKED };
线程的状态必须是以上枚举类型之一。当线程的状态改变时,status值相应的改变。线程有自己的线程栈和寄存器。Nachos的线程栈在线程状态从JUST_CREATED变为RUNNING时被申请。thread类的构造函数会设置threadname,并且将线程状态设置为JUST_CREATED。stack是指向栈底的指针(栈溢出检查)。stackTop指向栈顶。包括PC在内的其他寄存器都被存储在MachineState
数组内。数组的大小有MachineStateSize确定。
在Nachos中用户线程是从核心线程继承而来的。
userRegisters数组是用户存储用户寄存器值的数组。其大小由NumTotalRegs确定。
MachineState存储在内核状态下运行的线程的状态。而用userRegisters数组存储在用户模式下运行的线程状态。
在Nachos中,用户线程都是以内核线程的方式开始的,当加载用户程序且创建地址空间之后,内核线程就转变成了用户线程。
就绪队列用于存储所有处于就绪状态的线程或进程。与IO设备有关的队列存储处于阻塞态的线程或进程,它们都在等待IO请求完成。
线程调度程序在这些队列间移动线程或进程。
Fork方法让线程运行,将线程状态从JUST_CREATED变为RUNNING。
Yield方法会将线程从RUNNING变为READY状态。(放弃当前时间片)该方法就会当前线程添加到就绪队列尾部,并通过线程上下文转换从就绪队列中的一个线程变为运行状态。当就绪队列空时,Yield方法不执行任何操作,当前线程继续运行。
Sleep方法将线程状态从RUNNING切换到BLOCKED状态。并从就绪队列选择一个线程运行。当就绪队列空的时候,cpu保持空闲状态,直到有一个线程就绪为止。Sleep方法会在执行IO操作时或者是等待一个事件时经常被调用。在调用Sleep之前,线程经常把它自己放入IO设备等待队列。
Finish方法用于终止当前线程。
Nachos中,作业调度程序,是一个Scheduler类的对象实现的。它的方法提供了所有对线程或进程调度的功能。当系统启动时Scheduler对象会以一个全局变量scheduler的方式被创建。
//Scheduler类定义:
20 class Scheduler { 21 public: 22 Scheduler(); // Initialize list of ready threads 23 ˜Scheduler(); // De-allocate ready list 24 25 void ReadyToRun(Thread* thread); // Thread can be dispatched. 26 Thread* FindNextToRun(); // Dequeue first thread on the ready 27 // list, if any, and return thread. 28 void Run(Thread* nextThread); // Cause nextThread to start running 29 void Print(); // Print contents of ready list 30 31 private: 32 List *readyList; // queue of threads that are ready to run, 33 // but not running 34 };
Scheduler的唯一的数据成员是就绪队列。它存储所有处于READY(就绪)状态的线程。
ReadyToRun函数将一个线程添加到就绪队列的尾部。
FindNextToRun返回队首线程指针。
Scheduler类最有意思的方法是Run。该方法调用使用汇编写成的SWITCH函数来将当前线程上下文切换到另外一个线程的上下文。
当一个Thread类构造函数被调用时,它仅仅是初始化成员变量将线程状态变为JUST_CREATED状态。此时线程还不能运行,因为它的线程栈还没有被分配而且线程控制块还没有被初始化,更重要的PC寄存器没有赋值,程序不知道从哪里开始执行。
Fork(VoidFunctionPtrfunc,intarg)
Fork方法负责线程栈的分配,func是线程函数入口地址,arg是线程函数。
Fork函数实现:
87 void 88 Thread::Fork(VoidFunctionPtr func, _int arg) 89 { 90 #ifdef HOST_ALPHA 91 DEBUG(’t’, "Forking thread \"%s\" with func = 0x%lx, arg = %ld\n", 92 name, (long) func, arg); 93 #else 94 DEBUG(’t’, "Forking thread \"%s\" with func = 0x%x, arg = %d\n", 95 name, (int) func, arg); 96 #endif 97 98 StackAllocate(func, arg); 99 100 IntStatus oldLevel = interrupt->SetLevel(IntOff); 101 scheduler->ReadyToRun(this); // ReadyToRun assumes that interrupts 102 // are disabled! 103 (void) interrupt->SetLevel(oldLevel); 104 } AllocateStack函数首先被调用。它用于分配线程栈并且初始化MachineState数组。 AllocateStack实现: 257 void 258 Thread::StackAllocate (VoidFunctionPtr func, _int arg) 259 { 260 stack = (int *) AllocBoundedArray(StackSize * sizeof(_int)); ... 20 272 stackTop = stack + StackSize - 4; // -4 to be on the safe side! ... 284 machineState[PCState] = (_int) ThreadRoot; 285 machineState[StartupPCState] = (_int) InterruptEnable; 286 machineState[InitialPCState] = (_int) func; 287 machineState[InitialArgState] = arg; 288 machineState[WhenDonePCState] = (_int) ThreadFinish; 289 }
AllocBoundedArray分配线程栈并将stack指向线程栈底部。
stack指向线程栈顶部。
宏PCState,StartupPCState,InitialPCState,InitialArgState和WhenDonePCState,分别代表9,3,0,1和2。
ThreadRoot是一个函数名,它是由汇编实现。InterruptEnable和ThreadFinish是两个静态函数名称。它们都被存储在MachineState数组中。代表各个寄存器的值。
线程入口函数地址被存储在以InitialPCState为下标(0号位置)的数组中。线程函数参数被存储在以InitialArg(1号位置)为下表的MachineState数组中。
当线程开始运行时MachineState[InitialPCState]会被加载到ra寄存器。ra被称为返回地址寄存器,存储线程函数的第一条指令开始的位置。
ThreadRoot是以汇编形式写成的,它是在线程运行前第一个被运行的函数。
69 .globl ThreadRoot 70 .ent ThreadRoot,0 71 ThreadRoot: 72 or fp,z,z # Clearing the frame pointer here 73 # makes gdb backtraces of thread stacks 74 # end here (I hope!) 75 76 jal StartupPC # call startup procedure 77 move a0, InitialArg 78 jal InitialPC # call main procedure 79 jal WhenDonePC # when we are done, call clean up procedure 80 81 # NEVER REACHED 82 .end ThreadRoo
除了main线程外,所有其它线程都是从ThreadRoot开始运行的。它的语法是:
ThreadRoot(intInitialPC,intInitialArg,intWhenDonePC,intStartupPC)
其中,InitialPC指明新生成线程的入口函数地址,InitialArg是该入口函数的参数;StartupPC是在运行该线程是需要作的一些初始化工作指向InterruptEnable函数。,比如开中断;而WhenDonePC是当该线程运行结束时需要作的一些后续工作,指向ThreadFinish函数。在Nachos的源代码中,没有任何一个函数和方法显式地调用ThreadRoot函数,ThreadRoot函数只有在线程切换时才被调用到。
一个线程在其初始化的最后准备工作中调用StackAllocate方法,该方法设置了几个寄存器的值,(InterruptEnable函数指针,ThreadFinish函数指针以及该线程需要运行函数的函数指针和运行函数的参数),该线程第一次被运行时调用的就是ThreadRoot函数。其工作过程是:
1.调用StartupPC函数,它指向InterruptEnable函数。执行开中断操作;
2.调用InitialPC函数,指向线程入口函数;
3.调用WhenDonePC函数,该函数调用CurrentThread->Finish()结束线程的运行;
Nachos的线程上下文切换是通过调用Scheduler的Run函数来进行的。
90 void 91 Scheduler::Run (Thread *nextThread) 92 { 93 Thread *oldThread = currentThread; 94 22 95 #ifdef USER_PROGRAM // ignore until running user programs 96 if (currentThread->space != NULL) { // if this thread is a user program, 97 currentThread->SaveUserState(); // save the user’s CPU registers 98 currentThread->space->SaveState(); 99 } 100 #endif 101 102 oldThread->CheckOverflow(); // check if the old thread 103 // had an undetected stack overflow 104 105 currentThread = nextThread; // switch to the next thread 106 currentThread->setStatus(RUNNING); // nextThread is now running 107 108 DEBUG(’t’, "Switching from thread \"%s\" to thread \"%s\"\n", 109 oldThread->getName(), nextThread->getName()); 110 111 // This is a machine-dependent assembly language routine defined 112 // in switch.s. You may have to think 113 // a bit to figure out what happens after this, both from the point 114 // of view of the thread and from the perspective of the "outside world". 115 116 SWITCH(oldThread, nextThread); 117 118 DEBUG(’t’, "Now in thread \"%s\"\n", currentThread->getName()); 119 120 // If the old thread gave up the processor because it was finishing, 121 // we need to delete its carcass. Note we cannot delete the thread 122 // before now (for example, in Thread::Finish()), because up to this 123 // point, we were still running on the old thread’s stack! 124 if (threadToBeDestroyed != NULL) { 125 delete threadToBeDestroyed; 126 threadToBeDestroyed = NULL; 127 } 128 129 #ifdef USER_PROGRAM 130 if (currentThread->space != NULL) { // if there is an address space 131 currentThread->RestoreUserState(); // to restore, do it. 132 currentThread->space->RestoreState(); 133 } 134 #endif 135 }
该函数首先设置oldThread,将参数设置为currentThread。然后调用SWITCH进行线程上下文切换。SWITCH是用汇编实现。该函数首先将所有重要的寄存器的内容存储在当前线程的控制块中。回想下Thread类的第一个私有数据成员是stackTop,其后是MachineState数组。换句话说指向Thread对象的指针,也是在指向stackTop。它们的位置非常重要不能该变或颠倒。
当一个线程再次获得cpu时间时,所有存储在stackTop和MachineState的值会被加载到寄存器中,也包括存储返回地址的ra。
Run函数属于内核,它会在将线程上下文切换,且新线程运行后返回。
下面我们先来介绍下Nachos内核定义的几个全局变量。在threads/system.cc目录下至少定义了6个全局变量。
它们是:
Thread *currentThread; // 当前执行线程 Thread *threadToBeDestroyed; // 刚结束的线程 Scheduler *scheduler; //就绪队列 Interrupt *interrupt; //中断状态。 Statistics *stats; //性能标准。 Timer *timer; // 计时器设备,用于导致线程上下文切换。 #ifdef FILESYS_NEEDED 23 FileSystem *fileSystem; 24 #endif 25 26 #ifdef FILESYS 27 SynchDisk *synchDisk; 28 #endif 29 30 #ifdef USER_PROGRAM // requires either FILESYS or FILESYS_STUB 31 Machine *machine; // user program memory and registers 32 #endif 33 34 #ifdef NETWORK 35 PostOffice *postOffice; 36 #endif
currentThread指向当前运行线程。scheduler指向内核负责调度线程和管理就绪队列的Scheduler类对象。
这些全局变量会Nachos系统启动时被创建。创建这些变量的函数为:
Initialize(intargc,char**argv)
ThreadFinish会在线程从线程入口函数返回时被调用,它仅仅调用thread类的Finish函数。在Finish函数中它将threadToBeDestroyed设置为currentThread。此后,原来线程的清理工作就交给了新线程。在SWITCH执行完线程上下文切换后,我们看到有这样一句代码:
24 if (threadToBeDestroyed != NULL) { 125 delete threadToBeDestroyed; 126 threadToBeDestroyed = NULL; 127 }
这些代码就是新线程执行清理操作。
每次执行线程上下文切换后,新线程都会检查threadToBeDestroyed,将老线程清理掉。
接下来我们来讨论下Nachos内核。Nachos内核是Nachos操作系统的一部分。内核本身也是一个程序,它必须也有个main()函数。在threads/main.cc文件内可以找到main函数。main函数主要执行一下操作:
1:调用Initialize();
2:调用ThreadTest();
3:currentThread->Finish();
Initialize执行创建并初始化全局变量的工作。
ThreadTest是一个测试函数。定义如下:
41 void 42 ThreadTest() 43 { 44 DEBUG(’t’, "Entering SimpleTest"); 45 46 Thread *t = new Thread("forked thread"); 47 48 t->Fork(SimpleThread, 1); 49 SimpleThread(0); 50 }
这个函数创建了一个名为forkedthread的线程去执行SimpleThread函数。
SimpleThread定义如下:
24 void 25 SimpleThread(int which) 26 { 27 int num; 28 29 for (num = 0; num < 5; num++) { 30 printf("*** thread %d looped %d times\n", which, num); 31 currentThread->Yield(); 32 } 33 }
主线程和新建线程都执行SimpleThread函数。它们不断进行线程上下文切换直到结束。
main函数的最后调用的函数是currentThread->Finish()。这个函数决不会返回。因为在线程上下文切换后,这个线程会被新线程释放。
以上翻译自《Nachos study book》如有纰漏,请不吝赐教!!