저번 글에서는 세마포어를 이용하여 Condition Variable을 구현해보았습니다.

이번에는 커널 모드 이벤트를 이용하여 Condition Variable을 구현해보도록 하겠습니다.

기존 세마포어를 이용한 구현이 있음에도 불구하고 이벤트로 또다시 구현한 이유는 세마포어를 이용한 것 보다 이벤트를 이용하는 것이 성능이 좀더 좋기 때문입니다. 또한 세마포어를 이용한 것은 객체의 최대 갯수를 정해야 하지만 이벤트를 이용한 것은 객체의 최대 갯수를 정하지 않아도 됩니다.

구현 원리는 Notification Event를 이용하는 것입니다. 이벤트는 2가지가 있는데 Synchronization Evnet와 Notification Event가 있습니다. 유저 모드에서는 Synchronization Event를 Auto-reset Evnet(자동 리셋 이벤트), Notification Evnet를 Manual-reset Event(수동 리셋 이벤트)라고 부릅니다.

즉 Synchronization Event는 대기 상태에서 해제 되는 순간 자동으로 Reset 됩니다. 하지만 Notification Event는 대기 상태가 해제되더라도 계속 해제 상태로 머물러 있게 됩니다. 그래서 이 Notification Event의 특성을 이용하여 Condition Variable의 대기하고 있는 모든 객체를 깨우는 WakeAll 기능을 구현하는 것입니다. 이벤트를 Set 해주고 나서 대기하고 있는 모든 객체가 해제되고 마지막에 이벤트를 Reset 해주는 것입니다.

#define CONDITION_VARIABLE_LOCKMODE_SHARED 0x1

typedef struct _KCONDITION_VARIABLE {
    KEVENT Event;
    BOOLEAN Wake;
    BOOLEAN WakeAll;
    ULONG Waiters;
} KCONDITION_VARIABLE, *PKCONDITION_VARIABLE;

VOID
KeInitializeConditionVariable (
    __out PKCONDITION_VARIABLE ConditionVariable
    )
{
    KeInitializeEvent(&ConditionVariable->Event, NotificationEvent, FALSE);
    ConditionVariable->WakeAll = FALSE;
    ConditionVariable->Wake = FALSE;
    ConditionVariable->Waiters = 0;
}

KCONDITION_VARIABLE 구조체와 초기화 함수 입니다. 구조체는 이벤트와 대기하고 있는 객체를 하나만 깨울 것인지 모두 깨울 것인지 결정하는 플래그와 대기하고 있는 객체의 수를 저장하는 변수로 구성되어 있습니다. 앞서 설명한 것과 같이 이벤트는 Notification Evnet로 설정합니다.

NTSTATUS
KeSleepConditionVariableMX (
    __inout PKCONDITION_VARIABLE ConditionVariable,
    __inout PKMUTEX Mutex,
    __in_opt PLARGE_INTEGER Timeout
    )
{
    NTSTATUS status;
    KIRQL irql = KeGetCurrentIrql();
    ULONG order;

    // 순서 저장
    order = ConditionVariable->Waiters;

    while (1)
    {
        InterlockedIncrement(&ConditionVariable->Waiters);

        KeReleaseMutex(Mutex, TRUE);
        status = KeWaitForSingleObject(&ConditionVariable->Event, Executive, KernelMode,
            FALSE, Timeout);

        KeWaitForMutexObject(Mutex, Executive, KernelMode, FALSE, NULL);

        InterlockedDecrement(&ConditionVariable->Waiters);

        if (status == STATUS_TIMEOUT)
        {
            return status;
        }

        if (ConditionVariable->WakeAll)
        {
            // 대기하고 있는 객체가 더 이상 없을 경우 이벤트를 리셋한다.
            if (ConditionVariable->Waiters == 0)
            {
                ConditionVariable->WakeAll = FALSE;
                KeResetEvent(&ConditionVariable->Event);
            }

            // 아직 대기하고 있는 객체가 있을 경우 이벤트를 리셋하지 않고
            // 빠져나간다.
            break;
        }
        else if (ConditionVariable->Wake)
        {
            // 순서가 가장 빠른 객체만 깨운다.
            if (order == 0)
            {
                ConditionVariable->Wake = FALSE;
                KeResetEvent(&ConditionVariable->Event);

                break;
            }
        }
        // 순서가 0이 아닌 경우 루프를 빠져나가지 않고 자신의 순서를 올린다.
        order--;
    }

    return status;
}

뮤텍스와 함께 사용하는 Condition Variable 대기 함수입니다. 세마포를 이용한 구현 보다 조금 복잡합니다. 하지만 원리는 간단합니다.

먼저 대기하고 있는 객체를 하나만 깨우기 위해서 현재 대기하고 있는 객체의 순서를 저장합니다. Notification Event를 이용했기 때문에 대기하고 있는 모든 이벤트가 해제되어 루프를 돌 것입니다. 여기서 순서가 0이라면 이벤트를 Reset 하고 루프를 빠져나갑니다. 0이 아니라면 자신의 순서를 올리고 계속 대기합니다.

모든 객체를 깨울 때에는 그냥 루프를 빠져나가면 됩니다. 그리고 맨 마지막에 해제되는 객체 쪽에서 이벤트를 Reset 해줍니다.

NTSTATUS
KeSleepConditionVariableRS (
    __inout PKCONDITION_VARIABLE ConditionVariable,
    __inout PERESOURCE Resource,
    __in_opt PLARGE_INTEGER Timeout,
    __in ULONG Flags
    )
{
    NTSTATUS status;
    KIRQL irql = KeGetCurrentIrql();
    ULONG order;

    // 순서 저장
    order = ConditionVariable->Waiters;

    while (1)
    {
        InterlockedIncrement(&ConditionVariable->Waiters);

        ExReleaseResourceLite(Resource);
        status = KeWaitForSingleObject(&ConditionVariable->Event, Executive, KernelMode,
            FALSE, Timeout);

        if (Flags == CONDITION_VARIABLE_LOCKMODE_SHARED)
            ExAcquireResourceSharedLite(Resource, TRUE);
        else
            ExAcquireResourceExclusiveLite(Resource, TRUE);

        InterlockedDecrement(&ConditionVariable->Waiters);

        if (status == STATUS_TIMEOUT)
        {
            return status;
        }

        if (ConditionVariable->WakeAll)
        {
            // 대기하고 있는 객체가 더 이상 없을 경우 이벤트를 리셋한다.
            if (ConditionVariable->Waiters == 0)
            {
                ConditionVariable->WakeAll = FALSE;
                KeResetEvent(&ConditionVariable->Event);
            }

            // 아직 대기하고 있는 객체가 있을 경우 이벤트를 리셋하지 않고
            // 빠져나간다.
            break;
        }
        else if (ConditionVariable->Wake)
        {
            // 순서가 가장 빠른 쓰레드만 깨운다.
            if (order == 0)
            {
                ConditionVariable->Wake = FALSE;
                KeResetEvent(&ConditionVariable->Event);

                break;
            }
        }
        // 순서가 0이 아닌 경우 루프를 빠져나가지 않고 자신의 순서를 올린다.
        order--;
    }

    return status;
}

이번에는 리소스와 함께 사용하는 Condition Variable 대기 함수입니다. 리소스를 Shared로 사용할 때에는 Flags에 CONDITION_VARIABLE_LOCKMODE_SHARED를 지정해줍니다

VOID
KeWakeConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    if (ConditionVariable->Waiters > 0)
    {
        ConditionVariable->Wake = TRUE;
        KeSetEvent(&ConditionVariable->Event, IO_NO_INCREMENT, FALSE);
    }
}

대기하고 있는 Condition Variable 객체 하나를 깨우는 함수입니다. Wake를 TRUE로 설정해주고 이벤트를 Set 시킵니다.

VOID
KeWakeAllConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    if (ConditionVariable->Waiters > 0)
    {
        ConditionVariable->WakeAll = TRUE;
        KeSetEvent(&ConditionVariable->Event, IO_NO_INCREMENT, FALSE);
    }
}

대기하고 있는 모든 Condition Variable 객체를 깨우는 함수입니다. WakAll을 TRUE로 설정해주고 이벤트를 Set 시킵니다.

condvar_event.zip : Condition Variable 구현과 사용 예제입니다.

예제에는 3개의 쓰레드를 생성하는데, 2번과 3번 쓰레드는 Condition Variable 객체를 대기하고 있고 1번 쓰레드가 KeWakeAllConditionVariable 함수를 이용하여 2, 3번 쓰레드를 깨우도록 되어 있습니다.

관련글


저작권 안내

이 웹사이트에 게시된 모든 글의 무단 복제 및 도용을 금지합니다.
  • 블로그, 게시판 등에 퍼가는 것을 금지합니다.
  • 비공개 포스트에 퍼가는 것을 금지합니다.
  • 글 내용, 그림을 발췌 및 요약하는 것을 금지합니다.
  • 링크 및 SNS 공유는 허용합니다.

Published

27 June 2009