/*++ Copyright (c) 2000 Microsoft Corporation Module Name: pushlock.c Abstract: This module houses routines that do push locking. Push locks are capable of being acquired in both shared and exclusive mode. Properties include: They can not be acquired recursively. They are small (the size of a pointer) and can be used when embeded in pagable data. Acquire and release is done lock free. On lock contention the waiters are chained through the lock and local stack space. This is the structure of a push lock: E == Exclusive bit W == Waiters present SC == Share count P == Pointer to wait block +----------+---+---+ | SC | E | W | E, W are single bits W == 0 +----------+---+---+ +--------------+---+ | P | W | W == 1. Pointer is the address of a chain of stack local wait blocks +--------------+---+ The non-contented acquires and releases are trivial. Interlocked operations make the following transformations. (SC=0,E=0,W=0) === Exclusive acquire ===> (SC=0,E=1,W=0) (SC=n,E=0,W=0) === Shared acquire ===> (SC=n+1,E=0,W=0) (SC=0,E=1,W=0) === Exclusive release ===> (SC=0,E=0,W=0) (SC=n,E=0,W=0) === Shared release ===> (SC=n-1,E=0,W=0) n > 0 Contention causes the acquiring thread to produce a local stack based wait block and to enqueue it to the front of the list. (SC=n,E=e,W=0) === Exclusive acquire ===> (P=LWB(SSC=n,E=e),W=1) LWB = local wait block, SSC = Saved share count, n > 0 or e == 1. (SC=0,E=1,W=0) === Shared acquire ===> (P=LWB(SSC=0,E=0),W=1) LWB = local wait block, SSC = Saved share count. After contention has causes one or more threads to queue up wait blocks releases are more complicated. This following rights are granted to a releasing thread (shared or exclusive). 1) Shared release threads are allowed to search the wait list until they hit a wait block with a non-zero share count (this will be a wait block marked exclusive). This thread is allowed to use an interlocked operation to decrement this value. If this thread transitioned the value to zero then it obtains the rights of an exclusive release thread 2) Exclusive threads are allowed to search the wait list until they find a continuous chain of shared wait blocks or they find the last wait block is an exclusive waiter. This thread may then break the chain at this point or update the header to show a single exclusive owner or multiple shared owners. Breaking the list can be done with normal assignment but updating the header requires interlocked exchange compare. Author: Neill Clift (NeillC) 30-Sep-2000 Revision History: --*/ #include "exp.h" #pragma hdrstop #ifdef ALLOC_PRAGMA #pragma alloc_text(PAGE, ExBlockPushLock) #pragma alloc_text(PAGE, ExfAcquirePushLockExclusive) #pragma alloc_text(PAGE, ExfAcquirePushLockShared) #pragma alloc_text(PAGE, ExfReleasePushLock) #pragma alloc_text(PAGE, ExfUnblockPushLock) #pragma alloc_text(PAGE, ExAllocateCacheAwarePushLock) #pragma alloc_text(PAGE, ExFreeCacheAwarePushLock) #pragma alloc_text(PAGE, ExAcquireCacheAwarePushLockExclusive) #pragma alloc_text(PAGE, ExReleaseCacheAwarePushLockExclusive) #endif NTKERNELAPI VOID FASTCALL ExfAcquirePushLockExclusive ( IN PEX_PUSH_LOCK PushLock ) /*++ Routine Description: Acquire a push lock exclusively Arguments: PushLock - Push lock to be acquired Return Value: None --*/ { EX_PUSH_LOCK OldValue, NewValue; EX_PUSH_LOCK_WAIT_BLOCK WaitBlock; OldValue = *PushLock; while (1) { // // If the lock is already held exclusively/shared or there are waiters then // we need to wait. // if (OldValue.Value == 0) { NewValue.Value = OldValue.Value + EX_PUSH_LOCK_EXCLUSIVE; NewValue.Ptr = InterlockedCompareExchangePointer (&PushLock->Ptr, NewValue.Ptr, OldValue.Ptr); if (NewValue.Ptr == OldValue.Ptr) { break; } } else { KeInitializeEvent (&WaitBlock.WakeEvent, SynchronizationEvent, FALSE); WaitBlock.Exclusive = TRUE; // // Move the sharecount to our wait block if need be. // if (OldValue.Waiting) { WaitBlock.Next = (PEX_PUSH_LOCK_WAIT_BLOCK) (OldValue.Value - EX_PUSH_LOCK_WAITING); WaitBlock.ShareCount = 0; } else { WaitBlock.Next = NULL; WaitBlock.ShareCount = (ULONG) OldValue.Shared; } NewValue.Ptr = ((PUCHAR) &WaitBlock) + EX_PUSH_LOCK_WAITING; ASSERT ((NewValue.Value & EX_PUSH_LOCK_WAITING) != 0); NewValue.Ptr = InterlockedCompareExchangePointer (&PushLock->Ptr, NewValue.Ptr, OldValue.Ptr); if (NewValue.Ptr == OldValue.Ptr) { KeWaitForSingleObject (&WaitBlock.WakeEvent, Executive, KernelMode, FALSE, NULL); ASSERT ((WaitBlock.ShareCount == 0) && (WaitBlock.Next == NULL)); break; } } OldValue = NewValue; } } NTKERNELAPI VOID FASTCALL ExfAcquirePushLockShared ( IN PEX_PUSH_LOCK PushLock ) /*++ Routine Description: Acquire a push lock shared Arguments: PushLock - Push lock to be acquired Return Value: None --*/ { EX_PUSH_LOCK OldValue, NewValue; EX_PUSH_LOCK_WAIT_BLOCK WaitBlock; OldValue = *PushLock; while (1) { // // If the lock is already held exclusively or there are waiters then we need to wait // if (OldValue.Exclusive || OldValue.Waiting) { KeInitializeEvent (&WaitBlock.WakeEvent, SynchronizationEvent, FALSE); WaitBlock.Exclusive = 0; WaitBlock.ShareCount = 0; // // Chain the next block to us if there is one. // if (OldValue.Waiting) { WaitBlock.Next = (PEX_PUSH_LOCK_WAIT_BLOCK) (OldValue.Value - EX_PUSH_LOCK_WAITING); } else { WaitBlock.Next = NULL; } NewValue.Ptr = ((PUCHAR) &WaitBlock) + EX_PUSH_LOCK_WAITING; ASSERT ((NewValue.Value & EX_PUSH_LOCK_WAITING) != 0); NewValue.Ptr = InterlockedCompareExchangePointer (&PushLock->Ptr, NewValue.Ptr, OldValue.Ptr); if (NewValue.Ptr == OldValue.Ptr) { KeWaitForSingleObject (&WaitBlock.WakeEvent, Executive, KernelMode, FALSE, NULL); ASSERT (WaitBlock.ShareCount == 0); break; } } else { // // We only have shared accessors at the moment. We can just update the lock to include this thread. // NewValue.Value = OldValue.Value + EX_PUSH_LOCK_SHARE_INC; ASSERT (!(NewValue.Waiting || NewValue.Exclusive)); NewValue.Ptr = InterlockedCompareExchangePointer (&PushLock->Ptr, NewValue.Ptr, OldValue.Ptr); if (NewValue.Ptr == OldValue.Ptr) { break; } } OldValue = NewValue; } } NTKERNELAPI VOID FASTCALL ExfReleasePushLock ( IN PEX_PUSH_LOCK PushLock ) /*++ Routine Description: Release a push lock that was acquired exclusively or shared Arguments: PushLock - Push lock to be released Return Value: None --*/ { EX_PUSH_LOCK OldValue, NewValue; PEX_PUSH_LOCK_WAIT_BLOCK WaitBlock, NextWaitBlock, ReleaseWaitList, Previous; ULONG ShareCount; OldValue = *PushLock; while (1) { if (!OldValue.Waiting) { // // Either we hold the lock exclusive or shared but not both. // ASSERT (OldValue.Exclusive ^ (OldValue.Shared > 0)); // // We must hold the lock exclusive or shared. We make the assuption that // the exclusive bit is just below the share count here. // NewValue.Value = (OldValue.Value - EX_PUSH_LOCK_EXCLUSIVE) & ~EX_PUSH_LOCK_EXCLUSIVE; NewValue.Ptr = InterlockedCompareExchangePointer (&PushLock->Ptr, NewValue.Ptr, OldValue.Ptr); if (NewValue.Ptr == OldValue.Ptr) { break; } // // Either we gained a new waiter or another shared owner arrived or left // ASSERT (NewValue.Waiting || (NewValue.Shared > 0 && !NewValue.Exclusive)); OldValue = NewValue; } else { // // There are waiters chained to the lock. We have to release the share count, // last exclusive or last chain of shared waiters. // WaitBlock = (PEX_PUSH_LOCK_WAIT_BLOCK) (OldValue.Value - EX_PUSH_LOCK_WAITING); ReleaseWaitList = WaitBlock; Previous = NULL; ShareCount = 0; do { if (WaitBlock->Exclusive) { // // This is an exclusive waiter. If this was the first exclusive waited to a shared acquire // then it will have the saved share count. If we acquired the lock shared then the count // must contain a bias for this thread. Release that and if we are not the last shared // accessor then exit. A later shared release thread will wake the exclusive // waiter. // if (WaitBlock->ShareCount != 0) { if (InterlockedDecrement ((PLONG)&WaitBlock->ShareCount) != 0) { return; } } // // Reset count of share acquires waiting. // ShareCount = 0; } else { // // This is a shared waiter. Record the number of these to update the head or the // previous exclusive waiter. // ShareCount++; } NextWaitBlock = WaitBlock->Next; if (NextWaitBlock != NULL) { if (NextWaitBlock->Exclusive) { // // The next block is exclusive. This may be the entry to free. // Previous = WaitBlock; ReleaseWaitList = NextWaitBlock; } else { // // The next block is shared. If the chain start is exclusive then skip to this one // as the exclusive isn't the thread we will wake up. // if (ReleaseWaitList->Exclusive) { Previous = WaitBlock; ReleaseWaitList = NextWaitBlock; } } } WaitBlock = NextWaitBlock; } while (WaitBlock != NULL); // // If our release chain is everything then we have to update the header // if (Previous == NULL) { NewValue.Value = 0; NewValue.Exclusive = ReleaseWaitList->Exclusive; NewValue.Shared = ShareCount; ASSERT (((ShareCount > 0) ^ (ReleaseWaitList->Exclusive)) && !NewValue.Waiting); NewValue.Ptr = InterlockedCompareExchangePointer (&PushLock->Ptr, NewValue.Ptr, OldValue.Ptr); if (NewValue.Ptr != OldValue.Ptr) { // // We are releasing so we could have only gained another waiter // ASSERT (NewValue.Waiting); OldValue = NewValue; continue; } } else { // // Truncate the chain at this position and save the share count for all the shared owners to // decrement later. // Previous->Next = NULL; ASSERT (Previous->ShareCount == 0); Previous->ShareCount = ShareCount; // // We are either releasing multiple share accessors or a single exclusive // ASSERT ((ShareCount > 0) ^ ReleaseWaitList->Exclusive); } // // Release the chain of threads we located. // do { NextWaitBlock = ReleaseWaitList->Next; // // All the chain should have the same type (Exclusive/Shared). // ASSERT (NextWaitBlock == NULL || (ReleaseWaitList->Exclusive == NextWaitBlock->Exclusive)); ASSERT (!ReleaseWaitList->Exclusive || (ReleaseWaitList->ShareCount == 0)); KeSetEventBoostPriority (&ReleaseWaitList->WakeEvent, NULL); ReleaseWaitList = NextWaitBlock; } while (ReleaseWaitList != NULL); break; } } } NTKERNELAPI VOID FASTCALL ExBlockPushLock ( IN PEX_PUSH_LOCK PushLock, IN PEX_PUSH_LOCK_WAIT_BLOCK WaitBlock ) /*++ Routine Description: Block on a push lock Arguments: PushLock - Push lock to block on WaitBlock - Wait block to queue for waiting Return Value: None --*/ { EX_PUSH_LOCK OldValue, NewValue; // // Push the wait block on the list. // KeInitializeEvent (&WaitBlock->WakeEvent, SynchronizationEvent, FALSE); OldValue = *PushLock; while (1) { // // Chain the next block to us if there is one. // WaitBlock->Next = OldValue.Ptr; NewValue.Ptr = InterlockedCompareExchangePointer (&PushLock->Ptr, WaitBlock, OldValue.Ptr); if (NewValue.Ptr == OldValue.Ptr) { return; } OldValue = NewValue; } } NTKERNELAPI VOID FASTCALL ExfUnblockPushLock ( IN PEX_PUSH_LOCK PushLock, IN PEX_PUSH_LOCK_WAIT_BLOCK WaitBlock OPTIONAL ) /*++ Routine Description: Unblock on a push lock Arguments: PushLock - Push lock to block on WaitBlock - Wait block previously queued for waiting or NULL if there wasn't one Return Value: None --*/ { EX_PUSH_LOCK OldValue; PEX_PUSH_LOCK_WAIT_BLOCK tWaitBlock; BOOLEAN FoundOurBlock=FALSE; // // Pop the entire chain and wake them all up. // OldValue.Ptr = InterlockedExchangePointer (&PushLock->Ptr, NULL); while (OldValue.Ptr != NULL) { tWaitBlock = OldValue.Ptr; OldValue.Ptr = tWaitBlock->Next; if (tWaitBlock == WaitBlock) { FoundOurBlock = TRUE; } else{ KeSetEvent (&tWaitBlock->WakeEvent, 0, FALSE); } } if (WaitBlock != NULL && !FoundOurBlock) { KeWaitForSingleObject (&WaitBlock->WakeEvent, Executive, KernelMode, FALSE, NULL); } } NTKERNELAPI PEX_PUSH_LOCK_CACHE_AWARE ExAllocateCacheAwarePushLock ( VOID ) /*++ Routine Description: Allocate a cache aware (cache friendly) push lock Arguments: None Return Value: None --*/ { PEX_PUSH_LOCK_CACHE_AWARE PushLockCacheAware; PEX_PUSH_LOCK_CACHE_AWARE_PADDED PaddedPushLock; ULONG i, j; PushLockCacheAware = ExAllocatePoolWithTag (PagedPool, sizeof (EX_PUSH_LOCK_CACHE_AWARE), 'pclP'); if (PushLockCacheAware != NULL) { // // If we are a non-numa machine then allocate the padded push locks as a single block // if (KeNumberNodes == 1) { PaddedPushLock = ExAllocatePoolWithTag (PagedPool, sizeof (EX_PUSH_LOCK_CACHE_AWARE_PADDED)* EX_PUSH_LOCK_FANNED_COUNT, 'lclP'); if (PaddedPushLock == NULL) { ExFreePool (PushLockCacheAware); return NULL; } for (i = 0; i < EX_PUSH_LOCK_FANNED_COUNT; i++) { PaddedPushLock->Single = TRUE; ExInitializePushLock (&PaddedPushLock->Lock); PushLockCacheAware->Locks[i] = &PaddedPushLock->Lock; PaddedPushLock++; } } else { // // Allocate a different block for each lock and set affinity // so the allocation comes from that nodes memory. // for (i = 0; i < EX_PUSH_LOCK_FANNED_COUNT; i++) { KeSetSystemAffinityThread(AFFINITY_MASK(i % KeNumberProcessors)); PaddedPushLock = ExAllocatePoolWithTag (PagedPool, sizeof (EX_PUSH_LOCK_CACHE_AWARE_PADDED), 'lclP'); if (PaddedPushLock == NULL) { for (j = 0; j < i; j++) { ExFreePool (PushLockCacheAware->Locks[j]); } KeRevertToUserAffinityThread (); ExFreePool (PushLockCacheAware); return NULL; } PaddedPushLock->Single = FALSE; ExInitializePushLock (&PaddedPushLock->Lock); PushLockCacheAware->Locks[i] = &PaddedPushLock->Lock; } KeRevertToUserAffinityThread (); } } return PushLockCacheAware; } NTKERNELAPI VOID ExFreeCacheAwarePushLock ( PEX_PUSH_LOCK_CACHE_AWARE PushLock ) /*++ Routine Description: Frees a cache aware (cache friendly) push lock Arguments: PushLock - Cache aware push lock to be freed Return Value: None --*/ { ULONG i; if (!CONTAINING_RECORD (PushLock->Locks[0], EX_PUSH_LOCK_CACHE_AWARE_PADDED, Lock)->Single) { for (i = 0; i < EX_PUSH_LOCK_FANNED_COUNT; i++) { ExFreePool (PushLock->Locks[i]); } } else { ExFreePool (PushLock->Locks[0]); } ExFreePool (PushLock); } NTKERNELAPI VOID ExAcquireCacheAwarePushLockExclusive ( IN PEX_PUSH_LOCK_CACHE_AWARE PushLock ) /*++ Routine Description: Acquire a cache aware push lock exclusive. Arguments: PushLock - Cache aware push lock to be acquired Return Value: None --*/ { PEX_PUSH_LOCK *Start, *End; // // Exclusive acquires must obtain all the slots exclusive. // Take the first slot exclusive and then we can take the // rest of the slots in any order we want. // There is no deadlock here. A->B->C does not deadlock with A->C->B. // Start = &PushLock->Locks[1]; End = &PushLock->Locks[EX_PUSH_LOCK_FANNED_COUNT - 1]; ExAcquirePushLockExclusive (PushLock->Locks[0]); while (Start <= End) { if (ExTryAcquirePushLockExclusive (*Start)) { Start++; } else { ExAcquirePushLockExclusive (*End); End--; } } } NTKERNELAPI VOID ExReleaseCacheAwarePushLockExclusive ( IN PEX_PUSH_LOCK_CACHE_AWARE PushLock ) /*++ Routine Description: Release a cache aware push lock exclusive. Arguments: PushLock - Cache aware push lock to be released Return Value: None --*/ { PEX_PUSH_LOCK *Start, *End; // // Release the locks in order // End = &PushLock->Locks[EX_PUSH_LOCK_FANNED_COUNT]; for (Start = &PushLock->Locks[0]; Start < End; Start++) { ExReleasePushLockExclusive (*Start); } }