之前提供了一个在BREW等Arm和X86上的模拟多线程,核心部分是用汇编写的,里面的关键是栈的切换。今天偶尔清闲一下,突然想能不能不用汇编来实现,有些人一听汇编就头大了。于是就用了一下午来研究下如何操作。代码并不多,只是因为我在linux下写的,只能使用gdb来调试,而我对gdb非常不熟悉,所以弄起来非常的慢,大部分的时间都是靠仔细的分析代码来找错误的原因和地方的,浪费了不少精力,期间还在白纸上来描述流程,写部分实现,分析。就这些年的集中精力来说,可以说也花费了不少脑细胞了。
闲话不说了,苦也受过了,先看代码:
#include "stdio.h"
#include "stdlib.h"
class Runable
{
public:
virtual ~Runable()
{
}
virtual void run() = 0;
virtual void resume() = 0;
virtual void suspend() = 0;
virtual void start() = 0;
virtual bool isexecute() = 0;
};
class Thread
{
public:
Thread(){
}
virtual ~Thread(){
}
virtual void run() = 0;
virtual void start() {
if (isExecute()) return;
_pStack = (size_t*)(&_stack[1024-4]);
StartThread(this);
return;
}
virtual void suspend()
{
if (isSuspend() || !isExecute()) return;
_bSuspend = true;
SwitchThread(this);
}
virtual void resume()
{
if (!isSuspend()) return;
_bSuspend = false;
SwitchThread(this);
}
virtual bool isExecute()
{
return (_bExecute != 0);
}
virtual bool isSuspend()
{
return _bSuspend;
}
static void end(Thread* pThread)
{
register size_t* ret = (size_t*)(&pThread- 2);
register size_t bak = *(pThread->_pStack);
*ret = bak;
}
static void StartCallback(Thread* pThread)
{
pThread->run();
pThread->_bExecute = 0;
end(pThread);
}
static void StartThread(Thread* pthis)
{
pthis->_bExecute = 1;
register size_t* ret = (size_t*)(&pthis-2);
register size_t bak = *(pthis->_pStack);
*(pthis->_pStack) = *ret;
StartCallback(pthis);
}
static void SwitchThread(Thread* pthis)
{
register size_t* ret = (size_t*)(&pthis- 2);
register size_t bak = *(pthis->_pStack);
*(pthis->_pStack) = *ret;
*ret = bak;
}
size_t* _pStack;
int _bExecute;
char _stack[1024];
bool _bSuspend;
};
class test_thread: public Thread
{
public:
test_thread()
{
}
virtual ~test_thread()
{
}
virtual void run()
{
int k = 0;
while (k < 100) {
printf("k=%d\n", k++);
suspend();
}
}
};
int main(int argc, char *argv[])
{
test_thread* test = new test_thread();
int j = 0;
while (j < 1000) {
printf("j = %d\n", j++);
if (!test->isExecute()) {
test->start();
}
else {
test->resume();
}
}
delete test;
return 0;
}
抱歉没有注释,实在来不及写了。其实外部的接口和上次的完全一样,不一样的仅在于实现而已。
现在来进行技术说明,线程的关键是切换栈指针,及汇编里面的esp寄存器,用汇编无疑是最方便的,但C/C++的话也是可以的,如果内嵌汇编的话,我也不说了。修改的办法就是修改函数的返回地址。可能有些人比较蒙,先看下汇编的函数调用:
push 5
call fun
add esp, 4; 或者pop eax或其他等效的
上述的代码实现的就是调用fun(5)的汇编实现,而在栈内存中,呈如下排列:
5
ret_address
凑活着看吧,函数返回地址ret_address(调用函数语句的下一条语句)和变量5的地址的关系如下:
ret_address = address(5) – 4
我们在C/C++中其实可以调用&(5)来获得参数5的内存地址,而这个地址-4就是返回地址。现在已经很明了了,就是要巧妙的构造函数返回地址。
不过在C++的类中有些不太一样,就是在调用类函数的时候,默认会传递一个类的指针。所以在这里就将pthis/pThread的地址-2了(指针地址都是32位的,所以-2,相当于字节-8)。
剩下的就没有太多可说的了,在start函数中,设置自定义的栈顶(栈由高往低走)。然后调用StartThread,在StartThread中重新设置函数返回地址,然后调用StartCallback函数,StartCallback函数将会调用线程的thread::run函数。在一般情况下,run函数中最会用一个suspend,在suspend中最终会调用SwitchThread函数来重新设置返回值,使之返回到外部的主函数中。而StartCallback中的pThread->run()之后的语句,只有在Thread::run正常返回时(run函数的结束)才会执行,这样来完成一个流程。但这样有一个问题,由于我们不是直接修改esp值,我们其实是通过ebp值来修改esp值(我更乐意称这种行为是影响,而不是修改),这样以来就要保证栈平衡,即有多少调用函数就要有多少个返回(其实汇编里面更通常的说法是有多少个push就要有多少个pop),由于在start函数中多进行了一次函数调用(外部函数->start->StartThread),因此,返回的流程就是(StartCallback->end->外部函数),这样来实现栈平衡的。因此就有了下面的问题:
在使用的时候,必须严格控制调用的层次,在上次的线程模拟中,使用下面的方法:
class test_thread:public Thread
{
//....
void OnRun()
{
if (!isExecute) {
start();
}
else {
resume();
}
}
//....
};
上述语句在这次的调用中将会失败,原因在于多了一层,而上次的可以是由于我们直接修改esp值,更改了函数的调用(我有点怀疑上次的线程在机器上运行不稳定是否是这个原因)。这个就是使用高级语言实现的代价。在C库中,有一个setjmp的函数,可以保存寄存器中的值,我没有仔细研究那个函数,因此对于能否结合这次的程序,达到完美的不用汇编实现的多线程不太清楚。
我没有看过别的线程实现代码,比如pthread的,对于它们的实现方式和手段也不清楚。本文仅仅从研究的角度出发。
PS:上述代码在ubuntu 10.10 x86_32bit上测试通过,并未在其他系统上进行测试。
PS2:上述代码由于我对gdb使用不熟悉,分析过程大都依靠语句分析,并未结合实际状况,因此,如有错误,还请谅解。