Build a mini operating system simulation — implement process scheduling, memory management, file system, and shell interface in C to understand OS internals through hands-on coding.
Project Overview
Building a mini operating system is the ultimate OS learning project. You will create a simplified simulation that implements the core subsystems — process management, memory allocation, a basic file system, and a command shell. While this is not a real bootable OS, it simulates all the key interactions and data structures that a real OS uses internally.
By the end of this project, abstract concepts like "the scheduler picks the next process from the ready queue" become concrete code you wrote yourself.
Architecture
┌─────────────────────────────────────────┐
│ Shell (CLI) │
├─────────────────────────────────────────┤
│ Process Manager │ Memory Manager │ FS │
├─────────────────────────────────────────┤
│ Mini OS Kernel Core │
└─────────────────────────────────────────┘
The simulation runs as a single program but internally maintains the illusion of multiple processes, allocated memory blocks, and stored files.
Part 1: Process Management
Data Structures
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_PROCESSES 20
#define TIME_QUANTUM 3
typedef enum { NEW, READY, RUNNING, WAITING, TERMINATED } ProcessState;
typedef struct {
int pid;
char name[32];
ProcessState state;
int priority;
int burst_time;
int remaining_time;
int arrival_time;
int memory_required; // KB
int memory_start; // allocated memory start address
} Process;
Process process_table[MAX_PROCESSES];
int process_count = 0;
int next_pid = 1;
int system_time = 0;
Process Creation and Scheduling
int create_process(const char* name, int burst, int priority, int memory_kb) {
if (process_count >= MAX_PROCESSES) {
printf("Error: Process table full\n");
return -1;
}
Process* p = &process_table[process_count];
p->pid = next_pid++;
strncpy(p->name, name, 31);
p->state = NEW;
p->priority = priority;
p->burst_time = burst;
p->remaining_time = burst;
p->arrival_time = system_time;
p->memory_required = memory_kb;
p->memory_start = -1; // not yet allocated
process_count++;
printf("Process created: PID=%d, Name=%s, Burst=%d, Priority=%d\n",
p->pid, p->name, burst, priority);
return p->pid;
}
void schedule_round_robin() {
printf("\n--- Running Round Robin (quantum=%d) ---\n", TIME_QUANTUM);
int completed = 0;
int time = 0;
// Set all NEW processes to READY
for (int i = 0; i < process_count; i++)
if (process_table[i].state == NEW)
process_table[i].state = READY;
while (completed < process_count) {
for (int i = 0; i < process_count; i++) {
if (process_table[i].state == READY) {
process_table[i].state = RUNNING;
int exec = (process_table[i].remaining_time < TIME_QUANTUM)
? process_table[i].remaining_time : TIME_QUANTUM;
printf("Time %d-%d: P%d (%s) running\n",
time, time + exec, process_table[i].pid, process_table[i].name);
process_table[i].remaining_time -= exec;
time += exec;
if (process_table[i].remaining_time == 0) {
process_table[i].state = TERMINATED;
printf(" → P%d completed at time %d\n", process_table[i].pid, time);
completed++;
} else {
process_table[i].state = READY;
}
}
}
}
system_time = time;
}
Part 2: Memory Management
Implement a simple memory allocator using first-fit strategy with a memory map.
#define TOTAL_MEMORY 1024 // 1024 KB simulated memory
typedef struct {
int start;
int size;
int pid; // -1 if free
} MemoryBlock;
MemoryBlock memory_map[100];
int block_count = 1;
void init_memory() {
memory_map[0].start = 0;
memory_map[0].size = TOTAL_MEMORY;
memory_map[0].pid = -1; // all free
block_count = 1;
}
int allocate_memory(int pid, int size_kb) {
// First-fit allocation
for (int i = 0; i < block_count; i++) {
if (memory_map[i].pid == -1 && memory_map[i].size >= size_kb) {
int remaining = memory_map[i].size - size_kb;
memory_map[i].size = size_kb;
memory_map[i].pid = pid;
if (remaining > 0) {
// Split block — shift everything after
for (int j = block_count; j > i + 1; j--)
memory_map[j] = memory_map[j-1];
memory_map[i+1].start = memory_map[i].start + size_kb;
memory_map[i+1].size = remaining;
memory_map[i+1].pid = -1;
block_count++;
}
printf("Allocated %dKB to PID %d at address %d\n",
size_kb, pid, memory_map[i].start);
return memory_map[i].start;
}
}
printf("Error: Not enough memory for PID %d (%dKB requested)\n", pid, size_kb);
return -1;
}
void free_memory(int pid) {
for (int i = 0; i < block_count; i++) {
if (memory_map[i].pid == pid) {
memory_map[i].pid = -1;
printf("Freed memory at address %d (%dKB) from PID %d\n",
memory_map[i].start, memory_map[i].size, pid);
// Merge adjacent free blocks
// (left merge and right merge logic here)
break;
}
}
}
void display_memory() {
printf("\n--- Memory Map (%d KB total) ---\n", TOTAL_MEMORY);
for (int i = 0; i < block_count; i++) {
printf("[%4d - %4d] %4d KB %s\n",
memory_map[i].start,
memory_map[i].start + memory_map[i].size - 1,
memory_map[i].size,
memory_map[i].pid == -1 ? "FREE" : "USED");
}
}
Part 3: Simple File System
#define MAX_FILES 50
#define MAX_FILENAME 32
typedef struct {
char name[MAX_FILENAME];
int size; // bytes
int created_by; // PID
int is_active;
} FileEntry;
FileEntry file_table[MAX_FILES];
int file_count = 0;
int create_file(const char* name, int size, int pid) {
if (file_count >= MAX_FILES) {
printf("Error: File system full\n");
return -1;
}
strncpy(file_table[file_count].name, name, MAX_FILENAME - 1);
file_table[file_count].size = size;
file_table[file_count].created_by = pid;
file_table[file_count].is_active = 1;
file_count++;
printf("File '%s' created (%d bytes) by PID %d\n", name, size, pid);
return 0;
}
void list_files() {
printf("\n--- File System ---\n");
printf("%-20s %8s %8s\n", "Name", "Size", "Owner");
for (int i = 0; i < file_count; i++) {
if (file_table[i].is_active)
printf("%-20s %6dB PID %d\n",
file_table[i].name, file_table[i].size, file_table[i].created_by);
}
}
Part 4: Shell Interface
void shell() {
char command[64], arg1[32], arg2[32];
int num1, num2;
printf("\n=== Mini OS Shell v1.0 ===\n");
printf("Commands: create, schedule, memory, files, ps, help, exit\n\n");
while (1) {
printf("miniOS> ");
scanf("%s", command);
if (strcmp(command, "create") == 0) {
printf("Process name: "); scanf("%s", arg1);
printf("Burst time: "); scanf("%d", &num1);
printf("Memory (KB): "); scanf("%d", &num2);
int pid = create_process(arg1, num1, 1, num2);
if (pid > 0) allocate_memory(pid, num2);
}
else if (strcmp(command, "schedule") == 0) {
schedule_round_robin();
}
else if (strcmp(command, "memory") == 0) {
display_memory();
}
else if (strcmp(command, "files") == 0) {
list_files();
}
else if (strcmp(command, "ps") == 0) {
printf("\nPID Name State Remaining\n");
for (int i = 0; i < process_count; i++)
printf("%-4d %-15s %-10d %d\n",
process_table[i].pid, process_table[i].name,
process_table[i].state, process_table[i].remaining_time);
}
else if (strcmp(command, "exit") == 0) {
printf("Shutting down Mini OS...\n");
break;
}
else {
printf("Unknown command. Type 'help' for available commands.\n");
}
}
}
Building and Running
gcc -o mini_os mini_os.c -Wall
./mini_os
Project Extensions
- Add I/O simulation: Processes block for random durations simulating disk reads
- Implement virtual memory: Page table with simulated page faults
- Add inter-process communication: Message queues between processes
- Multi-level scheduling: Separate queues for interactive vs batch processes
- File permissions: Read/write/execute bits checked before file operations
- Boot sequence: Simulate BIOS → bootloader → kernel init startup messages
Learning Outcomes
- Understand how OS subsystems interact (process creation triggers memory allocation)
- Experience the data structure decisions that kernel developers make
- See how a shell translates user commands into kernel operations
- Appreciate the complexity of managing concurrent processes with shared resources
- Build confidence for OS course labs and technical interviews