NivaarExam PrepOfficial exam papers ↗

25-Comp-A5 Operating Systems · December 2014

Question 5 of 7: Critical Section Analysis, Monitors, and Semaphores

Nivaar worked solution (AI-drafted; not reviewed by a licensed engineer)

Notes on this paper

98-COMP A-5 Operating Systems — National Examinations, December 2014. 3 hours, closed book, 100 marks. Candidates were instructed to answer any five of the seven questions; all seven are answered below as a complete study resource.

Reference texts: Silberschatz, Galvin & Gagne, Operating System Concepts (10th ed.) — scheduling (ch. 5), process synchronization/monitors (ch. 6–7), deadlocks (ch. 8), memory management/paging (ch. 9–10), file systems and disk scheduling (ch. 11–12); Tanenbaum, Modern Operating Systems (5th ed.), corroborating chapters on scheduling and file systems.

Question 5: Critical Section Analysis, Monitors, and Semaphores (20 marks)

Question text not reproduced: the examination questions are © Engineers and Geoscientists BC. Open the official past paper (linked at the top of this page) to read the question, then follow the worked solution below.

(a) Given. The two-process algorithm above, each using only its own boolean flag (get_entry[i]) and the other's flag — there is no shared turn/priority variable. Find. Every distinct design flaw, with a concrete triggering scenario for each. Approach. Trace specific interleavings of the two do-while bodies against the shared flags to see which of mutual exclusion, progress, and bounded waiting can actually be broken, then separately note any non-correctness design weaknesses (the question explicitly asks for those too).

  1. Flaw 1 — genuine violation of mutual exclusion. The gap between "the while get_entry[1] loop observes the other flag is false and exits" and "set my own flag back to true and enter CS" is not re-checked, so a process can act on stale information. Concrete interleaving (both flags start false):
    StepActionget_entry[0]get_entry[1]Proc1 in CS?Proc2 in CS?
    1Proc1: get_entry[0]=trueTFnono
    2Proc2: get_entry[1]=trueTTnono
    3Proc1 sees get_entry[1]=true ⇒ enters if-block, sets get_entry[0]=falseFTnono
    4Proc2 sees get_entry[0]=false ⇒ skips its if-block, proceeds straight to CSFTnoyes
    5Proc2 leaves CS: get_entry[1]=falseFFnono
    6Proc1's while get_entry[1] now sees false ⇒ exits loop (but has not yet re-set its own flag or re-entered CS)FFnono
    7Proc2 begins a NEW iteration: get_entry[1]=trueFTnono
    8Proc2 sees get_entry[0]=false (still) ⇒ skips if-block, enters CS againFTnoyes
    9Proc1 (resuming step 6) now sets get_entry[0]=true and, per the algorithm, proceeds straight to CS without re-checking get_entry[1]TTyesyes
    At step 9, both processes are simultaneously inside their critical sections — mutual exclusion is broken. This is a real correctness bug, not just an inefficiency, and it happens because exiting the busy-wait loop is treated as a permanent permission grant rather than a snapshot that must be re-validated.
  2. Flaw 2 — no bounded waiting (possible indefinite postponement). There is no turn variable or FIFO queue: whichever process happens to win the race to re-set its flag and slip past the other's check can do so repeatedly. If the scheduler is unlucky enough to always interleave in Proc2's favour at the critical decision points (as in the trace above, and again on Proc2's very next iteration), Proc1 can be denied entry to its critical section indefinitely even though it keeps trying — there is no mechanism guaranteeing a bound on how many times the other process is served first.
  3. Flaw 3 — potential livelock. If both processes set their flags essentially together and then both read the other's (now-true) flag before either clears it, both back off symmetrically (set their own flag false, then busy-wait), and it is possible — on a sufficiently adversarial or perfectly synchronized scheduler — for this raise/back-off cycle to repeat indefinitely with neither process ever getting a "clean" window in which the other's flag reads false while its own critical check happens. No actual work is corrupted (unlike Flaw 1), but no progress is made either, which is exactly the "algorithm ties up the CPU without accomplishing anything" failure mode called livelock.
  4. Flaw 4 — busy-waiting (a design weakness even where correctness holds). The while get_entry[1] no-op; loop spins the CPU rather than blocking, wasting cycles on a uniprocessor (the waiting process cannot even yield the CPU to the very process it is waiting on without an explicit scheduler call) and wasting power/cache bandwidth on a multiprocessor. This is the design flaw the question flags as "not necessarily incorrect results but a design flaw" — a monitor or semaphore-based solution (parts (b)/(c)) blocks the waiting process instead.
Final Results — Q5(a)
FlawCategoryTrigger
1. Mutual exclusion violationCorrectnessStale "loop exited" state acted on without re-checking the other flag (trace above, step 9)
2. No bounded waitingCorrectnessNo turn/queue mechanism; one process can always win the race
3. Possible livelockCorrectness/livenessSymmetric simultaneous raise/back-off with no progress
4. Busy-waitingDesign/efficiency onlyCPU spins in the no-op loop instead of blocking

(b) A monitor bundles shared data together with the only procedures allowed to touch it, and the language runtime automatically ensures at most one process executes inside any of the monitor's procedures at a time — this alone gives mutual exclusion "for free," with no explicit flags or locks written by the programmer. Synchronization beyond simple exclusion is provided by condition variables, each supporting wait() (block the calling process and release the monitor lock) and signal() (wake one waiting process). Example — a bounded buffer of capacity $N$ shared between a producer and a consumer:

monitor BoundedBuffer {
    item buf[N]; int count = 0, in = 0, out = 0;
    condition notFull, notEmpty;

    procedure insert(item x) {
        while (count == N) notFull.wait();   // buffer full: block, don't spin
        buf[in] = x; in = (in+1) % N; count++;
        notEmpty.signal();                    // wake a consumer if one is waiting
    }
    procedure remove() returns item {
        while (count == 0) notEmpty.wait();   // buffer empty: block
        item x = buf[out]; out = (out+1) % N; count--;
        notFull.signal();                     // wake a producer if one is waiting
        return x;
    }
}

Mutual exclusion (only one of insert/remove runs at a time) is automatic; synchronization (a producer must wait when full, a consumer when empty) is explicit via the two condition variables, and each side signals the condition the other side is plausibly waiting on exactly when that condition becomes true.

(c) A counting or binary semaphore guarding a critical section prevents starvation when its internal blocked-process queue is served in FIFO order: every process that calls wait() (P) while the semaphore is unavailable is appended to the end of the queue, and every signal() (V) wakes the process at the front of that queue, not an arbitrary or newest one. Example: three processes P1, P2, P3 all call wait(mutex) in that order while P0 holds the critical section; they queue as [P1, P2, P3]. Each time P0 (or whichever process currently holds the section) calls signal(mutex), the process at the front of the queue — P1 first, then P2, then P3 — is admitted next, regardless of how many *new* processes arrive and call wait() afterward (any newcomer is appended to the back of the queue, behind P1–P3). This guarantees a process waits at most (queue length at the time it joined) turns before entering, which is a hard bound — exactly what rules out starvation. (A "weak" semaphore implementation that wakes an arbitrary blocked process instead of the front of a FIFO queue does not give this guarantee and can starve a process indefinitely.)