All-in-one scheduler

In the world of computer science, it's really important to know how computers decide which tasks to do first. This article takes you through a hands-on journey by using a simple program written in C language. We're going to build a mini version of a task manager that computers use, known as a process scheduler. Our project will show you how computers handle four different ways of deciding the order of tasks: First-Come-First-Serve (FCFS), Shortest Job First (SJF) which can work in two ways, Priority Scheduling that also has two types, and the Round Robin method. By creating this scheduler, you'll get a closer look at how these methods work and why they are important for making computers run smoothly and efficiently.

Define the Process Structure

You'll need a struct to represent each process. This structure will contain various fields such as arrival time, burst time, priority, remaining time, and flags for tracking the process state.

typedef struct {
    int processId;
    int arrivalTime;
    int burstTime;
    int priority;
    int remainingTime;
    int terminated;
    int timeSliceRemaining;
    int responseTime;
    int waitingTime;
    int turnaroundTime;
} Process;

Create Process List

You'll need a way to manage your processes. This could be an array or a linked list of Process structures.

Scheduler Function

Write a function for each scheduling algorithm. Each function will take the process list and organize the execution order based on the algorithm's rules.

int fcfsScheduler(Process *processes, int count, int lastProcessRunning, int currentTime);
int sjfScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime);
int priorityScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime);
int roundRobinScheduler(Process *processes, int count, int timeQuantum, int lastProcessRunning);

Main Function

In the main() function, you'll initialize your processes and call the appropriate scheduling function based on user input or configuration.

int main() {
    // Initialize processes
    // Read scheduling algorithm choice and parameters
    // Call the appropriate scheduler function
    return 0;
}

Have all processes terminated?

The function haveAllProcessesTerminated will iterate through the list of processes to check if all of them have terminated. The function returns true if all processes are terminated, and false otherwise.

#include <stdio.h> // For printf
#include <stdlib.h> // For stdlib functions

// Assuming the Process structure is defined as before
typedef struct {
    int processId;
    int isTerminated; // Non-zero if the process is terminated
    // Other process attributes...
} Process;

// Function to check if all processes have terminated
int haveAllProcessesTerminated(Process *processes, int count) {
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated) {
            return 0; // False, at least one process has not terminated
        }
    }
    return 1; // True, all processes have terminated
}

FCFS Logic

int fcfsScheduler(Process *processes, int count, int lastProcessRunning, int currentTime) {
    // If the last process is not idle and not terminated, continue running the same process
    if (lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Initialize with an invalid process index
    int earliestArrivalProcess = -1;
    int earliestArrivalTime = INT_MAX;

    // Loop through the processes to find the one with the earliest arrival time
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated && processes[i].arrivalTime <= currentTime) {
            if (processes[i].arrivalTime < earliestArrivalTime) {
                earliestArrivalTime = processes[i].arrivalTime;
                earliestArrivalProcess = i;
            }
        }
    }

    // If no process is found, return -1 to indicate CPU is idle
    // Otherwise, return the index of the process with the earliest arrival time
    return earliestArrivalProcess;
}

In this implementation:

  • The function first checks if the last running process (lastProcessRunning) is not idle (!= -1) and has not terminated. If so, it returns the same process index.

  • If the last process has terminated, it looks for the non-terminated process that has already arrived (arrivalTime <= currentTime) and has the earliest arrival time. This is done by iterating over all processes.

  • If no such process is found (i.e., all processes have either not arrived or are terminated), the function returns -1 to indicate that the CPU will be idle.

Make sure to include limits.h for INT_MAX if it's not already included in your project. This represents the maximum value for an int in C, used here to initialize the earliest arrival time.

SJF Logic

int sjfScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime) {
    // If non-preemptive and last process is not finished, continue running the same process
    if (!isPreemptive && lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Initialize with an invalid process index and maximum remaining time
    int shortestJobProcess = -1;
    int shortestRemainingTime = INT_MAX;

    // Loop through the processes to find the one with the shortest remaining time
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated && processes[i].arrivalTime <= currentTime) {
            if (processes[i].remainingTime < shortestRemainingTime) {
                shortestRemainingTime = processes[i].remainingTime;
                shortestJobProcess = i;
            }
        }
    }

    // If no process is found, return -1 to indicate CPU is idle
    // Otherwise, return the index of the process with the shortest remaining time
    return shortestJobProcess;
}

In this implementation:

  • If the scheduler is non-preemptive and the last process has not yet finished, it continues running the same process.

  • If the scheduler is preemptive, or the last process has finished, it searches for the process that has arrived (arrivalTime <= currentTime), is not terminated, and has the shortest remaining time.

  • If no such process is found, it returns -1, indicating that the CPU will be idle.

This function assumes that remainingTime is properly updated elsewhere in your program to reflect the current state of each process.

Priority Logic

int priorityScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime) {
    // If non-preemptive and last process is not finished, continue running the same process
    if (!isPreemptive && lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Initialize with an invalid process index and highest possible priority value
    int highestPriorityProcess = -1;
    int highestPriorityValue = INT_MAX;

    // Loop through the processes to find the one with the highest priority (lowest priority value)
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated && processes[i].arrivalTime <= currentTime) {
            if (processes[i].priority < highestPriorityValue) {
                highestPriorityValue = processes[i].priority;
                highestPriorityProcess = i;
            }
        }
    }

    // If no process is found, return -1 to indicate CPU is idle
    // Otherwise, return the index of the process with the highest priority
    return highestPriorityProcess;
}

In this implementation:

  • If the scheduler is non-preemptive and the last process has not yet finished, it continues running the same process.

  • If the scheduler is preemptive, or the last process has finished, it searches for the process that has arrived (arrivalTime <= currentTime), is not terminated, and has the highest priority (indicated by the lowest priority value).

  • If no such process is found, it returns -1, indicating that the CPU will be idle.

As with the other schedulers, this function assumes that the priority attribute of each Process is correctly set and represents the priority of the process, with a lower value indicating a higher priority.

Round Robin Logic

We will need a queue for this algorithm. To implement a queue mechanism for the Round Robin scheduler, we will modify the Process structure to include a tokenNumber field and manage a global token counter. The enqueue function will assign a token number to a process and increment the global counter, while the dequeue function will find and return the process with the smallest token number, indicating it's next in line, and then set its token number to -1, signifying it's no longer in the queue.

First, we add a tokenNumber field to the Process structure:

typedef struct {
    int processId;
    // ... other fields ...
    int tokenNumber;
} Process;

We declare a global variable to keep track of the next token number:

int globalTokenCounter = 0;

The enqueue function assigns a new token number to a process:

void enqueue(Process *process) {
    process->tokenNumber = globalTokenCounter++;
}

The dequeue function returns the process with the smallest token number and sets its token to -1:

int dequeue(Process *processes, int count) {
    int minTokenValue = INT_MAX;
    int processIndex = -1;

    for (int i = 0; i < count; i++) {
        if (processes[i].tokenNumber != -1 && processes[i].tokenNumber < minTokenValue) {
            minTokenValue = processes[i].tokenNumber;
            processIndex = i;
        }
    }

    if (processIndex != -1) {
        processes[processIndex].tokenNumber = -1; // Process is removed from the queue
        return processIndex;
    }

    return -1; // No process found in the queue
}

These functions (enqueue and dequeue) can be used within your Round Robin scheduler to manage the process queue. Processes are enqueued when they arrive or complete a time quantum, and dequeued when they are selected to run.

Here's how the logic would be implemented:

int roundRobinScheduler(Process *processes, int count, int timeQuantum, int lastProcessRunning) {
    // If the current process has time slice remaining and has not terminated, continue running the same process
    if (lastProcessRunning != -1 && processes[lastProcessRunning].timeSliceRemaining > 0 && !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Enqueue the last process running if it has not terminated
    if (lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        enqueue(&processes[lastProcessRunning]);
    }

    // Dequeue the next process
    int nextProcess = dequeue(processes, count);

    // If a process is dequeued, set its time slice remaining and return it
    if (nextProcess != -1) {
        processes[nextProcess].timeSliceRemaining = timeQuantum;
        return nextProcess;
    }

    // If no process is available, return -1 to indicate CPU is idle
    return -1;
}

In this implementation:

  • If the current process (lastProcessRunning) has a remaining time slice and has not terminated, it continues running.

  • If the last process has finished its time slice or has terminated, it is enqueued for its next turn (if not terminated).

  • The scheduler then dequeues the next process in line. If a process is available, its time slice is reset to the timeQuantum, and it is returned for execution.

  • If no process is available in the queue (dequeue returns -1), the function returns -1 to indicate that the CPU is idle.

This approach assumes that the timeSliceRemaining for each process is appropriately managed elsewhere in your code, and that enqueue and dequeue functions are implemented as previously discussed.

The main function

We'll initialize the time, accept the number of processes from the user, dynamically allocate memory for the process array using malloc, and initialize the processes with appropriate values.

#include <stdio.h>
#include <stdlib.h>

// Assuming the Process structure and global token counter are defined as before
typedef struct {
    int processId;
    // ... other fields ...
    int tokenNumber;
    // ... other fields ...
} Process;

int globalTokenCounter = 0;

int main() {
    int time = 0; // Initialize time
    int numOfProcesses;

    // Accept the number of processes from the user
    printf("Enter the number of processes: ");
    scanf("%d", &numOfProcesses);

    // Dynamically create the processes array
    Process *processes = (Process *)malloc(numOfProcesses * sizeof(Process));
    if (processes == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1; // Exit with error code if malloc fails
    }

    // Initialize the processes
    for (int i = 0; i < numOfProcesses; i++) {
        processes[i].processId = i; // Assign process ID 0 to n-1
        processes[i].tokenNumber = -1; // Set token number to -1
        // Initialize other fields to zero
        // For example: processes[i].isTerminated = 0;
        // ... Initialize other fields as necessary ...
    }

    // ... Rest of the code for the scheduler ...

    // Free the dynamically allocated memory
    free(processes);

    return 0;
}

In this implementation:

  • The number of processes is taken as input from the user.

  • Memory for the processes array is allocated dynamically using malloc.

  • Each process is initialized with a process ID from 0 to n-1, the tokenNumber is set to -1, and other fields are set to zero.

  • Don't forget to free the dynamically allocated memory at the end of the program to avoid memory leaks.

Accept user input

We'll modify the main function to accept user input for the scheduling type and associated parameters like preemptiveness for SJF and Priority scheduling, and time quantum for Round Robin scheduling. Let's implement this:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Assuming the Process structure and other necessary global variables/functions are defined as before

int main() {
    int time = 0;
    int numOfProcesses;
    char schedulerType[4];
    int isPreemptive = 0;
    int timeQuantum = 0;

    printf("Enter the number of processes: ");
    scanf("%d", &numOfProcesses);

    Process *processes = (Process *)malloc(numOfProcesses * sizeof(Process));
    if (processes == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    printf("Enter the scheduling type (fcfs, sjf, pri, rr): ");
    scanf("%s", schedulerType);

    if (strcmp(schedulerType, "sjf") == 0 || strcmp(schedulerType, "pri") == 0) {
        printf("Is it preemptive? (1 for yes, 0 for no): ");
        scanf("%d", &isPreemptive);
    }

    if (strcmp(schedulerType, "rr") == 0) {
        printf("Enter the time quantum: ");
        scanf("%d", &timeQuantum);
    }

    // Process input based on scheduler type
    for (int i = 0; i < numOfProcesses; i++) {
        processes[i].processId = i;
        processes[i].tokenNumber = -1; // Default value

        printf("Enter arrival time for process %d: ", i);
        scanf("%d", &processes[i].arrivalTime);

        printf("Enter burst time for process %d: ", i);
        scanf("%d", &processes[i].burstTime);

        if (strcmp(schedulerType, "pri") == 0) {
            printf("Enter priority for process %d: ", i);
            scanf("%d", &processes[i].priority);
        }

        // Initialize other fields as necessary
    }

    // ... Rest of the code for the scheduler ...

    free(processes);
    return 0;
}

In this implementation:

  • The user is prompted to enter the scheduling type and corresponding parameters (preemptive flag for SJF/Priority, time quantum for Round Robin).

  • Based on the scheduling type, the program prompts for the appropriate information for each process: arrival time and burst time are common for all, but priority is specific to Priority scheduling.

  • This data will be used by your scheduler to manage the processes according to the chosen algorithm.

The loop

We'll write a loop that simulates the process scheduling according to the selected algorithm (FCFS, SJF, Priority, or Round Robin). The loop continues until all processes are finished. Within the loop, we'll handle the process selection, execution simulation, and various time calculations.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Assuming the Process structure and other necessary global variables/functions are defined as before

int main() {
    // ... [Initialization and user input code as previously discussed] ...

    int processToRun = -1;
    int allProcessesTerminated = 0;

    // Set remaining time to burst time for all processes
    for (int i = 0; i < numOfProcesses; i++) {
        processes[i].remainingTime = processes[i].burstTime;
        processes[i].isTerminated = 0;
        processes[i].waitingTime = 0;
    }

    while (!allProcessesTerminated) {
        // Check for process arrival and enqueue if using Round Robin
        if (strcmp(schedulerType, "rr") == 0) {
            for (int i = 0; i < numOfProcesses; i++) {
                if (processes[i].arrivalTime == time && processes[i].tokenNumber == -1) {
                    enqueue(&processes[i]);
                }
            }
        }

        // Select the next process to run based on the scheduling algorithm
        processToRun = nextProcessToRun(processes, numOfProcesses, time, processToRun, schedulerType, isPreemptive, timeQuantum);

        if (processToRun == -1) {
            // CPU is idle
            printf("From time %d to %d, the CPU was IDLE.\n", time, time + 1);
        } else {
            // Simulate process running
            if (processes[processToRun].remainingTime == processes[processToRun].burstTime) {
                processes[processToRun].responseTime = time - processes[processToRun].arrivalTime;
            }

            processes[processToRun].remainingTime--;

            // Check if the process has finished
            if (processes[processToRun].remainingTime == 0) {
                processes[processToRun].isTerminated = 1;
                processes[processToRun].turnaroundTime = time + 1 - processes[processToRun].arrivalTime;
            }

            // Increment waiting time for other non-terminated processes
            for (int i = 0; i < numOfProcesses; i++) {
                if (i != processToRun && !processes[i].isTerminated && processes[i].arrivalTime <= time) {
                    processes[i].waitingTime++;
                }
            }

            // For Round Robin, decrement the time slice remaining
            if (strcmp(schedulerType, "rr") == 0 && processes[processToRun].timeSliceRemaining > 0) {
                processes[processToRun].timeSliceRemaining--;
            }

            printf("From time %d to %d, the CPU ran process %d.\n", time, time + 1, processToRun);
        }

        time++; // Increment time

        // Check if all processes have terminated
        allProcessesTerminated = haveAllProcessesTerminated(processes, numOfProcesses);
    }

    // ... [Any additional code such as displaying results] ...

    free(processes);
    return 0;
}

This implementation follows your specified logic:

  • It loops through each time unit, selecting and running processes according to the scheduling algorithm.

  • It handles process arrival, execution simulation, and updates various timings (waiting, response, turnaround).

  • For Round Robin, it enqueues newly arrived processes and manages the time slice for the running process.

  • The loop continues until all processes are terminated.

Make sure you've implemented the enqueue, dequeue, and nextProcessToRun functions correctly for each scheduling algorithm, as well as the haveAllProcessesTerminated function to check for completion of all processes.

This function assumes that the nextProcessToRun function is a generalized function that decides the next process to run based on the selected scheduling algorithm and its specific requirements. The implementation of this function will vary based on the algorithm used. We will write that below.

The nextProcessToRun function needs to decide which process to run next based on the chosen scheduling algorithm and the current state of the system. This function will consider FCFS, SJF (both preemptive and non-preemptive), Priority Scheduling (both preemptive and non-preemptive), and Round Robin scheduling.

Given the complexity and differences between each scheduling algorithm, the nextProcessToRun function will act as a dispatcher, calling specific scheduling functions based on the algorithm selected by the user. It will need to handle inputs such as the list of processes, the number of processes, the current time, the last process that was running, and additional parameters for preemptive scheduling and time quantum for Round Robin.

Here's a prototype implementation that takes into account the discussions:

#include <stdio.h>
#include <string.h>

// Assuming the Process structure and the scheduler function prototypes are defined as before

// Function prototypes for scheduler algorithms
int fcfsScheduler(Process *processes, int count, int lastProcessRunning, int currentTime);
int sjfScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime);
int priorityScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime);
int roundRobinScheduler(Process *processes, int count, int timeQuantum, int lastProcessRunning);

// Dispatcher function to decide the next process to run
int nextProcessToRun(Process *processes, int count, int currentTime, int lastProcessRunning, const char *schedulerType, int isPreemptive, int timeQuantum) {
    if (strcmp(schedulerType, "fcfs") == 0) {
        return fcfsScheduler(processes, count, lastProcessRunning, currentTime);
    } else if (strcmp(schedulerType, "sjf") == 0) {
        return sjfScheduler(processes, count, isPreemptive, lastProcessRunning, currentTime);
    } else if (strcmp(schedulerType, "pri") == 0) {
        return priorityScheduler(processes, count, isPreemptive, lastProcessRunning, currentTime);
    } else if (strcmp(schedulerType, "rr") == 0) {
        return roundRobinScheduler(processes, count, timeQuantum, lastProcessRunning);
    }

    return -1; // Return -1 if an invalid scheduler type is passed
}

In this implementation:

  • The function takes the process list, count, current time, last running process, scheduler type, preemptive flag (for SJF and Priority), and time quantum (for Round Robin) as parameters.

  • It dispatches the call to the appropriate scheduler function based on the schedulerType parameter.

  • Each scheduler function is responsible for implementing its algorithm's logic to select the next process to run.

This approach modularizes the scheduler logic, making it easier to manage and extend. The specific implementations of the scheduler functions (fcfsScheduler, sjfScheduler, priorityScheduler, roundRobinScheduler) will contain the logic for each scheduling algorithm, as discussed previously.

Output

After the main scheduling loop is complete and all processes have terminated, you can print the response time (RT), waiting time (WT), and turnaround time (TAT) for each process. This can be done by iterating through the processes array and accessing these values for each process.

// ... [The scheduling loop and other parts of the main function] ...

// After the main loop, when all processes have terminated
printf("\nProcess Execution Statistics:\n");
printf("PID\tResponse Time\tWaiting Time\tTurnaround Time\n");
for (int i = 0; i < numOfProcesses; i++) {
    printf("%d\t%d\t\t%d\t\t%d\n",
           processes[i].processId,
           processes[i].responseTime,
           processes[i].waitingTime,
           processes[i].turnaroundTime);
}

// ... [Free memory and any final code] ...

In this implementation:

  • After the main scheduling loop, the program prints a header line for clarity.

  • It then iterates through each process, printing the process ID (PID), response time (RT), waiting time (WT), and turnaround time (TAT).

  • The data is presented in a tabular format for easy reading.

This output will give you a clear overview of how each process fared in terms of these key performance metrics, which are crucial for evaluating the effectiveness of your scheduling algorithm.

Combined Code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

typedef struct {
    int processId;
    int arrivalTime;
    int burstTime;
    int priority;
    int remainingTime;
    int isTerminated;
    int tokenNumber;
    int timeSliceRemaining;
    int responseTime;
    int waitingTime;
    int turnaroundTime;
} Process;

int globalTokenCounter = 0;

// Utility function prototypes
void enqueue(Process *process);

int dequeue(Process *processes, int count);

int haveAllProcessesTerminated(Process *processes, int count);

// Scheduler function prototypes
int fcfsScheduler(Process *processes, int count, int lastProcessRunning, int currentTime);

int sjfScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime);

int priorityScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime);

int roundRobinScheduler(Process *processes, int count, int timeQuantum, int lastProcessRunning);

// Dispatcher function prototype
int nextProcessToRun(Process *processes, int count, int currentTime, int lastProcessRunning, const char *schedulerType,
                     int isPreemptive, int timeQuantum);

// Main function
int main() {
    int time = 0;
    int numOfProcesses;
    char schedulerType[4];
    int isPreemptive = 0;
    int timeQuantum = 0;

    printf("Enter the number of processes: ");
    scanf("%d", &numOfProcesses);

    Process *processes = (Process *) malloc(numOfProcesses * sizeof(Process));
    if (processes == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }

    printf("Enter the scheduling type (fcfs, sjf, pri, rr): ");
    scanf("%s", schedulerType);

    if (strcmp(schedulerType, "sjf") == 0 || strcmp(schedulerType, "pri") == 0) {
        printf("Is it preemptive? (1 for yes, 0 for no): ");
        scanf("%d", &isPreemptive);
    }

    if (strcmp(schedulerType, "rr") == 0) {
        printf("Enter the time quantum: ");
        scanf("%d", &timeQuantum);
    }

    // Process input based on scheduler type
    for (int i = 0; i < numOfProcesses; i++) {
        processes[i].processId = i;
        processes[i].tokenNumber = -1; // Default value

        printf("Enter arrival time for process %d: ", i);
        scanf("%d", &processes[i].arrivalTime);

        printf("Enter burst time for process %d: ", i);
        scanf("%d", &processes[i].burstTime);

        if (strcmp(schedulerType, "pri") == 0) {
            printf("Enter priority for process %d: ", i);
            scanf("%d", &processes[i].priority);
        }
    }

    int processToRun = -1;
    int allProcessesTerminated = 0;

    // Set remaining time to burst time for all processes
    for (int i = 0; i < numOfProcesses; i++) {
        processes[i].remainingTime = processes[i].burstTime;
        processes[i].isTerminated = 0;
        processes[i].waitingTime = 0;
    }

    while (!allProcessesTerminated) {
        // Check for process arrival and enqueue if using Round Robin
        if (strcmp(schedulerType, "rr") == 0) {
            for (int i = 0; i < numOfProcesses; i++) {
                if (processes[i].arrivalTime == time && processes[i].tokenNumber == -1) {
                    enqueue(&processes[i]);
                }
            }
        }

        // Select the next process to run based on the scheduling algorithm
        processToRun = nextProcessToRun(processes, numOfProcesses, time, processToRun, schedulerType, isPreemptive,
                                        timeQuantum);

        if (processToRun == -1) {
            // CPU is idle
            printf("From time %d to %d, the CPU was IDLE.\n", time, time + 1);
        } else {
            // Simulate process running
            if (processes[processToRun].remainingTime == processes[processToRun].burstTime) {
                processes[processToRun].responseTime = time - processes[processToRun].arrivalTime;
            }

            processes[processToRun].remainingTime--;

            // Check if the process has finished
            if (processes[processToRun].remainingTime == 0) {
                processes[processToRun].isTerminated = 1;
                processes[processToRun].turnaroundTime = time + 1 - processes[processToRun].arrivalTime;
            }

            // Increment waiting time for other non-terminated processes
            for (int i = 0; i < numOfProcesses; i++) {
                if (i != processToRun && !processes[i].isTerminated && processes[i].arrivalTime <= time) {
                    processes[i].waitingTime++;
                }
            }

            // For Round Robin, decrement the time slice remaining
            if (strcmp(schedulerType, "rr") == 0 && processes[processToRun].timeSliceRemaining > 0) {
                processes[processToRun].timeSliceRemaining--;
            }

            printf("From time %d to %d, the CPU ran process %d.\n", time, time + 1, processToRun);
        }

        time++; // Increment time

        // Check if all processes have terminated
        allProcessesTerminated = haveAllProcessesTerminated(processes, numOfProcesses);
    }

    printf("\nProcess Execution Statistics:\n");
    printf("PID\tResponse Time\tWaiting Time\tTurnaround Time\n");
    for (int i = 0; i < numOfProcesses; i++) {
        printf("%d\t%d\t\t%d\t\t%d\n",
               processes[i].processId,
               processes[i].responseTime,
               processes[i].waitingTime,
               processes[i].turnaroundTime);
    }

    free(processes);
    return 0;

}

int haveAllProcessesTerminated(Process *processes, int count) {
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated) {
            return 0; // False, at least one process has not terminated
        }
    }
    return 1; // True, all processes have terminated
}

int fcfsScheduler(Process *processes, int count, int lastProcessRunning, int currentTime) {
    // If the last process is not idle and not terminated, continue running the same process
    if (lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Initialize with an invalid process index
    int earliestArrivalProcess = -1;
    int earliestArrivalTime = INT_MAX;

    // Loop through the processes to find the one with the earliest arrival time
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated && processes[i].arrivalTime <= currentTime) {
            if (processes[i].arrivalTime < earliestArrivalTime) {
                earliestArrivalTime = processes[i].arrivalTime;
                earliestArrivalProcess = i;
            }
        }
    }

    // If no process is found, return -1 to indicate CPU is idle
    // Otherwise, return the index of the process with the earliest arrival time
    return earliestArrivalProcess;
}

int sjfScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime) {
    // If non-preemptive and last process is not finished, continue running the same process
    if (!isPreemptive && lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Initialize with an invalid process index and maximum remaining time
    int shortestJobProcess = -1;
    int shortestRemainingTime = INT_MAX;

    // Loop through the processes to find the one with the shortest remaining time
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated && processes[i].arrivalTime <= currentTime) {
            if (processes[i].remainingTime < shortestRemainingTime) {
                shortestRemainingTime = processes[i].remainingTime;
                shortestJobProcess = i;
            }
        }
    }

    // If no process is found, return -1 to indicate CPU is idle
    // Otherwise, return the index of the process with the shortest remaining time
    return shortestJobProcess;
}

int priorityScheduler(Process *processes, int count, int isPreemptive, int lastProcessRunning, int currentTime) {
    // If non-preemptive and last process is not finished, continue running the same process
    if (!isPreemptive && lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Initialize with an invalid process index and highest possible priority value
    int highestPriorityProcess = -1;
    int highestPriorityValue = INT_MAX;

    // Loop through the processes to find the one with the highest priority (lowest priority value)
    for (int i = 0; i < count; i++) {
        if (!processes[i].isTerminated && processes[i].arrivalTime <= currentTime) {
            if (processes[i].priority < highestPriorityValue) {
                highestPriorityValue = processes[i].priority;
                highestPriorityProcess = i;
            }
        }
    }

    // If no process is found, return -1 to indicate CPU is idle
    // Otherwise, return the index of the process with the highest priority
    return highestPriorityProcess;
}

void enqueue(Process *process) {
    process->tokenNumber = globalTokenCounter++;
}

int dequeue(Process *processes, int count) {
    int minTokenValue = INT_MAX;
    int processIndex = -1;

    for (int i = 0; i < count; i++) {
        if (processes[i].tokenNumber != -1 && processes[i].tokenNumber < minTokenValue) {
            minTokenValue = processes[i].tokenNumber;
            processIndex = i;
        }
    }

    if (processIndex != -1) {
        processes[processIndex].tokenNumber = -1; // Process is removed from the queue
        return processIndex;
    }

    return -1; // No process found in the queue
}

int roundRobinScheduler(Process *processes, int count, int timeQuantum, int lastProcessRunning) {
    // If the current process has time slice remaining and has not terminated, continue running the same process
    if (lastProcessRunning != -1 && processes[lastProcessRunning].timeSliceRemaining > 0 &&
        !processes[lastProcessRunning].isTerminated) {
        return lastProcessRunning;
    }

    // Enqueue the last process running if it has not terminated
    if (lastProcessRunning != -1 && !processes[lastProcessRunning].isTerminated) {
        enqueue(&processes[lastProcessRunning]);
    }

    // Dequeue the next process
    int nextProcess = dequeue(processes, count);

    // If a process is dequeued, set its time slice remaining and return it
    if (nextProcess != -1) {
        processes[nextProcess].timeSliceRemaining = timeQuantum;
        return nextProcess;
    }

    // If no process is available, return -1 to indicate CPU is idle
    return -1;
}

// Dispatcher function
int nextProcessToRun(Process *processes, int count, int currentTime, int lastProcessRunning, const char *schedulerType,
                     int isPreemptive, int timeQuantum) {
    if (strcmp(schedulerType, "fcfs") == 0) {
        return fcfsScheduler(processes, count, lastProcessRunning, currentTime);
    } else if (strcmp(schedulerType, "sjf") == 0) {
        return sjfScheduler(processes, count, isPreemptive, lastProcessRunning, currentTime);
    } else if (strcmp(schedulerType, "pri") == 0) {
        return priorityScheduler(processes, count, isPreemptive, lastProcessRunning, currentTime);
    } else if (strcmp(schedulerType, "rr") == 0) {
        return roundRobinScheduler(processes, count, timeQuantum, lastProcessRunning);
    }
    return -1; // Invalid scheduler type
}