diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e85cfec --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,52 @@ +cmake_minimum_required(VERSION 3.10) + +project(Robinwaita LANGUAGES C) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) + +find_package(Python3 COMPONENTS Interpreter Development REQUIRED) +find_package(PkgConfig REQUIRED) + +pkg_check_modules(GTK REQUIRED gtk4) +pkg_check_modules(ADWAITA REQUIRED libadwaita-1) +pkg_check_modules(GLIB REQUIRED glib-2.0) +pkg_check_modules(GIOUNIX REQUIRED gio-unix-2.0) +pkg_check_modules(GRAPHENE REQUIRED graphene-1.0) + +include_directories( + ${Python3_INCLUDE_DIRS} + ${GIOUNIX_INCLUDE_DIRS} + ${GTK_INCLUDE_DIRS} + ${ADWAITA_INCLUDE_DIRS} + ${GLIB_INCLUDE_DIRS} + ${GRAPHENE_INCLUDE_DIRS} +) + +add_executable(robinwaita + main.c + process.c + process.h + scheduler.c + scheduler.h + main.h +) + +target_link_libraries(robinwaita + ${Python3_LIBRARIES} + ${GTK_LIBRARIES} + ${ADWAITA_LIBRARIES} + ${GLIB_LIBRARIES} + ${GIOUNIX_LIBRARIES} + ${GRAPHENE_LIBRARIES} +) + +link_directories( + ${GTK_LIBRARY_DIRS} + ${ADWAITA_LIBRARY_DIRS} + ${GLIB_LIBRARY_DIRS} + ${GRAPHENE_LIBRARY_DIRS} + ${GIOUNIX_LIBRARY_DIRS} +) + +target_compile_options(robinwaita PRIVATE ${GTK_CFLAGS_OTHER}) \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4d09a1 --- /dev/null +++ b/README.md @@ -0,0 +1,57 @@ +# Robinwaita +Robinwaita is user-friendly round-robin process scheduler for Linux written in C. It comes with a modern Libadwaita & GTK 4-based user interface. + +## Screenshots +![A screenshot of Robinwaita having completed process scheduling](screenshots/img1.png "Robinwaita heading image") +![A screenshot of Robinwaita showing how processes are added to the scheduler](screenshots/scheduler.png "Adding processes to the scheduler") +![A screenshot of Robinwaita's statistics screen, showing context switches and average running time](screenshots/statistics.png "Robinwaita's statistics tab") +![A picture of the source code, showing a segment of the timerfd implementation](screenshots/timerfd.png "Robinwaita uses timerfd") + +## Details +* Easily add processes to the scheduling queue +* Round-robin scheduling algorithm, with context switches initiated by `timerfd` +* See how processes get managed by the algorithm in real-time +* Modern GTK 4 interface with light and dark modes +* Check basic statistics like total context switches in a separate tab + +## Motivation for this project +* Before we get into anything -- yes, this project is useless. I know. +* That being said, given the additional low-level programming experience I've been getting recently, as well as my growing experience with operating systems concepts, I wanted to challenge myself by programming a process scheduler from scratch. +* I've also loved GNOME and its technologies like GTK for several years. I've thought about making a GTK app for a while now, so combining the two seemed like a fun fit. Ultimately, while the user interface for this project is undeniably basic, I'm still really happy with how it turned out. + +## Try Robinwaita for yourself +### Dependencies +* Robinwaita has been developed with the following versions of these packages: + * `libadwaita`/`libadwaita-devel` version 1.5 + * `libgtk-4-dev`/`gtk4-devel` 4.14 + * `libglib2.0-dev`/`glib2-devel` + * `libgio-unix-2.0-dev`/`gio-unix-devel` 2.0 + * `libgraphene-1.0-dev`/`graphene-devel` 1.0 + * `python3-dev`/`python3-devel` 3.12.6 + * `gcc` 14.2.1 + * `cmake` 3.28.2 + * `pkg-config`/`pkgconf-pkg-config` +* Other versions may work, but I provide no guarantees. +* I provide an ARM64 build under Releases, but it's expected your system still satisfies dependencies above. +* My build environment was a Fedora 40 VM under Parallels on my M2 Pro MacBook Pro. + +### Building the project +1. Clone the repository: +``` +git clone https:// +cd robinwaita +``` +2. Install dependencies: +* Install the dependencies noted above. Keep in mind these instructions are different across distributions, so instructions aren't provided here. +3. Generate the build system using CMake, and build: +``` +mkdir build +cd build +cmake .. +cmake --build . +``` +4. Run the application: +``` +chmod +x robinwaita # set file as executable +./robinwaita # run it! (you can also open it from your file manager) +``` \ No newline at end of file diff --git a/desktop_integration/robinwaita.desktop b/desktop_integration/robinwaita.desktop new file mode 100755 index 0000000..bf92a81 --- /dev/null +++ b/desktop_integration/robinwaita.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Name=Robinwaita +Exec=/path/to/Robinwaita/build/robinwaita +Icon=robinwaita +Terminal=false +Type=Application +Categories=Utility; +Path=/path/to/Robinwaita/build/ diff --git a/desktop_integration/robinwaita.svg b/desktop_integration/robinwaita.svg new file mode 100644 index 0000000..fedc4eb --- /dev/null +++ b/desktop_integration/robinwaita.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/main.c b/main.c new file mode 100644 index 0000000..85dc6ae --- /dev/null +++ b/main.c @@ -0,0 +1,314 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "main.h" + +int main(int argc, char **argv) { + + g_autoptr(AdwApplication) app = NULL; + int status; + + //allocate initialData + struct initData *initialData = malloc(sizeof(struct initData)); + if (initialData == NULL) { + g_print("Failed to allocate memory for initialData"); + return -1; + } + + //create the gtk application and pass initialData to activate + app = adw_application_new("com.asolonari.Robinwaita", G_APPLICATION_DEFAULT_FLAGS); + g_signal_connect(app, "activate", G_CALLBACK(activate), initialData); + + //start gtk application + status = g_application_run(G_APPLICATION(app), argc, argv); + fflush(stdout); + + //free memory after the event loop exits + freeAll(initialData); + + return status; +} + +void updateStatistics(gpointer userData){ + initData *data = (initData *) userData; + AdwActionRow *averageRunningTimeRow, *totalContextSwitchesRow; + double averageTime = data->totalTime / data->processAmt; + + //get objects from builder + averageRunningTimeRow = ADW_ACTION_ROW(gtk_builder_get_object(data->builder, "averageRunningTimeRow")); + if(!averageRunningTimeRow) g_print("Error: averageRunningTimeRow is NULL\n"); + totalContextSwitchesRow = ADW_ACTION_ROW(gtk_builder_get_object(data->builder, "totalContextSwitchesRow")); + if(!totalContextSwitchesRow) g_print("Error: totalContextSwitchesRow is NULL\n"); + + //format strings + gchar *averageTimeStr = g_strdup_printf("%.1lf seconds", averageTime); + gchar *totalContextSwitchesStr = g_strdup_printf("%d context switches", data->contextSwitches); + + //update subtitles with information + adw_action_row_set_subtitle(averageRunningTimeRow, averageTimeStr); + adw_action_row_set_subtitle(totalContextSwitchesRow, totalContextSwitchesStr); + + //free new strings + g_free(averageTimeStr); + g_free(totalContextSwitchesStr); +} + +gboolean schedulerUpdateFromTimerfd(gint fd, GIOCondition condition, gpointer userData) { + initData *data = (initData *) userData; + + if (condition & G_IO_IN) { + uint64_t expirations; + ssize_t bytes = read(fd, &expirations, sizeof(expirations)); + if (bytes != sizeof(expirations)) { + g_print("Warning: read unexpected number of bytes from timerfd\n"); + } + + //scheduler logic + manageQueue(data); + } + + return TRUE; //while monitoring file descriptor +} + +void addProcess(int processType, struct initData *data) { + Process* createdProcess = newProcess(processType); + + enQueue(data->allProcesses, createdProcess); + displayProcess(createdProcess, data->allProcesses, data); +} + +void addCPUProcess(GtkWidget *widget, gpointer userData) { + struct initData *data = (struct initData *) userData; + addProcess(1, data); // 1 = CPU-bound +} + +void addIOProcess(GtkWidget *widget, gpointer userData) { + struct initData *data = (struct initData *) userData; + addProcess(2, data); // 2 = IO-bound +} + +void addMixedProcess(GtkWidget *widget, gpointer userData) { + struct initData *data = (struct initData *) userData; + addProcess(3, data); // 3 = Mixed-bound +} + +void updateButtonSensitivity(initData *data, gboolean sensitive) { + //find buttons by id + GtkWidget *cpuButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "cpuButton")); + GtkWidget *ioButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "ioButton")); + GtkWidget *mixedButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "mixedButton")); + GtkWidget *runAllButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "runAllButton")); + + //set button sensitivity + gtk_widget_set_sensitive(cpuButton, sensitive); + gtk_widget_set_sensitive(ioButton, sensitive); + gtk_widget_set_sensitive(mixedButton, sensitive); + gtk_widget_set_sensitive(runAllButton, sensitive); +} + +void runScheduler(GtkButton *button, gpointer userData) { + initData *data = (initData *)userData; + + if (data->schedulerRunning) { + g_print("Scheduler is already running.\n"); + return; + } + + //reset statistics variables + data->contextSwitches = 0; + data->processAmt = 0; + data->totalTime = 0; + + //call timerSetup to initialize timerfd + data->timerfd = timerSetup(data->allProcesses); + + if (data->timerfd == -1) { + g_print("Failed to set up the scheduler timer.\n"); + return; + } + + //add timerfd to glib main loop to monitor it + data->timerWatchID = g_unix_fd_add(data->timerfd, G_IO_IN | G_IO_HUP, (GUnixFDSourceFunc) schedulerUpdateFromTimerfd, data); + if (data->timerWatchID == 0) { + g_print("Failed to add timerfd to main loop.\n"); + close(data->timerfd); + return; + } + + updateButtonSensitivity(data, FALSE); + + data->schedulerRunning = 1; + appendTextToView("Scheduler started.\n", data->textView); +} + +static void activate(GtkApplication *app, gpointer userData) { + GtkWidget *window, *runButton, *processListView, *cpuButton, *ioButton, *mixedButton; + struct initData *data = (struct initData *) userData; + + //init gtk ui + data->builder = gtk_builder_new_from_file("../user_interface.xml"); + window = GTK_WIDGET(gtk_builder_get_object(data->builder, "main_window")); + gtk_window_set_application(GTK_WINDOW(window), GTK_APPLICATION(app)); + + //retrieve textview + processListView = GTK_WIDGET(gtk_builder_get_object(data->builder, "ProcessList")); + if (!processListView) { + g_print("Error: processListView is NULL\n"); + } else { + data->textView = GTK_TEXT_VIEW(processListView); + } + + //connect "run all" button to scheduler + runButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "runAllButton")); + if (!runButton) + g_print("Error: runButton is NULL\n"); + else + g_signal_connect(runButton, "clicked", G_CALLBACK(runScheduler), data); + + //connect all buttons + cpuButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "cpuButton")); + if (!cpuButton) + g_print("Error: cpuButton is NULL\n"); + else + g_signal_connect(cpuButton, "clicked", G_CALLBACK(addCPUProcess), data); + + ioButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "ioButton")); + if (!ioButton) { + g_print("Error: ioButton is NULL\n"); + } else { + g_signal_connect(ioButton, "clicked", G_CALLBACK(addIOProcess), data); + } + + mixedButton = GTK_WIDGET(gtk_builder_get_object(data->builder, "mixedButton")); + if (!mixedButton) { + g_print("Error: mixedButton is NULL\n"); + } else { + g_signal_connect(mixedButton, "clicked", G_CALLBACK(addMixedProcess), data); + } + + //add a timeout to continue updating the textview every 100ms + g_timeout_add(100, updateTextviewFromLog, data); + + //initialize rest of scheduler + data->schedulerRunning = 0; + data->timerWatchID = 0; + data->logBuffer = g_string_new(NULL); + + //allocate queue + data->allProcesses = (Queue *) malloc(sizeof(Queue)); + if (data->allProcesses == NULL) { + perror("main: Couldn't allocate memory for process queue"); + exit(1); + } + initQueue(data->allProcesses); //initialize variables + + //show gtk window + gtk_window_present(GTK_WINDOW(window)); +} + +void scrollToBottom(GtkTextView *textView) { + GtkAdjustment *adjustment = gtk_scrollable_get_vadjustment(GTK_SCROLLABLE(textView)); + + //get upper limit of the vertical adjustment (the bottom) + gdouble upper = gtk_adjustment_get_upper(adjustment); + + //set adjustment value to the upper limit (scroll to bottom) + gtk_adjustment_set_value(adjustment, upper); +} + +void appendTextToView(const char *text, GtkTextView *textView) { + if (!textView) { + g_print("Error: textView is NULL in appendTextToView\n"); + return; + } + + GtkTextBuffer *buffer = gtk_text_view_get_buffer(textView); + if (!buffer) { + g_print("Error: GtkTextBuffer is NULL\n"); + return; + } + + GtkTextIter start, end; + gtk_text_buffer_get_start_iter(buffer, &start); + gtk_text_buffer_get_end_iter(buffer, &end); + + //keep textview buffer within certain size + int current_size = gtk_text_iter_get_offset(&end); + if (current_size > 10000) { + GtkTextIter new_start; + gtk_text_buffer_get_iter_at_offset(buffer, &new_start, current_size - 10000); + gtk_text_buffer_delete(buffer, &start, &new_start); + } + + //insert new text + gtk_text_buffer_get_end_iter(buffer, &end); + gtk_text_buffer_insert(buffer, &end, text, -1); + + //scroll to bottom + scrollToBottom(textView); +} + +gboolean updateTextviewFromLog(gpointer userData) { + initData *data = (initData *) userData; + + if (data->logBuffer->len > 0) { + //copy logBuffer content + gchar *newText = g_strdup(data->logBuffer->str); + + //clear logBuffer + g_string_truncate(data->logBuffer, 0); + + //append new text to GtkTextView + appendTextToView(newText, data->textView); + + g_free(newText); + } + + return TRUE; //call this function continuously +} + +void freeAll(struct initData *initialData) { + Queue *q = initialData->allProcesses; + Node *current = q->front; + Node *nextNode; + + while (current != NULL) { + nextNode = current->next; + + //free process + if (current->process != NULL) { + free(current->process->name); + for (int i = 0; current->process->args[i] != NULL; i++) { + free(current->process->args[i]); + } + free(current->process->args); + free(current->process); + } + + //free node + free(current); + current = nextNode; + } + + q->front = NULL; + q->rear = NULL; + q->numElements = 0; + + free(q); + + //unref gtk objects + if (initialData->builder) { + g_object_unref(initialData->builder); + } + + if(initialData->logBuffer) { + g_string_free(initialData->logBuffer, TRUE); + } + + free(initialData); +} \ No newline at end of file diff --git a/main.h b/main.h new file mode 100644 index 0000000..cc02a6a --- /dev/null +++ b/main.h @@ -0,0 +1,52 @@ +#ifndef ROBINWAITA_MAIN_H +#define ROBINWAITA_MAIN_H +#include +#include +#include +#include "scheduler.h" + +struct Queue; + +//data that gets passed through gpointer, contains queue and other info +typedef struct initData { + //whether scheduler is running + int schedulerRunning; + + //statistics variables + int timerfd; + int processAmt; + double totalTime; + int contextSwitches; + + //timer info + guint timerWatchID; + + //allProcesses queue + Queue *allProcesses; + + //gtk text view + GtkTextView *textView; + + //gtk builder + GtkBuilder *builder; + + //in-memory buffer + GString *logBuffer; + +} initData; + +void updateStatistics(gpointer userData); //calculate & update statistics screen +gboolean schedulerUpdateFromTimerfd(gint fd, GIOCondition condition, gpointer userData); //call scheduler from timerfd updates +void addProcess(int processType, struct initData *data); //enqueue process and display newest +void addCPUProcess(GtkWidget *widget, gpointer userData); //button to add cpu process to queue +void addIOProcess(GtkWidget *widget, gpointer userData); //button to add io process to queue +void addMixedProcess(GtkWidget *widget, gpointer userData); //button to add mixed-bound process to queue +void updateButtonSensitivity(initData *data, gboolean sensitive); //gray out buttons if scheduler is running +void runScheduler(GtkButton *button, gpointer userData); //init & start running scheduler +static void activate(GtkApplication *app, gpointer userData); //init gtk window objects +void scrollToBottom(GtkTextView *textView); //keep gtk textview scrolled to bottom +void appendTextToView(const char *text, GtkTextView *textView); //add text to gtk textview +gboolean updateTextviewFromLog(gpointer userData); //copy text from buffer and call appendTextToView with the text +void freeAll(struct initData *initialData); //free dynamically allocated memory + +#endif //ROBINWAITA_MAIN_H diff --git a/process.c b/process.c new file mode 100644 index 0000000..3571565 --- /dev/null +++ b/process.c @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include +#include +#include +#include "process.h" +#include "main.h" + +Process* newProcess(int processType){ + + Process* createdProcess = (Process*) malloc(sizeof(Process)); + + if(createdProcess == NULL){ + perror("newProcess: Failed to create process struct"); + return NULL; + } + + char newName[16]; + char testProcess[35]; + + //init integer values + createdProcess->state = 0; + createdProcess->pid = 0; + + //set process information + if(processType == 1) { + strcpy(newName, "CPUBound"); + strcpy(testProcess, "../testing_processes/cpubound.py"); + } else if(processType == 2) { + strcpy(newName, "IOBound"); + strcpy(testProcess, "../testing_processes/iobound.py"); + } else if(processType == 3) { + strcpy(newName, "MixedBound"); + strcpy(testProcess, "../testing_processes/mixbound.py"); + } + + //allocate memory for name + createdProcess->name = (char*) malloc(sizeof(char) * (strlen(newName)+1)); + if (createdProcess->name == NULL) { + perror("newProcess: Failed to allocate memory for process struct name"); + free(createdProcess); + return NULL; + } + strcpy(createdProcess->name, newName); + + //allocate memory for the arguments array + createdProcess->args = (char**) malloc(3 * sizeof(char*)); //allocate python3, path, NULL terminator, and amount of addl arguments + if (createdProcess->args == NULL) { + perror("newProcess: Failed to allocate memory for process struct arguments"); + free(createdProcess->name); + free(createdProcess); + return NULL; + } + + //allocate and set the python argument + createdProcess->args[0] = (char*) malloc(strlen("python3")+1); + if (createdProcess->args[0] == NULL) { + perror("newProcess: Failed to allocate memory for python3 argument"); + free(createdProcess->args); + free(createdProcess->name); + free(createdProcess); + return NULL; + } + strcpy(createdProcess->args[0], "python3"); + + //allocate and set the name of the script + createdProcess->args[1] = (char*) malloc(strlen(testProcess)+1); + if (createdProcess->args[1] == NULL) { + perror("newProcess: Failed to allocate memory for script argument"); + for (int i = 0; createdProcess->args[i] != NULL; i++) { + free(createdProcess->args[i]); + } + free(createdProcess->args); + free(createdProcess->name); + free(createdProcess); + return NULL; + } + strcpy(createdProcess->args[1], testProcess); + + //null-terminate arguments array + createdProcess->args[2] = NULL; + + return createdProcess; +} + +void startProcess(Process *process, initData *data) { + pid_t pid; + pid = fork(); //fork process + + if (pid == 0) { //child process + execvp(process->args[0], process->args); + + //will only run if execvp fails + perror("startProcess: execvp failed"); + exit(EXIT_FAILURE); + } else if (pid > 0) { //parent process + process->pid = pid; + process->state = 1; + + //append message for gtktextview + gchar *message = g_strdup_printf("Started process %d (%s)\n", pid, process->name); + g_string_append(data->logBuffer, message); + g_free(message); + } else { //fork failed + perror("startProcess: fork failed"); + } +} + +void stopProcess(Process *process, initData *data) { + int status; + pid_t result = waitpid(process->pid, &status, WNOHANG); + + if (result == 0) { + //process is still running, safe to send SIGSTOP + if (kill(process->pid, SIGSTOP) == -1) { + perror("Failed to stop process"); + exit(EXIT_FAILURE); + } else { + process->state = 2; + + gchar *message = g_strdup_printf("Stopped process %d (%s)\n", process->pid, process->name); + g_string_append(data->logBuffer, message); + g_free(message); + } + } else if (result > 0) { + //process has already finished + process->state = 3; + fprintf(stderr, "Program %d has already completed, cannot stop\n", process->pid); + } else { + //an error occurred in waitpid + perror("stopProcess: waitpid failed"); + exit(EXIT_FAILURE); + } +} + +void continueProcess(Process *process, initData *data) { + if (kill(process->pid, SIGCONT) == -1) { //resume process by sending SIGSTOP signal + fprintf(stderr, "Program %d failed to resume\n", process->pid); //print error to stderr + exit(EXIT_FAILURE); + } else { + gchar *message = g_strdup_printf("Continued process %d (%s)\n", process->pid, process->name); + g_string_append(data->logBuffer, message); + g_free(message); + + process->state = 1; + } +} \ No newline at end of file diff --git a/process.h b/process.h new file mode 100644 index 0000000..2e8fe5a --- /dev/null +++ b/process.h @@ -0,0 +1,22 @@ +#ifndef ROBINWAITA_PROCESS_H +#define ROBINWAITA_PROCESS_H +#include +#include + +typedef struct initData initData; + +typedef struct { + char *name; //process name + char **args; //any arguments we're passing to the process + int state; //STATES || 0: not started, not running | 1: running | 2: started, paused | 3: completed + pid_t pid; //process ID + struct timeval startTime; //process start time + struct timeval endTime; //process end time +} Process; + +Process *newProcess(int processType); //create new process struct +void startProcess(Process *process, initData *data); //start process (fork into child and then execute python process) +void stopProcess(Process *process, initData *data); //stop process +void continueProcess(Process *process, initData *data); //continue process from stop + +#endif //ROBINWAITA_PROCESS_H diff --git a/scheduler.c b/scheduler.c new file mode 100644 index 0000000..04822dd --- /dev/null +++ b/scheduler.c @@ -0,0 +1,193 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "main.h" + +void displayProcess(Process *p, Queue *q, initData *data){ + gchar *output = g_strdup_printf("Process %d: %s\n", q->numElements, p->name); + g_string_append(data->logBuffer, output); + g_free(output); +} + +void initQueue(Queue *q){ + q->front = NULL; + q->rear = NULL; + q->numElements = 0; +} + +int isEmpty(Queue *q){ + return (q->front == NULL); +} + +void enQueue(Queue *q, Process *p) { + Node *newNode = (Node*) malloc(sizeof(Node)); + if (newNode == NULL) { + perror("enQueue: Failed to allocate memory for new node"); + return; + } + + newNode->process = p; + newNode->next = NULL; + + if (isEmpty(q)) { + q->front = newNode; + q->rear = newNode; + } else { + q->rear->next = newNode; + q->rear = newNode; + } + + q->numElements++; +} + +void deQueue(Queue *q) { + if (isEmpty(q)) { + perror("deQueue: Attempt to dequeue from empty queue"); + return; + } + + Node *temp = q->front; + q->front = q->front->next; + + if (temp->process->state == 3) { + //free if completed + free(temp->process->name); + for (int i = 0; temp->process->args[i] != NULL; i++) { + free(temp->process->args[i]); + } + free(temp->process->args); + free(temp->process); + } + + free(temp); + + if (q->front == NULL) { + q->rear = NULL; + } + + q->numElements--; +} + +int timerSetup(Queue *q){ + struct itimerspec its; + int timerfd = 0; + + timerfd = timerfd_create(CLOCK_REALTIME, 0); + if (timerfd == -1) { + perror("timerSetup: timerfd_create failed"); + exit(EXIT_FAILURE); + } + + //configure timer expiration intervals + its.it_value.tv_sec = 1; //first expiration after 1 second + its.it_value.tv_nsec = 0; + its.it_interval.tv_sec = 1; //recurring interval every 1 second + its.it_interval.tv_nsec = 0; + + if (timerfd_settime(timerfd, 0, &its, NULL) == -1) { + perror("timerSetup: timerfd_settime failed"); + exit(EXIT_FAILURE); + } + + return timerfd; + +} + +void checkCompletion(Process *p){ + + //if the process hasn't started or the process has already been marked as completed + if(p == NULL || p->state == 0 || p->state == 3) return; + + int status; + pid_t result = waitpid(p->pid, &status, WNOHANG); + if (result > 0 && (WIFEXITED(status) || result == WIFSIGNALED(status))) { //process has completed, so log that + p->state = 3; + gettimeofday(&p->endTime, NULL); + } else if (result < 0) { //error occurred + perror("checkCompletion: waitpid encountered an error when checking status\n"); + } + +} + +void manageQueue(struct initData *data) { + + Process *currentProcess; + + //check if there's even anything to schedule + if (isEmpty(data->allProcesses)){ + if(close(data->timerfd) == -1){ + perror("manageQueue: Error closing timerfd"); + } else { + data->schedulerRunning = 0; + gchar *message = g_strdup_printf("All processes have finished executing. Timer stopped.\n"); + g_string_append(data->logBuffer, message); + g_free(message); + + updateStatistics(data); + updateButtonSensitivity(data, TRUE); + + //remove the giochannel watch + if (data->timerWatchID != 0) { + g_source_remove(data->timerWatchID); + data->timerWatchID = 0; + } + + } + + return; //exit loop + + } + + currentProcess = data->allProcesses->front->process; + checkCompletion(currentProcess); + + if (currentProcess->state != 1) { //if the process isn't running + + if (currentProcess->state == 3) { //if the process has completed + data->processAmt += 1; + addElapsedTime(currentProcess, &data->totalTime, data); + deQueue(data->allProcesses); + } else if (currentProcess->state == 0) { //if the process hasn't even started + startProcess(currentProcess, data); + gettimeofday(¤tProcess->startTime, NULL); //set process start time + } else if (currentProcess->state == 2) { //if process has started but is paused + data->contextSwitches += 1; + continueProcess(currentProcess, data); + } + + return; //start the loop again + + } + + //section: if the process is running + + //only apply timer-based scheduling logic if there's more than one process in the queue + if(data->allProcesses->numElements > 1){ //stop current process and start next one + data->contextSwitches += 1; + stopProcess(currentProcess, data); + deQueue(data->allProcesses); //increments front + enQueue(data->allProcesses, currentProcess); //enqueue process at end of queue + return; + } + +} + +void addElapsedTime(Process *p, double *totalTime, initData *data) { + + long seconds = p->endTime.tv_sec - p->startTime.tv_sec; + long microseconds = p->endTime.tv_usec - p->startTime.tv_usec; + double processTime = (double) seconds + (double) microseconds * 1e-6; + + gchar *message = g_strdup_printf("The %s process took %.0f seconds to complete.\n", p->name, processTime); + g_string_append(data->logBuffer, message); + g_free(message); + + *totalTime += processTime; + +} \ No newline at end of file diff --git a/scheduler.h b/scheduler.h new file mode 100644 index 0000000..98e19f2 --- /dev/null +++ b/scheduler.h @@ -0,0 +1,28 @@ +#ifndef ROBINWAITA_SCHEDULER_H +#define ROBINWAITA_SCHEDULER_H +#include "process.h" + +typedef struct initData initData; +struct Queue; + +typedef struct Node { + Process *process; + struct Node *next; +} Node; + +typedef struct Queue { + Node *front; + Node *rear; + int numElements; +} Queue; + +void displayProcess(Process *p, Queue *q, initData *data); //display new process when added to queue +void initQueue(Queue *q); //initialize process queue +int isEmpty(Queue *q); //check if queue empty +void enQueue(Queue *q, Process *p); //add process struct to queue +void deQueue(Queue *q); //remove process struct from queue +int timerSetup(Queue *q); //setup timerfd +void manageQueue(struct initData *data); //scheduling logic, gets called repeatedly +void addElapsedTime(Process *p, double *totalTime, initData *data); //get total time for running processes and display + +#endif //ROBINWAITA_SCHEDULER_H diff --git a/screenshots/img1.png b/screenshots/img1.png new file mode 100644 index 0000000..0b54be8 Binary files /dev/null and b/screenshots/img1.png differ diff --git a/screenshots/scheduler.png b/screenshots/scheduler.png new file mode 100644 index 0000000..e7abc69 Binary files /dev/null and b/screenshots/scheduler.png differ diff --git a/screenshots/statistics.png b/screenshots/statistics.png new file mode 100644 index 0000000..d886609 Binary files /dev/null and b/screenshots/statistics.png differ diff --git a/screenshots/timerfd.png b/screenshots/timerfd.png new file mode 100644 index 0000000..e088c8d Binary files /dev/null and b/screenshots/timerfd.png differ diff --git a/testing_processes/cpubound.py b/testing_processes/cpubound.py new file mode 100644 index 0000000..0893f1f --- /dev/null +++ b/testing_processes/cpubound.py @@ -0,0 +1,28 @@ +import os + +def is_prime(n): + if n <= 1: + return False + if n <= 3: + return True + if n % 2 == 0 or n % 3 == 0: + return False + i = 5 + while i * i <= n: + if n % i == 0 or n % (i + 2) == 0: + return False + i += 6 + return True + +def cpu_bound(passed_pid): + count = 0 + for i in range(2, 1000000): # check how many prime numbers exist between 2 and 100,000 + if is_prime(i): + count += 1 + print(f"CPU-bound process {passed_pid} found that {count} numbers were prime.") + + +if __name__ == "__main__": + process_id = os.getpid() + print(f"Running CPU-bound process {process_id}...") + cpu_bound(process_id) \ No newline at end of file diff --git a/testing_processes/iobound.py b/testing_processes/iobound.py new file mode 100644 index 0000000..9355659 --- /dev/null +++ b/testing_processes/iobound.py @@ -0,0 +1,18 @@ +import time +import os + +def io_bound(passed_pid): + file_name = f"iobound_test_{passed_pid}.txt" + with open(file_name, "a") as f: + for i in range(10): + f.write(f"Writing iteration {i + 1} from process {passed_pid}\n") + f.flush() # ensure data written + time.sleep(0.1) # simulate i/o delay + print(f"I/O-bound process {passed_pid} iteration {i + 1} completed.") + print(f"I/O-bound process {passed_pid} completed.") + + +if __name__ == "__main__": + process_id = os.getpid() + print(f"Running I/O bound process {process_id}...") + io_bound(process_id) diff --git a/testing_processes/mixbound.py b/testing_processes/mixbound.py new file mode 100644 index 0000000..667acc3 --- /dev/null +++ b/testing_processes/mixbound.py @@ -0,0 +1,36 @@ +import time +import os + +def is_prime(n): + if n <= 1: + return False + if n <= 3: + return True + if n % 2 == 0 or n % 3 == 0: + return False + i = 5 + while i * i <= n: + if n % i == 0 or n % (i + 2) == 0: + return False + i += 6 + return True + +def mix_bound(passed_pid): + file_name = f"mixbound_test_{passed_pid}.txt" + count = 0 + for i in range(10): + for j in range(2, 500000): + if is_prime(j): + count += 1 + with open(file_name, "a") as f: + f.write(f"Writing iteration {i + 1} from process {passed_pid}\n") + f.flush() # ensure data written + time.sleep(0.05) # simulate i/o delay + print(f"Mixed-bound process {passed_pid} iteration {i + 1} completed.") + print(f"Mixed-bound process {passed_pid} found that {count} numbers were prime.") + + +if __name__ == "__main__": + process_id = os.getpid() + print(f"Running mixed-bound process {process_id}...") + mix_bound(process_id) diff --git a/user_interface.xml b/user_interface.xml new file mode 100644 index 0000000..005f55e --- /dev/null +++ b/user_interface.xml @@ -0,0 +1,318 @@ + + + + + + + + + + + stack + wide + + + + + + + + + + + + view1 + Scheduler + computer-symbolic + + + vertical + 10 + + + 40 + 40 + 40 + 40 + vertical + + + center + center + All Processes + + + + + + center + 10 + Run the round-robin process scheduler, or add new processes to the queue here. + + + + + + + horizontal + 20 + 20 + 20 + True + + + horizontal + 5 + True + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + False + none + 10 + 10 + True + + + + + + + + + vertical + 10 + end + False + center + + + Add Process + start + 10 + + + + + + horizontal + + + CPU-Bound + start + True + + + + + 15 + end + False + + + list-add-symbolic + Add + False + + + + + + + + + horizontal + + + IO-Bound + start + True + + + + + 15 + end + False + + + list-add-symbolic + Add + False + + + + + + + + + horizontal + + + Mixed-Bound + start + True + + + + + 15 + end + False + + + list-add-symbolic + Add + False + + + + + + + + + + + + + 20 + 20 + 10 + 30 + center + 200 + + + media-playback-start-symbolic + Run All + False + + + + + + + + + + + + + + stats + Statistics + power-profile-performance-symbolic + + + vertical + 10 + + + 40 + 40 + 40 + 40 + vertical + + + center + center + Your Last Run + + + + + + center + 10 + View information on the scheduler's last run here. + + + + + + + 40 + 40 + 40 + vertical + + + none + + + + Average Running Time + Run to get results + false + + + + + + Total Context Switches + Run to get results + false + + + + + + Time Quantum + 1 second + false + + + + + + + + center + 40 + Robinwaita 1.0.0 -- made with ❤️ by Alexei S + + + + + + + + + + + + + + + stack + + + + + + \ No newline at end of file