Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Shared Memory for Inter-Process Communication on Linux

Tech 1

Multi-threading and multi-processing are core approaches for concurrent software development, with key differences in how they handle memory access. Threads belonging to the same parent process share the entire process address space by default, so data exchange between threads can happen directly via shared memory regions with very low overhead. This simplicity comes with tradeoffs: uncoordinated concurrent writes to shared memory can trigger race conditions, data corruption, or inconsistent state, requiring synchronization primitives like mutex locks, read-write locks or spinlocks too guarantee data integrity.

In contrast, each process runs in an isolated virtual address space managed by the kernel. No process can directly access memory allocated to another process without explicit kernel-mediated mechanisms. This isolation eliminates cross-process race conditions by default, but requires specialized inter-process communication (IPC) tools to exchange data between processes, such as pipes, Unix domain sockets, message queues, or shared memory. Data transfer across process boundaries incurs additional overhead from context switching and data copying, which can be prohibitive for high-throughput workloads.

Multi-threading and multi-processing are not mutually exclusive, and many production systems combine both to optimize performance and reliability. For example, a service may spawn multiple independent worker processes to leverage multi-core CPU resources and isolate faults, with each worker process spawning multiple threads to handle concurrent I/O-bound tasks. Such hybrid architectures require careful design of synchronization and data sharing rules across both threads and processes to avoid data integrity issues.

Shared memory is an IPC mechanism that enables multiple independent processes or threads to access the same physical memory region, facilitating zero-copy data exchange between participents.

The standard workflow for using System V shared memory (the most widely supported implementation on Linux) follows four core steps:

  1. Allocate a shared memory segment: Use a kernel system call to reserve a memory region with a specified size and access permissions, and assign it a unique identifier.
  2. Map the shared segment: Each process that needs access to the shared memory attaches the segment to its own virtual address space via a system call.
  3. Read/write to the shared segment: Once mapped, processes can read and write to the shared memory directly as if it were local heap or stack memory.
  4. Unmap and clean up: When a process no longer needs access to the shared segment, it detaches the segment from its address space. Once all processes have detached, the segment can be deleted to free system resources.

Like thread-local shared memory, cross-process shared memory is vulnerable to race conditions when multiple participants write to the same offset concurrently. Developers must implement synchronization controls such as POSIX semaphores, mutexes stored in shared memory, or file locks to coordinate access, depending on the use case.

Shared memory offers significantly higher throughput and lower latency than other IPC mechanisms because it avoids the kernel-mediated data copying required for pipes and message queues. However, it requires stricter error handling and synchronization design to prevent memory leaks, data corruption, and unauthorized access.

shmget() System Call

The shmget() system call is used to allocate a new System V shared memory segment or retrieve the identifier of an existing segment. Its function prototype is defined as:

int shmget(key_t segment_key, size_t segment_size, int flags);

Parameter breakdown:

  • segment_key: A unique key used to identify the shared memory segment. This can be set to IPC_PRIVATE to create a segment only accessible to related processes, or generated via the ftok() utility to create a key tied to a filesystem path for cross-process access.
  • segment_size: The size of the requested shared memory segment in bytes. When retrieving an existing segment, this value can be set to 0.
  • flags: A bitmask combining access permissions (in standard Unix octal format) and control flags. Common flags innclude IPC_CREAT (create the segment if it does not already exist) and IPC_EXCL (return an error if the segment already exists, to avoid conflicts).

Return values:

  • On success, returns a non-negative integer representing the shared memory segment identifier (shmid) that is used for all subsequent operations on the segment.
  • On failure, returns -1 and sets the errno variable to indicate the specific error type.

Standard usage steps for shmget():

  1. Generate a unique segment key, either via ftok() using a pre-agreed filesystem path, or by using a hardcoded non-zero integer key for simple use cases.
  2. Call shmget() with the key, desired segment size, and appropriate permission and control flags.
  3. Validate the returned shmid to confirm the segment was created or retrieved successfully.
  4. Use additional system calls like shmat() to map the segment, shmdt() to unmap it, and shmctl() to modify segment attributes or delete the segment when no longer needed.

All processes that need to access the same shared memory segment must use the same segment_key value. When using IPC_CREAT without IPC_EXCL, the call will return the identifier of an existing segment with the same key if one exists, which may lead to unexpected behavior if the existing segment has a different size or permissions than expected.

Below is a complete working example demonstrating shared memory creation, mapping, read/write operations, and cleanup:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>

using namespace std;

// Struct stored in shared memory: avoid dynamic allocation types (e.g. std::string)
// These allocate memory in the local process heap which is not accessible to other processes
struct shared_user_record {
    int user_id;
    char username[64];
};

int main(int argc, char *argv[]) {
    if (argc < 3) {
        cerr << "Usage: " << argv[0] << " <user_id> <username>\n";
        return EXIT_FAILURE;
    }

    // Allocate or retrieve shared memory segment with key 0x7A2F, size matching our struct, permissions 0640
    int shm_identifier = shmget(0x7A2F, sizeof(struct shared_user_record), 0640 | IPC_CREAT);
    if (shm_identifier == -1) {
        perror("Failed to get shared memory segment");
        return EXIT_FAILURE;
    }
    cout << "Successfully obtained shared memory segment, ID: " << shm_identifier << "\n";

    // Attach shared memory segment to current process address space
    struct shared_user_record *shared_data = static_cast<struct shared_user_record*>(shmat(shm_identifier, nullptr, 0));
    if (shared_data == reinterpret_cast<void*>(-1)) {
        perror("Failed to attach shared memory segment");
        return EXIT_FAILURE;
    }

    // Print existing values stored in shared memory
    cout << "Existing values in shared memory: user_id = " << shared_data->user_id 
         << ", username = " << shared_data->username << "\n";

    // Write new values from command line arguments to shared memory
    shared_data->user_id = atoi(argv[1]);
    strncpy(shared_data->username, argv[2], sizeof(shared_data->username) - 1);
    shared_data->username[sizeof(shared_data->username) - 1] = '\0'; // Ensure null termination

    cout << "Updated values in shared memory: user_id = " << shared_data->user_id 
         << ", username = " << shared_data->username << "\n";

    // Detach shared memory segment from current process
    if (shmdt(shared_data) == -1) {
        perror("Failed to detach shared memory segment");
        return EXIT_FAILURE;
    }

    // Delete shared memory segment (run this only when no processes need access to it anymore)
    if (shmctl(shm_identifier, IPC_RMID, nullptr) == -1) {
        perror("Failed to delete shared memory segment");
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

You can list all active System V shared memory segments on your system with the ipcs -m command. To manually delete a stray shared memory segment, use ipcrm -m <segment_id>, replacing <segment_id> with the ID returned by shmget or listed in ipcs output.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.