A simulated operating-system process table in C: worker threads concurrently fork, exit, wait and kill simulated processes, while a monitor thread records a consistent snapshot after every change. It's built on POSIX threads, with mutexes and condition variables and no busy-waiting.
Processes here are Process Control Block (PCB) entries, not real OS processes. The interesting part is keeping one shared table correct while many threads mutate it at once.
- Unix-style lifecycle:
RUNNING → BLOCKED → ZOMBIE → TERMINATED, withwaitreaping zombies and returning exit codes. wait(-1): waits for whichever child exits first.- Orphan adoption: when a parent exits, its children are re-parented to
init(PID 1) so they can still be reaped, as in Unix. - Kill-safe waiting: killing a process that is blocked in
waitwakes it up instead of leaving it hung. - Event-driven monitor: a dedicated thread sleeps on a condition variable and prints a table snapshot per change. It never polls.
- Race-checked: every scenario runs clean under ThreadSanitizer, including a 6-thread stress test.
stateDiagram-v2
[*] --> RUNNING: pm_fork(parent)
RUNNING --> BLOCKED: pm_wait, no zombie child yet
BLOCKED --> RUNNING: child exits, zombie reaped
RUNNING --> ZOMBIE: pm_exit / pm_kill
BLOCKED --> ZOMBIE: pm_kill while waiting
ZOMBIE --> TERMINATED: parent's pm_wait reaps it
TERMINATED --> [*]
| State | Meaning |
|---|---|
RUNNING |
Exists and is active; the first state after pm_fork |
BLOCKED |
Inside pm_wait, sleeping until a matching child exits |
ZOMBIE |
Has exited; exit status kept until the parent reaps it |
TERMINATED |
Reaped and removed from the table (not shown in snapshots) |
| Concern | Mechanism |
|---|---|
| Shared process table (64 slots) | One pthread_mutex_t table_lock guards every read and write of the table |
| Parent waiting for a child | Each PCB owns a pthread_cond_t; pm_wait sleeps on it with table_lock released |
| Waking the right waiter | pm_exit broadcasts to the parent's condition (and its own, for kill-while-blocked) |
| Monitor notifications | A pending flag plus two condition variables form a handshake. A mutating thread hands the monitor a message and waits until it's written, so every snapshot is taken while the table is still consistent. |
| Per-thread command labels | A __thread buffer holds each worker's current command text |
| Function | Behaviour |
|---|---|
pm_fork(ppid) |
Allocates the next PID, creates the PCB and links it to the parent. Rejects zombie parents. |
pm_exit(pid, status) |
Marks the process ZOMBIE, records the status, re-parents its children to init and wakes waiters |
pm_wait(ppid, cpid) |
Reaps child cpid, or the first child to exit if cpid = -1. Blocks if none has exited yet and returns immediately if there's no such child. |
pm_kill(pid) |
Termination request, recorded as exit status -1 |
pm_ps() |
Prints a tabular snapshot of all non-terminated processes |
Each worker thread runs one script file, one command per line:
fork 1 # create a child of PID 1
exit 2 10 # PID 2 exits with status 10
wait 1 -1 # PID 1 waits for any child (or: wait 1 3)
kill 4 # termination request to PID 4
sleep 200 # this worker pauses for 200 ms
Requirements: GCC (or Clang) with POSIX threads, and make. That means Linux or macOS; on Windows, use WSL.
git clone https://github.com/aksaN000/process-manager.git
cd process-manager
make # builds ./pm_sim
make run # runs the two-thread example and prints snapshots.txt
make test # every scenario under ThreadSanitizerTo run your own scripts, pass one file per worker thread; threads are numbered from 0 in argument order:
./pm_sim thread0.txt thread1.txt thread2.txt
cat snapshots.txtscripts/orphans_t0.txt exits a parent that still has a running child and a zombie child. Both are adopted by init (PPID becomes 1) and the zombie is reaped by init's next wait:
Thread 0 calls pm_exit 2 1
PID PPID STATE EXIT_STATUS
----------------------------------------------
1 0 RUNNING -
2 1 ZOMBIE 1
3 1 ZOMBIE 7
4 1 RUNNING -
Thread 0 calls pm_wait 1 -1
PID PPID STATE EXIT_STATUS
----------------------------------------------
1 0 RUNNING -
3 1 ZOMBIE 7
4 1 RUNNING -
Full logs for each scenario are in examples/.
| Scripts | What it checks |
|---|---|
basic_t0, basic_t1 |
The reference two-thread interleaving |
killblocked_t0, killblocked_t1 |
A process blocked in wait is killed and must not hang |
orphans_t0 |
Children of an exiting parent are adopted and reaped by init |
stress_t0 … stress_t5 |
Six threads: 48 concurrent forks, exits and waits; ends with only init left |
pm_sim.c process manager, script interpreter, workers and monitor
Makefile build, run, and ThreadSanitizer test targets
scripts/ worker scripts for each scenario
examples/ snapshot logs produced by the scenarios
This started as the Operating Systems (CSE321) project at BRAC University. After submitting it, I fixed two issues the original version had:
- Kill-while-blocked hang: killing a process that was blocked in
waitleft it hung. - Orphans: they were never re-parented, so zombie children could never be reaped.
I also added the ThreadSanitizer test suite.