Hello! Welcome to the next lesson in our course on C pointers.
In the previous lesson, we saw how to use volatile pointers to interact directly with hardware, accessing memory-mapped registers to control peripherals. Today, we'll move up a layer of abstraction from the hardware-software boundary to communication between software components. We will explore how pointers are fundamental to one of the fastest methods of Inter-Process Communication (IPC).
This lesson addresses the learning outcome: Describe the role of pointers in accessing OS-managed shared memory segments for Inter-Process Communication (IPC).
This is a critical topic for modern automotive systems. As vehicles become more like data centers on wheels, platforms running advanced operating systems like Linux or QNX host multiple processes that need to share data with high throughput and low latency—for example, a process capturing RADAR data passing it to a sensor fusion process. Shared memory is a go-to solution for this, and understanding how pointers enable it is key.
1. The Need for Inter-Process Communication (IPC)
Operating systems are built on a fundamental principle of process isolation. Each process runs in its own private virtual address space, firewalled from other processes. This is essential for stability and security; a crash in one process shouldn't bring down the entire system.
However, complex applications are often built from multiple cooperating processes. How can they communicate if they are isolated? This is where IPC mechanisms come in.
To understand the basic concepts and the two primary models of IPC—shared memory and message passing—let's start with a short video.
IPC: To Share Memory Or To Send Messages
The video 'IPC: To Share Memory Or To Send Messages' from the Core Dumped channel provides an excellent overview of why processes need to communicate and introduces the core concepts of shared memory.
Please watch from 00:34 to 05:04. Focus on: Why process isolation necessitates IPC. How the shared memory model works by asking the OS to map a common memory region into the address space of cooperating processes.
As the video explained, the key advantages of shared memory are its simplicity and speed. Once the shared segment is set up, data transfer happens at memory speed without involving the kernel in every transaction. This makes it extremely efficient for passing large amounts of data.
2. The Mechanics: How Pointers Unlock Shared Memory
The core idea is that multiple processes ask the operating system to map the same block of physical memory into their individual virtual address spaces. The OS provides each process with a pointer to the start of this mapped region. This pointer is the process's handle to the shared "blackboard" that all participating processes can read from and write to.
Let's look at the general workflow using the widely implemented POSIX shared memory API, which is the modern standard on Linux and other Unix-like systems.
The POSIX Shared Memory API Workflow:
shm_open(): One process creates (or opens an existing) shared memory "object." This is conceptually like opening a file and returns a file descriptor. The object is identified by a name (e.g.,/my_shm_region).ftruncate(): The creating process sets the size of the shared memory object.mmap(): This is the crucial step. Each process callsmmap()to map the shared memory object into its own address space. The function returns avoid*pointer—this is the base address of the shared memory segment within that specific process.- Access: Processes use this pointer to read and write data. You can cast this
void*to a pointer of any type (char*,int*, or a pointer to a struct) and access the memory just as you would with memory allocated bymalloc(). munmap(): When a process is finished, it unmaps the memory region.shm_unlink(): The last process can remove the shared memory object from the system.
This slide deck provides a concise summary of these POSIX APIs.
These slides from Michael Kerrisk's 'Linux/UNIX System Programming' course cover the essential POSIX shared memory APIs.
Please review slides 23-7 through 23-28. Don't worry about memorizing every flag. Focus on the roles of shm_open, ftruncate, and mmap, and notice how mmap returns a pointer that is then used with functions like memcpy or direct dereferencing.
The pointer returned by mmap() is the gateway. All interactions with the shared data happen through this pointer, using standard pointer arithmetic and dereferencing operations.
3. The Address Space Problem: Absolute vs. Relative Pointers
Here is a critical challenge when using shared memory. While multiple processes map the same physical memory, the OS does not guarantee it will be mapped at the same virtual address in each process.

This has a profound implication: You cannot safely store an absolute pointer (a raw memory address) inside the shared memory segment itself.
Imagine Process 1 stores a pointer to a data structure within the shared segment. That pointer's value might be 0x70000010. When Process 2 reads that address, it's meaningless in its own context; 0x70000010 might point to unmapped memory or completely unrelated data in Process 2's address space.
The Solution: Relative Offsets
To create complex data structures like linked lists or trees within shared memory, you must use offsets relative to the base address of the segment.
- To store a "pointer": Calculate its offset from the base address returned by
mmap().offset = (char*)target_address - (char*)base_address; - To use a "pointer": Reconstruct the absolute address by adding the offset to the process's own base address.
target_address = (char*)base_address + offset;
This ensures that the reference is valid regardless of where the segment is mapped in each process's virtual memory.
The 'POSIX Shared Memory' slides explain this exact problem and the solution using relative offsets.
Please read slides 23-29 and 23-30. They provide a clear, visual explanation of why absolute addresses fail and how to correctly use relative offsets.
For a more in-depth C++ perspective, the following article shows how to encapsulate this logic in a smart pointer-like class, a common practice in robust software.
Applied Reading - Taming Shared Memory - ACCU
The article 'Taming Shared Memory' from ACCU demonstrates a sophisticated approach to this problem by creating a custom shm_ptr class that internally manages offsets.
Read the section that begins 'The shm_segment class makes it a little easier to get a segment...' and introduces the shm_ptr class. You don't need to dive into the C++ template details, but focus on the concept: the class stores an offset_ member instead of a raw pointer to solve the address space problem.
4. The Unavoidable Companion: Synchronization
Shared memory is fast because the kernel steps aside after the initial setup. This is also its greatest danger. If one process is writing to a data structure while another is reading it, the reader may see corrupt, partially updated data. If two processes try to write to the same location at the same time, the result is a classic race condition.
Your background in ISO 26262 functional safety makes the severity of such issues immediately apparent. Undefined behavior resulting from race conditions is unacceptable in safety-critical systems.
Therefore, all access to a shared memory segment must be synchronized.
This is typically done using synchronization primitives like semaphores or mutexes. These mechanisms can also be placed within the shared memory segment, allowing all participating processes to use them to coordinate access. Before a process reads or writes, it must acquire a lock (e.g., sem_wait()); after it's done, it must release the lock (sem_post()).
How to Set up Shared Memory in Your Linux and MacOS Programs. (shmget, shmat, shmdt, shmctl, ftok)
This clip from Jacob Sorber's tutorial on shared memory offers a clear warning about the necessity of synchronization.
Watch from 11:31 to 12:29. The video explicitly links the dangers of unsynchronized shared memory to the same issues seen in multi-threaded programming and highlights semaphores as a solution.
The Kerrisk slides (ID: LINK) also provide a concrete example of placing a POSIX semaphore inside a shared struct to protect a sequence number (slides 23-34 through 23-42). This is a very common and effective pattern.
Test your understanding!
You are designing a system where an ADAS perception process (Process P) writes a complex object, containing multiple data fields and pointers to sub-objects, into shared memory. A sensor fusion process (Process F) reads this object. Why would the following approach fail?
- Process P allocates the main object and all sub-objects in shared memory.
- It stores the absolute addresses of the sub-objects in the pointer fields of the main object.
- Process F reads the main object and tries to dereference the pointers to access the sub-objects.
Show answer
This approach would fail because Process P and Process F will likely have the shared memory segment mapped at different virtual base addresses. The absolute addresses stored by Process P are only valid within its own address space. When Process F tries to use those addresses, they will point to invalid or incorrect locations, leading to a segmentation fault or data corruption. The correct solution is for Process P to store relative offsets to the sub-objects, which Process F can then use to calculate the correct absolute addresses within its own address space.
Conclusion
In this lesson, we explored the powerful role of pointers in enabling high-performance inter-process communication via shared memory. This technique is a cornerstone of performant software architecture in complex embedded systems, including the software-defined vehicle platforms you work with.
Key Takeaways:
- Shared memory is an IPC mechanism where the OS maps a common memory region into the virtual address spaces of multiple, isolated processes.
- The role of the pointer is central:
mmap()(orshmat()) returns a pointer that acts as the handle for a process to read from and write to the shared segment. - Once mapped, the shared memory can be accessed via this pointer just like any other dynamically allocated memory.
- Pointers stored within a shared memory segment must be relative offsets from the base, not absolute virtual addresses, because the mapping address can differ between processes.
- Access to shared memory is not inherently safe. Synchronization using primitives like mutexes or semaphores is mandatory to prevent race conditions and ensure data integrity.
Preview of the Next Lesson
Having covered the theory, our next step is to put it into practice. In the next lesson, we will tackle the learning outcome: Implement a program to create/attach, write to, and read from a shared memory segment using pointers. We will write a pair of simple C programs that communicate using the POSIX shared memory APIs, solidifying the concepts we discussed today.