A CPU process scheduler implemented from scratch in C, featuring four classic scheduling algorithms with a clean modular architecture.
Simulates how an operating system decides which process runs on the CPU and for how long. Each algorithm makes a different tradeoff between fairness, throughput, and responsiveness.
| Algorithm | Strategy | Preemptive | Best for |
|---|---|---|---|
| FCFS | Run in arrival order | No | Simple batch jobs |
| SJF | Run shortest job first | No | Minimizing avg wait time |
| Round Robin | Fixed time slices | Yes | Interactive / fair scheduling |
| Priority | Run highest priority first | No | Real-time or tiered workloads |
scheduler/
├── Makefile
├── main.c ← entry point, process definitions
├── include/
│ ├── pcb.h ← Process Control Block struct & state enum
│ ├── queue.h ← ready queue interface
│ └── scheduler.h ← scheduler function declarations
└── src/
├── pcb.c ← PCB init and print helpers
├── queue.c ← linked-list ready queue implementation
├── stats.c ← compute and print wait/turnaround metrics
├── fcfs.c ← First Come First Served
├── sjf.c ← Shortest Job First
├── rr.c ← Round Robin
└── priority.c ← Priority scheduling
Each algorithm lives in its own file. Adding a new algorithm means creating one .c file — nothing else changes.
How the files #include each other and what main.c links against at compile time — the diagram above (middle section) shows this in full.
Requirements: GCC and Make.
# Build
make
# Run
./scheduler
# Clean
make cleanOr compile manually:
gcc -Wall -Iinclude -o scheduler main.c src/pcb.c src/queue.c \
src/stats.c src/fcfs.c src/sjf.c src/rr.c src/priority.c=== FCFS ===
PID Arrival Burst Start Finish Wait Turnaround
P1 arr=0 burst=6 start=0 finish=6 wait=0 tat=6
P2 arr=1 burst=4 start=6 finish=10 wait=5 tat=9
...
Avg wait: 7.60 | Avg TAT: 12.20
=== SJF (non-preemptive) ===
...
Avg wait: 6.00 | Avg TAT: 10.60
SJF produces the lowest average wait time by running the shortest available job first. Round Robin has the highest average wait but gives every process a turn quickly — better for interactive systems where responsiveness matters more than throughput.
The diagram above (bottom section) shows the full scheduling flow — from main.c defining processes, through pcb_reset, the clock loop, the pick() function each algorithm implements, and finally print_stats.
Process Control Block (PCB) — the struct that represents a process. Tracks pid, arrival time, burst time, priority, state, and timing stats.
Ready queue — a linked list of processes that have arrived and are waiting for CPU time. The scheduler picks from this queue on every scheduling decision.
Context switch — saving one process's state and restoring another's. Simulated here by updating PCB fields; in a real kernel this saves/restores CPU registers.
Starvation — Priority and SJF can starve low-priority or long processes if higher-priority ones keep arriving. The fix is aging: gradually increasing priority of waiting processes over time.
- Waiting time = time spent in the ready queue = turnaround − burst
- Turnaround time = total time from arrival to completion = finish − arrival
- Avg wait and Avg TAT are computed across all processes after each run
Edit the process table in main.c:
PCB base[] = {
/* pid arrival burst remaining priority */
{1, 0, 6, 6, 3},
{2, 1, 4, 4, 1},
{3, 2, 2, 2, 4},
};Change the TIME_QUANTUM for Round Robin in include/scheduler.h:
#define TIME_QUANTUM 2- SRTF (Shortest Remaining Time First) — preemptive version of SJF, preempts the running process when a shorter one arrives
- Aging — prevent starvation in Priority scheduling by incrementally boosting priority of waiting processes
- Multi-level feedback queue — multiple queues with different quanta, processes move between them based on CPU usage behavior
- Real context switching — use POSIX
ucontext_t(makecontext/swapcontext) for actual coroutine-style switching - I/O bursts — alternate CPU and I/O phases to make the waiting state real
- Gantt chart output — print a visual timeline of which process ran at each ticks