What is Synchronization?
Imagine two processes both trying to update the same file at the exact same moment. Without any coordination, one process’s changes might overwrite the other’s — producing garbage data. This is called a race condition.
Synchronization is the set of techniques used to coordinate processes (or threads) so they don’t interfere with each other. The section of code that accesses shared resources is called a critical section, and only one process should be in it at a time.
Linux provides two main synchronization primitives for IPC: semaphores and file locks.
| Time | Process A | Process B | counter value in RAM |
|---|---|---|---|
| T=1 | reads counter → gets 5 | — | 5 |
| T=2 | — | reads counter → gets 5 | 5 |
| T=3 | adds 1 → result=6 | — | 5 (not written yet) |
| T=4 | — | adds 1 → result=6 | 5 (not written yet) |
| T=5 | writes 6 to counter | — | 6 ← WRONG! |
| T=6 | — | writes 6 to counter | 6 ← should be 7! |
The fix is to use a semaphore or file lock to ensure only one process reads-modifies-writes the counter at a time.
A semaphore is essentially an integer counter maintained by the kernel. Two operations are defined:
Decrement the semaphore. If it becomes negative, block (sleep) until it goes positive again. Used to “enter” a critical section.
Increment the semaphore. If other processes are waiting, wake one up. Used to “exit” a critical section.
Resource available
Resource in use
Processes waiting
Two Semaphore APIs on Linux
| Feature | System V Semaphore | POSIX Semaphore |
|---|---|---|
| API functions | semget, semop, semctl | sem_open, sem_wait, sem_post, sem_close |
| Named? | Integer key (not user-friendly) | Named (/name) or unnamed |
| Granularity | Array of semaphores per set | Single semaphore per object |
| Complexity | High (semop uses sembuf struct) | Low (simple functions) |
| Preferred for | Legacy code | New code |
Coding Example 1 – POSIX Named Semaphore (Protecting Shared Memory)
/*
* posix_sem_shm.c
* Uses a POSIX named semaphore to protect a shared memory counter.
* Run two instances simultaneously to see synchronization in action.
*
* Compile: gcc posix_sem_shm.c -o posix_sem_shm -lrt -lpthread
* Run:
* ./posix_sem_shm write & # process A in background
* ./posix_sem_shm write # process B in foreground
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#define SHM_NAME "/counter_shm"
#define SEM_NAME "/counter_sem"
#define ITERATIONS 100000
int main(int argc, char *argv[]) {
/* Open/create shared memory for the counter */
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0644);
ftruncate(shm_fd, sizeof(int));
int *counter = mmap(NULL, sizeof(int),
PROT_READ | PROT_WRITE, MAP_SHARED,
shm_fd, 0);
/* Open/create the semaphore (initial value = 1 = unlocked) */
sem_t *sem = sem_open(SEM_NAME, O_CREAT, 0644, 1);
if (sem == SEM_FAILED) {
perror("sem_open");
exit(EXIT_FAILURE);
}
if (argc > 1 && strcmp(argv[1], "init") == 0) {
*counter = 0;
printf("Initialized counter to 0\n");
goto cleanup;
}
printf("[PID %d] Starting increment loop...\n", getpid());
for (int i = 0; i < ITERATIONS; i++) {
/* --- CRITICAL SECTION START --- */
sem_wait(sem); /* Lock: decrement semaphore */
(*counter)++; /* Safe: only one process here at a time */
sem_post(sem); /* Unlock: increment semaphore */
/* --- CRITICAL SECTION END --- */
}
printf("[PID %d] Done. Counter = %d\n", getpid(), *counter);
cleanup:
munmap(counter, sizeof(int));
close(shm_fd);
sem_close(sem);
/* Uncomment to clean up after both processes finish: */
/* shm_unlink(SHM_NAME); sem_unlink(SEM_NAME); */
return 0;
}
/*
* Without sem_wait/sem_post:
* Final counter might be less than 200000 (race condition)
*
* With sem_wait/sem_post:
* Final counter = exactly 200000 (two processes × 100000 each)
*/
Coding Example 2 – Unnamed POSIX Semaphore (Thread Synchronization)
/*
* sem_threads.c
* Unnamed POSIX semaphore used to synchronize producer/consumer threads.
* sem_init() for unnamed semaphores (in shared memory or stack).
*
* Compile: gcc sem_threads.c -o sem_threads -lpthread
*/
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <pthread.h>
#include <unistd.h>
#define BUFFER_SIZE 5
int buffer[BUFFER_SIZE];
int buf_in = 0; /* Next slot to write */
int buf_out = 0; /* Next slot to read */
sem_t empty_slots; /* Counts empty slots (starts at BUFFER_SIZE) */
sem_t full_slots; /* Counts filled slots (starts at 0) */
void *producer(void *arg) {
for (int i = 1; i <= 10; i++) {
sem_wait(&empty_slots); /* Wait for an empty slot */
buffer[buf_in] = i; /* Write item */
buf_in = (buf_in + 1) % BUFFER_SIZE;
printf("[Producer] Produced: %d\n", i);
sem_post(&full_slots); /* Signal: one more item available */
usleep(100000);
}
return NULL;
}
void *consumer(void *arg) {
for (int i = 0; i < 10; i++) {
sem_wait(&full_slots); /* Wait for an item to consume */
int item = buffer[buf_out];
buf_out = (buf_out + 1) % BUFFER_SIZE;
printf("[Consumer] Consumed: %d\n", item);
sem_post(&empty_slots); /* Signal: one slot is now empty */
usleep(200000);
}
return NULL;
}
int main(void) {
pthread_t prod_thread, cons_thread;
/* Initialize unnamed semaphores
* sem_init(sem, pshared, initial_value)
* pshared=0: shared between threads of same process
* pshared=1: shared between processes (place in shared memory)
*/
sem_init(&empty_slots, 0, BUFFER_SIZE); /* BUFFER_SIZE empty slots */
sem_init(&full_slots, 0, 0); /* 0 items to consume */
pthread_create(&prod_thread, NULL, producer, NULL);
pthread_create(&cons_thread, NULL, consumer, NULL);
pthread_join(prod_thread, NULL);
pthread_join(cons_thread, NULL);
sem_destroy(&empty_slots);
sem_destroy(&full_slots);
return 0;
}
/*
* Output (interleaved):
* [Producer] Produced: 1
* [Consumer] Consumed: 1
* [Producer] Produced: 2
* [Producer] Produced: 3
* [Consumer] Consumed: 2
* ...
*/
A file lock is a synchronization mechanism that lets processes coordinate access to a file or a region of a file. Linux provides two interfaces:
- Locks an entire file
- Two types: LOCK_SH (shared/read) and LOCK_EX (exclusive/write)
- Simple to use
- Not visible to NFS (network file systems)
- All open file descriptions of the same file in same process share the lock
- Can lock a byte range (part of a file)
- Works over NFS (advisory)
- F_SETLK (non-blocking), F_SETLKW (blocking wait)
- Locks released on file close or process exit
- Used in databases, lock files
| Process requests → | Shared (Read) Lock | Exclusive (Write) Lock |
|---|---|---|
| No lock held | ✓ GRANTED | ✓ GRANTED |
| Shared lock held by others | ✓ GRANTED (multiple readers OK) | ✗ BLOCKED (wait for readers) |
| Exclusive lock held by another | ✗ BLOCKED | ✗ BLOCKED |
Coding Example 3 – flock() for Whole-File Locking
/*
* flock_demo.c
* Two processes cooperate on a shared log file.
* flock() ensures only one process writes at a time.
*
* Compile: gcc flock_demo.c -o flock_demo
* Run multiple instances: ./flock_demo & ./flock_demo & ./flock_demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h> /* flock() */
#include <string.h>
#include <time.h>
#define LOG_FILE "/tmp/shared_log.txt"
int main(void) {
int fd = open(LOG_FILE, O_CREAT | O_WRONLY | O_APPEND, 0644);
if (fd == -1) {
perror("open");
exit(EXIT_FAILURE);
}
for (int i = 1; i <= 5; i++) {
/* Acquire exclusive lock — blocks if another process holds it */
printf("[PID %d] Waiting for lock...\n", getpid());
if (flock(fd, LOCK_EX) == -1) {
perror("flock");
exit(EXIT_FAILURE);
}
printf("[PID %d] Lock acquired. Writing log entry %d\n", getpid(), i);
/* Write log entry */
char buf[128];
int len = snprintf(buf, sizeof(buf),
"PID=%d, entry=%d, time=%ld\n",
getpid(), i, (long)time(NULL));
write(fd, buf, len);
/* Simulate work while holding lock */
usleep(200000);
/* Release the lock */
flock(fd, LOCK_UN);
printf("[PID %d] Lock released.\n", getpid());
usleep(50000); /* Brief pause before next iteration */
}
close(fd);
return 0;
}
/*
* Without flock: log entries from different processes interleave,
* producing garbled output.
*
* With flock: each process writes its complete entry before another
* can acquire the lock.
*/
Coding Example 4 – fcntl() Record Lock (Byte-Range Lock)
/*
* fcntl_lock_demo.c
* fcntl() locks a specific byte range in a file.
* Useful when multiple processes update different records of a database file.
*
* Compile: gcc fcntl_lock_demo.c -o fcntl_lock_demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#define RECORD_SIZE 64
#define DB_FILE "/tmp/demo.db"
/* Lock a record (byte range) in the file */
int lock_record(int fd, int record_num, short lock_type) {
struct flock fl;
fl.l_type = lock_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */
fl.l_whence = SEEK_SET;
fl.l_start = record_num * RECORD_SIZE; /* Offset of record */
fl.l_len = RECORD_SIZE; /* Size of record */
fl.l_pid = 0; /* Not used for setting */
/* F_SETLKW: blocking wait until lock is available */
return fcntl(fd, F_SETLKW, &fl);
}
/* Unlock a record */
int unlock_record(int fd, int record_num) {
return lock_record(fd, record_num, F_UNLCK);
}
int main(void) {
int fd = open(DB_FILE, O_CREAT | O_RDWR, 0644);
if (fd == -1) { perror("open"); exit(EXIT_FAILURE); }
/* Pre-fill with 5 records */
char record[RECORD_SIZE];
for (int i = 0; i < 5; i++) {
snprintf(record, RECORD_SIZE, "Record-%d: value=%d\n", i, i * 10);
pwrite(fd, record, RECORD_SIZE, i * RECORD_SIZE);
}
/* Lock and update only record 2 */
int rec = 2;
printf("[PID %d] Locking record %d for writing...\n", getpid(), rec);
if (lock_record(fd, rec, F_WRLCK) == -1) {
perror("lock_record");
exit(EXIT_FAILURE);
}
printf("[PID %d] Lock acquired. Updating record %d...\n", getpid(), rec);
/* Update record 2 while other processes can still access records 0,1,3,4 */
snprintf(record, RECORD_SIZE, "Record-%d: value=%d UPDATED\n", rec, 999);
pwrite(fd, record, RECORD_SIZE, rec * RECORD_SIZE);
sleep(2); /* Hold lock briefly */
unlock_record(fd, rec);
printf("[PID %d] Record %d unlocked.\n", getpid(), rec);
/* Read and print all records */
for (int i = 0; i < 5; i++) {
memset(record, 0, RECORD_SIZE);
pread(fd, record, RECORD_SIZE, i * RECORD_SIZE);
printf(" Record %d: %s", i, record);
}
close(fd);
return 0;
}
/*
* Key advantage of fcntl over flock:
* Process A locks record 2, Process B can simultaneously lock record 4.
* With flock, locking the file would block both from accessing ANY record.
* fcntl() enables fine-grained (record-level) concurrent access.
*/
| Scenario | Best Choice | Why |
|---|---|---|
| Parent-child on same machine | Pipe | Simple, built-in, no setup needed |
| Unrelated processes, same machine | FIFO or POSIX MQ | Named, both can open independently |
| Network communication | Socket (TCP/UDP) | Only IPC mechanism that works over network |
| Large data, high frequency | Shared Memory + Semaphore | Zero-copy; fastest for bulk data |
| Coordinating file access | flock() or fcntl() | Direct integration with file I/O |
| Thread synchronization | POSIX mutex / sem_init(pshared=0) | Lower overhead than cross-process semaphores |
| Async notification only | Signal (SIGUSR1/SIGUSR2) | Lightweight, no data needed |
Three categories: Communication, Synchronization, Signals. Historical reasons for multiple similar facilities.
Data transfer (byte stream vs message) vs shared memory. Trade-offs: speed, copy overhead, destructive reads, synchronization needs.
Semaphores (POSIX/SysV), file locks (flock/fcntl). Race conditions, critical sections, advisory vs mandatory locking.
← File 1: IPC Overview ← File 2: Communication Start from Beginning →
