repos 已經更新過了,這份筆記提到的 code 是在 commit Add TraceCode Note
code 全放在 code 資料夾
Makefile 也推薦高速掃過看一下
最後的章節會以執行流程的方式紀錄整個執行過程,追蹤 userprog/nachos
並會統整一些架構出來,放在中間的章節
初始化在 threads/main.cc
用參數 -d
設定的字串創造一個 Debug Obj
Debug Class 定義於 lib/debug.h
假設 -d
設定為 ti
(e.g. ./userprog/nachos -e test/test2 -d ti
)
那 Debug Class 的 enableFlags 會是字串 ti
接著在整個專案的任意處寫類似以下的語句:
DEBUG(dbgThread, "My debug message")
DEBUG 是一個 macro,也定義在 lib/debug.h,會去看 dbgThread 代表的字元 t
(同樣也寫在 lib/debug.h) 是否有在剛剛 -d
設定的字串中,有就輸出 My debug message
看 Trace3
以下 Trace 在 code 底下執行 ./userprog/nachos -e test/test1
的過程
進入點為 threads/main.cc
這裡也設定了 Debug 物件,詳細請看Debug
接著 call kernel = new KernelType(argc, argv);
,KernelType 是一個 Macro,定義在 threads/main.h,會根據 define 了以下 symbol 做不一樣的行為
現在來追看看到底什麼 symbol 有被 define
因為我們追蹤的是 userprog/nachos
,看一下 userprog/Makefile,以下 focus 在 跟 define 有關的部分
userprog/Makefile 中有一行
DEFINES = -DTHREADS -DUSER_PROGRAM -DFILESYS_NEEDED -DFILESYS_STUB
再往下一點有兩行
include ../Makefile.common
include ../Makefile.dep
Makefile.common 裡有一行
CFLAGS = -g -Wall $(INCPATH) $(DEFINES) $(HOST) -DCHANGED
Makefile.dep 只有這樣
# CPU = x86, OS = linux
HOST = -Dx86 -DLINUX -DBSD
DISASM = disasm
對於 Makefile 的 =
跟 :=
兩個指派運算子的觀念可以看看這篇文,簡單來說,=
是會到要用到的時候才會展開
而 CFLAGS 是 compile 時,會餵給 compiler 吃的參數
最後整個 CFLAGS 是 -g -Wall $(INCPATH這個我們先不考慮) -DTHREADS -DUSER_PROGRAM -DFILESYS_NEEDED -DFILESYS_STUB -Dx86 -DLINUX -DBSD -DCHANGED
這些參數會使 compiler 在 compile 時預先 define
因為 define 了 USER_PROGRAM,所以前面 Trace1 的 main.h 會 define KernelType 為 UserProgKernel
在 threads/main.cc 中執行 kernel = new KernelType(argc, argv);
,實際上是創造 UserProgKernel 物件
UserProgKernel 實作於 userprog/userkernel.cc,會先執行父類別 ThreadedKernel 的 Constructor
ThreadedKernel 實作於 threads/kernel.cc,設定 randomSlice = FALSE;
會在後續創造 Alarm obj 時用到
回到 UserProgKernel,吃到參數 -e
,將 execfile[0]
設為 test/test1
,execfileNum
為 1
可以看到 UserProgKernel 的 Constructor 只是吃吃參數而已,真正有做些初始化是在後面
在 threads/main.cc 中執行 kernel->Initialize();
執行 userprog/userkernel.cc 中的UserProgKernel::Initialize()
UserProgKernel::Initialize()
會執行
ThreadedKernel::Initialize()
machine = new Machine(debugUserProg);
fileSystem = new FileSystem();
執行 threads/kernel.cc 中的ThreadedKernel::Initialize()
ThreadedKernel::Initialize()
會執行
stats = new Statistics();
Statistics::Statistics()
實作於 machine/stats.cc 中interrupt = new Interrupt;
Interrupt::Interrupt()
實作於 machine/interrupt.cc 中level = IntOff;
pending = new SortedList<PendingInterrupt *>(PendingCompare);
inHandler = FALSE;
yieldOnReturn = FALSE;
status = SystemMode;
scheduler = new Scheduler(type);
Scheduler::Scheduler(SchedulerType type)
實作於 threads/scheduler.cc 中alarm = new Alarm(randomSlice);
Alarm::Alarm(bool doRandom)
實作於 threads/alarm.cc 中currentThread = new Thread("main");
Thread::Thread(char* threadName)
實作於 threads/threads.cc
中Thread::Fork()
currentThread->setStatus(RUNNING);
interrupt->Enable();
Interrupt::SetLevel(IntOn);
Interrupt::OneTick()
Timer::Timer(bool doRandom, CallBackObj *toCall)
實作於 machine/timer.cc 中
將 callPeriodically
設定為 toCall
並呼叫 Timer::SetInterrupt()
Timer::SetInterrupt()
中,若 Timer 為 Enable,則呼叫 Interrupt::Schedule
創造一個 PendingInterrupt 物件,紀錄著
並插入到 Pending list
Interrupt::OneTick()
實作於 machine/interrupt.cc
Trace 到此時,還在 kernel mode,totalTicks 會加上 SystemTick
(各種 Ticks 定義在 machine/stats.h 中)
先禁止接受中斷後執行
CheckIfDue(FALSE);
Interrupt::CheckIfDue(bool advanceClock)
簡單來說是檢查 Pending list 有無已經到達預定執行時間的中斷要做,有就呼叫此中斷的 Callback,再呼叫中斷的 Callback 的期間,將 inHandler 設為 True,表示正在處理中斷,回傳值的狀況分為以下狀況
由於 Pending List 中已經有一個 TimerInt 的中斷,時間還沒到,應該要快轉,但 advanceClock 設 false,所以回傳 false
處理完中斷後,重新開啟接受中斷
看看有無要進行 content switch,有則執行 kernel->currentThread->Yield();
不過 Trace 到目前為止是還不會需要 content switch 的。
Machine::Machine(bool debug)
實作於 machine/machine.cc,debug 參數為 false,簡單來說,就是初始化機器的暫存器為 0,清空 mainMemory
FileSystem::FileSystem(bool format)
實作於 filesys/filesys.cc
,format 預設為 true,這個就先不深追
在 threads/main.cc 中執行
CallOnUserAbort(Cleanup); //設定按下 ctrl+c 就 call `Cleanup`
kernel->SelfTest(); // 不是很重要,For debugging
kernel->Run(); // 繼續追蹤這個 function call
main 到此告一段落,Control flow 移轉到 kernel->Run()
UserProgKernel::Run()
首先一個 for 迴圈,為每支 userprogram 建立 Thread、分配 AddrSpace,並且執行 Fork
本例子是執行
t[n] = new Thread("test/test1");
t[n]->space = new AddrSpace();
t[n]->Fork((VoidFunctionPtr) &ForkExecute, (void *)t[n]);
最後執行 ThreadedKernel::Run();
t[n] = new Thread("test/test1");
t[n]->space = new AddrSpace();
AddrSpace::AddrSpace()
實作於 userprog/addrspace.cct[n]->Fork((VoidFunctionPtr) &ForkExecute, (void *)t[n]);
StackAllocate(func, arg);
分配一塊 stack
Thread::StackAllocate (VoidFunctionPtr func, void *arg)
首先執行
stack = (int *) AllocBoundedArray(StackSize * sizeof(int));
AllocBoundedArray(StackSize * sizeof(int))
實作於 lib/sysdep.cc
getpagesize()
取得一個 page 多大pgSize * 2 + size
的空間回到 Thread::StackAllocate
,接下來會根據不同架構做不一樣的事情,在 Trace2 我們知道有 define x86,所以會執行
stackTop = stack + StackSize - 4;
*(--stackTop) = (int) ThreadRoot;
*stack = STACK_FENCEPOST;
machineState[PCState] =(void *)ThreadRoot;
machineState[StartupPCState] = (void *)ThreadBegin;
machineState[InitialPCState] = (void *)func;
machineState[InitialArgState] = (void *)arg;
machineState[WhenDonePCState] = (void *)ThreadFinish;
#define PCState (_PC/4-1)
#define FPState (_EBP/4-1)
#define InitialPCState (_ESI/4-1)
#define InitialArgState (_EDX/4-1)
#define WhenDonePCState (_EDI/4-1)
#define StartupPCState (_ECX/4-1)
machineState[PC]
設定為 ThreadRoot目前為止 thread test/test1
的 stack 上的資料大概像
回到 UserProgKernel::Run()
,for 迴圈跑完後執行 ThreadedKernel::Run();
ThreadedKernel::Run()
裡頭執行 currentThread->Finish();
currentThread
為在 Trace4_Init_ThreadedKernel 設定的 main
Thread main
的 Thread::Finish
執行了
Sleep(TRUE)
Thread main
的 Thread::Sleep (bool finishing)
執行了
main
status 設為 BLOCKEDtest/test1
kernel->scheduler->Run(Thread test/test1, TRUE);
Scheduler::Run (Thread test/test1, TRUE)
執行了
main
main
的 CheckOverflow
test/test1
test/test1
status 設為 RUNNINGmain
, test/test1
)
CheckToBeDestroyed()
main
void SWITCH(Thread *oldThread, Thread *newThread);
在 Trace2 我們知道 define 了 x86,所以 switch.s 中的 SWITCH code 為
.comm _eax_save,4
.globl SWITCH
SWITCH:
movl %eax,_eax_save # save the value of eax
movl 4(%esp),%eax # move pointer to t1 into eax
movl %ebx,_EBX(%eax) # save registers
movl %ecx,_ECX(%eax)
movl %edx,_EDX(%eax)
movl %esi,_ESI(%eax)
movl %edi,_EDI(%eax)
movl %ebp,_EBP(%eax)
movl %esp,_ESP(%eax) # save stack pointer
movl _eax_save,%ebx # get the saved value of eax
movl %ebx,_EAX(%eax) # store it
movl 0(%esp),%ebx # get return address from stack into ebx
movl %ebx,_PC(%eax) # save it into the pc storage
movl 8(%esp),%eax # move pointer to t2 into eax
movl _EAX(%eax),%ebx # get new value for eax into ebx
movl %ebx,_eax_save # save it
movl _EBX(%eax),%ebx # retore old registers
movl _ECX(%eax),%ecx
movl _EDX(%eax),%edx
movl _ESI(%eax),%esi
movl _EDI(%eax),%edi
movl _EBP(%eax),%ebp
movl _ESP(%eax),%esp # restore stack pointer
movl _PC(%eax),%eax # restore return address into eax
movl %eax,4(%esp) # copy over the ret address on the stack
movl _eax_save,%eax
ret
(AT&T 語法的組語, src在左, dst在右)
對應 SWITCH(main, test/test1)
的組合語言是
push Thread * test/test1
push Thread * main
call SWITCH
所以執行 nachos 的主機的 stack 上會長這樣
解釋 SWITCH 做的事情
movl %eax,_eax_save
eax
的值存到 _eax_save
movl 4(%esp),%eax # move pointer to t1 into eax
movl %ebx,_EBX(%eax) # save registers
movl %ecx,_ECX(%eax)
movl %edx,_EDX(%eax)
movl %esi,_ESI(%eax)
movl %edi,_EDI(%eax)
movl %ebp,_EBP(%eax)
movl %esp,_ESP(%eax) # save stack pointer
#define _ESP 0
#define _EAX 4
#define _EBX 8
#define _ECX 12
#define _EDX 16
#define _EBP 20
#define _ESI 24
#define _EDI 28
#define _PC 32
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
void *machineState[MachineStateSize]; // all registers except for stackTop
test/test1
+ 0 就會存取到 stackToptest/test1
+ 4 就會存取到 machineState[0]test/test1
+ 8 就會存取到 machineState[1]eax
的值改為 esp + 4
,也就是 Thread * main
ebx
存到 [eax + _EBX]
,也就是 [eax + 8]
,也就是 Thread * main
的 machineState[1]
,之後以 machineState[EBX]
形式表示_ESP
為 0,所以改 machineState[ESP]
實際上是改 Thread class 裡的 stackTop movl _eax_save,%ebx
movl %ebx,_EAX(%eax)
eax
放到 ebx
main
的 machineState[EAX]
movl 0(%esp),%ebx
movl %ebx,_PC(%eax)
main
的 machineState[PC]
到目前為止存好了 Thread * main
(oldThread) 各個暫存器的值
movl 8(%esp),%eax
test/test1
movl _EAX(%eax),%ebx # get new value for eax into ebx
movl %ebx,_eax_save # save it
movl _EBX(%eax),%ebx # retore old registers
movl _ECX(%eax),%ecx
movl _EDX(%eax),%edx
movl _ESI(%eax),%esi
movl _EDI(%eax),%edi
movl _EBP(%eax),%ebp
movl _ESP(%eax),%esp # restore stack pointer
test/test1
紀錄的各個暫存器的值恢復eax
先存到 _eax_save
_ESP(%eax)
實際上是取出 Thread * test/test1
的 stackTop
movl _PC(%eax),%eax # restore return address into eax
movl %eax,4(%esp) # copy over the ret address on the stack
movl _eax_save,%eax
ret
test/test1
的 Program Counter 放到 return addresseax
esp
+ 4,並不會改動值)現在要跳轉到 Thread test/test1
的 Program counter 上
可以回顧 Trace6_StackAllocate,Thread test/test1
machineState[PC]
是 ThreadRoot
的位址
所以 Control flow 現在會跑到 ThreadRoot
上執行
void ThreadRoot()
code 如下
.globl ThreadRoot
/* void ThreadRoot( void )
**
** expects the following registers to be initialized:
** eax points to startup function (interrupt enable)
** edx contains inital argument to thread function
** esi points to thread function
** edi point to Thread::Finish()
*/
ThreadRoot:
pushl %ebp
movl %esp,%ebp
pushl InitialArg
call StartupPC
call InitialPC
call WhenDonePC
# NOT REACHED
movl %ebp,%esp
popl %ebp
ret
ebp
值初始為 0,push 上去後將 esp
的值放到 ebp
pushl InitialArg
call StartupPC
,會 push 下一個指令位址StartupPC
定義在 threads/switch.h,就是 %ecx
在剛剛的 SWITCH 中,%ecx
的值設定為 Thread * test/test1
的 machineState[ECX]
而 Thread * test/test1
的 machineState[ECX]
的值在 Trace6_StackAllocate 中有設定
machineState[StartupPCState] = (void *)ThreadBegin;
所以現在會呼叫 ThreadBegin
static void ThreadBegin()
實作於 threads/thread.cc
static void ThreadBegin() { kernel->currentThread->Begin(); }
kernel->currentThread
現在為 Thread test/test1
所以會呼叫到 Thread test/test1
的 Begin()
Thread::Begin()
執行
kernel->scheduler->CheckToBeDestroyed();
main
在此被砍掉kernel->interrupt->Enable();
void Enable() { (void) SetLevel(IntOn); }
Interrupt::SetLevel(IntStatus now)
中斷從關閉到打開,會 call 一次 OneTick()
Interrupt::OneTick()
執行
CheckIfDue(FALSE);
執行完了 return,從 stack pop 出 return address 到 PC,執行call InitialPC
,會 push 下一個要執行的指令
InitialPC
定義在 threads/switch.h,就是 %esi
在剛剛的 SWITCH 中,%esi
的值設定為 Thread * test/test1
的 machineState[ESI]
而 Thread * test/test1
的 machineState[ESI]
的值在 Trace6_StackAllocate 中有設定
machineState[InitialPCState] = (void *)func;
func
為 ForkExecute
,所以現在會呼叫 ForkExecute(Thread *t)
t
這第一個參數為 esp
+ 4 的 InitialArg,此時回頭追一下 InitialArg 是什麼
t[n]
machineState[InitialArgState] = (void *)arg;
t
,是 Thread test/test1
ForkExecute(Thread *t)
執行 t->space->Execute(t->getName());
執行 Thread test/test1
的 space->Execute("test/test1")
AddrSpace::Execute(char *fileName)
實作於 userprog/addrspace.cc,執行了
Load(fileName)
kernel->machine->mainMemory
this->InitRegisters()
NumTotalRegs
是定義於 machine/machine.h,為 user program 能用的 CPU registerskernel->machine
的所有 registers 先填為 0
kernel->machine
的 PC register 寫為 0 (user program 的 start,也就是 entry point,要放在 memory address 0)kernel->machine
的 NextPC register 寫為 4kernel->machine
的 StackReg 寫為最高位址this->RestoreState()
kernel->machine
的 pageTable 改為此 Thread 的 AddrSpace 的 pageTablekernel->machine->Run()
執行以下事情
nstruction *instr = new Instruction;
kernel->interrupt->setStatus(UserMode);
for (;;) {
OneInstruction(instr);
kernel->interrupt->OneTick();
if (singleStep && (runUntilTime <= kernel->stats->totalTicks))
Debugger();
}
Instruction
只是一個用來存放指令的結構kernel->interrupt
設為 UserMode
kernel->interrupt->OneTick()
這邊沒有認真追完,但看個大概還是能看得出來執行了什麼
ReadMem(registers[PCReg], 4, &raw)
instr->value = raw;
instr->Decode();
int pcAfter = registers[NextPCReg] + 4;
switch (instr->opCode)
{
case OP_xxx:
...
...
}
RaiseException(SyscallException, 0);
nextLoadReg
、nextLoadValue
,在待會的DelayedLoad(nextLoadReg, nextLoadValue);
會用到DelayedLoad(nextLoadReg, nextLoadValue);
registers[registers[LoadReg]] = registers[LoadValueReg];
registers[LoadReg] = nextReg
registers[LoadValueReg] = nextValue;
統整來說,call 了 DelayedLoad(MyReg1, MyValue1);
,並不會馬上將 MyValue1
存到 Register MyReg1
上,而是要等下次再 call 到這個 function 才會存過去 (所以叫做 DelayedLoad
)
registers[PrevPCReg] = registers[PCReg];
registers[PCReg] = registers[NextPCReg];
registers[NextPCReg] = pcAfter;
Machine::RaiseException(ExceptionType which, int badVAddr)
當執行 user 的 MIPS 程式時,若有 call syscall 或有其他狀況,都會觸發這個 function (寫在 Trace8_OneInstruction 中)
執行了以下事情
registers[BadVAddrReg] = badVAddr;
DelayedLoad(0, 0);
kernel->interrupt->setStatus(SystemMode);
SystemMode
(KernelMode
)ExceptionHandler(which);
kernel->interrupt->setStatus(UserMode);
UserMode
ExceptionHandler(ExceptionType which)
Interrupt::OneTick()
UserMode
在此整理一下 OneTick 被 call 的狀況
第一次 UserMode
OneTick 之前會歷經三次 SystemMode
OneTick
SystemMode
OneTick
ThreadedKernel->Initialize()
開啟接受中斷,進而呼叫到SystemMode
OneTick
UserProgKernel->Run()
初始化 user program,呼叫到 Fork 時,將 thread 放進 readylist 時有關開一次中斷,進而呼叫到SystemMode
OneTick
SWITCH
過後,執行每個 user thread 都會先執行的 Thread::Begin()
,裡頭有將中斷開啟,進而呼叫到在此例中,若後面加上 debug flag -d i
,可以看到前三個 Ticks 是一次跳 10,後面則一次跳 1,是因為設定在 machine/stats.h 中的 SystemTick 為 10,UserTick 為 1,而前三次 Ticks 是在 SystemMode 運行,後面的 Ticks 是在 UserMode
這邊高速 Trace 一下,以 debug 資訊為輔助
在執行 nachos 的指令後面加上 -d i
,可以看到跟 interrupt 有關的 debug 資訊
這邊小小複習一下 Trace8_Machine::Run
裡頭的無限迴圈做的事情就是
RaiseException
進而陷入 SystemMode 執行 syscallkernel->interrupt->OneTick()
還有整個程式執行完後的部分也還未追到
在初始化時,有創造 Timer
,並且有註冊在未來 TimerTicks 過後要執行 Timer
的 callback
Timer
的 callback在剛剛的 Trace9,在無限迴圈時會一直 call OneTick
因為是 UserMode,所以 totalTicks 一次加 UserTick
當 totalTicks 來到 100 時,Interrupt::OneTick()
的執行流程是
CheckIfDue(FALSE);
,裡頭執行了
next = pending->Front();
kernel->machine->DelayedLoad(0, 0);
inHandler = TRUE;
設定正在處理中斷next = pending->RemoveFront();
next->callOnInterrupt->CallBack();
Timer->CallBack()
Alarm->CallBack()
Timer
interrupt->YieldOnReturn();
SetInterrupt();
,若 Timer 沒被 disable,則設定在未來 100 個 Tick 後要再一次執行 Timer 中斷inHandler = FALSE;
設定沒在處理中斷kernel->currentThread->Yield();
準備讓出 CPU
所以整個流程可以理解為
Thread::Yield()
執行的事情是
ReadyToRun()
把 oldThread 丟進 ready listkernel->scheduler->Run(nextThread, FALSE);
kernel->scheduler->Run
主要就是做 Content switch此例子只有一個 user program,所以這裡不會做 Content switch
這邊在執行 nachos 的指令後面多加一個 flag -d ita
,可以多看到跟 Interrupt、Thread、AddrSpace 有關的 debug 資訊
User program 最後會 call syscall SC_Exit
Function call stack:
Machine::Run
-> OneInstruction
-> RaiseException
-> ExceptionHandler
SC_Exit
執行了
kernel->currentThread->Finish();
kernel->currentThread->Finish();
執行了
Sleep(TRUE)
永久睡去(其實就是死了)
kernel->interrupt->Idle();
Timer::SetInterrupt()
kernel->interrupt->Idle();
Halt()
進行關機OS
, NachOS