본문 바로가기

프로그래밍/API

Windows에서 Coroutine 구현에 필요한 함수


Windows에는

LPVOID WINAPI CreateFiber(
  __in      SIZE_T dwStackSize,
  __in      LPFIBER_START_ROUTINE lpStartAddress,
  __in_opt  LPVOID lpParameter
);

계열의 함수가 존재한다.

여기서는 다른 방법으로 Coroutine을 구현하는 방법을 설명하도록 하겠다.

Windows에는 프로그램의 Context를 제어하기 위한 함수가 있다.

BOOL GetThreadContext(
  HANDLE hThread,  LPCONTEXT lpContext);



BOOL SetThreadContext(
  HANDLE hThread,  const CONTEXT* lpContext);


이 존재한다. 함수명만 봐서는 쓰레드에서 사용될것 같은 함수라고 다들 짐작을 하겠지만 실제로는 더 많은
처리를 할수 있다. == 여기에서 CONTEXT는 프로그램의 실행정보가 포함되어 있다(즉, REG값을 보관한다)

해당 함수를 통해 구현에 필요한 기본적인 처리를 해보자.

1. 현재 구동되고 있는 위치(쓰레드)의 CONTEXT를 저장할 필요가 있다.
 

ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext( GetCurrentThread(), &ctx);


   이렇게 하여, 현재 프로세서(쓰레드) 위치의 REG정보를 저장해 둔다.

2. 구동에 필요한 CONTEXT를 생성한다.

#define PAGE_SIZE   16384
#define PAGE_ALIGN(x)  (((x) + (PAGE_SIZE - 1)) & ~(PAGE_SIZE - 1))

size_t mstack = PAGE_ALIGN(STACK_SIZE + 1) + sizeof(CONTEXT);
if (!(stack = (void *)GlobalAlloc( GPTR, mstack))) return NULL;

// STACK설정이 가능하도록 메모리 정렬을 한다.
u = (CONTEXT *)((DWORD_PTR)
        ((struct CONTEXT *)(((char *)stack) + mstack)) & ~(sizeof(DWORD_PTR) - 1));
(--u)->ContextFlags = CONTEXT_FULL;
if (GetThreadContext( GetCurrentThread(), u) == FALSE)
   ;
else {
    stack = ((char *)(u - 1)) - sizeof(__int64); {
     u->Eip = (DWORD)((DWORD_PTR)CALL_FUNC); // CALL_FUNC( void *PARAM)
     u->Esp = (DWORD)((DWORD_PTR)(((char *)stack) - sizeof(DWORD)));
     u->ContextFlags = CONTEXT_FULL;
     *((__int64 *)stack) = (__int64)PARAM;
     return u;
    }
} return NULL;

   여기에서 중요한 것은, STACK의 위치이다. 소스코드를 보면 알겠지만 스택의 위치를 메모리 할당 시작
   위치로 하지 않고, 마지막 위치로 설정하는 것을 알수 있을 것이다.

   이는 스텍의 구조를 생각해 보면 왜 그렇게 하는지 쉽게 이해할수 있다.

   참고로, x86_64에서는 EIP나 ESP대신 다른 REG에 메모리를 설정해야 할 필요가 있다.
   64bit로 아직 서버를 만들지 않아 해당 루틴을 수정하지는 않았다.

3. 이렇게 만들어진 CONTEXT간의 전환을 구현해 보자

 SELF_CTX.ContextFlags = CONTEXT_FULL;
 if (!GetThreadContext( GetCurrentThread(), SELF_CTX) ||
       !SetThreadContext( GetCurrentThread(), TARGET_CTX))
   FATAL( "context switch failed");

이러한 방식으로, 하여 Fiber와 같은 역활을 담당하는 Coroutine을 만들어 낼수 있다.

위에 설명한 부분은 필자가 설명을 하기 위해 실제 소스에서 일부 루틴을 가지고 설명한 것이고, 실제로 위의
방식으로 필자는 다양한 방식의 처리를 한다.

예를 들어,
   1. 일정 시간 이후 해당 함수로 진입 하도록 처리 - sleep()과 같은 처리
   2. 생성된 coroutine간의 스케쥴링 처리
      - 스케쥴링 처리에 특정 coroutine을 제외 하는 기능
   3. 쓰레드별 독립적인 coroutine 처리
   4. coroutine의 순환 반복 처리
      - 일반 함수와 같이 함수의 return을 통해 호출한 타 coroutine으로 값을 전달하는 기능
      - 설정된 coroutine을 재 호출 처리 하는 기능
   5. 물론, coroutine의 resume 및 suspend기능도 존재
   6. 현재 구동 중인 coroutine을 제거하는 기능
   7. coroutine별 독립 param 전달 처리

등, 수많은 기능을 구현하기 위해 Fiber를 사용하지 않고, 이렇게 구현을 하였으며 물론, 기존에 가지고 있던 LINUX버젼과의 API 호환성 문제를 해결하기 위해서도 필요했다.


다음에 더... 알아보도록 하겠다~