Nachos Threads
Nachos线程
In Nachos (and many systems) a process consists of:
在Nachos或其它系统中,一个进程是由以下内容组成:
1. An address space. The address space includes all the memory the process is allowed to reference.
1. 地址空间。地址空间包括该进程能够引用的所有内存。
The address space is further broken down into
地址空间可以更进一步分为以下几部分:
1) Executable code (e.g., the program's instructions),
1)可以运行的代码(如:程序指令)
2) Stack space for local variables
2)为了存储局部变量而产生的栈空间
3) Heap space for global variables and dynamically allocated memory (e.g., such as obtained by the Unix malloc or C++ new operator).
3)堆空间用于存放全局变量以及动态的分配内存(如,Unix的malloc或C++的new)。
In Unix, heap space is further broken down into BSS (contains variables initialized to 0) and DATA sections (initialized variables and other constants).
在Unix中,堆空间可以进一步分为BSS(包括全部初始化为零的变量)和DATA区(初始化的变量和其它常量)。
2. A single thread of control, e.g., the CPU executes instructions sequentially within the process.
2. 用于控制的单个线程,如,在进程内CPU顺序的执行指令。
3. Other objects, such as open file descriptors.
3. 其它对象,像打开文件的描述符。
That is, a process consists of a program, its data and all the state information (memory, registers, program counter, open files, etc.) associated with it.
也就是说,一个进程是由一个程序,其包含的数据以及所有的状态信息(内存,寄存器,程序计数器,打开文件等等.)所组成。
It is sometimes useful to allow multiple threads of control to execute concurrently within a single process.
有时对于单个进程中允许多个同时执行的控制线程是十分有用的。
All the threads of a particular process share the same address space. Although threads share many objects with other threads of that process, threads have their own private local stack.
单个进程中的所有线程共享相同的地址空间。虽然多个线程共享一个进程中的大部分对象,但每个线程都有其私有的栈。
One big difference between threads and processes is that global variables are shared among all threads.
线程与进程最大的不同是全局变量为所有的线程所共享。
Because threads execute concurrently with other threads, they must worry about synchronization and mutual exclusion when accessing shared memory.
因为线程可以并行执行,当访问共享资源时,我们必须小心的处理同步及互斥。
Nachos provides threads. Nachos threads execute and share the same code (the Nachos source code) and share the same global variables.
Nachos提供线程。Nachos线程执行并共享相同的代码(Nachos源代码),同时也共享全局变量。
The Nachos scheduler maintains a data structure called a ready list, which keeps track of the threads that are ready to execute.
Nachos调试器管理一种被称为准备好队列的数据结构,该结构保存了准备好运行的线程。
Threads on the ready list are ready to execute and can be selected for executing by the scheduler at any time.
所有在准备好队列中的线程都已准备完毕并且随时都可以被调度器选中开始执行。
Each thread has an associated state describing what the thread is currently doing. Nachos' threads are in one of four states:
每个线程都有与之相关的状态以描述当前线程正在做什么。Nachos的线程都处于以下四个状态之一:
READY:
准备好
The thread is eligible to use the CPU (e.g, it's on the ready list), but another thread happens to be running.
线程只在等待CPU资源(如,已经在准备好队列中),但有其它的线程正在运行。
When the scheduler selects a thread for execution, it removes it from the ready list and changes its state from READY to RUNNING.
当调度器选择了一个线程执行,该线程将从准备好队列中移除并且将其状态从Ready改为RUNNING。
Only threads in the READY state should be found on the ready list.
只有状态为READY的线程才能在准备好队列中找到。
RUNNING:
运行中:
The thread is currently running. Only one thread can be in the RUNNING state at a time.
当前线程正在运行。每一时刻只能有一个处于RUNNING状态的线程。
In Nachos, the global variable currentThread always points to the currently running thread.
在Nachos中,全局变量currentThread总是指向当前正在运行的线程。
BLOCKED:
堵塞:
The thread is blocked waiting for some external event; it cannot execute until that event takes place.
线程因等待外部事件而被堵塞,该线程不会执行直到外部事件发生。
Specifically, the thread has put itself to sleep via Thread::Sleep().
线程通过调用Thread::Sleep()将自己进行休眠。
It may be waiting on a condition variable, semaphore, etc. By definition, a blocked thread does not reside on the ready list.
线程可能在等待一个条件变量或信号量等等。根据定义,一个堵塞的线程不会处于准备好队列中。
JUST_CREATED:
刚创建:
The thread exists, but has no stack yet. This state is a temporary state used during thread creation.
线程刚存在但还没有分配栈。该状态只是在创建线程时的一个临时状态。
The Thread constructor creates a thread, whereas Thread::Fork() actually turns the thread into one that the CPU can execute (e.g., by placing it on the ready list).
Thread对象的构造器创建一个线程,Thread::Fork()的调用才使得该线程可以被CPU执行(如:将其添加到准备好队列中)
In non-object oriented systems, operating systems maintain a data structure called a process table.
在非面向对象的系统中,操作系统管理一种被称为进程表的数据结构。
Process (thread) table entries contain all the information associated with a process (e.g., saved register contents, current state, etc.).
进程表的表项包括所有与进程相同的信息(如:保存的寄存器内容,当前进程状态等等)。
The process table information is frequently called a context block.
进程表信息通常也被称为上下文块。
In contrast to other systems, Nachos does not maintain an explicit process table. Instead, information associated with thread is maintained as (usually) private data of a Thread object instance.
与其它系统比较,Nachos并不管理一个显式的进程表。替代的做法是与线程相关的信息都保存在该线程实例的私有成员变量中。
To get at a specific thread's information, a pointer to the thread instance is needed.
如果要获得一个线程的信息,就需要一个指向线程对象的指针。
Operations:
操作:
Thread *Thread(char *debugName):
The Thread constructor does only minimal initialization.
Thread对象的构造函数,只进行少量的初始化。
The thread's status is set to JUST_CREATED, its stack is initialized to NULL, its given the name debugName, etc.
将线程的状态设置为JUST_CREATED,初始化线程所使用的栈为空,将线程名设置为debugName等等。
Fork(VoidFunctionPtr func, int arg):
does the interesting work of thread creation, turning a thread into one that the CPU can schedule and execute.
完成与创建线程相关的工作,将线程置为让CPU可以进行安排和执行。
Argument func is the address of a procedure where execution is to begin when the thread starts executing.
参数func提供了线程开始执行时所执行例程的开始地址。
Argument arg is a an argument that should be passed to the new thread. (Of course, procedure func must expect a single argument to be passed to it if it is to access the supplied argument.)
参数arg用于作为参数传递给新线程。(当然,例程函数也需要一个参数?)
Fork allocates stack space for the new thread, initializes the registers (by saving the initial value's in the thread's context block), etc.
Fork调用为新线程分配栈空间,初始化寄存器(通过将初始化值存储到线程的上下文块)等等。
One important detail must be considered. What should happen when procedure func returns?
有个重要的细节需要考虑。当线程调用的例程函数返回时会发生什么?
Since func was not called as a regular procedure call, there is no place for it to return to.
首先该例程并不像通常的函数调用一样,所有它没有返回地址。
Indeed, rather than returning, the thread running func should terminate.
的确,与函数返回比较,应中止线程所运行的例程函数。
Fork takes care of this detail by building an initial activation record that makes this happen (described in detail below).
Fork会通过初始化一个活动记录来处理这个中止例程函数的细节(后面将详细描述)。
void StackAllocate(VoidFunctionPtr func, int arg):
This routine does the dirty work of allocating the stack and creating an initial activation record that causes execution to appear to begin in func.
该例程将在开始时做些分配栈空间和创建一个活动记录之类的工作。
StackAllocate does the following:
StackAllocate完成以下的工作:
1. Allocate memory for the stack. The default stack size is StackSize (4096) 4-byte integers.
1. 分配栈空间。缺省的栈大小为StackSize(4096)4字节整型。
2. Place a sentinel(n. 哨兵 v.守卫,放哨) value at the top of the allocated stack.
2. 在栈空间的最顶端放置一个观察值。
Whenever it switches to a new thread, the scheduler verifies that the sentinel value of the thread being switched out has not changed, as might happen if a thread overflows its stack during execution.
当切换到一个新线程时,线程调度器会验证将被切换出线程的观察值看其是否改变。该值在栈溢出的情况下可能改变。
3. Initialize the program counter PC to point to the routine ThreadRoot.
3. 初始化程序计数器PC指向ThreadRoot例程。
Instead of beginning execution at the user-supplied routine, execution actually begins in routine ThreadRoot.
线程开始运行时并不直接的执行用户提供的例程,而是首先执行ThreadRoot例程。
ThreadRoot does the following:
ThreadRoot完成下面的工作:
Calls an initialization routine that simply enables interrupts.
调用初始化例程来启用中断。
Calls the user-supplied function, passing it the supplied argument.
以相应的参数调用用户提供的例程。
Calls thread::Finish(), to terminate the thread.
调用thread::Finish()来终止线程。
Having thread execution begin in ThreadRoot rather than in the user-supplied routine makes it straightforward to terminate the thread when it finishes.
将线程从ThreadRoot而不是用户提供的例程开始执行使得能够使得终止线程更为直接。
The code for ThreadRoot is written in assembly language and is found in switch.s
ThreadRoot的代码是使用汇编语言编写并保存在switch.s文件中。
Note: ThreadRoot isn't run by the thread that calls Fork().
注意:ThreadRoot例程并不会因为调用了线程的Fork()而执行。
The newly created thread executes the instructions in ThreadRoot when it is scheduled and starts execution.
新创建的线程只有当其被调度且开始执行后才会执行ThreadRoot相应的指令。
void Yield():
Suspend the calling thread and select a new one for execution (by calling Scheduler::FindNextToRun()).
暂停调用该方法的线程并选择一个新的线程执行(通过调用Scheduler::FindNextToRun())
If no other threads are ready to execute, continue running the current thread.
如果没有其它的线程准备好执行,则继续执行当前线程。
void Sleep():
Suspend the current thread, change its state to BLOCKED, and remove it from the ready list. If the ready list is empty, invoke interrupt->Idle() to wait for the next interrupt.
暂停当前线程,将其状态修改为BLOCKED并且将其从准备好队列中移除。如果准备好队列为空,调用interrupt->Idle()等待下一次中断。
Sleep is called when the current thread needs to be blocked until some future event takes place.
在当前进程需被堵塞直到一些未来事件发生时调用Sleep。
It is the responsibility of this ``future event'' to wake up the blocked thread and move it back on to the ready list.
“未来事件”有责任唤醒被堵塞的线程并将其加入到准备好队列中。
void Finish():
Terminate the currently running thread. In particular, return such data structures as the stack to the system, etc.
中止当前正在运行的线程。将当前Thread数据结构以及栈空间返回给系统等。
Note that it is not physically possible for a currently running thread to terminate itself properly.
注意:没有什么物理方法可以合理的让一个正在运行的线程中止它自己。
As the current thread executes, it makes use of its stack. How can we free the stack while the thread is still using it?
当前线程正在执行并使用着它的栈。我们如何才能释放其正在使用的栈?
The solution is to have a different thread actually deallocate the data structures of a terminated thread.
解决办法是有另一个不同的线程真正的执行中止线程的资源释放。
Thus, Finish sets the global variable threadToBeDestroyed to point to the current thread, but does not actually terminate it.
因些,设置全局变量threadToBeDestroyed指向当前线程,但并不真正的中止它。
Finish then calls Sleep, which effectively terminates the thread (e.g., it will never run again).
设置完后调用Sleep,该操作有效的中止了该线程(如,该进行将不再运行)。
Later, when the scheduler starts running another thread, the newly scheduled thread examines the threadToBeDestroyed variable and finishes the job.
然后,当调度器开始执行另一个线程时,被调度的新线程检查threadToBeDestoryed变量并完成资源释放任务。
Mechanics of Thread Switching
Switching the CPU from one thread to another involves suspending the current thread, saving its state (e.g., registers), and then restoring the state of the thread being switched to.
切换CPU从一个线程到另一个包括暂停当前线程,保存当前线程状态(如:寄存器),然后加载要切换到线程的状态。
The thread switch actually completes at the moment a new program counter is loaded into PC; at that point, the CPU is no longer executing the thread switching code, it is executing code associated with the new thread.
线程切换真正完成的时刻是一个新的程序计数器被加载到PC,在该时刻,CPU将不再执行线程切换代码,而是开始执行新线程的代码。
The routine Switch(oldThread, nextThread) actually performs a thread switch.
Switch(oldThread, nextThread)例程执行真正的线程切换。
Switch saves all of oldThread's state (oldThread is the thread that is executing when Switch is called), so that it can resume executing the thread later, without the thread knowing it was suspended.
切换时保存原线程的所有状态(原线程是指当Switch调用时正在执行的线程),为的是原线程可以在以后恢复执行而不让其察觉到其被暂停过。
Switch does the following:
切换时进行了以下操作:
1. Save all registers in oldThread's context block.
1. 将所有的寄存器信息保存到原线程的上下文块。
2. What address should we save for the PC? That is, when we later resume running the just-suspended thread, where do we want it to continue execution?
2. 我们要为PC保存什么样的地址?也就是,当以后我们恢复刚暂停的线程时,我们将从何处开始继续执行?
We want execution to resume as if the call to Switch() had returned via the normal procedure call mechanism.
我们希望执行到恢复就像调用Switch()返回通过通常的方法调用机制。
Specifically, we want to resume execution at the instruction immediately following the call to Switch().
特别地,我们希望在调用Switch()后的第一条指令处恢复执行。
Thus, instead of saving the current PC, we place the return address (found on the stack in the thread's activation record) in the thread's context block.
因些,我们不保存当前PC的内容,我们保存返回地址(可以在线程栈的活动记录处找到)在当前线程的上下文块。
When the thread is resumed later, the resuming address loaded into the PC will be the instruction immediately following the ``call'' instruction that invoked Switch() earlier.
当线程恢复执行,加载到PC中的恢复地址将是跟在“call”(调用Switch())指令后的首条地址。
Note: It is crucial that Switch() appear to be a regular procedure call to whoever calls it. That way, threads may call Switch() whenever they want.
注意:关键在于不管哪个对象调用Switch()都应以通常的调用方法。通过这种方法,线程可以在任何时候调用Switch()。
The call will appear to return just like a normal procedure call except that the return does not take place right away.
调用会像正常的方法调用一样返回,除了返回并不立即发生。
Only after the scheduler decides to resume execution of the switched thread will it run again.
只有等到调度器决定恢复切换的线程时返回才会发生。
3. Once the current thread's state has been saved, load new values into the registers from the context block of the next thread.
3. 一旦当前线程的状态被保存了,从下一个线程的上下文块中获得新值并加载到寄存器中。
4. At what exact point has a context switch taken place?
4. 在什么确切点已发生上下文切换?
When the current PC is replaced by the saved PC found in the process table.
在当前PC被在进程表中找到的保存的PC替换时。
Once the saved PC is loaded, Switch() is no longer executing;
一旦所保存的PC被加载后,Switch()将不再执行;
we are now executing instructions associated with the new thread, which should be the instruction immediately following the call to Switch().
现在我们执行的是与线程相关的指令,该指令应该是跟随在调用Switch()后的首条指令。
As soon as the new PC is loaded, a context switch has taken place.
当新的PC被加载后就已经发生了上下文切换。
The routine Switch() is written in assembly language because it is a machine-depended routine.
Switch()例程是使用汇编语言编写,因为它的操作是与机器相关的。
It has to manipulate registers, look into the thread's stack, etc.
它要操作寄存器,查看线程的栈等等。
Note: After returning from Switch, the previous thread is no longer running. Thread nextThread is running now.
注意:当从Switch返回时,前一个线程就不再运行。nextThread将处于运行状态。
But because it also called Switch() previously, it will return to the ``right place'' (the instruction after the call to Switch()).
但是因为在前面调用了Switch(),返回时将到“正确地方”(指令应该是跟随在调用Switch()后的首条指令)。
Thus, it looks like Switch() is a ``normal'' procedure call, but in fact, a thread switch has taken place.
因此,看上去Switch()就像一个正常的方法调用 ,但是事实上,线程切换已经发生。
Threads & Scheduling
线程与调度
Threads that are ready to run are kept on the ready list. A process is in the READY state only if it has all the resources it needs, other than the CPU.
准备运行的线程被保存到准备好队列中。一个线程当其状态为READY时它已获得了除了CPU以外的其它所有资源。
Processes blocked waiting for I/O, memory, etc. are generally stored in a queue associated with the resource being waited on.
当进程堵塞等待I/O,内存等等时,它们通常存储在与资源相关的等待队列中。
The scheduler decides which thread to run next. The scheduler is invoked whenever the current thread wishes to give up the CPU.
调度器决定哪个线程将被运行。调度器将被调用当当前线程准备释放CPU时。
For example, the current thread may have initiated an I/O operation and must wait for it to complete before executing further.
例如,当前线程初始化了一个I/O操作并且必须等待到I/O操作完成才能继续执行。
Alternatively, Nachos may preempt the current thread in order to prevent one thread from monopolizing the CPU.
与此不同,Nachos为了避免一个线程长时间的占有CPU可能会抢占当前线程。
The Nachos scheduling policy is simple: threads reside on a single, unprioritized ready list, and threads are selected in a round-robin fashion.
Nachos调度策略很简单:线程保存在一个没有优先级的准备好队列中并且线程通过round-robin策略被选择。
That is, threads are always appended to the end of the ready list, and the scheduler always selects the thread at the front of the list.
也就是说,线程总是添加到准备好队列的末端,调度器总是从队列的最前端选择一个线程(FIFS——First In First Service)。
Operations:
操作:
void ReadyToRun(Thread *thread):
Make thread ready to run and place it on the ready list.
将线程状态设置为READY并将其添加进准备好队列。
Note that ReadyToRun doesn't actually start running the thread; it simply changes its state to READY and places it on the ready list.
注:ReadToRun并不真正的开始执行线程,它只是简单的改变线程的状态为READY并加入到准备好队列。
The thread won't start executing until later, when the scheduler chooses it.
线程只有在调度器选择了它才能真正的开始执行。
Thread *FindNextToRun():
Select a ready thread and return it. FindNextToRun simply returns the thread at the front of the ready list.
从准备好队列中选择一个线程并返回。FindNextToRun只是简单的返回在准备好队列前前端的线程。
void Run(Thread *nextThread):
Do the dirty work of suspending the current thread and switching to the new one.
做些与暂停当前线程有关的工作并切换到下一个线程。
Note that it is the currently running thread that calls Run(). A thread calls this routine when it no longer wishes to execute.
注:是由当前正运行的线程调用Run()。当一个线程不再想运行时调用该例程。
Run() does the following:
Run()完成以下工作:
1. Before actually switching to the new thread, check to see if the current thread overflowed its stack.
1. 在真正切换到一个新线程之前,查看当前线程的栈是否溢出。
This is done by placing a sentinel value at the top of the stack when the thread is initially created.
通过线程初始化时在栈顶放置一个观察值来完成相应的任务。
If the running thread ever overflows its stack, the sentinel value will be overwritten, changing its value.
如果当前运行的线程栈溢出,观察值将被改写。
By checking for the sentinel value every time we switch threads, we can catch threads overflowing their stacks.
我们每次进行线程切换时都会检查观察值,这样我们就能捕获到栈溢出。
2. Change the state of newly selected thread to RUNNING.
2. 改变要切换到的线程状态为RUNNING。
Nachos assumes that the calling routine (e.g. the current thread) has already changed its state to something else, (READY, BLOCKED, etc.) before calling Run().
Nachos假设调用Run例程的线程在调用Run之前已经改变其状态(READY,BLOCKED等)。
3. Actually switch to the next thread by invoking Switch(). After Switch returns, we are now executing as the new thread.
3. 实际上通过调用Switch()来切换到下一个线程,当调用返回时我们就在执行新的线程。
4. If the previous thread is terminating itself (as indicated by the threadToBeDestroyed variable), kill it now (after Switch()).
4. 如果前一个线程中止了它自己(通过设置threadToBeDestroyed变量),则杀死前一个线程(在调用Switch()之后)。
As described in last Section, threads cannot terminate themselves directly; another thread must do so.
如上一节所描述的,线程不会直接中止自己,必须通过另一个线程来完成。
It is important to understand that it is actually another thread that physically terminates the one that called Finish().
理解真正在物理上中止一个线程是通过另一个线程调用Finish()来完成是很重要的。
Synchronization and Mutual Exclusion
同步与互斥
Low-level Nachos routines frequently disable and re-enable interrupts to achieve mutual exclusion (e.g., by calling Interrupt::SetLevel()).
底层的Nachos例程经常通过开启和禁用中断来完成互斥(如,通过调用Interrupt::SetLevel())。
Synchronization facilities are provided through semaphores.
同步机制则是通过信号灯来完成。
The Semaphore object provides the following operations:
信号灯对象提供下面的操作:
Semaphore(char* debugName, int initialValue):
The constructor creates a new counting semaphore having an initial value of initialValue.
构造器创建一个新的用于计数的信号灯并初始化其值为initialValue。
The string debugName is also associated with the semaphore to simplify debugging.
debugName也与信号灯相关联用于简单的调试。
void P():
Decrement the semaphore's count, blocking the caller if the count is zero.
减小信号灯的计数。如果其计数为零则堵塞调用者。
void V():
Increment the semaphore's count, releasing one thread if any are blocked waiting on the count.
增加信号灯计数,释放一个等待于当前计数值并被堵塞的线程。
Special Notes
特别说明
When Nachos first begins executing, it is executing as a single Unix process. Nachos turns this single user process into a single Nachos thread.
当Nachos被运行时,它被作为一个Unix进行被执行。Nachos将该用户进程转化为一个Nachos线程。
Thus, by default, Nachos executes a single thread. When the Nachos entry point routine main returns, that thread exits as well.
因些,缺省下,Nachos执行单个线程。当Nachos进入main并返回时,线程同样也退出。
However, if other threads have been created and continue to exist, the Unix process continues executing Nachos.
尽管如此,如果还有其它的线程已被创建并持续存在,Unix进程将继续执行Nachos。
Only after all threads have terminated does the Unix Nachos process exit.
Nachos线程
In Nachos (and many systems) a process consists of:
在Nachos或其它系统中,一个进程是由以下内容组成:
1. An address space. The address space includes all the memory the process is allowed to reference.
1. 地址空间。地址空间包括该进程能够引用的所有内存。
The address space is further broken down into
地址空间可以更进一步分为以下几部分:
1) Executable code (e.g., the program's instructions),
1)可以运行的代码(如:程序指令)
2) Stack space for local variables
2)为了存储局部变量而产生的栈空间
3) Heap space for global variables and dynamically allocated memory (e.g., such as obtained by the Unix malloc or C++ new operator).
3)堆空间用于存放全局变量以及动态的分配内存(如,Unix的malloc或C++的new)。
In Unix, heap space is further broken down into BSS (contains variables initialized to 0) and DATA sections (initialized variables and other constants).
在Unix中,堆空间可以进一步分为BSS(包括全部初始化为零的变量)和DATA区(初始化的变量和其它常量)。
2. A single thread of control, e.g., the CPU executes instructions sequentially within the process.
2. 用于控制的单个线程,如,在进程内CPU顺序的执行指令。
3. Other objects, such as open file descriptors.
3. 其它对象,像打开文件的描述符。
That is, a process consists of a program, its data and all the state information (memory, registers, program counter, open files, etc.) associated with it.
也就是说,一个进程是由一个程序,其包含的数据以及所有的状态信息(内存,寄存器,程序计数器,打开文件等等.)所组成。
It is sometimes useful to allow multiple threads of control to execute concurrently within a single process.
有时对于单个进程中允许多个同时执行的控制线程是十分有用的。
All the threads of a particular process share the same address space. Although threads share many objects with other threads of that process, threads have their own private local stack.
单个进程中的所有线程共享相同的地址空间。虽然多个线程共享一个进程中的大部分对象,但每个线程都有其私有的栈。
One big difference between threads and processes is that global variables are shared among all threads.
线程与进程最大的不同是全局变量为所有的线程所共享。
Because threads execute concurrently with other threads, they must worry about synchronization and mutual exclusion when accessing shared memory.
因为线程可以并行执行,当访问共享资源时,我们必须小心的处理同步及互斥。
Nachos provides threads. Nachos threads execute and share the same code (the Nachos source code) and share the same global variables.
Nachos提供线程。Nachos线程执行并共享相同的代码(Nachos源代码),同时也共享全局变量。
The Nachos scheduler maintains a data structure called a ready list, which keeps track of the threads that are ready to execute.
Nachos调试器管理一种被称为准备好队列的数据结构,该结构保存了准备好运行的线程。
Threads on the ready list are ready to execute and can be selected for executing by the scheduler at any time.
所有在准备好队列中的线程都已准备完毕并且随时都可以被调度器选中开始执行。
Each thread has an associated state describing what the thread is currently doing. Nachos' threads are in one of four states:
每个线程都有与之相关的状态以描述当前线程正在做什么。Nachos的线程都处于以下四个状态之一:
READY:
准备好
The thread is eligible to use the CPU (e.g, it's on the ready list), but another thread happens to be running.
线程只在等待CPU资源(如,已经在准备好队列中),但有其它的线程正在运行。
When the scheduler selects a thread for execution, it removes it from the ready list and changes its state from READY to RUNNING.
当调度器选择了一个线程执行,该线程将从准备好队列中移除并且将其状态从Ready改为RUNNING。
Only threads in the READY state should be found on the ready list.
只有状态为READY的线程才能在准备好队列中找到。
RUNNING:
运行中:
The thread is currently running. Only one thread can be in the RUNNING state at a time.
当前线程正在运行。每一时刻只能有一个处于RUNNING状态的线程。
In Nachos, the global variable currentThread always points to the currently running thread.
在Nachos中,全局变量currentThread总是指向当前正在运行的线程。
BLOCKED:
堵塞:
The thread is blocked waiting for some external event; it cannot execute until that event takes place.
线程因等待外部事件而被堵塞,该线程不会执行直到外部事件发生。
Specifically, the thread has put itself to sleep via Thread::Sleep().
线程通过调用Thread::Sleep()将自己进行休眠。
It may be waiting on a condition variable, semaphore, etc. By definition, a blocked thread does not reside on the ready list.
线程可能在等待一个条件变量或信号量等等。根据定义,一个堵塞的线程不会处于准备好队列中。
JUST_CREATED:
刚创建:
The thread exists, but has no stack yet. This state is a temporary state used during thread creation.
线程刚存在但还没有分配栈。该状态只是在创建线程时的一个临时状态。
The Thread constructor creates a thread, whereas Thread::Fork() actually turns the thread into one that the CPU can execute (e.g., by placing it on the ready list).
Thread对象的构造器创建一个线程,Thread::Fork()的调用才使得该线程可以被CPU执行(如:将其添加到准备好队列中)
In non-object oriented systems, operating systems maintain a data structure called a process table.
在非面向对象的系统中,操作系统管理一种被称为进程表的数据结构。
Process (thread) table entries contain all the information associated with a process (e.g., saved register contents, current state, etc.).
进程表的表项包括所有与进程相同的信息(如:保存的寄存器内容,当前进程状态等等)。
The process table information is frequently called a context block.
进程表信息通常也被称为上下文块。
In contrast to other systems, Nachos does not maintain an explicit process table. Instead, information associated with thread is maintained as (usually) private data of a Thread object instance.
与其它系统比较,Nachos并不管理一个显式的进程表。替代的做法是与线程相关的信息都保存在该线程实例的私有成员变量中。
To get at a specific thread's information, a pointer to the thread instance is needed.
如果要获得一个线程的信息,就需要一个指向线程对象的指针。
Operations:
操作:
Thread *Thread(char *debugName):
The Thread constructor does only minimal initialization.
Thread对象的构造函数,只进行少量的初始化。
The thread's status is set to JUST_CREATED, its stack is initialized to NULL, its given the name debugName, etc.
将线程的状态设置为JUST_CREATED,初始化线程所使用的栈为空,将线程名设置为debugName等等。
Fork(VoidFunctionPtr func, int arg):
does the interesting work of thread creation, turning a thread into one that the CPU can schedule and execute.
完成与创建线程相关的工作,将线程置为让CPU可以进行安排和执行。
Argument func is the address of a procedure where execution is to begin when the thread starts executing.
参数func提供了线程开始执行时所执行例程的开始地址。
Argument arg is a an argument that should be passed to the new thread. (Of course, procedure func must expect a single argument to be passed to it if it is to access the supplied argument.)
参数arg用于作为参数传递给新线程。(当然,例程函数也需要一个参数?)
Fork allocates stack space for the new thread, initializes the registers (by saving the initial value's in the thread's context block), etc.
Fork调用为新线程分配栈空间,初始化寄存器(通过将初始化值存储到线程的上下文块)等等。
One important detail must be considered. What should happen when procedure func returns?
有个重要的细节需要考虑。当线程调用的例程函数返回时会发生什么?
Since func was not called as a regular procedure call, there is no place for it to return to.
首先该例程并不像通常的函数调用一样,所有它没有返回地址。
Indeed, rather than returning, the thread running func should terminate.
的确,与函数返回比较,应中止线程所运行的例程函数。
Fork takes care of this detail by building an initial activation record that makes this happen (described in detail below).
Fork会通过初始化一个活动记录来处理这个中止例程函数的细节(后面将详细描述)。
void StackAllocate(VoidFunctionPtr func, int arg):
This routine does the dirty work of allocating the stack and creating an initial activation record that causes execution to appear to begin in func.
该例程将在开始时做些分配栈空间和创建一个活动记录之类的工作。
StackAllocate does the following:
StackAllocate完成以下的工作:
1. Allocate memory for the stack. The default stack size is StackSize (4096) 4-byte integers.
1. 分配栈空间。缺省的栈大小为StackSize(4096)4字节整型。
2. Place a sentinel(n. 哨兵 v.守卫,放哨) value at the top of the allocated stack.
2. 在栈空间的最顶端放置一个观察值。
Whenever it switches to a new thread, the scheduler verifies that the sentinel value of the thread being switched out has not changed, as might happen if a thread overflows its stack during execution.
当切换到一个新线程时,线程调度器会验证将被切换出线程的观察值看其是否改变。该值在栈溢出的情况下可能改变。
3. Initialize the program counter PC to point to the routine ThreadRoot.
3. 初始化程序计数器PC指向ThreadRoot例程。
Instead of beginning execution at the user-supplied routine, execution actually begins in routine ThreadRoot.
线程开始运行时并不直接的执行用户提供的例程,而是首先执行ThreadRoot例程。
ThreadRoot does the following:
ThreadRoot完成下面的工作:
Calls an initialization routine that simply enables interrupts.
调用初始化例程来启用中断。
Calls the user-supplied function, passing it the supplied argument.
以相应的参数调用用户提供的例程。
Calls thread::Finish(), to terminate the thread.
调用thread::Finish()来终止线程。
Having thread execution begin in ThreadRoot rather than in the user-supplied routine makes it straightforward to terminate the thread when it finishes.
将线程从ThreadRoot而不是用户提供的例程开始执行使得能够使得终止线程更为直接。
The code for ThreadRoot is written in assembly language and is found in switch.s
ThreadRoot的代码是使用汇编语言编写并保存在switch.s文件中。
Note: ThreadRoot isn't run by the thread that calls Fork().
注意:ThreadRoot例程并不会因为调用了线程的Fork()而执行。
The newly created thread executes the instructions in ThreadRoot when it is scheduled and starts execution.
新创建的线程只有当其被调度且开始执行后才会执行ThreadRoot相应的指令。
void Yield():
Suspend the calling thread and select a new one for execution (by calling Scheduler::FindNextToRun()).
暂停调用该方法的线程并选择一个新的线程执行(通过调用Scheduler::FindNextToRun())
If no other threads are ready to execute, continue running the current thread.
如果没有其它的线程准备好执行,则继续执行当前线程。
void Sleep():
Suspend the current thread, change its state to BLOCKED, and remove it from the ready list. If the ready list is empty, invoke interrupt->Idle() to wait for the next interrupt.
暂停当前线程,将其状态修改为BLOCKED并且将其从准备好队列中移除。如果准备好队列为空,调用interrupt->Idle()等待下一次中断。
Sleep is called when the current thread needs to be blocked until some future event takes place.
在当前进程需被堵塞直到一些未来事件发生时调用Sleep。
It is the responsibility of this ``future event'' to wake up the blocked thread and move it back on to the ready list.
“未来事件”有责任唤醒被堵塞的线程并将其加入到准备好队列中。
void Finish():
Terminate the currently running thread. In particular, return such data structures as the stack to the system, etc.
中止当前正在运行的线程。将当前Thread数据结构以及栈空间返回给系统等。
Note that it is not physically possible for a currently running thread to terminate itself properly.
注意:没有什么物理方法可以合理的让一个正在运行的线程中止它自己。
As the current thread executes, it makes use of its stack. How can we free the stack while the thread is still using it?
当前线程正在执行并使用着它的栈。我们如何才能释放其正在使用的栈?
The solution is to have a different thread actually deallocate the data structures of a terminated thread.
解决办法是有另一个不同的线程真正的执行中止线程的资源释放。
Thus, Finish sets the global variable threadToBeDestroyed to point to the current thread, but does not actually terminate it.
因些,设置全局变量threadToBeDestroyed指向当前线程,但并不真正的中止它。
Finish then calls Sleep, which effectively terminates the thread (e.g., it will never run again).
设置完后调用Sleep,该操作有效的中止了该线程(如,该进行将不再运行)。
Later, when the scheduler starts running another thread, the newly scheduled thread examines the threadToBeDestroyed variable and finishes the job.
然后,当调度器开始执行另一个线程时,被调度的新线程检查threadToBeDestoryed变量并完成资源释放任务。
Mechanics of Thread Switching
Switching the CPU from one thread to another involves suspending the current thread, saving its state (e.g., registers), and then restoring the state of the thread being switched to.
切换CPU从一个线程到另一个包括暂停当前线程,保存当前线程状态(如:寄存器),然后加载要切换到线程的状态。
The thread switch actually completes at the moment a new program counter is loaded into PC; at that point, the CPU is no longer executing the thread switching code, it is executing code associated with the new thread.
线程切换真正完成的时刻是一个新的程序计数器被加载到PC,在该时刻,CPU将不再执行线程切换代码,而是开始执行新线程的代码。
The routine Switch(oldThread, nextThread) actually performs a thread switch.
Switch(oldThread, nextThread)例程执行真正的线程切换。
Switch saves all of oldThread's state (oldThread is the thread that is executing when Switch is called), so that it can resume executing the thread later, without the thread knowing it was suspended.
切换时保存原线程的所有状态(原线程是指当Switch调用时正在执行的线程),为的是原线程可以在以后恢复执行而不让其察觉到其被暂停过。
Switch does the following:
切换时进行了以下操作:
1. Save all registers in oldThread's context block.
1. 将所有的寄存器信息保存到原线程的上下文块。
2. What address should we save for the PC? That is, when we later resume running the just-suspended thread, where do we want it to continue execution?
2. 我们要为PC保存什么样的地址?也就是,当以后我们恢复刚暂停的线程时,我们将从何处开始继续执行?
We want execution to resume as if the call to Switch() had returned via the normal procedure call mechanism.
我们希望执行到恢复就像调用Switch()返回通过通常的方法调用机制。
Specifically, we want to resume execution at the instruction immediately following the call to Switch().
特别地,我们希望在调用Switch()后的第一条指令处恢复执行。
Thus, instead of saving the current PC, we place the return address (found on the stack in the thread's activation record) in the thread's context block.
因些,我们不保存当前PC的内容,我们保存返回地址(可以在线程栈的活动记录处找到)在当前线程的上下文块。
When the thread is resumed later, the resuming address loaded into the PC will be the instruction immediately following the ``call'' instruction that invoked Switch() earlier.
当线程恢复执行,加载到PC中的恢复地址将是跟在“call”(调用Switch())指令后的首条地址。
Note: It is crucial that Switch() appear to be a regular procedure call to whoever calls it. That way, threads may call Switch() whenever they want.
注意:关键在于不管哪个对象调用Switch()都应以通常的调用方法。通过这种方法,线程可以在任何时候调用Switch()。
The call will appear to return just like a normal procedure call except that the return does not take place right away.
调用会像正常的方法调用一样返回,除了返回并不立即发生。
Only after the scheduler decides to resume execution of the switched thread will it run again.
只有等到调度器决定恢复切换的线程时返回才会发生。
3. Once the current thread's state has been saved, load new values into the registers from the context block of the next thread.
3. 一旦当前线程的状态被保存了,从下一个线程的上下文块中获得新值并加载到寄存器中。
4. At what exact point has a context switch taken place?
4. 在什么确切点已发生上下文切换?
When the current PC is replaced by the saved PC found in the process table.
在当前PC被在进程表中找到的保存的PC替换时。
Once the saved PC is loaded, Switch() is no longer executing;
一旦所保存的PC被加载后,Switch()将不再执行;
we are now executing instructions associated with the new thread, which should be the instruction immediately following the call to Switch().
现在我们执行的是与线程相关的指令,该指令应该是跟随在调用Switch()后的首条指令。
As soon as the new PC is loaded, a context switch has taken place.
当新的PC被加载后就已经发生了上下文切换。
The routine Switch() is written in assembly language because it is a machine-depended routine.
Switch()例程是使用汇编语言编写,因为它的操作是与机器相关的。
It has to manipulate registers, look into the thread's stack, etc.
它要操作寄存器,查看线程的栈等等。
Note: After returning from Switch, the previous thread is no longer running. Thread nextThread is running now.
注意:当从Switch返回时,前一个线程就不再运行。nextThread将处于运行状态。
But because it also called Switch() previously, it will return to the ``right place'' (the instruction after the call to Switch()).
但是因为在前面调用了Switch(),返回时将到“正确地方”(指令应该是跟随在调用Switch()后的首条指令)。
Thus, it looks like Switch() is a ``normal'' procedure call, but in fact, a thread switch has taken place.
因此,看上去Switch()就像一个正常的方法调用 ,但是事实上,线程切换已经发生。
Threads & Scheduling
线程与调度
Threads that are ready to run are kept on the ready list. A process is in the READY state only if it has all the resources it needs, other than the CPU.
准备运行的线程被保存到准备好队列中。一个线程当其状态为READY时它已获得了除了CPU以外的其它所有资源。
Processes blocked waiting for I/O, memory, etc. are generally stored in a queue associated with the resource being waited on.
当进程堵塞等待I/O,内存等等时,它们通常存储在与资源相关的等待队列中。
The scheduler decides which thread to run next. The scheduler is invoked whenever the current thread wishes to give up the CPU.
调度器决定哪个线程将被运行。调度器将被调用当当前线程准备释放CPU时。
For example, the current thread may have initiated an I/O operation and must wait for it to complete before executing further.
例如,当前线程初始化了一个I/O操作并且必须等待到I/O操作完成才能继续执行。
Alternatively, Nachos may preempt the current thread in order to prevent one thread from monopolizing the CPU.
与此不同,Nachos为了避免一个线程长时间的占有CPU可能会抢占当前线程。
The Nachos scheduling policy is simple: threads reside on a single, unprioritized ready list, and threads are selected in a round-robin fashion.
Nachos调度策略很简单:线程保存在一个没有优先级的准备好队列中并且线程通过round-robin策略被选择。
That is, threads are always appended to the end of the ready list, and the scheduler always selects the thread at the front of the list.
也就是说,线程总是添加到准备好队列的末端,调度器总是从队列的最前端选择一个线程(FIFS——First In First Service)。
Operations:
操作:
void ReadyToRun(Thread *thread):
Make thread ready to run and place it on the ready list.
将线程状态设置为READY并将其添加进准备好队列。
Note that ReadyToRun doesn't actually start running the thread; it simply changes its state to READY and places it on the ready list.
注:ReadToRun并不真正的开始执行线程,它只是简单的改变线程的状态为READY并加入到准备好队列。
The thread won't start executing until later, when the scheduler chooses it.
线程只有在调度器选择了它才能真正的开始执行。
Thread *FindNextToRun():
Select a ready thread and return it. FindNextToRun simply returns the thread at the front of the ready list.
从准备好队列中选择一个线程并返回。FindNextToRun只是简单的返回在准备好队列前前端的线程。
void Run(Thread *nextThread):
Do the dirty work of suspending the current thread and switching to the new one.
做些与暂停当前线程有关的工作并切换到下一个线程。
Note that it is the currently running thread that calls Run(). A thread calls this routine when it no longer wishes to execute.
注:是由当前正运行的线程调用Run()。当一个线程不再想运行时调用该例程。
Run() does the following:
Run()完成以下工作:
1. Before actually switching to the new thread, check to see if the current thread overflowed its stack.
1. 在真正切换到一个新线程之前,查看当前线程的栈是否溢出。
This is done by placing a sentinel value at the top of the stack when the thread is initially created.
通过线程初始化时在栈顶放置一个观察值来完成相应的任务。
If the running thread ever overflows its stack, the sentinel value will be overwritten, changing its value.
如果当前运行的线程栈溢出,观察值将被改写。
By checking for the sentinel value every time we switch threads, we can catch threads overflowing their stacks.
我们每次进行线程切换时都会检查观察值,这样我们就能捕获到栈溢出。
2. Change the state of newly selected thread to RUNNING.
2. 改变要切换到的线程状态为RUNNING。
Nachos assumes that the calling routine (e.g. the current thread) has already changed its state to something else, (READY, BLOCKED, etc.) before calling Run().
Nachos假设调用Run例程的线程在调用Run之前已经改变其状态(READY,BLOCKED等)。
3. Actually switch to the next thread by invoking Switch(). After Switch returns, we are now executing as the new thread.
3. 实际上通过调用Switch()来切换到下一个线程,当调用返回时我们就在执行新的线程。
4. If the previous thread is terminating itself (as indicated by the threadToBeDestroyed variable), kill it now (after Switch()).
4. 如果前一个线程中止了它自己(通过设置threadToBeDestroyed变量),则杀死前一个线程(在调用Switch()之后)。
As described in last Section, threads cannot terminate themselves directly; another thread must do so.
如上一节所描述的,线程不会直接中止自己,必须通过另一个线程来完成。
It is important to understand that it is actually another thread that physically terminates the one that called Finish().
理解真正在物理上中止一个线程是通过另一个线程调用Finish()来完成是很重要的。
Synchronization and Mutual Exclusion
同步与互斥
Low-level Nachos routines frequently disable and re-enable interrupts to achieve mutual exclusion (e.g., by calling Interrupt::SetLevel()).
底层的Nachos例程经常通过开启和禁用中断来完成互斥(如,通过调用Interrupt::SetLevel())。
Synchronization facilities are provided through semaphores.
同步机制则是通过信号灯来完成。
The Semaphore object provides the following operations:
信号灯对象提供下面的操作:
Semaphore(char* debugName, int initialValue):
The constructor creates a new counting semaphore having an initial value of initialValue.
构造器创建一个新的用于计数的信号灯并初始化其值为initialValue。
The string debugName is also associated with the semaphore to simplify debugging.
debugName也与信号灯相关联用于简单的调试。
void P():
Decrement the semaphore's count, blocking the caller if the count is zero.
减小信号灯的计数。如果其计数为零则堵塞调用者。
void V():
Increment the semaphore's count, releasing one thread if any are blocked waiting on the count.
增加信号灯计数,释放一个等待于当前计数值并被堵塞的线程。
Special Notes
特别说明
When Nachos first begins executing, it is executing as a single Unix process. Nachos turns this single user process into a single Nachos thread.
当Nachos被运行时,它被作为一个Unix进行被执行。Nachos将该用户进程转化为一个Nachos线程。
Thus, by default, Nachos executes a single thread. When the Nachos entry point routine main returns, that thread exits as well.
因些,缺省下,Nachos执行单个线程。当Nachos进入main并返回时,线程同样也退出。
However, if other threads have been created and continue to exist, the Unix process continues executing Nachos.
尽管如此,如果还有其它的线程已被创建并持续存在,Unix进程将继续执行Nachos。
Only after all threads have terminated does the Unix Nachos process exit.
只有当所有的线程都中止时,Unix进程才会退出。