5 min read

크리티컬섹션(critical section) 은 정말로 유저 영역일까?

몇몇 책들과 네이버 블로그 및 구글 블로그에서는 크리티컬 세션에 대해 설명을 유저영역이여 세마포어와 뮤덱스에 비해 빠르다구 설명에 나와있다.

하지만 반대로 생각하자면 어떻게 프로그램 레벨에서 커널 함수를 호출안하고 해당 스레드를 중지 시킬수 있을까? 라고 반문을 하면 불가능하다.

오늘은 이거에 대해 알아 볼려고 한다.

Windows의 OS는 기본적으로 소스코드는 비공개지만 이를 역 리버싱해서 구현한 OS가 존재 한다.

ReactOS이며 이를 통해 알아볼려고한다.

깃 주소는 다음과 같다.

https://github.com/reactos/reactos

이 본문의 순서는 크리티컬섹션의 초기화 ,진입 이 두개 만 살펴볼려고 한다.

크리티컬 세션은의 초기화 NT커널 함수는 RtlInitializeCriticalSection이며 Wine 에서도 공개되어있다.

https://source.winehq.org/WineAPI/RtlInitializeCriticalSection.html

이 함수를 찾아보자

코드는 다음과 같다. 하지만 코드의 양이 좀 길어 중요한 부분만 출력하였다.

NTSTATUS
NTAPI
RtlInitializeCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
{
    /* Call the Main Function */
    return RtlInitializeCriticalSectionAndSpinCount(CriticalSection, 0);
}
NTSTATUS
NTAPI
RtlInitializeCriticalSectionAndSpinCount(PRTL_CRITICAL_SECTION CriticalSection,
                                         ULONG SpinCount)
{
    PRTL_CRITICAL_SECTION_DEBUG CritcalSectionDebugData;

    /* First things first, set up the Object */
    DPRINT("Initializing Critical Section: %p\n", CriticalSection);
    CriticalSection->LockCount = -1;
    CriticalSection->RecursionCount = 0;
    CriticalSection->OwningThread = 0;
    CriticalSection->SpinCount = (NtCurrentPeb()->NumberOfProcessors > 1) ? SpinCount : 0;
    CriticalSection->LockSemaphore = 0;

    /* Allocate the Debug Data */
    CritcalSectionDebugData = RtlpAllocateDebugInfo();
    DPRINT("Allocated Debug Data: %p inside Process: %p\n",
           CritcalSectionDebugData,
           NtCurrentTeb()->ClientId.UniqueProcess);

    if (!CritcalSectionDebugData)
    {
        /* This is bad! */
        DPRINT1("Couldn't allocate Debug Data for: %p\n", CriticalSection);
        return STATUS_NO_MEMORY;
    }

    /* Set it up */
    CritcalSectionDebugData->Type = RTL_CRITSECT_TYPE;
    CritcalSectionDebugData->ContentionCount = 0;
    CritcalSectionDebugData->EntryCount = 0;
    CritcalSectionDebugData->CriticalSection = CriticalSection;
    CritcalSectionDebugData->Flags = 0;
    CriticalSection->DebugInfo = CritcalSectionDebugData;

    /*
     * Add it to the List of Critical Sections owned by the process.
     * If we've initialized the Lock, then use it. If not, then probably
     * this is the lock initialization itself, so insert it directly.
     */
    if ((CriticalSection != &RtlCriticalSectionLock) && (RtlpCritSectInitialized))
    {
       .....
        RtlEnterCriticalSection(&RtlCriticalSectionLock);

    }.......
    return STATUS_SUCCESS;
}

초기화는 위와 같다. 코드를 천천히 보면 알겠지만 멤버변수를 초기화만 해주는코드가 전부다 이다. 즉 초기화란 멤버 변수를 초기화를 해준다는 것이다.

하지만 여기서 변수를 잘보면 다음과 같은 초기화가 있다.

    CriticalSection->LockSemaphore = 0;

세마포어를 초기화하는거를 알수있고 임계영역은 결국 커널영역의 도움을 받는다는거를 알수있다.

여기서 크리티컬은 섹션은 유저영역이 아니다라는거를 알수있지만

왜 책에서는 유저영역이라구 하고 기존의 세마포어와 속도비교가 발생하다는 이유는 납들할수 있을것이다. 이는 임계영역을 선언하는 부분에서 알수있는데 크리티컬섹션의 임계영역 선언은 다음 함수를 사용한다.

RtlEnterCriticalSection

이 함수의 정의의 코드는 다음과 같다.

NTSTATUS
NTAPI
RtlEnterCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
{
    HANDLE Thread = (HANDLE)NtCurrentTeb()->ClientId.UniqueThread;

    /* Try to lock it */
    if (InterlockedIncrement(&CriticalSection->LockCount) != 0)
    {
        /* We've failed to lock it! Does this thread actually own it? */
        if (Thread == CriticalSection->OwningThread)
        {
            /*
             * You own it, so you'll get it when you're done with it! No need to
             * use the interlocked functions as only the thread who already owns
             * the lock can modify this data.
             */
            CriticalSection->RecursionCount++;
            return STATUS_SUCCESS;
        }

        /* NOTE - CriticalSection->OwningThread can be NULL here because changing
                  this information is not serialized. This happens when thread a
                  acquires the lock (LockCount == 0) and thread b tries to
                  acquire it as well (LockCount == 1) but thread a hasn't had a
                  chance to set the OwningThread! So it's not an error when
                  OwningThread is NULL here! */

        /* We don't own it, so we must wait for it */
        RtlpWaitForCriticalSection(CriticalSection);
    }

    /*
     * Lock successful. Changing this information has not to be serialized
     * because only one thread at a time can actually change it (the one who
     * acquired the lock)!
     */
    CriticalSection->OwningThread = Thread;
    CriticalSection->RecursionCount = 1;
    return STATUS_SUCCESS;
}

어려워 보이는 코드가 많아 정리하자면 아래와 같다.

NTSTATUS
NTAPI
RtlEnterCriticalSection(PRTL_CRITICAL_SECTION CriticalSection)
{
    // 아무도 사용안했으면 아래 IF문에 들어간다. 이 증가문은 Atimic을 보장한다.
    if (InterlockedIncrement(&CriticalSection->LockCount) != 0)
    {
            // 값을 증가시키고 종료시킨다.
            CriticalSection->RecursionCount++;
            return
    }
    임계영역이 중접중이면 아래 함수를 들어간다.
    else{
        RtlpWaitForCriticalSection(CriticalSection);
     }
  return 
}

임계영역을 아무도 선언 안하였으면 단순 함수처럼 사용하는거를 알수 있다.

하지만 임계영역을 두번(중첩)이나 있을 경우는 특수한 함수를 들어가는데 해당함수의 코드를 보게되면 다음과 같다.

여기서는 RtlpWaitForCriticalSection함수에 대해 정의를 간단히 만 소개한다.

RtlpWaitForCriticalSection의 함수 내용을 간단히 줄인 것이다.

 /* Do we have an Event yet? */
    if (!CriticalSection->LockSemaphore)
    {
        RtlpCreateCriticalSectionSem(CriticalSection);
    }

    for (;;)
    {
                    Status = NtWaitForSingleObject(CriticalSection->LockSemaphore,
                                           FALSE,
                                           &RtlpTimeout);
}

코드를 읽어보면 세마포어를 선언하고 세마포어가 종료될때까지 대기를 한다는거를 알수 있다. 이로써 크리티컬 섹션에 원리에 대해 말할수 있다.

아무도 임계영역에 안들어가면 일반 함수처럼 들어가지만 중첩된경우 세마포어를 사용하여 임계영역을 보장한다.