Interprocess Communication With Memory-Mapped Files, Part 1

Memory-mapped files are great concept that have been buried in various versions of Windows for years.  Finally they've been exposed and incorporated into .NET 4.0 and are available in Visual Studio 2010.

A memory-mapped file is essentially just a file, given a unique name-- no extension necessary-- that exists only in memory.  It can be used to cache a large file into memory for faster access and manipulation.  In addition, since they're accessible from the system level, these files can be used to allow multiple processes to communicate (called "interprocess communication").  It's this method of communication that I'd like to try to implement here.

Suppose we have two separate processes, one to perform work, and the other that's simply responsible for starting the first-- like a Windows Service.  In this situation, we're assuming that the Windows Service can't simply perform the work in a child thread, but relies on that other process to do it.

I've created two C# projects, with one intended perform "processing" and the other to launch and communicate with the first:

  • ConsoleAppListener
    Responsible for performing work and shutting down gracefully when it receives a "shutdown" message, after it has finished processing a full unit of work.  Multiple instances can exist, but each has a different "unique name" and a separate ConsoleAppController responsible for starting and monitoring each instance.
  • ConsoleAppController
    Responsible for starting ConsoleAppListener, ensuring it stays running, and sending a "shutdown" message instructing the ConsoleAppListener to shutdown gracefully when it's finished processing.  Multiple instances can exist, but each is responsible for monitoring a ConsoleAppListener with a different "unique name".
Since a memory-mapped file has a unique name and persists only while a process is using it, it is a strong candidate for meeting these requirements.  The first process to start, expected to be the ConsoleAppController, is responsible for creating the memory-mapped file.  The second process, the ConsoleAppListener, will look for a memory-mapped file with the same unique name it was provided in its command-line arguments.
Here's a rough order of execution, although many of these things can happen in parallel:
  1. ConsoleAppController starts, checks if a memory-mapped file exists with the name "SampleApp"
  2. ConsoleAppController reads the memory-mapped file, checks if a process ID has already been recorded. If so, this means ConsoleAppListener is already running and will now be monitored.  Otherwise launches a new ConsoleAppListener process with the "SampleApp" passed as a command-line argument
  3. ConsoleAppListener starts, creates or opens memory-mapped file with the name "SampleApp"
  4. ConsoleAppListener saves its process ID to the memory-mapped file
  5. ConsoleAppListener performs processing, checking periodically for the shutdown message, "-1"
  6. ConsoleAppController monitors, restarts ConsoleAppListener if it shuts down on its own (e.g. user closes the window)
  7. ConsoleAppController saves a shutdown message, "-1", to the memory-mapped file, instructing ConsoleAppListener to shutdown at an appropriate time (when it's not performing processing)
So in this scenario, the mem0ry-mapped file only needs to be large enough to store an int32, representing either the ConsoleAppListener's process ID or a shutdown message of "-1".  The direction of either message is implied, as the listener only cares about receiving a "-1" and the controller is only interested in a process ID (which is greater than -1).  For that reason, I consider this mechanism more of a passive, broadcast-based communication system, rather than an active, full-duplex sort of client-server arrangement.
Here's some of the code used by the ConsoleAppListener, responsible for listening for a shutdown message:
private static bool ShouldClose()
        {
            using (var stream = _memoryMappedFile.CreateViewStream())
            {
                var reader = new BinaryReader(stream);
                var message = reader.ReadInt32();
                if (message == CloseMessage)
                {
                    Console.WriteLine("Received shutdown message");
                    return true;
                }
            }

            return false;
        }
Here's part of ConsoleAppController, responsible for locating the memory-mapped file ("destinationAppName" is the name of the memory-mapped file) and sending a shutdown message to the ConsoleAppListener if prompted by the user:
        private static void WantProcessToShutDown(string destinationAppName)
        {
            if (!_isClientProcessRunning)
            {
                return;
            }

            var memoryMappedFile = MemoryMappedFile.OpenExisting(destinationAppName);

            using (var accessor = memoryMappedFile.CreateViewAccessor(0, MessageByteSize))
            {
                accessor.Write(0, CloseMessage);
            }

            Console.WriteLine("Sent message to shut down");
        }
I hope this preview gives at least some insight into the use of memory-mapped files for primitive interprocess communication.  In tomorrow's post I'll be providing all of the sample code.

Leave a Reply

Your email address will not be published. Required fields are marked *