Synchronization Facilities Semaphores · File Locks · Race Conditions · Critical Sections

 

Chapter 43 – Synchronization Facilities
Semaphores · File Locks · Race Conditions · Critical Sections
43.3
Section
2
Lock Types
File 3/3
of Chapter 43

Key Terms in This File
Synchronization Semaphore File Lock flock() fcntl() Race Condition Critical Section Mutex POSIX Semaphore System V Semaphore

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.

Why Synchronization is Needed — Race Condition Demo

Timeline: Two processes updating a counter without synchronization
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!
Result: Expected counter = 7 (two increments), but got 6. One increment was lost. This is a race condition.

The fix is to use a semaphore or file lock to ensure only one process reads-modifies-writes the counter at a time.

Semaphores

A semaphore is essentially an integer counter maintained by the kernel. Two operations are defined:

P() / wait() / sem_wait()

Decrement the semaphore. If it becomes negative, block (sleep) until it goes positive again. Used to “enter” a critical section.

V() / post() / sem_post()

Increment the semaphore. If other processes are waiting, wake one up. Used to “exit” a critical section.

Semaphore State Diagram (Binary Semaphore = Mutex)
sem = 1
UNLOCKED
Resource available
sem_wait() → sem=0
sem=0 → sem=1 ← sem_post()
sem = 0
LOCKED
Resource in use
sem_wait() → sem=-1 (block)
sem < 0
CONTENDED
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
 *   ...
 */

File Locks – flock() and fcntl()

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:

flock() — BSD-style locks

  • 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
fcntl() — POSIX record locks

  • 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

Lock Compatibility Matrix
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.
 */
Advisory vs. Mandatory Locks: Both flock() and fcntl() implement advisory locks on Linux by default. This means the kernel won’t enforce them — they only work if ALL processes that access the file agree to check/acquire the lock. Mandatory locking exists but is deprecated in modern kernels.

Choosing the Right IPC Mechanism
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

Interview Questions – Synchronization Facilities
Q1. What is a race condition? Give a real-world example.
A race condition occurs when the result of a computation depends on the unpredictable ordering of operations in multiple concurrent processes/threads. Example: Two bank processes read an account balance of ₹1000, each add ₹500, and write back. The final balance might be ₹1500 instead of ₹2000 because the second write overwrites the first’s result.
Q2. What is a semaphore and what are its two fundamental operations?
A semaphore is a kernel-maintained integer counter used for synchronization. The two operations are: P/wait/sem_wait() — decrement; if it reaches negative, block. V/post/sem_post() — increment; wake a blocked process if any are waiting. A binary semaphore (initial value 1) acts like a mutex.
Q3. What is the difference between flock() and fcntl() for file locking?
flock() locks an entire file (whole-file locking), is simpler, but not visible over NFS. fcntl() provides record locking — it can lock specific byte ranges within a file, works over NFS, and supports both blocking (F_SETLKW) and non-blocking (F_SETLK) variants. fcntl() is preferred for database-style applications where different processes need concurrent access to different parts of the same file.
Q4. Are file locks on Linux mandatory or advisory by default?
Advisory by default. This means the kernel does not prevent an uncooperative process from reading/writing a locked file — the lock only works if all processes accessing the file agree to check and respect the locks. Mandatory locking existed via mount options but is deprecated in modern Linux kernels.
Q5. What is the difference between a named and an unnamed POSIX semaphore?
Named semaphores (sem_open, sem_close, sem_unlink) are accessible by any process that knows the name — they persist in the kernel like files. Unnamed semaphores (sem_init, sem_destroy) are just a sem_t variable in memory; if pshared=0, for threads only; if pshared=1 and placed in shared memory, usable between processes. Named semaphores are easier for unrelated processes; unnamed are lighter for threads.
Q6. What happens if a process holding a semaphore crashes?
For POSIX named semaphores, the semaphore value is NOT automatically reset — other processes waiting on sem_wait() will block forever (deadlock). For System V semaphores, the SEM_UNDO flag can be used with semop() to automatically undo semaphore operations if the process exits. POSIX robust mutexes (pthread_mutexattr_setrobust) address a similar problem for shared mutexes.
Q7. What is a critical section? How do you protect it?
A critical section is a block of code that accesses a shared resource (shared memory, file, counter) and must not be executed by more than one process/thread simultaneously. It is protected by acquiring a lock (semaphore, mutex, file lock) before entering and releasing it after exiting. The lock ensures mutual exclusion.
Q8. What is the difference between a counting semaphore and a binary semaphore?
A binary semaphore has values 0 or 1 only — it acts as a mutex (mutual exclusion lock). A counting semaphore can have any non-negative integer value — used to manage a pool of resources (e.g., 5 database connections available). sem_wait() decrements (claim a resource), sem_post() increments (return a resource). When the count reaches 0, all further sem_wait() calls block.

Chapter 43 – Complete Summary
File 1: IPC Taxonomy

Three categories: Communication, Synchronization, Signals. Historical reasons for multiple similar facilities.

File 2: Communication

Data transfer (byte stream vs message) vs shared memory. Trade-offs: speed, copy overhead, destructive reads, synchronization needs.

File 3: Synchronization

Semaphores (POSIX/SysV), file locks (flock/fcntl). Race conditions, critical sections, advisory vs mandatory locking.

Leave a Reply

Your email address will not be published. Required fields are marked *