Home | Info | Research | Blog | Repos | Messages | Contact Me

 


저번 글에서는 세마포어를 이용하여 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번 쓰레드를 깨우도록 되어 있습니다.



안녕하세요. 이재홍입니다.

윈도우는 Windows Vista, Windows Server 2008 부터 유저 모드에서 사용할 수 있는 Condition Variable 함수들이 추가되었습니다. (Windows Vista에 새로 추가된 동기화 기본 형식)

세마포어를 이용하여 커널 모드에서 사용할 수 있는 Condition Variable을 구현해보도록 하겠습니다.

Condition Variable은 기존 동기화 객체와는 약간 다른 특징을 가지고 있습니다. 뮤텍스 혹은 스핀락의 경우 대기와 해제가 1:1 대응입니다. 즉 한번에 하나씩 대기하고 있는 객체를 깨울 수 있습니다. Condition Variable은 대기하고 있는 하나의 객체를 깨울 수도 있고, 대기하고 있는 여러개의 객체를 한번에 모두 깨울 수도 있습니다.

구현 원리는 세마포어의 특성을 이용하는 것입니다. 세마포어는 대기하고 있는 객체를 원하는 개수 만큼 깨울 수 있는 기능이 있습니다. 따라서 대기하는 객체의 개수를 저장하고 있고, 대기하는 객체를 한번에 깨울 때에는 저장하고 있던 객체의 개수를 이용하는 것입니다.

#define MAXIMUM_WAIT_CONDITION_VARIABLES 1024

typedef struct _KCONDITION_VARIABLE {
    KSEMAPHORE Semaphore;
    KSPIN_LOCK SpinLock;
    ULONG Waiters;
} KCONDITION_VARIABLE, *PKCONDITION_VARIABLE, *PRKCONDITION_VARIABLE;

VOID
KeInitializeConditionVariable (
    __out PKCONDITION_VARIABLE ConditionVariable
    )
{
    ConditionVariable->Waiters = 0;
    KeInitializeSpinLock(&ConditionVariable->SpinLock);
    KeInitializeSemaphore(&ConditionVariable->Semaphore, 0,
        MAXIMUM_WAIT_CONDITION_VARIABLES);
}

먼저 KCONDITION_VARIABLE 구조체와 초기화 함수입니다. 대기하고 있는 객체의 수를 저장하는 변수를 초기화 하고, 스핀락과 세마포어를 초기화 합니다. 스핀락은 Waiters 변수를 보호하기 위해 사용합니다.


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

    KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
    ConditionVariable->Waiters++;
    KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);

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

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

    return status;
}

뮤텍스와 함께 사용하는 Condition Variable 대기 함수입니다. 대기할 때 마다 Waiters 변수를 증가시킵니다. Windows Vista(Server 2008)에 있는 SleepConditionVariableCS라는 함수와 비슷한 용도입니다. 이 함수는 크리티컬 섹션을 사용하는 함수입니다.


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

    KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
    ConditionVariable->Waiters++;
    KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);

    ExReleaseResourceLite(Resource);

    status = KeWaitForSingleObject(&ConditionVariable->Semaphore, Executive,
        KernelMode, FALSE, Timeout);

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

    return status;   
}

이번에는 리소스와 함께 사용하는 Condition Variable 대기 함수입니다. Windows Vista(Server 2008)에 있는 SleepConditionVariableSRW 함수에 대응합니다. 리소스를 Shared로 사용할 때에는 Flags에 CONDITION_VARIABLE_LOCKMODE_SHARED를 지정해줍니다.


VOID
KeWakeConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    KIRQL irql = KeGetCurrentIrql();

    if (ConditionVariable->Waiters > 0)
    {
        KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
        KeReleaseSemaphore(&ConditionVariable->Semaphore, IO_NO_INCREMENT, 1, FALSE);
        ConditionVariable->Waiters--;
        KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);
    }
}

대기하고 있는 Condition Variable 객체 하나를 깨우는 함수입니다. 세마포어의 특성을 이용하는 것인데, KeReleaseSemaphore에 1을 지정하여 대기하고 있는 세마포어 객체 하나를 깨웁니다. 그리고 대기하고 있는 객체의 개수를 감소 시킵니다.


VOID
KeWakeAllConditionVariable (
    __inout PKCONDITION_VARIABLE ConditionVariable
    )
{
    KIRQL irql = KeGetCurrentIrql();

    if (ConditionVariable->Waiters > 0)
    {
        KeAcquireSpinLock(&ConditionVariable->SpinLock, &irql);
        KeReleaseSemaphore(&ConditionVariable->Semaphore, IO_NO_INCREMENT,
            ConditionVariable->Waiters, FALSE);
        ConditionVariable->Waiters = 0;
        KeReleaseSpinLock(&ConditionVariable->SpinLock, irql);
    }
}

대기하고 있는 모든 Condition Variable 객체를 깨우는 함수입니다. KeReleaseSemaphore에는 대기하고 있는 객체의 개수를 지정합니다. 그래서 대기하고 있는 개수 만큼 세마포어 객체를 깨웁니다. 끝으로 대기하고 있는 객체의 개수를 0으로 초기화 합니다.

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

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