497 lines
14 KiB
C
497 lines
14 KiB
C
/*
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
* 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 <phbase.h>
|
|
|
|
/**
|
|
* 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);
|
|
}
|