1、makecontext、setcontext、swapcontext可以用于实现用户态线程。windows下已有Fiber相关机制,但实现一个swapcontext系统函数,可以编写与linux下相似的代码。
2、在实现中,遇到了NT_TIB问题。在swap到makecontext构造的函数环境时,出现_chkstk失败的问题。
typedef struct _NT_TIB{
struct_EXCEPTION_REGISTRATION_RECORD * ExceptionList;
PVOIDStackBase;
PVOIDStackLimit;
PVOIDSubSystemTib;
union
{
PVOIDFiberData;
ULONGVersion;
};
PVOIDArbitraryUserPointer;
struct_NT_TIB * Self;
} NT_TIB, *PNT_TIB;
(1)在编译函数时,VC有时会插入_chkstk代码。(在函数中要求分配的栈大小超过1个page时)
(2) windows下要求访问栈时,只能访问当前页或下一页,如果超过1页,则循环访问下一页的地址,产生缺页中断,_chkstk就是做这件事。由于makecontext使用了malloc出来的一块内存作为新的栈空间,该栈空间位置与原来的栈空间位置距离很远。
(3) NT_TIB类似于线程的局部存储,存储着当前线程的相关信息,win64下,寄存器gs指向该结构,win32下fs指向该结构(每个cpu是独立并行的,在每个cpu上运行的内核代码都是一样的,页表也一样,因此需要一种方法,可以在每个cpu中都用同一个符号记录状态,但这些符号却是映射到不同的地址。既然页表一样,自然不能用一个绝对的数值来寻址,因此使用页表之上的段表)。
(4)在_chkstk中,会从NT_TIB取得stackbase 与stacklimit,所以我们在实现swapcontext时,需要保存和恢复这两个字段,即gs:[8], gs:[16]。(win32下是fs:[4], fs:[8])
(5)gs:[0]指向用于stack unwind的异常结构,但好象在windows x64中没有用到。
3、异常处理
在win32下,要进行异常处理,需要切换coroutine时,存储与恢复fs:[0]。否则
try{
...
swapcontext(...)
...
}catch(...){}
就会出问题。
ps:win32汇编如果需要访问gs或fs,需要添加行:assume gs:flat,fs:flat
4、调用socket函数创建socket时,出现0x0000005
coroutine函数中调用socket函数创建socket,在makecontext后,用swapcontext跳转到该函数执行,每次执行到socket创建时,出现未处理的异常,报读取位置0x0000000000000000时发生访问冲突。
中断后,发现异常出现在mswsock.dll的SockGetTdiName中的汇编代码movaps xmm0,xmmword ptr [rsp+0C0h] ,查看了rsp + 0xC0h的值,不能被16整除,而movaps指令要求:
When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) is generated.
原因是,在makecontext中设置栈的内容时,需要考虑跳转地址的位置,要16位对齐。
5、_RTC_CheckStackVars
在makecontext,setcontext,swapcontext上实现了coroutine机制,模拟用户级线程,在调试模式下,触发了一个断点,位于_RTC_CheckStackVars。
因为在实现的coroutine中,每个coroutine使用同一块1M空间作为stack,在coroutine被换出时,在栈上放一个临时变量char dummy(会处于栈的最低地址),然后将该变量到栈底的内容保存下来。当一个coroutine被执行时,再将上次保存的内容再复制到stack。这样就不需要为每个coroutine都分配一个1M空间的独立stack空间,而只保存实际使用的栈内容。
但在windows下,函数会调用_RTC_CheckStackVars检查是否出现栈溢出,编译器生成代码时,临时变量dummy的位置并不是已使用的栈空间的最低地址,上面还有一段0xcccccccc,用于检查。所以,需要在dummy位置再减去0x40,将该地址到栈底的内容保存起来。
5、stl的map的iterator析构问题
实际上,coroutine使用相同的1M空间作为stack,在切换时进行stack的保存与恢复的方式,遇到了很多之前未想到的问题。
windows的visual studio 2010 win64 debug模式下(linux下没有问题,可能是stl实现不同),
在coroutine中使用了map的iterator, 该iterator保存在栈中,然后切换到其它coroutine,再切回来时,该iterator析构时_Orphan_me()出现异常,访问了不正确的地址。
void _Orphan_me()
{ // cut ties with parent
#if _ITERATOR_DEBUG_LEVEL == 2
if (_Myproxy != 0)
{ // adopted, remove self from list
_Iterator_base12 **_Pnext = &_Myproxy->_Myfirstiter;
while (*_Pnext != 0 && *_Pnext != this)
_Pnext = &(*_Pnext)->_Mynextiter;
if (*_Pnext == 0)
_DEBUG_ERROR("ITERATOR LIST CORRUPTED!");
*_Pnext = _Mynextiter;
_Myproxy = 0;
}
#endif /* _ITERATOR_DEBUG_LEVEL == 2 */
}
该问题只在debug模式下出现, release下不出现。 如果将该iterator在coroutine切换前析构,也没有问题。
可能是因为在_ITERATOR_DEBUG_LEVEL == 2时,容器中会保存iterator列表,指向在stack中创建的iterator,而在coroutine切换时,内存倒换,另一个coroutine在相同位置又建了一个iterator,然后容器认为这两个是同一个iterator,然后搞乱掉。
所以考虑还是将stack分开,每个coroutine使用独立的栈空间。
6、函数中变量优化的问题
同一函数中,swapcontext的前面与后面都代码,这些代码访问相同的变量名。可能出现在DEBUG下运行正常的代码,在RELEASE下由于优化,发生彻底的错误(或是Linux下加-O2优化等)。例如使用两个线程执行coroutine,每个线程包含一个线程局部存储变量,用于存放当前线程正在执行的coroutine指针。coroutine函数类似:
_declspec (thread) coroutine* tls_current= 0;
void fun()
{
//section1
tls_current.status = ....
//section 2
swapcontext(...)
//section 3
tls_current.status = ....
}
当section1在线程一上执行, 然后由于调度,section3在线程2上执行。section1中tls_current访问的是线程一的局部存储,然而section3可能仍然访问的是线程一的局部存储!!!而不是线程二的!!! 这是因为在代码优化时,在section1已经将tls_current的地址放入寄存器,section3直接访问了寄存器,而不会再重新获取。
所以swapcontext前后使用的变量名的意义不能出现变化(实际的位置已经不同)。
7、浮点数问题
在底层用多个线程调度所有的coroutines时(coroutine在ready状态时,可能被任一个线程选取,并运行),在DEBUG模式下正常,RELEASE模式下,下面代码打出 :elapsed is: -1.#IND00, slept is 2000。
get_time (t1);
my_sleep (2000); //context switch
get_time (t2);
printf ("elapsed is: %f, slept is 2000\n",
((t2.tv_sec * (double)1000.0) + t2.tv_usec / (double)1000.0) -
((t1.tv_sec * (double)1000.0) + t1.tv_usec / (double)1000.0));
可能是实现的swapcontext没有对浮点状态寄存器没有进行备份恢复。加上了fnstenv/ fldenv 调用,保存和恢复X87的FPU环境(占用28字节空间)。加上stmxcsr/ldmxcsr保存和恢复MXCSR控制和状态寄存器(占用4字节空间)。
xmm0-xmm3用于 传递浮点参数,查看了一下汇编代码,应该不需要保存恢复,即使有release模式下,每次使用浮点时,都会重新装载到xmm寄存器。
8、实现协程可能用到的宏与函数
内存屏障与与让出CPU:
Windows Linux
void MemoryBarrier(void) __asm__ __volatile__ ("" : : : "memory");
void YieldProcessor(void) sched_yield()