Understanding Processes, Threads, and Multiprocessing in Operating Systems

Process and Thread Management

1. PCBs (Process Control Blocks) enable multiprocessing by storing and managing vital information about each process. During process switching, the OS preserves this data and loads the next process’s information, facilitating the concurrent execution of multiple processes. A process represents an independent unit of execution, while a thread is a smaller execution unit residing within a process, sharing resources with other threads within the same process. Processes generally operate in isolation.

2. Linux is often considered more secure due to its user-based permissions model, which adds an extra layer of security by restricting access to system resources.

Long-Term vs. Short-Term Scheduling

3. The long-term scheduler determines which processes enter the system and when, ensuring efficient and balanced resource utilization over extended periods. Conversely, the short-term scheduler manages the execution of processes on the CPU, making rapid decisions to optimize CPU utilization in the short term. The short-term scheduler operates more frequently and rapidly than the long-term scheduler.

Shared Memory and Processes

4. Processes typically avoid using shared memory directly because unsynchronized access can lead to data inconsistency issues. When multiple processes modify shared data without coordination, they may overwrite each other’s changes, resulting in data corruption.

Fork System Call and Threading

Fork

1. The appearance of delayed outputs suggests the presence of threading, where multiple threads execute concurrently within a process.

2. C threads can declare variables globally, allowing direct access from all threads. In contrast, Java threads require passing references to share data between threads.

Code Example

int main() { // Process 1
    // Thread:
    void *threadFunc1(void *arg) {
        char* text = (char*) arg;
        printf("First Thread: %s\n", text);
        pthread_exit(0);
    }

    pid_t pid1, pid2;
    // Thread:
    void *threadFunc2(void *arg) {
        char* text = (char*) arg;
        printf("Second Thread: %s\n", text);
        pthread_exit(0);
    }

    printf("Process 1: %d\n", getpid()); // Output the pid of Process 1
    int main(int argc, char *argv[]) {
        char* name = "Wonderbread!";
        int LEN = 12;
        char* p1 = (char*)malloc(sizeof(char) * ((LEN - 1) / 2 + 1));
        // (do same for p2)

        pid1 = fork();
        for (int i = 0; i < ...
    }

    if (pid1 < 0) {
        fprintf(stderr, "Fork Failed");
        return 1;
    } else if (pid1 == 0) { // Process 2
        printf("Process 2: %d\n", getpid()); // Output the pid of Process 2
        pid2 = fork();
        if (pid2 < 0) {
            fprintf(stderr, "Fork Failed");
            return 1;
        } else if (pid2 == 0) { // Process 3
            printf("Process 3: %d\n", getpid()); // Output the pid of Process 3
        } else {
            wait(NULL); // Runs in all three processes. In P1 and P2, causes them
                        // to wait for child. (P3 has no child.)
            return 0;
        }
    } else {
        pthread_join(thread1, NULL);
        pthread_join(thread2, NULL);
        return 0;
    }
}

Challenges in Multicore Programming

Five common issues in multicore programming:

  1. Identifying Tasks: Accurately identifying independent tasks within a program for parallel execution.
  2. Balance: Ensuring that each thread or process performs a comparable amount of work to avoid load imbalances.
  3. Data Dependency: Managing dependencies between tasks to prevent data races and ensure correct results.
  4. Synchronization: Coordinating access to shared resources to prevent data corruption and maintain consistency.
  5. Testing and Debugging: Thoroughly testing and debugging multithreaded or multiprocess programs to identify and resolve concurrency-related issues.

Synchronization and Critical Sections

1. Without proper synchronization mechanisms, the order of output in concurrent programs becomes unpredictable, leading to race conditions where the final result depends on the unpredictable timing of thread or process execution.

2. Blocking communication can introduce synchronization by forcing processes to wait for each other to confirm message transmission and reception. This synchronization is momentary and limited to the communication points.

3. Critical sections provide a mechanism to protect shared resources from concurrent access. By ensuring that only one thread or process can enter a critical section at a time, data corruption caused by simultaneous writes is prevented.

4. Atomic operations, such as test-and-set and compare-and-swap, execute as a single, uninterruptible unit. This atomicity prevents race conditions by ensuring that the operation cannot be preempted or partially modified by other threads or processes.

5. Locking should be applied judiciously, only when necessary to protect shared resources. Locking an entire function can lead to performance degradation due to increased contention and reduced concurrency. Critical sections should be as small as possible to minimize the time spent holding locks.

Mutex Implementation Example

struct mutex_lock {
    int lock;
};

struct mutex_lock* mutex_create() {
    struct mutex_lock* m = (struct mutex_lock*)malloc(sizeof(struct mutex_lock));
    m->lock = 0;
    return m;
}

void mutex_acquire(mutex_lock* m) {
    while (test_and_set(&m->lock));
}

void mutex_release(mutex_lock* m) {
    m->lock = 0;
}

wdYea1+aVb5fwAAAABJRU5ErkJggg== X9pDKESodVejAAAAABJRU5ErkJggg==