模拟线程切换 C++

前言:

本文主要是剖析NachOs的线程切换原理,并通过一个简化的例子

(就是将线程部分代码抽取出来再加以修改)来说明。

本文 githbu代码:https://github.com/Ldpe2G/ThreadSwitch--Simulation

正文:

Thread类的声明:

#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 函数,这两个函数的内部实现用到了内联汇编。这两个函数就是

重点要解释的部分。

ThreadRoot函数定义:

//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就被调用。

SWITCH函数定义:

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的运行。

simulator的声明:

#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

main.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;
}

测试结果:

参考资料:

函数调用约定学习

还有以前师兄的论文,这里无法提供链接

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏架构师之旅

解读java中volatile关键字的含义

在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉。 Java语言是支...

1665
来自专栏python学习路

八、线程和进程 什么是线程(thread)?什么是进程(process)? 线程和进程的区别?Python GIL(Global Interpreter Lock)全局解释器锁

什么是线程(thread)? 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一...

3677
来自专栏北京马哥教育

python线程笔记

豌豆贴心提醒,本文阅读时间5分钟 来源:伯乐在线 原文:http://python.jobbole.com/87498/ 引言&动机 考虑一下...

1865
来自专栏好好学java的技术栈

java大公司后端多线程面试题最强分享

抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。

811
来自专栏Android 研究

Android Handler机制1之Thread

每一个进程都有自己的独立的一块内存空间、一组资源系统。其内部数据和状态都是完全独立的。进程的优点是提高CPU的运行效率,在同一个时间内执行多个程序,即并发执行。...

732
来自专栏orientlu

FreeRTOS 信号量

FreeRTOS 信号量和互斥锁是基于队列实现的, 队列介绍见 << FreeRTOS 消息队列 >>。 使用信号量需要在源文件中包含头文件 semphr....

1052
来自专栏zingpLiu

浅析Python多线程

学习Python多线程的资料很多,吐槽Python多线程的博客也不少。本文主要介绍Python多线程实际应用,且假设读者已经了解多线程的基本概念。如果读者对进程...

897
来自专栏CaiRui

多线程编程

1、多线程对于具有如下特点的编程任务是非常理想的:1、本质上是异步的 2、需要多个并发活动 3、每个活动的处理顺序是不确定的。 2、使用多线程编程,以及类似Qu...

1887
来自专栏老马说编程

(65) 线程的基本概念 / 计算机程序的思维逻辑

在之前的章节中,我们都是假设程序中只有一条执行流,程序从main方法的第一条语句逐条执行直到结束。从本节开始,我们讨论并发,在程序中创建线程来启动多条执行流,并...

2067
来自专栏CaiRui

多线程编程

1、多线程对于具有如下特点的编程任务是非常理想的:1、本质上是异步的 2、需要多个并发活动 3、每个活动的处理顺序是不确定的。 2、使用多线程编程,以及类似Qu...

1819

扫码关注云+社区