Hello! Welcome to the next lesson in our series on pointers in C.
In our last session, we successfully built a pair of programs that communicated using shared memory. A "writer" process placed data into a memory segment, and a "reader" process accessed it. However, as we concluded, our simple implementation has a critical flaw: it's not safe for concurrent execution.
Today, we will fix that. Our learning goal is to modify our shared memory access code to include synchronization primitives to prevent race conditions. This is an absolutely essential concept for building robust, multi-process systems, especially in domains like automotive software where functional safety (ISO 26262) is paramount.
1. The Problem: Race Conditions and Critical Sections
The issue with our previous code is the risk of a race condition. This occurs when multiple processes access and manipulate shared data concurrently, and the final outcome depends on the particular order in which their operations are interleaved by the OS scheduler.
Imagine our writer process is in the middle of executing strcpy to update a message in shared memory. If the scheduler preempts it and runs the reader process, the reader might see a partially-written, corrupted message.

To prevent this, we must identify the parts of our code that access the shared resource. These are known as critical sections. We then need to use a synchronization mechanism to ensure that only one process can be inside its critical section at any given time. This principle is called mutual exclusion.
Let's start by seeing what a race condition looks like in practice. The following video introduces the concept and shows how it manifests in a simple counter example.
how does a Mutex even work? (atoms in the computer??)
The video 'how does a Mutex even work?' from the Low Level channel clearly explains what a race condition is and why it's a problem.
Watch the first minute of the video (from 00:09 to 01:05). Focus on the explanation of how two threads trying to decrement a global counter can lead to an incorrect result.
2. The Solution: Synchronization Primitives
Operating systems provide several tools, called synchronization primitives, to enforce mutual exclusion. The most common are mutexes and semaphores.
-
Mutex (Mutual Exclusion Object): The simplest lock. It's like a talking stick. A process must "acquire" or "lock" the mutex before entering a critical section and "release" or "unlock" it upon exiting. If a process tries to lock a mutex that is already held by another process, it will be forced to wait (block) until the mutex is released.
-
Semaphore: A more general synchronization tool. It maintains a counter. The two primary operations on a semaphore are
wait(orP) andpost(orV).waitdecrements the counter (and blocks if the counter is zero), whilepostincrements it. A semaphore with a count of 1 is called a binary semaphore and can be used just like a mutex.
How Does a Lock Work Without a Race Condition?
You might wonder: if we're trying to prevent a race condition on our shared data, how do we avoid a race condition when trying to acquire the lock itself? Couldn't two processes check the lock's state, see it's free, and both decide to grab it at the same time?
This is prevented at the hardware level. Given your background in electronics, you'll find the underlying mechanism interesting. Modern CPUs provide atomic instructions. These are special instructions that are guaranteed by the processor to execute as a single, uninterruptible operation. A common example is a "Compare-and-Swap" instruction.
The following video segment explains this brilliantly.
how does a Mutex even work? (atoms in the computer??)
Let's continue with the 'how does a Mutex even work?' video. It does an excellent job explaining how atomic hardware instructions are the foundation upon which OS-level mutexes are built.
Watch from 01:33 to 03:46. Pay attention to the concept of atomic operations and how the compare and swap instruction in MIPS assembly solves the race condition of acquiring the lock itself.
This hardware guarantee is the bedrock of all software synchronization.
3. Implementing Synchronization with Semaphores
Now, let's apply these concepts to fix the shared memory programs from our last lesson. We'll use semaphores to coordinate our writer (producer) and reader (consumer) processes. This is a classic producer-consumer problem.
The video series by Jacob Sorber provides a perfect continuation from the last lesson. He takes the exact same shared memory example and adds semaphores to make it safe.
First, let's see a live demonstration of the race condition we're trying to solve.
What is a semaphore? How do they work? (Example in C)
This segment of 'What is a semaphore?' demonstrates the problem. When the writer and reader run concurrently without synchronization, messages are lost.
Watch from 05:02 to 07:55. Observe how running multiple writer programs results in the reader missing most of the messages. This is the race condition in action.
To solve this, we will use two semaphores:
- A "producer" semaphore: The writer will
postto this semaphore after it has written new data. The reader willwaiton it to know when new data is ready. - A "consumer" semaphore: The reader will
postto this semaphore after it has finished consuming the data. The writer willwaiton it to know when the buffer is free to be written to again.
The following video walks through the entire implementation using POSIX named semaphores.
What is a semaphore? How do they work? (Example in C)
Now, let's watch the solution. This video explains the semaphore operations and then implements them step-by-step in the C code.
Please watch the following segments: 00:33 - 03:25: This explains what semaphores are and the mechanics of the wait and post operations. 07:55 - 12:53: This is the core of the lesson. Follow along as the code is modified: Notice the use of sem_open() to create/open named semaphores. In the reader (consumer), see how sem_wait() on the producer semaphore makes it block until data is ready. In the writer (producer), see how sem_wait() on the consumer semaphore makes it block until the buffer is free. Trace how sem_post() is used by each process to signal the other. Finally, observe the successful result where no messages are lost.
This producer-consumer pattern using two semaphores is a powerful and widely-used technique for safely passing data between processes or threads.
4. Alternative: Unnamed Semaphores in Shared Memory
The video used named semaphores, which are identified by a string like /my_semaphore and exist as system-wide objects. Another common type is the unnamed semaphore. These are not identified by a name but exist in memory.
For two processes to share an unnamed semaphore, the semaphore variable itself (sem_t) must be placed in a region of shared memory. This is a critical point and a common source of bugs. If you just declare a sem_t variable in your code, each process will get its own private copy, and they won't be synchronizing at all!
Let's explore this with some reading and a code example.
These lecture notes from Purdue University discuss unnamed semaphores and explicitly state the requirement for placing them in shared memory for inter-process communication.
Please read through the slides from 'Using unnamed semaphores' to 'Finally, we will correct the errors...'. Pay close attention to: The rule: 'A process-shared semaphore must be placed in a shared memory region.' The 'bad example' (test1.c) and 'even worse example' (stuck.c) that show what happens when this rule is violated. The 'correct' example (nonstuck.c) which places the sem_t* p_mutex into the shared memory segment.
To see this in a complete, runnable example, we can refer to another resource that combines System V shared memory with POSIX unnamed semaphores.
Shared Memory Segments and POSIX Semaphores
This document provides a concise and complete code example that demonstrates how to create a shared memory segment just large enough for a semaphore, and then initialize and use it.
Please review the code for semtest3.c on slides 19 and 20. This is a master example for this technique. Trace the following steps: shmget is called to create a shared memory segment of size sizeof(sem_t). shmat is called to attach the segment, and the returned void* is cast to a sem_t *sp. sem_init(sp, 1, 2) is called. The first argument is the pointer to the shared memory location. The second argument 1 is crucial; it tells the OS this semaphore will be shared between processes. The semaphore is then used via the pointer sp.
This pattern of embedding the synchronization primitive within the shared memory region it protects is very common in embedded systems design as it co-locates the data and its lock.
Test your understanding!
You are designing an IPC mechanism where a camera process writes image frames into a large shared memory buffer, and a processing process reads them. You decide to use an unnamed semaphore placed at the very beginning of the shared memory buffer to act as a mutex, ensuring only one process accesses the buffer's metadata (e.g., frame counters, timestamps) at a time.
Assume shm_ptr is the void* returned from shmat() or mmap(). How would you initialize and then lock this semaphore?
Show answer
- Get a pointer to the semaphore: First, you would cast the pointer to the beginning of the shared memory to a
sem_t*.sem_t *mutex = (sem_t *)shm_ptr; - Initialize the semaphore (in one process only): The process that creates the shared memory would initialize the semaphore for inter-process sharing.
// The '1' for pshared is essential for inter-process use. // The '1' for value initializes it as an unlocked mutex. sem_init(mutex, 1, 1); - Lock the semaphore: Before accessing the shared metadata, any process would lock the semaphore using the pointer.
sem_wait(mutex); // ... critical section: access shared metadata ... sem_post(mutex);
Conclusion
Today, we took a critical step from functional but unsafe code to robust, concurrent code. You've seen why synchronization is necessary and how to implement it using one of the most fundamental primitives, the semaphore.
Key Takeaways:
- Concurrent access to shared memory without protection leads to race conditions, a major source of bugs and safety hazards.
- Code sections that access shared resources are called critical sections and must be protected to ensure mutual exclusion.
- Synchronization primitives like mutexes and semaphores are OS tools to enforce this protection. Their implementation relies on atomic hardware instructions.
- The producer-consumer pattern using two semaphores is a classic solution for safely passing data between processes.
- POSIX semaphores come in two flavors: named (system-wide objects) and unnamed (memory-based). For IPC, unnamed semaphores must be placed inside a shared memory segment.
Preview of the Next Lesson
These synchronization mechanisms are the low-level nuts and bolts. In your work with AUTOSAR, you operate at a higher level of abstraction. The COM stack handles the serialization of signals into PDUs and manages transmission, but deep down, it relies on these very principles of protected, shared buffers to pass data between different software components and drivers.
In our next lesson, we will analyze pointer usage patterns in AUTOSAR communication middleware, connecting these foundational IPC concepts to the high-level architectures you work with daily.