本文主要是剖析NachOs的线程切换原理,并通过一个简化的例子(就是将线程部分代码抽取出来再加以修改)
来说明。本文 githbu代码:https://github.com/Ldpe2G/ThreadSwitch--Simulation
#ifndef THREAD_H #define THREAD_H /* the offsets of the registers from the beginning of the thread object */ /* 寄存器存储的位置对应的线程对象内存地址的偏移量 */ #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 /* These definitions are used in Thread::AllocateStack(). */ #define PCState (_PC/4-1) #define InitialPCState (_ESI/4-1) #define InitialArgState (_EDX/4-1) #define MachineStateSize 75 typedef void (*VoidFunctionPtr)(void *arg); const int StackSize = (8 * 1024); // stack size = 8 * 1024 * 4 bytes class Thread{ private: // the current stack pointer int *stackTop; // all registers except for stackTop // 线程切换时存储寄存器的数组 void *machineState[MachineStateSize]; public: Thread(char* name); // initialize a Thread ~Thread(); // deallocate a Thread // 分配栈空间和初始化machinState然后 // 将该线程放入就绪队列 void Fork(VoidFunctionPtr func, void *arg); //从队列中找出下一个要运行的线程, //然后该线程放入就绪队列 void yield(); // Put the thread to sleep void Ssleep(bool finishing); // thread Startup void Begin(); // The thread is done executing void Finish(); // Check if thread stack has overflowed void CheckOverflow(); char* getName(){return name;} private: int *stack; // Bottom of the stack char* name; void StackAllocate(VoidFunctionPtr func, void *arg); }; extern "C" { // First frame on thread execution stack; // call ThreadBegin // call "func" // (when func returns) call ThreadFinish() void ThreadRoot(); // Stop running oldThread and start running newThread void SWITCH(Thread *oldThread, Thread *newThread); } #endif //THREAD_H
具体的函数定义可以看Github上的代码:Thread.cpp。
现在结合上面的代码来分析。
NachOS的多线程其实就是多个代码段,通过人为调度的方式将它们调度作为线程代码执行。就像单核CPU上的
多线程实现,其实就是线程之间轮换时间片。
我们可以看到在Thread类的开头有两个私有变量:
stackTop (线程运行时的栈指针)和 machineState(线程切换时用于保存当前寄存器的值以便于恢复)。
这两个变量必须是放在开头,接下来解释线程切换的原理的时候读者就会明白为什么要放在开头。
接下来我们看到代码底部的 ThreadRoot 和 SWITCH 函数,这两个函数的内部实现用到了内联汇编。
这两个函数就是重点要解释的部分。
//Func线程保存函数地址 //IniArg保存函数参数地址 void* IniArg = (void*)0; void* Func = (void*)0; void ThreadRoot(){ simulator->currentThread->Begin(); __asm{ push IniArg /* 线程函数func的参数入栈 */ call Func /* call线程函数func */ add esp,4 /* 释放参数传递的栈空间 */ } simulator->currentThread->Finish(); }
在NachOS中,除了main线程外,其他线程都是从ThreadRoot入口运行的。
要注意的是ThreadRoot函数并没有被显式的调用,而是在SWITCH函数执行完之后才被调用。
一个线程在初始化的左后准备工作中调用StackAllocate方法,初始化栈空间和设置寄存器的值,
开始的时候保存在machineState中。然后当该线程被切换上CPU的时候,ThreadRoot就被调用。
unsigned long _eax_save = 0; //全局中间变量 void SWITCH(Thread *oldThread, Thread *newThread){ __asm{ pop edi /* 恢复edi */ pop esi /* 恢复esi */ pop ebx /* 恢复ebx */ mov esp,ebp /* 释放要函数局部变量空间 */ pop ebp /* 恢复ebp */ mov _eax_save,eax /* 暂存eax, 注意:_eax_save为全局变量 */ mov eax, [esp+4] /* eax 指向oldThread */ mov [_EBX+eax],ebx /* 保存相关寄存器值, 到oldThread的空间中 */ mov [_ECX+eax],ecx mov [_EDX+eax],edx mov [_ESI+eax],esi mov [_EDI+eax],edi mov [_EBP+eax],ebp mov [_ESP+eax],esp /* 保存栈指针 */ mov ebx,_eax_save /* 取暂存的eax,从全局变量 _eax_save中 */ mov [_EAX+eax],ebx /* 保存初始eax的值 */ mov ebx,[esp+0] /* 取返回地址 */ mov [_PC +eax],ebx /* 保存返回地址 */ mov eax,[esp+8] /* eax指向newThread */ mov ebx,[_EAX+eax] /* 取newThread保存的eax值*/ mov _eax_save,ebx /* 暂存到 _eax_save */ mov ebx,[_EBX+eax] /* 恢复newThread保存的寄存器值 */ mov ecx,[_ECX+eax] mov edx,[_EDX+eax] mov IniArg,edx // 将线程函数参数保存到全局变量 mov esi,[_ESI+eax] mov Func,esi // 保存线程函数地址 mov edi,[_EDI+eax] mov ebp,[_EBP+eax] mov esp,[_ESP+eax] //恢复栈指针 mov eax,[_PC +eax] //保存返回地址到eax mov [esp+0],eax mov eax,[_eax_save] ret /*直接返回,然后继续新线程的执行*/ } }
从代码上我们可以看到,NachOS中的线程切换是要借助宿主主机。
其中oldThread是原来正在运行的线程,newThread值需要切换到的线程指针。
线程切换过程是:
1、保存原来正在运行的线程的状态,就是保存寄存器的值;
2、恢复新运行线程的状态;
3、然后最后ret语句执行完就继续新线程的运行。
要理解上述的汇编代码我们首先来看看Thread对象的内存布局:
在StackAllocate初始化然后调用了StackAllocate函数之后的内存布局。
我们可以看到stackTop 和 machinState第一个元素 的地址为线程对象地址分别加0和加4就能找到。
再结合SWITCH汇编代码详细分析:
在代码开头可以看到连续3个pop语句然后恢复ebp,这是因为编译器在帮我们编译的时候做了一些工作。
现在我们来看看SWITCH的反汇编的代码:
unsigned long _eax_save = 0; //全局中间变量 void SWITCH(Thread *oldThread, Thread *newThread){ 001748F0 push ebp 001748F1 mov ebp,esp 001748F3 sub esp,0C0h 001748F9 push ebx 001748FA push esi 001748FB push edi 001748FC lea edi,[ebp-0C0h] 00174902 mov ecx,30h 00174907 mov eax,0CCCCCCCCh 0017490C rep stos dword ptr es:[edi] __asm{ //align 2 pop edi /* 恢复edi */ 0017490E pop edi pop esi /* 恢复esi */ 0017490F pop esi pop ebx /* 恢复ebx */ 00174910 pop ebx mov esp,ebp /* 释放要函数局部变量空间 */ 00174911 mov esp,ebp pop ebp /* 恢复ebp */ 00174913 pop ebp
我们可以看到编译器帮我们保存ebp,接着分配函数局部空间然后自动保存了edi,esi,ebx的值。
所以这就是为什么需要先pop的原因,然后释放函数局部空间,恢复ebp的值。
接着才是将寄存器的值保存到oldTHread的machineState变量中,然后还有保存返回地址。
然后将newThread的状态恢复然后继续newThread的执行。
现在来分析具体的切换过程:
首先在进入SWTICH函数时,栈的内容如下:
这也是在刚开始要保存oldThread状态时的栈内容。即是在释放了函数局部空间之后的状态。
所以oldThread 和 newThread 的地址就是 [ esp + 4 ] 和 [ esp + 8 ] ,
然后代码中eax用作线程对象内部变量寻址,[ eax ] 就是stackTop的地址,
而 [ eax + 4 ] 就是machineState第一个元素的地址。
在恢复newThread状态的时候,最后将esp指向了newThread的stackTop所指向的地址,
然后再修改其内容为返回的地址,如果newThread是第一次运行,则返回地址就是ThreadRoot函数的
地址。如果不是就是ThreadRoot函数或者线程函数运行到某一条指令的地址。然后就是继续newThead
的运行。
#include<vector> #include"Thread.h" using namespace std; #ifndef SIMULATOR_H #define SIMULATOR_H class Simulator{ public: Simulator(); ~Simulator(); //将线程加入就绪队列 void ReadyToRun(Thread* thread); //随机从就绪队列中返回一个线程来运行 Thread* FindNextToRun(); //切换新旧线程,运行新线程,finishing表示旧线程是否销毁 void Run(Thread* nextThread, bool finishing); //检测有无线程需要销毁 void CheckToBeDestroyed(); //开始模拟多线程 void Start(); Thread* currentThread; //指向当前正在运行的线程 private: vector<Thread*> readyList; //保存就绪状态的线程 Thread *toBeDestroyed; //指向将要销毁的线程 }; //声明全局变量,在main.cpp中定义 extern Simulator* simulator; #endif //SIMULATOR_H
具体的函数定义可以看Github上的代码:Simulator.cpp。
#include"Simulator.h" #include"Thread.h" #include<iostream> using namespace std; Simulator* simulator; //线程要执行的函数 void ThreadTest(int which){ for(int i = 0; i < which; i++) { std::cout << "*** thread " << simulator->currentThread->getName() << " looped " << i << " times\n"; //没一次循环结束,令当前正在运行的线程放弃CPU //进入就绪队列,然后切换下一个线程,以此来模拟多线程抢占CPU simulator->currentThread->yield(); } } int main(int argc, char* argv[]){ simulator = new Simulator; Thread* t; for(int i=0; i<5; i++){ char* threadName = new char[30]; sprintf(threadName, "%s%d", "Test Thread ", i); t = new Thread(threadName); t->Fork((VoidFunctionPtr)ThreadTest, (void*)(i + 2)); } simulator->Start(); return 0; }
函数调用约定学习
还有以前师兄的论文,这里无法提供链接