[C/C++]windows下实现swapcontext等函数实践

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()

 

转载于:https://my.oschina.net/u/136074/blog/704475

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值