Operating System Process Synchronization and Communication Mechanisms

Critical Resources and Mutual Exclusion

Resources that require processes to access them in a mutually exclusive manner are termed critical resources.

Principles of Synchronization Mechanisms

  • Wait if idle: Allow entry when the resource is free.
  • Busy then wait: Block entry while the resource is occupied.
  • Bounded waiting: Guarantee that no process waits indefinitely.
  • Yield while waiting: A waiting process should release the CPU rather than busy-looping.

Hardware-Based Synchronization: Disabling Interrupts

By turning off interrupts, a process can guarantee that its critical section will not be preempted.

Drawbacks:

  • Increases system risk; a malicious or erroneous process could monopolize the CPU.
  • Only applicable in uniprocessor environments.

Hardware-Based Synchronization: Test-and-Set (TSL)

This atomic instruction records the current lock state, sets the lock to occupied, and returns the previous state.

bool test_and_set(bool *mutex_flag) {
    bool previous_state = *mutex_flag;
    *mutex_flag = true;
    return previous_state;
}

Usage in a spinlock pattern:

while (true) {
    // Other operations...
    while (test_and_set(&mutex_flag)) {
        // Spin until the lock is released
    }
    // Critical section entry
    // Execute mutually exclusive operations
    mutex_flag = false;
    // Critical section exit
    // Remainder section
}

Hardware-Based Synchronization: Swap

The Swap instruction atomically exchanges the values of two memory words.

void exchange_state(bool *ptr1, bool *ptr2) {
    bool temp_val = *ptr1;
    *ptr1 = *ptr2;
    *ptr2 = temp_val;
}

Usage for mutual exclusion:

while (true) {
    bool intent_flag = true;
    do {
        exchange_state(&mutex_flag, &intent_flag);
    } while (intent_flag != false);
    // Critical section entry
    // Execute mutually exclusive operations
    mutex_flag = false;
    // Remainder section
}

Semaphore Mechanisms

A semaphore represents the quantity of available instances of a specific system resource.

  • Integer semaphore: Tracks resource count, supporting only initialization, wait (P), and signal (V) operations.
  • Structured (Record-type) semaphore: Utilizes a data structure combining a counter and a process waiting queue.
  • Semaphore sets: Allows requesting multiple units across various resource categories simultaneously.

Integer Semaphore

Initialized to a non-negative integer. The P and V operations are defined as:

void acquire(int &sem) {
    while (sem <= 0) { /* busy wait */ }
    sem--;
}

void release(int &sem) {
    sem++;
}

Structured Semaphore

Defined using a record structure to handle both the resource count and blocked processes.

typedef struct {
    int resource_count;
    ProcessControlBlock *blocked_queue;
} SyncSemaphore;

The resource_count reflects available resources when positive, or the count of waiting processes when negative. The blocked_queue links to the PCBs of suspended processes.

void acquire(SyncSemaphore *sem) {
    sem->resource_count--;
    if (sem->resource_count < 0) {
        suspend_process(sem->blocked_queue);
    }
}

void release(SyncSemaphore *sem) {
    sem->resource_count++;
    if (sem->resource_count <= 0) {
        resume_process(sem->blocked_queue);
    }
}

AND Semaphores

Used when a process requires simultaneous allocation of multiple resource types to proceed.

void acquire_all(SyncSemaphore *sem_array[], int count) {
    while (true) {
        bool all_available = true;
        for (int i = 0; i < count; i++) {
            if (sem_array[i]->resource_count < 1) {
                all_available = false;
                suspend_process(sem_array[i]->blocked_queue);
                // Restart acquisition attempt upon wakeup
                break;
            }
        }
        if (all_available) {
            for (int i = 0; i < count; i++) {
                sem_array[i]->resource_count--;
            }
            break;
        }
    }
}

Semaphore Sets

Processes can request multiple units of multiple resource types. acquire_set(S1, t1, d1, ..., Sn, tn, dn) allocates d1 units of S1 only if S1 has at least t1 units available.

Special cases:

  • acquire_set(S, d, d): Requests d units; fails if fewer than d are available.
  • acquire_set(S, 1, 1): Behaves as a standard mutex if S=1, or a counting semaphore if S>1.
  • acquire_set(S1, 1, 1, S2, 1, 1): Equivalent to an AND semaphore.
  • acquire_set(S, 1, 0): Functions as a conditional switch (checking availability without consuming it).

Applying Semaphores for Mutual Exclusion

Define a mutex semaphore initialized to 1. Wrap critical sections with acquire(mutex) and release(mutex).

Applying Semaphores for Precedence Relations

To enforce execution order P1 -> P2, introduce a semaphore sync_var initialized to 0. P1 calls release(sync_var) after its task, and P2 calls acquire(sync_var) before its task.

SyncSemaphore sync_var = 0;

void task1() {
    execute_step1();
    release(sync_var);
}

void task2() {
    acquire(sync_var);
    execute_step2();
}

void main() {
    parallel_begin(task1(), task2());
    parallel_end();
}

The parallel_begin and parallel_end constructs allow concurrent execution, while the semaphore enforces the required sequence.

Classic Synchronization Problems

Producer-Consumer Problem

Producer logic:

void producer() {
    while (true) {
        Data item = generate_item();
        acquire(empty_slots);
        acquire(access_mutex);
        storage[insert_idx] = item;
        insert_idx = (insert_idx + 1) % capacity;
        release(access_mutex);
        release(filled_slots);
    }
}

Consumer logic:

void consumer() {
    while (true) {
        acquire(filled_slots);
        acquire(access_mutex);
        Data item = storage[extract_idx];
        extract_idx = (extract_idx + 1) % capacity;
        release(access_mutex);
        release(empty_slots);
        process_item(item);
    }
}

Dining Philosophers Problem

SyncSemaphore utensils[5] = {1, 1, 1, 1, 1};

void philosopher(int idx) {
    while (true) {
        acquire(utensils[idx]);
        acquire(utensils[(idx + 1) % 5]);
        eat();
        release(utensils[idx]);
        release(utensils[(idx + 1) % 5]);
        think();
    }
}

Readers-Writers Problem

Basic solution with a single mutex allows concurrent reads but risks writer starvation.

SyncSemaphore entry_mutex = 1;

void reader() {
    acquire(entry_mutex);
    read_data();
    release(entry_mutex);
}

void writer() {
    acquire(entry_mutex);
    write_data();
    release(entry_mutex);
}

Introducing a reader count allows multiple readers but still requires a mutex to modify the count safely.

SyncSemaphore entry_mutex = 1;
SyncSemaphore count_mutex = 1;
int active_readers = 0;

void reader() {
    acquire(count_mutex);
    active_readers++;
    if (active_readers == 1) acquire(entry_mutex);
    release(count_mutex);

    read_data();

    acquire(count_mutex);
    active_readers--;
    if (active_readers == 0) release(entry_mutex);
    release(count_mutex);
}

void writer() {
    acquire(entry_mutex);
    write_data();
    release(entry_mutex);
}

Monitors as Synchronization Tools

A monitor is an abstract data type encapsulating shared data and the operations permitted on it, ensuring mutual exclusion.

monitor ResourceController {
    shared data declarations;
    condition variables;

    public void operation1() { /* ... */ }
    public void operation2() { /* ... */ }

    initialization_code() { /* ... */ }
}

Characteristics: Modular, abstract, inherently mutually exclusive (only one process active inside at a time).

Handling concurrent entry:

  • Hoare: The awakening process yields immediately to the waiting process.
  • Mesa: The waiting process must re-evaluate conditions upon waking.
  • Hansen: The signal operation must be the final step in a monitor procedure.

Producer-Consumer using a monitor:

monitor BufferManager {
    Data queue[SIZE];
    int head, tail, count;
    condition not_full, not_empty;

    public void insert(Data val) {
        if (count >= SIZE) cwait(not_full);
        queue[tail] = val;
        tail = (tail + 1) % SIZE;
        count++;
        csignal(not_empty);
    }

    public Data extract() {
        if (count <= 0) cwait(not_empty);
        Data val = queue[head];
        head = (head + 1) % SIZE;
        count--;
        csignal(not_full);
        return val;
    }

    initialization_code() {
        head = 0; tail = 0; count = 0;
    }
} buffer_instance;

void producer() {
    while (true) {
        Data item = generate_item();
        buffer_instance.insert(item);
    }
}

void consumer() {
    while (true) {
        Data item = buffer_instance.extract();
        process_item(item);
    }
}

Inter-Process Communication (IPC)

IPC enables information exchange between processes.

  • Low-level: Restricted to control signals, inefficient for bulk data.
  • High-level: Supports substantial data transfers efficiently and transparently.

Message Passing Systems

Direct communication uses explicit naming:

  • send(destination, message)
  • receive(source, message)

Synchronization variants: Blocking send/blocking receive, Non-blocking send/blocking receive, Non-blocking both.

Message buffer queue implementation:

typedef struct {
    int origin_id;
    int length;
    char *payload;
    struct MsgBuffer *next;
} MsgBuffer;

Send primitive:

void dispatch_message(int target, MessageEnvelope env) {
    MsgBuffer *node = allocate_buffer(env.length);
    node->origin_id = env.origin_id;
    node->length = env.length;
    copy_payload(node->payload, env.payload);
    node->next = null;

    ProcessControlBlock *dest_pcb = find_pcb(target);
    acquire(dest_pcb->queue_mutex);
    append_buffer(dest_pcb->msg_queue, node);
    release(dest_pcb->queue_mutex);
    release(dest_pcb->msg_semaphore);
}

Receive primitive:

void fetch_message(MessageEnvelope env) {
    ProcessControlBlock *self_pcb = current_pcb();
    acquire(self_pcb->msg_semaphore);
    acquire(self_pcb->queue_mutex);
    MsgBuffer *node = remove_first(self_pcb->msg_queue);
    release(self_pcb->queue_mutex);

    env.origin_id = node->origin_id;
    env.length = node->length;
    copy_payload(env.payload, node->payload);
    free_buffer(node);
}

Indirect Communication (Mailboxes)

Processes communicate via shared intermediary entities known as mailboxes or ports.

Client-Server Systems

  • Sockets: File-based (local) or network-based endpoints for bidirectional data streams.
  • Remote Procedure Calls (RPC): Enables a client to invoke a procedure on a remote server host as if it were local.

Tags: operating systems Process Synchronization Semaphores Concurrency IPC

Posted on Fri, 30 Oct 2026 16:43:37 +0000 by hamishrock