/*
* Process Hacker -
* misc. synchronization utilities
*
* Copyright (C) 2010-2015 wj32
*
* This file is part of Process Hacker.
*
* Process Hacker is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Process Hacker is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Process Hacker. If not, see .
*/
/*
* This file contains code for several synchronization objects.
*
* Event. This is a lightweight notification event object that does not create a kernel event object
* until needed. Additionally the kernel event object is automatically freed when no longer needed.
* Note that PhfResetEvent is NOT thread-safe.
*
* Barrier. This is a non-traditional implementation of a barrier, built on the wake event object. I
* have identified three types of participants in this process:
* 1. The slaves, who wait for the master to release them. This is the main mechanism through which
* the threads are synchronized.
* 2. The master, who is the last thread to wait on the barrier. This thread triggers the waking
* process, and waits until all slaves have woken.
* 3. The observers, who are simply threads which were slaves before, were woken, and have tried to
* wait on the barrier again before all other slaves have woken.
*
* Rundown protection. This object allows a thread to wait until all other threads have finished
* using a particular resource before freeing the resource.
*
* Init-once. This is a lightweight one-time initialization mechanism which uses the event object
* for any required blocking. The overhead is very small - only a single inlined comparison.
*/
#include
/**
* Initializes an event object.
*
* \param Event A pointer to an event object.
*/
VOID FASTCALL PhfInitializeEvent(
_Out_ PPH_EVENT Event
)
{
Event->Value = PH_EVENT_REFCOUNT_INC;
Event->EventHandle = NULL;
}
/**
* Dereferences the event object used by an event.
*
* \param Event A pointer to an event object.
* \param EventHandle The current value of the event object.
*/
FORCEINLINE VOID PhpDereferenceEvent(
_Inout_ PPH_EVENT Event,
_In_opt_ HANDLE EventHandle
)
{
ULONG_PTR value;
value = _InterlockedExchangeAddPointer((PLONG_PTR)&Event->Value, -PH_EVENT_REFCOUNT_INC);
// See if the reference count has become 0.
if (((value >> PH_EVENT_REFCOUNT_SHIFT) & PH_EVENT_REFCOUNT_MASK) - 1 == 0)
{
if (EventHandle)
{
NtClose(EventHandle);
Event->EventHandle = NULL;
}
}
}
/**
* References the event object used by an event.
*
* \param Event A pointer to an event object.
*/
FORCEINLINE VOID PhpReferenceEvent(
_Inout_ PPH_EVENT Event
)
{
_InterlockedExchangeAddPointer((PLONG_PTR)&Event->Value, PH_EVENT_REFCOUNT_INC);
}
/**
* Sets an event object. Any threads waiting on the event will be released.
*
* \param Event A pointer to an event object.
*/
VOID FASTCALL PhfSetEvent(
_Inout_ PPH_EVENT Event
)
{
HANDLE eventHandle;
// Only proceed if the event isn't set already.
if (!_InterlockedBitTestAndSetPointer((PLONG_PTR)&Event->Value, PH_EVENT_SET_SHIFT))
{
eventHandle = Event->EventHandle;
if (eventHandle)
{
NtSetEvent(eventHandle, NULL);
}
PhpDereferenceEvent(Event, eventHandle);
}
}
/**
* Waits for an event object to be set.
*
* \param Event A pointer to an event object.
* \param Timeout The timeout value.
*
* \return TRUE if the event object was set before the timeout period expired, otherwise FALSE.
*
* \remarks To test the event, use PhTestEvent() instead of using a timeout of zero.
*/
BOOLEAN FASTCALL PhfWaitForEvent(
_Inout_ PPH_EVENT Event,
_In_opt_ PLARGE_INTEGER Timeout
)
{
BOOLEAN result;
ULONG_PTR value;
HANDLE eventHandle;
value = Event->Value;
// Shortcut: if the event is set, return immediately.
if (value & PH_EVENT_SET)
return TRUE;
// Shortcut: if the timeout is 0, return immediately if the event isn't set.
if (Timeout && Timeout->QuadPart == 0)
return FALSE;
// Prevent the event from being invalidated.
PhpReferenceEvent(Event);
eventHandle = Event->EventHandle;
if (!eventHandle)
{
NtCreateEvent(&eventHandle, EVENT_ALL_ACCESS, NULL, NotificationEvent, FALSE);
assert(eventHandle);
// Try to set the event handle to our event.
if (_InterlockedCompareExchangePointer(
&Event->EventHandle,
eventHandle,
NULL
) != NULL)
{
// Someone else set the event before we did.
NtClose(eventHandle);
eventHandle = Event->EventHandle;
}
}
// Essential: check the event one last time to see if it is set.
if (!(Event->Value & PH_EVENT_SET))
{
result = NtWaitForSingleObject(eventHandle, FALSE, Timeout) == STATUS_WAIT_0;
}
else
{
result = TRUE;
}
PhpDereferenceEvent(Event, eventHandle);
return result;
}
/**
* Resets an event's state.
*
* \param Event A pointer to an event object.
*
* \remarks This function is not thread-safe. Make sure no other threads are using the event when
* you call this function.
*/
VOID FASTCALL PhfResetEvent(
_Inout_ PPH_EVENT Event
)
{
assert(!Event->EventHandle);
if (PhTestEvent(Event))
Event->Value = PH_EVENT_REFCOUNT_INC;
}
VOID FASTCALL PhfInitializeBarrier(
_Out_ PPH_BARRIER Barrier,
_In_ ULONG_PTR Target
)
{
Barrier->Value = Target << PH_BARRIER_TARGET_SHIFT;
PhInitializeWakeEvent(&Barrier->WakeEvent);
}
FORCEINLINE VOID PhpBlockOnBarrier(
_Inout_ PPH_BARRIER Barrier,
_In_ ULONG Role,
_In_ BOOLEAN Spin
)
{
PH_QUEUED_WAIT_BLOCK waitBlock;
ULONG_PTR cancel;
PhQueueWakeEvent(&Barrier->WakeEvent, &waitBlock);
cancel = 0;
switch (Role)
{
case PH_BARRIER_MASTER:
cancel = ((Barrier->Value >> PH_BARRIER_COUNT_SHIFT) & PH_BARRIER_COUNT_MASK) == 1;
break;
case PH_BARRIER_SLAVE:
cancel = Barrier->Value & PH_BARRIER_WAKING;
break;
case PH_BARRIER_OBSERVER:
cancel = !(Barrier->Value & PH_BARRIER_WAKING);
break;
default:
ASSUME_NO_DEFAULT;
}
if (cancel)
{
PhSetWakeEvent(&Barrier->WakeEvent, &waitBlock);
return;
}
PhWaitForWakeEvent(&Barrier->WakeEvent, &waitBlock, Spin, NULL);
}
/**
* Waits until all threads are blocking on the barrier, and resets the state of the barrier.
*
* \param Barrier A barrier.
* \param Spin TRUE to spin on the barrier before blocking, FALSE to block immediately.
*
* \return TRUE for an unspecified thread after each phase, and FALSE for all other threads.
*
* \remarks By checking the return value of the function, in each phase an action can be performed
* exactly once. This could, for example, involve merging the results of calculations.
*/
BOOLEAN FASTCALL PhfWaitForBarrier(
_Inout_ PPH_BARRIER Barrier,
_In_ BOOLEAN Spin
)
{
ULONG_PTR value;
ULONG_PTR newValue;
ULONG_PTR count;
ULONG_PTR target;
value = Barrier->Value;
while (TRUE)
{
if (!(value & PH_BARRIER_WAKING))
{
count = (value >> PH_BARRIER_COUNT_SHIFT) & PH_BARRIER_COUNT_MASK;
target = (value >> PH_BARRIER_TARGET_SHIFT) & PH_BARRIER_TARGET_MASK;
assert(count != target);
count++;
if (count != target)
newValue = value + PH_BARRIER_COUNT_INC;
else
newValue = value + PH_BARRIER_COUNT_INC + PH_BARRIER_WAKING;
if ((newValue = (ULONG_PTR)_InterlockedCompareExchangePointer(
(PVOID *)&Barrier->Value,
(PVOID)newValue,
(PVOID)value
)) == value)
{
if (count != target)
{
// Wait for the master signal (the last thread to reach the barrier).
// Once we get it, decrement the count to allow the master to continue.
do
{
PhpBlockOnBarrier(Barrier, PH_BARRIER_SLAVE, Spin);
} while (!(Barrier->Value & PH_BARRIER_WAKING));
value = _InterlockedExchangeAddPointer((PLONG_PTR)&Barrier->Value, -PH_BARRIER_COUNT_INC);
if (((value >> PH_BARRIER_COUNT_SHIFT) & PH_BARRIER_COUNT_MASK) - 1 == 1)
{
PhSetWakeEvent(&Barrier->WakeEvent, NULL); // for the master
}
return FALSE;
}
else
{
// We're the last one to reach the barrier, so we become the master.
// Wake the slaves and wait for them to decrease the count to 1. This is so that
// we know the slaves have woken and we don't clear the waking bit before they
// wake.
PhSetWakeEvent(&Barrier->WakeEvent, NULL); // for slaves
do
{
PhpBlockOnBarrier(Barrier, PH_BARRIER_MASTER, Spin);
} while (((Barrier->Value >> PH_BARRIER_COUNT_SHIFT) & PH_BARRIER_COUNT_MASK) != 1);
_InterlockedExchangeAddPointer((PLONG_PTR)&Barrier->Value, -(PH_BARRIER_WAKING + PH_BARRIER_COUNT_INC));
PhSetWakeEvent(&Barrier->WakeEvent, NULL); // for observers
return TRUE;
}
}
}
else
{
// We're too early; other threads are still waking. Wait for them to finish.
PhpBlockOnBarrier(Barrier, PH_BARRIER_OBSERVER, Spin);
newValue = Barrier->Value;
}
value = newValue;
}
}
VOID FASTCALL PhfInitializeRundownProtection(
_Out_ PPH_RUNDOWN_PROTECT Protection
)
{
Protection->Value = 0;
}
BOOLEAN FASTCALL PhfAcquireRundownProtection(
_Inout_ PPH_RUNDOWN_PROTECT Protection
)
{
ULONG_PTR value;
// Increment the reference count only if rundown has not started.
while (TRUE)
{
value = Protection->Value;
if (value & PH_RUNDOWN_ACTIVE)
return FALSE;
if ((ULONG_PTR)_InterlockedCompareExchangePointer(
(PVOID *)&Protection->Value,
(PVOID)(value + PH_RUNDOWN_REF_INC),
(PVOID)value
) == value)
return TRUE;
}
}
VOID FASTCALL PhfReleaseRundownProtection(
_Inout_ PPH_RUNDOWN_PROTECT Protection
)
{
ULONG_PTR value;
while (TRUE)
{
value = Protection->Value;
if (value & PH_RUNDOWN_ACTIVE)
{
PPH_RUNDOWN_WAIT_BLOCK waitBlock;
// Since rundown is active, the reference count has been moved to the waiter's wait
// block. If we are the last user, we must wake up the waiter.
waitBlock = (PPH_RUNDOWN_WAIT_BLOCK)(value & ~PH_RUNDOWN_ACTIVE);
if (_InterlockedDecrementPointer(&waitBlock->Count) == 0)
{
PhSetEvent(&waitBlock->WakeEvent);
}
break;
}
else
{
// Decrement the reference count normally.
if ((ULONG_PTR)_InterlockedCompareExchangePointer(
(PVOID *)&Protection->Value,
(PVOID)(value - PH_RUNDOWN_REF_INC),
(PVOID)value
) == value)
break;
}
}
}
VOID FASTCALL PhfWaitForRundownProtection(
_Inout_ PPH_RUNDOWN_PROTECT Protection
)
{
ULONG_PTR value;
ULONG_PTR count;
PH_RUNDOWN_WAIT_BLOCK waitBlock;
BOOLEAN waitBlockInitialized;
// Fast path. If the reference count is 0 or rundown has already been completed, return.
value = (ULONG_PTR)_InterlockedCompareExchangePointer(
(PVOID *)&Protection->Value,
(PVOID)PH_RUNDOWN_ACTIVE,
(PVOID)0
);
if (value == 0 || value == PH_RUNDOWN_ACTIVE)
return;
waitBlockInitialized = FALSE;
while (TRUE)
{
value = Protection->Value;
count = value >> PH_RUNDOWN_REF_SHIFT;
// Initialize the wait block if necessary.
if (count != 0 && !waitBlockInitialized)
{
PhInitializeEvent(&waitBlock.WakeEvent);
waitBlockInitialized = TRUE;
}
// Save the existing reference count.
waitBlock.Count = count;
if ((ULONG_PTR)_InterlockedCompareExchangePointer(
(PVOID *)&Protection->Value,
(PVOID)((ULONG_PTR)&waitBlock | PH_RUNDOWN_ACTIVE),
(PVOID)value
) == value)
{
if (count != 0)
PhWaitForEvent(&waitBlock.WakeEvent, NULL);
break;
}
}
}
VOID FASTCALL PhfInitializeInitOnce(
_Out_ PPH_INITONCE InitOnce
)
{
PhInitializeEvent(&InitOnce->Event);
}
BOOLEAN FASTCALL PhfBeginInitOnce(
_Inout_ PPH_INITONCE InitOnce
)
{
if (!_InterlockedBitTestAndSetPointer(&InitOnce->Event.Value, PH_INITONCE_INITIALIZING_SHIFT))
return TRUE;
PhWaitForEvent(&InitOnce->Event, NULL);
return FALSE;
}
VOID FASTCALL PhfEndInitOnce(
_Inout_ PPH_INITONCE InitOnce
)
{
PhSetEvent(&InitOnce->Event);
}