Skip to content

Commit

Permalink
Add named mutex for cross-process synchronization
Browse files Browse the repository at this point in the history
Fixes #3422

- On systems that support pthread process-shared robust recursive mutexes, they will be used
- On other systems, file locks are used. File locks unfortunately don't have a timeout in the blocking wait call, and I didn't find any other sync object with a timed wait with the necessary properties, so polling is done for timed waits.

Shared memory files:
- Session-scoped mutexes (name not prefixed, or prefixed with Local\) go in /tmp/coreclr/shm/session<sessionId>/<mutexName>
- Globally-scoped mutexes (name prefixed with Global\) go in /tmp/coreclr/shm/global/<mutexName>
- Contains shared state, and is mmap'ped into the process, see SharedMemorySharedDataHeader and NamedMutexSharedData for data stored
- Creation and deletion is synchronized using an exclusive file lock on the shm directory
- Any process using the shared memory file holds a shared file lock on the shared memory file
- Upon creation, if the shared memory file already exists, an exclusive file lock is attempted on it, to see if the file data is valid. If no other processes have the mutex open, the file is reinitialized.
- Upon releasing the last reference to a mutex in a process, it will try to get an exclusive lock on the shared memory file to see if any other processes have the mutex opened. If not, the file is deleted, along with the session directory if it's empty. The coreclr and shm directories are not deleted.
- This allows managing the lifetime of mutex state based on active processes that have the mutex open. Depending on how the process terminated, the file may still be left over in the tmp directory, I haven't found anything that can be done about that.

Lock files when using file locks:
- In addition to the shared memory file, we need another file for the actual synchronization file lock, since a file lock on the shared memory file is used for lifetime purposes.
- These files go in /tmp/coreclr/lockfiles/session<sessionId>|global/<mutexName>
- The file is empty, and is only used for file locks

Process data
- See SharedMemoryProcessDataHeader and NamedMutexProcessData for data stored
- Per mutex name, there is only one instance of process data that is ref-counted. They are currently stored in a linked list in SharedMemoryManager. It should use a hash table, but of the many hash table implementations that are already there, none seem to be easily usable in the PAL. I'll look into that and will fix later.
- Refers to the associated shared memory, and knows how to clean up both the process data and shared data
- When using file locks for synchronization, a process-local mutex is also created for synchronizing threads, since file locks are owned at the file descriptor level and there is only one open file descriptor in the process per mutex name. The process-local mutex is locked around the file lock, so that only one thread per process is ever trying to flock on a given file descriptor.

Abandon detection
- When a lock is acquired, the process data is added to a linked list on the owning thread
- When a thread exits, the list is walked, each mutex is flagged as abandoned and released
- For detecting process abruptly terminating, pthread robust mutexes give us that. When using file locks, the file lock is automatically released by the system. Upon acquiring a lock, the lock owner info in the shared memory is checked to see if the mutex was abandoned.

Miscellaneous
- CreateMutex and OpenMutex both create new handles for each mutex opened. Each handle just refers to the process data header for the mutex name.
- Some of the above features are already available in the PAL, but not quite in a way that I can use for this purpose. The existing shared memory, naming, and waiting infrastructure is not suitable for this purpose, and is not used.
  • Loading branch information
kouvel committed May 18, 2016
1 parent acd2bba commit a5f4d37
Show file tree
Hide file tree
Showing 32 changed files with 3,705 additions and 149 deletions.
6 changes: 3 additions & 3 deletions src/mscorlib/src/System/IO/Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ public static class Path
internal static readonly int MaxPath = 1024;
#endif

private static readonly int MaxDirectoryLength = 255;
internal static readonly int MaxPathComponentLength = 255;

// Windows API definitions
internal const int MAX_PATH = 260; // From WinDef.h
Expand Down Expand Up @@ -513,7 +513,7 @@ internal unsafe static String NormalizePath(String path, bool fullCheck, int max
}
#endif
int thisPos = newBuffer.Length - 1;
if (thisPos - lastDirectorySeparatorPos > MaxDirectoryLength)
if (thisPos - lastDirectorySeparatorPos > MaxPathComponentLength)
{
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
}
Expand Down Expand Up @@ -590,7 +590,7 @@ internal unsafe static String NormalizePath(String path, bool fullCheck, int max
index++;
} // end while

if (newBuffer.Length - 1 - lastDirectorySeparatorPos > MaxDirectoryLength)
if (newBuffer.Length - 1 - lastDirectorySeparatorPos > MaxPathComponentLength)
{
throw new PathTooLongException(Environment.GetResourceString("IO.PathTooLong"));
}
Expand Down
72 changes: 46 additions & 26 deletions src/mscorlib/src/System/Threading/Mutex.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,17 @@ public Mutex(bool initiallyOwned, String name, out bool createdNew)
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
public unsafe Mutex(bool initiallyOwned, String name, out bool createdNew, MutexSecurity mutexSecurity)
{
if (name != null)
if (name == string.Empty)
{
#if PLATFORM_UNIX
throw new PlatformNotSupportedException(Environment.GetResourceString("PlatformNotSupported_NamedSynchronizationPrimitives"));
#else
if (System.IO.Path.MaxPath < name.Length)
{
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong", name));
}
#endif
// Empty name is treated as an unnamed mutex. Set to null, and we will check for null from now on.
name = null;
}
#if !PLATFORM_UNIX
if (name != null && System.IO.Path.MaxPath < name.Length)
{
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong", Path.MaxPath), "name");
}
#endif
Contract.EndContractBlock();
Win32Native.SECURITY_ATTRIBUTES secAttrs = null;
#if FEATURE_MACL
Expand All @@ -86,17 +86,17 @@ public unsafe Mutex(bool initiallyOwned, String name, out bool createdNew, Mutex
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
internal Mutex(bool initiallyOwned, String name, out bool createdNew, Win32Native.SECURITY_ATTRIBUTES secAttrs)
{
if (name != null)
if (name == string.Empty)
{
#if PLATFORM_UNIX
throw new PlatformNotSupportedException(Environment.GetResourceString("PlatformNotSupported_NamedSynchronizationPrimitives"));
#else
if (System.IO.Path.MaxPath < name.Length)
{
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong", name));
}
#endif
// Empty name is treated as an unnamed mutex. Set to null, and we will check for null from now on.
name = null;
}
#if !PLATFORM_UNIX
if (name != null && System.IO.Path.MaxPath < name.Length)
{
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong", Path.MaxPath), "name");
}
#endif
Contract.EndContractBlock();

CreateMutexWithGuaranteedCleanup(initiallyOwned, name, out createdNew, secAttrs);
Expand Down Expand Up @@ -131,6 +131,8 @@ internal class MutexTryCodeHelper
[PrePrepareMethod]
internal MutexTryCodeHelper(bool initiallyOwned,MutexCleanupInfo cleanupInfo, String name, Win32Native.SECURITY_ATTRIBUTES secAttrs, Mutex mutex)
{
Contract.Assert(name == null || name.Length != 0);

m_initiallyOwned = initiallyOwned;
m_cleanupInfo = cleanupInfo;
m_name = name;
Expand Down Expand Up @@ -173,8 +175,20 @@ internal void MutexTryCode(object userData)
if (mutexHandle.IsInvalid)
{
mutexHandle.SetHandleAsInvalid();
if(null != m_name && 0 != m_name.Length && Win32Native.ERROR_INVALID_HANDLE == errorCode)
throw new WaitHandleCannotBeOpenedException(Environment.GetResourceString("Threading.WaitHandleCannotBeOpenedException_InvalidHandle", m_name));
if (m_name != null)
{
switch (errorCode)
{
#if PLATFORM_UNIX
case Win32Native.ERROR_FILENAME_EXCED_RANGE:
// On Unix, length validation is done by CoreCLR's PAL after converting to utf-8
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong", Path.MaxPathComponentLength), "name");
#endif

case Win32Native.ERROR_INVALID_HANDLE:
throw new WaitHandleCannotBeOpenedException(Environment.GetResourceString("Threading.WaitHandleCannotBeOpenedException_InvalidHandle", m_name));
}
}
__Error.WinIOError(errorCode, m_name);
}
m_newMutex = errorCode != Win32Native.ERROR_ALREADY_EXISTS;
Expand Down Expand Up @@ -305,9 +319,6 @@ public static bool TryOpenExisting(string name, MutexRights rights, out Mutex re
[System.Security.SecurityCritical]
private static OpenExistingResult OpenExistingWorker(string name, MutexRights rights, out Mutex result)
{
#if PLATFORM_UNIX
throw new PlatformNotSupportedException(Environment.GetResourceString("PlatformNotSupported_NamedSynchronizationPrimitives"));
#else
if (name == null)
{
throw new ArgumentNullException("name", Environment.GetResourceString("ArgumentNull_WithParamName"));
Expand All @@ -317,10 +328,12 @@ private static OpenExistingResult OpenExistingWorker(string name, MutexRights ri
{
throw new ArgumentException(Environment.GetResourceString("Argument_EmptyName"), "name");
}
#if !PLATFORM_UNIX
if(System.IO.Path.MaxPath < name.Length)
{
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong",name));
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong", Path.MaxPath), "name");
}
#endif
Contract.EndContractBlock();

result = null;
Expand All @@ -340,11 +353,19 @@ private static OpenExistingResult OpenExistingWorker(string name, MutexRights ri
{
errorCode = Marshal.GetLastWin32Error();

#if PLATFORM_UNIX
if (name != null && errorCode == Win32Native.ERROR_FILENAME_EXCED_RANGE)
{
// On Unix, length validation is done by CoreCLR's PAL after converting to utf-8
throw new ArgumentException(Environment.GetResourceString("Argument_WaitHandleNameTooLong", Path.MaxPathComponentLength), "name");
}
#endif

if(Win32Native.ERROR_FILE_NOT_FOUND == errorCode || Win32Native.ERROR_INVALID_NAME == errorCode)
return OpenExistingResult.NameNotFound;
if (Win32Native.ERROR_PATH_NOT_FOUND == errorCode)
return OpenExistingResult.PathNotFound;
if (null != name && 0 != name.Length && Win32Native.ERROR_INVALID_HANDLE == errorCode)
if (null != name && Win32Native.ERROR_INVALID_HANDLE == errorCode)
return OpenExistingResult.NameInvalid;

// this is for passed through Win32Native Errors
Expand All @@ -353,7 +374,6 @@ private static OpenExistingResult OpenExistingWorker(string name, MutexRights ri

result = new Mutex(myHandle);
return OpenExistingResult.Success;
#endif
}

// Note: To call ReleaseMutex, you must have an ACL granting you
Expand Down
5 changes: 3 additions & 2 deletions src/mscorlib/src/mscorlib.txt
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ Argument_ResolveFieldHandle = Type handle '{0}' and field handle with declaring
Argument_ResourceScopeWrongDirection = Resource type in the ResourceScope enum is going from a more restrictive resource type to a more general one. From: "{0}" To: "{1}"
Argument_BadResourceScopeTypeBits = Unknown value for the ResourceScope: {0} Too many resource type bits may be set.
Argument_BadResourceScopeVisibilityBits = Unknown value for the ResourceScope: {0} Too many resource visibility bits may be set.
Argument_WaitHandleNameTooLong = The name can be no more than 260 characters in length.
Argument_WaitHandleNameTooLong = The name can be no more than {0} characters in length.
Argument_EnumTypeDoesNotMatch = The argument type, '{0}', is not the same as the enum type '{1}'.
InvalidOperation_MethodBuilderBaked = The signature of the MethodBuilder can no longer be modified because an operation on the MethodBuilder caused the methodDef token to be created. For example, a call to SetCustomAttribute requires the methodDef token to emit the CustomAttribute token.
InvalidOperation_GenericParametersAlreadySet = The generic parameters are already defined on this MethodBuilder.
Expand Down Expand Up @@ -2138,7 +2138,8 @@ event_Barrier_PhaseFinished=Barrier finishing phase {1}.

#if PLATFORM_UNIX
; Unix threading
PlatformNotSupported_NamedSynchronizationPrimitives=Named synchronization primitives are not supported on this platform.
PlatformNotSupported_NamedSynchronizationPrimitives=The named version of this synchronization primitive is not supported on this platform.
PlatformNotSupported_NamedSyncObjectWaitAnyWaitAll=Wait operations on multiple wait handles including a named synchronization primitive, are not supported on this platform.
#endif

;
Expand Down
6 changes: 6 additions & 0 deletions src/pal/inc/pal.h
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,12 @@ PALAPI
GetCurrentProcessId(
VOID);

PALIMPORT
DWORD
PALAPI
GetCurrentSessionId(
VOID);

PALIMPORT
HANDLE
PALAPI
Expand Down
1 change: 1 addition & 0 deletions src/pal/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ set(SOURCES
safecrt/wsplitpath_s.c
safecrt/xtoa_s.c
safecrt/xtow_s.c
sharedmemory/sharedmemory.cpp
shmemory/shmemory.cpp
sync/cs.cpp
synchobj/event.cpp
Expand Down
1 change: 1 addition & 0 deletions src/pal/src/config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
#cmakedefine01 ERROR_FUNC_FOR_GLOB_HAS_FIXED_PARAMS
#cmakedefine01 HAS_FTRUNCATE_LENGTH_ISSUE
#cmakedefine01 UNWIND_CONTEXT_IS_UCONTEXT_T
#cmakedefine01 HAVE_FULLY_FEATURED_PTHREAD_MUTEXES
#cmakedefine BSD_REGS_STYLE(reg, RR, rr) @BSD_REGS_STYLE@
#cmakedefine FREEBSD_LIBC "@FREEBSD_LIBC@"
#cmakedefine01 HAVE_SCHED_OTHER_ASSIGNABLE
Expand Down
35 changes: 35 additions & 0 deletions src/pal/src/configure.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,41 @@ int main(int argc, char **argv)
return 0;
}" UNWIND_CONTEXT_IS_UCONTEXT_T)

set(CMAKE_REQUIRED_LIBRARIES pthread)
check_cxx_source_compiles("
#include <errno.h>
#include <pthread.h>
#include <time.h>
int main()
{
pthread_mutexattr_t mutexAttributes;
pthread_mutexattr_init(&mutexAttributes);
pthread_mutexattr_setpshared(&mutexAttributes, PTHREAD_PROCESS_SHARED);
pthread_mutexattr_settype(&mutexAttributes, PTHREAD_MUTEX_RECURSIVE);
pthread_mutexattr_setrobust(&mutexAttributes, PTHREAD_MUTEX_ROBUST);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &mutexAttributes);
pthread_mutexattr_destroy(&mutexAttributes);
struct timespec timeoutTime;
timeoutTime.tv_sec = 1; // not the right way to specify absolute time, but just checking availability of timed lock
timeoutTime.tv_nsec = 0;
pthread_mutex_timedlock(&mutex, &timeoutTime);
pthread_mutex_consistent(&mutex);
pthread_mutex_destroy(&mutex);
int error = EOWNERDEAD;
error = ENOTRECOVERABLE;
error = ETIMEDOUT;
error = 0;
return error;
}" HAVE_FULLY_FEATURED_PTHREAD_MUTEXES)
set(CMAKE_REQUIRED_LIBRARIES)

if(CMAKE_SYSTEM_NAME STREQUAL Darwin)
if(NOT HAVE_LIBUUID_H)
unset(HAVE_LIBUUID_H CACHE)
Expand Down
1 change: 1 addition & 0 deletions src/pal/src/include/pal/corunix.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ namespace CorUnix
otiAutoResetEvent = 0,
otiManualResetEvent,
otiMutex,
otiNamedMutex,
otiSemaphore,
otiFile,
otiFileMapping,
Expand Down
Loading

0 comments on commit a5f4d37

Please sign in to comment.