Exercise 3: Termination Mid-Turn Breaks the Automatic Re-Enqueue — Possible Solution ==================================================================== THE TEST ------------------------------ a = kernel.create_process(num_pages=1) b = kernel.create_process(num_pages=1) a.transition(ProcessState.RUNNING) scheduler.add(b) a.transition(ProcessState.TERMINATED) # 'a' finishes and terminates itself run_cooperative(cpu, scheduler, a) # naive: treat it just like a normal yield RESULT ------------------------------ calling run_cooperative() on an already-TERMINATED process raised: cannot transition PID 7 from TERMINATED to READY An IllegalStateTransition is raised -- run_cooperative() does NOT silently succeed on a process that has already terminated itself. WHY THIS HAPPENS ------------------------------ run_cooperative() always calls context_switch_fixed(cpu, current_pcb, next_pcb), and context_switch_fixed()'s very first action on the OUTGOING process is: old_pcb.transition(ProcessState.READY) This line assumes the outgoing process is simply pausing and will run again later -- exactly what a normal cooperative yield means. But 'a' already transitioned itself to TERMINATED before run_cooperative() was ever called. Chapter 4's own ALLOWED_TRANSITIONS table has no entry allowing TERMINATED -> READY (TERMINATED's own allowed-transitions set is empty), so the attempt is caught and rejected loudly, rather than silently resurrecting a dead process back into a schedulable state. THE FIX ------------------------------ def run_cooperative_safe(cpu, scheduler, current_pcb): next_pcb = scheduler.pick_next() if next_pcb is None: return current_pcb still_alive = current_pcb.state != ProcessState.TERMINATED if still_alive: context_switch_fixed(cpu, current_pcb, next_pcb) scheduler.add(current_pcb) else: cpu.registers = dict(next_pcb.registers) next_pcb.transition(ProcessState.RUNNING) cpu.current_pid = next_pcb.pid return next_pcb VERIFIED RESULT WITH THE FIX ------------------------------ fixed version switched to PID: 10 state: ProcessState.RUNNING terminated process ever re-added to the ready queue? False The fixed version checks whether the outgoing process is still alive BEFORE deciding what to do with it. When it's already TERMINATED, the function switches cleanly to the next process without ever trying to save the dead process's own registers or re-enqueue it -- it's simply left out of the ready queue for good, exactly as a terminated process should be. WHY THIS IS A GENUINELY DIFFERENT PROBLEM FROM FINDING 3 ------------------------------ Finding 3's own bug was a process ending up in the queue TOO MANY times (added twice by two different code paths). This exercise's problem is the opposite shape: run_cooperative() assuming the outgoing process should ALWAYS go back in the queue at all, when sometimes it genuinely shouldn't. Both are the same underlying lesson from a different angle -- the scheduler's own fairness guarantee (Finding 1) only holds as long as every function that touches the ready queue respects what a process's own real state actually says about it. WHY THIS WORKS AS AN ANSWER ------------------------------ Deliberately triggering the naive path first and confirming exactly which check catches it (Chapter 4's own state machine, not something new written for this exercise) shows the fix isn't inventing a new safety net -- it's correctly consulting a safety net this course had already built, at the one point where the original code forgot to.