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번 쓰레드를 깨우도록 되어 있습니다.



Critical Region은 공유 자원을 보호하는 가장 기본적인 동기화 요소입니다.

KeEnterCriticalRegion 함수로 Critical Region에 진입하며 KTHREAD의 KernelApcDisable에서 1을 뺍니다. 반대로 KeLeaveCriticalRegion 함수로 Critical Region에서 벗어나며 KernelApcDisable에 1을 더합니다. KernelApcDisable의 초기값은 0 입니다. 이렇게 해서 노멀 APC를 차단합니다. 하지만 스페셜 APC는 차단하지 않습니다.

Critical Region으로 진입하더라도 IRQL은 변화시키지 않고 계속 PASSIVE_LEVEL에서 실행됩니다. 커널은 KTHREAD의 KernelApcDisable을 확인하여 이 값이 -1이라면 노멀 APC를 실행하지 않도록 합니다. 하지만 스페셜 APC는 APC_LEVEL에서 실행되기 때문에 PASSIVE_LEVEL에서 실행되고 있는 Critical Region은 스페셜 APC에게 실행권을 빼앗길 수 있습니다. (선점 당할 할 수 있습니다.)
 
APC_LEVEL에서 Critical Region으로 진입했다면 계속 APC_LEVEL에서 실행되며 PASSIVE_LEVEL에서 실행되는 노멀 APC는 KernelApcDisable의 여부와 상관 없이 실행이 차단 됩니다. 그리고 같은 IRQL에서 실행중인 스페셜 APC도 차단됩니다.

윈도우 2003에서는 새로운 함수가 추가 되었는데, 바로 Guarded Region입니다.

이 Guarded Region은 노멀 APC와 스페셜 APC를 모두 차단합니다. KeEnterGuardedRegion 함수로 Guarded Region에 진입하며 KTHREAD의 SpecialApcDisable에 1을 뺍니다. KeLeaveGuardedRegion으로 Guarded Region에서 벗어나며 SpecialApcDisable에 1을 더합니다. 커널은 SpecialApcDisable의 상태를 보고 노멀 APC, 스페셜 APC 모두 차단합니다.

앞의 Critical Region은 PASSIVE_LEVEL에서는 노멀 APC만 차단되고 APC_LEVEL에 와서야 노멀, 스페셜 APC 모두가 차단됩니다. 하지만 Guarded Region은 IRQL을 변화시키지 않고 스페셜 APC를 차단한다는 것입니다. 즉 PASSIVE_LEVEL에서도 노멀, 스페셜 APC가 모두 차단됩니다.

이것은 윈도우 2003 커널이 그렇게 바뀌었기 때문이고 2003 이전 운영체제인 2000, XP에서는 모든 APC를 차단하려면 IRQL을 APC_LEVEL로 높이는 수 밖에 없습니다.

윈도우 2003에 새롭게 추가된 Guarded Mutex는 내부적으로 이 Guarded Region을 이용합니다.




윈도우 2003에는 Fast Mutex보다 성능이 더 좋아진 Guarded Mutex가 추가되었습니다. Fast Mutex와 Guarded Mutex의 기능은 똑같습니다.

Fast Mutex는 IRQL을 APC_LEVEL로 올려주어 노멀 APC, 스페셜 APC를 차단합니다.

IRQL이 APC_LEVEL로 높아지면 PASSIVE_LEVEL에서 실행되고 있는 노멀 APC는 IRQL이 높아졌기 때문에 제어권을 빼앗을 수 없습니다. 그리고 APC_LEVEL에서 실행되고 있는 스페셜 APC도 IRQL이 같기 때문에 제어권을 빼앗을 수 없습니다.

하지만 Guarded Mutex는 IRQL을 APC_LEVEL로 높이지 않고 노멀 APC, 스페셜 APC를 차단합니다.

GuardMutex는 IRQL을 높이는 대신 KeEnterGuardedRegion()를 호출해서 KTHREAD의 SpecialApcDisable에서 1을 뺍니다.

이렇게 해서 운영체제는 쓰레드의 SpecialApcDisable를 체크하여 노멀, 스페셜 APC를 실행 할 수 있는 지 판단합니다.

윈도우 2003 이전 운영체제에서는 KTHREAD에 ULONG 타입의 KernelApcDisable 하나 밖에 없지만, 윈도우 2003의 KTHREAD는 SHORT 타입으로 KernelApcDisable가 있고 바로 뒤에 SHORT 타입의 SpecialApcDisable이 있습니다. 그리고 ULONG 타입의 CombinedApcDisable 하나로 KernelApcDisable, SpecialApcDisable 두개의 값을 동시에 참조 할 수 있게 되어 있습니다.
union
{
    struct
    {
        SHORT KernelApcDisable;
        SHORT SpecialApcDisable;
    };
    ULONG CombinedApcDisable;
};

KernelApcDisable는 노멀 APC이고, SpecialApcDisable는 스페셜 APC입니다.

스페셜 APC를 차단하면 당연히 노멀 APC도 차단됩니다.

뮤텍스 소유 및 해제가 매우 빈번히 일어날 경우 Guarded Mutex는 Fast Mutex 처럼 IRQL을 변화시키지 않기 때문에 성능이 더 좋은 것입니다.