With race conditions involved, safely sharing memory between processes or threads requires some attention to detail. In order to address memory mapping, semaphores, mutexes, and processes/threads creation, this sample is in fact two go-to source codes that highlight the right way to deal with these concepts!
The zip file you can download at the end of this post contains two files:
- fork-signal-kill.c: A program that forks and waits for an external signal to kill the execution of both the parent and the child.
- thread-signal-kill.c: A program that creates another thread and waits for an external signal to kill its execution.
How to Use These Files
First, to compile the programs you’ll need to run the following commands on a terminal.
$ gcc -pthread -o fork-signal-kill fork-signal-kill.c
$ gcc -pthread -o thread-signal-kill thread-signal-kill.c
Be advised that each program expects SIGUSR1 to quietly terminate. As both print their respective PIDs, you’ll need to issue a signal to them using the terminal:
$ kill -SIGUSR1 <pid>
Execution-wise, both programs prints strings until said signal is received.
They may look the same and that’s the idea. Their differences can be better spotted if you place their source code side by side.
OBS: The programs are running in the background. Highlighted lines indicate where the terminal was active.
This program creates two processes and use an anonymous memory mapping to share a variable between them. Race conditions are addressed using semaphores.
Below you can see each process PID being printed as each check for whether their loop condition is still valid.
[fork-signal-kill] parent start
[fork-signal-kill] parent PID: 21258
[fork-signal-kill] child PID: 21260
[fork-signal-kill] child start
 Signal received
[fork-signal-kill] the child terminated normally
[fork-signal-kill] child's exit status: 0
[fork-signal-kill] parent end
+ Done ./fork-signal-kill
This program creates two threads and no memory mapping is needed. Race conditions are addressed using mutexes.
Below you can see a representation of each thread ID being printed as each check for whether their loop condition is still valid.
[thread-signal-kill] main thread start
[thread-signal-kill] My PID: 15571
[thread-signal-kill] 2nd thread start
[15571->0] Signal received!
[thread-signal-kill] 2nd thread returned as expected
[thread-signal-kill] main thread end
+ Done ./thread-signal-kill
Generally, both programs do the same thing:
- Once started, they set a custom function to handle the signal SIGUSR1;
- They proceed to create a way to handle access to a shared memory (a memory map and semaphore, or a mutex);
- They start either another process or another thread;
- Now each process/thread begins a loop that just prints to stdout;
- Both loops use the same variable as its condition;
- As soon as SIGUSR1 is received, the previously configured function changes the loop’s condition to false;
- Both processes/threads exit their loops and return;
- Before exiting, main() deallocate and destroy whatever is necessary.
The difference between them is precisely their use of processes or threads and the implications that follow.
Sharing Memory Between Processes
By default, two processes don’t share resources, not even after a fork() call. To achieve this, one process needs to create a mapping in its virtual address space so once it forks, its child will have a copy of the mapped address.
The attached code uses mmap() to create an anonymous map (not backed by any actual file), in what can be thought as the simplest way to make two processes share variables. Once the mapping is successful, its address is stored in a global pointer and by the end of the program, munmap() deletes the address range stored in it, concluding its use.
Before that, though, two process see the same virtual address range and some protection is needed to guarantee no two concurrent access, particularly if both write to it. The answer to this problem is the use of a semaphore calling the function sem_init() to initialize an unnamed semaphore inside the shared memory space. From this point on, every access to the mapped area uses this semaphore to coordinate reading from and writing to it.
Semaphores have two main uses: mutual exclusion and flow control. This example code uses it just to force one process to wait while the other uses the shared memory because no flow control is needed.
There is still one particular problem with shared memory: you should never forget to release everything inside it before calling munmap(). Afterwards, references to it will generate SIGSEGV (an invalid memory reference signal) and whatever resource that existed prior to the unmaping will have its behavior undefined at best…
Protecting Memory Access Between Threads
The other way to share memory is to create multiple threads of the same process since all of them have access to the same resources. Memory sharing, in this case, is the default, but not without its share of complications, though.
Same as before, there is a need to protect access to global variables. This time, I choose to use a mutex to do this.
Mutexes are used for mutual exclusion between threads because they act as a lock that has only one key. They behave as the semaphore does in the other exemple, blocking threads while one is using a shared resource.
Keep in mind that once a process creates multiple threads, more than one can be running at the same time if the processor has more than one core. Race conditions are common in this scenario, so never forget to use either semaphores or mutexes in your code to avoid bugs.
Signals In Unix
There’s still a final subject to be approached: what happens when you send SIGUSR1 to any of these programs?
Consider what happens after both codes change the default handler and set a specific action: as soon as the signal is received, any given thread stops and jumps to the callback function.
In Unix, no new thread is created to receive a signal. This is specially important in multithreaded applications, there is no way of knowing which will be chosen!
I once run into the following bug: the callback function blocked on SIGINT (aka CTRL-C) trying to lock a mutex. After the first thread got stuck, every SIGINT that followed ended up blocking another thread until there were not a single one available to handle incoming signals, meaning all threads were blocked because of a mutex misuse…
The attached source codes may be used and modified at your will, except for commercial use.
I tried to include as much good practices as I could so they can be pretty useful as go-to examples whenever you need a refresh on these subjects!
Don’t forget to leave any questions in the comments, in case you need some help, and good luck!