diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e8adbc4 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +monitor: monitor.c + gcc -o $@ $< + +create_load: create_load.c + gcc -o $@ $< -lpthread + +all: monitor create_load + +.PHONY: clean + +clean: + rm -f monitor create_load + diff --git a/create_load.c b/create_load.c new file mode 100644 index 0000000..a06372b --- /dev/null +++ b/create_load.c @@ -0,0 +1,226 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +/* + * These defines are the number of seconds for which we load + * CPU or I/O + * */ +#define CPU_LOAD_TIME_SECS 10 +#define IO_LOAD_TIME_SECS 10 + +/* + * We split a list of directories to traverse between 2 I/O + * Loader threads. This struct is passed to each of them, + * letting them know the starting index of that list and + * number of directories to traverse. + * + * */ + +typedef struct dir_list { + char **dirs; + int begin_idx; + int count; +}dir_list; + +/* + One function that prints the system call and the error details + and then exits with error code 1. Non-zero meaning things didn't go well. + */ +void fatal_error(const char *syscall) +{ + perror(syscall); + exit(1); +} + +/* + * Get all the top level directories from the root directory. + * */ +char **get_root_dir_entries() { + char **entries = NULL; + DIR *root_dir = opendir("/"); + if (root_dir == NULL) + fatal_error("readdir()"); + + struct dirent *dir; + int i = 0; + while ((dir = readdir(root_dir)) != NULL) { + /* We only save directories and those with names other than "." or ".." */ + if (dir->d_type != DT_DIR || strcmp(dir->d_name, ".") == 0 || strcmp(dir->d_name, "..") == 0) + continue; + + entries = realloc(entries, sizeof(char *) * (i + 1)); + entries[i] = malloc(strlen(dir->d_name) + 2); + strcpy(entries[i], "/"); + strcat(entries[i], dir->d_name); + i++; + } + closedir(root_dir); + + /* We NULL-terminate the list */ + entries = realloc(entries, sizeof(char *) * (i + 1)); + entries[i] = NULL; + + return entries; +} + +/* + * This function is the one that causes the actual I/O load. + * It recursively traverses the directory passed as an argument. + * */ + +void read_dir_contents(char *dir_path) { + struct dirent *entry; + struct stat st; + char buff[16384]; + DIR *dir = opendir(dir_path); + if (dir == NULL) + return; + + while ((entry = readdir(dir)) != NULL) { + /* Let's get the attributes of this entry. + * Though we don't need it, this generates more I/O. */ + stat(entry->d_name, &st); + + if (entry->d_type == DT_REG) { + /* Regular file. Read a little bit from it. */ + int fd = open(entry->d_name, O_RDONLY); + if (fd > 0) { + read(fd, buff, sizeof(buff)); + close(fd); + } + } + if (entry->d_type == DT_DIR && strcmp(entry->d_name, ".") != 0 && strcmp(entry->d_name, "..") != 0) { + /* Found a directory, let's get into it recursively */ + char new_path[1024]; + snprintf(new_path, sizeof(new_path), "%s/%s", dir_path, entry->d_name ); + read_dir_contents(new_path); + } + } + closedir(dir); +} + +/* + * This function is called in a thread. It it iterates through the list + * of directories passed and calls read_dir_contents() for each directory + * in the list. + * + * Since 2 threads are created and they get passed the same list of + * directories, we pass the starting index and the count of directories + * to traverse so that each thread can, in parallel, act on its own + * unique set of directories. This creates more I/O load since 2 threads + * access the filesystem information / data in parallel. + * + * */ + +void *iterate_dirs(void *data) { + time_t time1 = time(NULL); + time_t time2; + dir_list *dl = (dir_list *) data; + printf("I/O Loader thread starting with %d directories to traverse.\n", dl->count); + char **dirs = dl->dirs; + char *dname; + int i = dl->begin_idx; + while (dl->count--) { + dname = dl->dirs[i++]; + read_dir_contents(dname); + time2 = time(NULL); + if (time2 - time1 >= IO_LOAD_TIME_SECS) + break; + } + + return NULL; +} + +/* + * This function gets the names of top-level directories in the root + * directory, splits up that list and passes it to two threads both + * running the same function, iterate_dirs(). + * */ + +void load_disk() { + int i = 0; + pthread_t pthread1, pthread2; + + char **root_dir_entries = get_root_dir_entries(); + while (root_dir_entries[i++] != NULL); + + dir_list dl1, dl2; + dl1.dirs = root_dir_entries; + dl1.begin_idx = 0; + dl1.count = i/2; + + dl2.dirs = root_dir_entries; + dl2.begin_idx = dl1.count - 1; + dl2.count = i - dl1.count; + + pthread_create(&pthread1, NULL, iterate_dirs, (void *) &dl1); + pthread_create(&pthread2, NULL, iterate_dirs, (void *) &dl2); + + /* Wait for both the threads to run to completion */ + pthread_join(pthread1, NULL); + pthread_join(pthread2, NULL); + + printf("********************************************************************************\n"); + printf("Now that the I/O loader threads have run, disk blocks will be cached in RAM.\n"); + printf("You are unlikely to see further I/O-related PSI notifications should you run\n"); + printf("this again. If you want to however, you can run this again after dropping all\n"); + printf("disk caches like so as root:\n"); + printf("\necho 3 > /proc/sys/vm/drop_caches\n"); + printf("\nOr with sudo:\n"); + printf("echo 3 | sudo tee /proc/sys/vm/drop_caches\n"); + printf("********************************************************************************\n"); +} + +/* + * This routine runs in threads. This creates load on the CPU + * by running a tight loop for CPU_LOAD_TIME_SECS seconds. + * + * We create a thread more than there are CPUs. e.g: If there + * are 2 CPUs, we create 3 threads. This is to ensure that + * the system is loaded *beyond* capacity. This creates + * pressure, which is then notified by the PSI subsystem + * to our monitor.c program. + * + * */ +void *cpu_loader_thread(void *data) { + long tid = (long) data; + time_t time1 = time(NULL); + printf("CPU Loader thread %ld starting...\n", tid); + + while (1) { + for (tid=0; tid < 50000000; tid++); + time_t time2 = time(NULL); + if (time2 - time1 >= CPU_LOAD_TIME_SECS) + break; + } + return NULL; +} + +void load_cpu() { + /* Some crazy future-proofing when this runs + * on a 1024-core Arm CPU. Sorry, Intel.*/ + pthread_t threads[1024]; + + /* Get the number of installed CPUs and create as many +1 threads. */ + long num_cpus = sysconf(_SC_NPROCESSORS_ONLN); + for (long i=0; i < num_cpus + 1; i++) { + pthread_create(&threads[i], NULL, cpu_loader_thread, (void *) i); + } + + /* Wait for all threads to complete */ + for (long i=0; i < num_cpus; i++) { + pthread_join(threads[i], NULL); + } +} + +int main() { + load_cpu(); + load_disk(); + return 0; +} diff --git a/monitor.c b/monitor.c new file mode 100644 index 0000000..6355fc2 --- /dev/null +++ b/monitor.c @@ -0,0 +1,126 @@ +#include +#include +#include +#include +#include +#include +#include + +#define CPU_TRACKING_WINDOW_SECS 1 +#define IO_TRACKING_WINDOW_SECS 1 +#define CPU_TRIGGER_THRESHOLD_MS 100 +#define IO_TRIGGER_THRESHOLD_MS 100 +#define CPU_PRESSURE_FILE "/proc/pressure/cpu" +#define IO_PRESSURE_FILE "/proc/pressure/io" +#define FD_CPU_IDX 0 +#define FD_IO_IDX 1 + +struct pollfd fds[2]; + +/* + One function that prints the system call and the error details + and then exits with error code 1. Non-zero meaning things didn't go well. + */ +void fatal_error(const char *syscall) +{ + perror(syscall); + exit(1); +} + +/* + * PSI allows programs to wait for events related to pressure stalls + * via poll() so that they can avoid continuously polling files in the + * /proc/pressure directory. + * + * We setup to be notified via poll for two types of PSI events, one + * for CPU and the other for I/O. + * + * */ + +void setup_polling() { + /* Let's first setup our CPU PSI trigger */ + fds[FD_CPU_IDX].fd = open(CPU_PRESSURE_FILE, O_RDWR | O_NONBLOCK); + if (fds[FD_CPU_IDX].fd < 0) + fatal_error("open(): " CPU_PRESSURE_FILE); + + /* Next, our I/O PSI trigger */ + fds[FD_IO_IDX].fd = open(IO_PRESSURE_FILE, O_RDWR | O_NONBLOCK); + if (fds[FD_IO_IDX].fd < 0) + fatal_error("open(): " IO_PRESSURE_FILE); + + fds[FD_CPU_IDX].events = fds[FD_IO_IDX].events = POLLPRI; + + char trigger[128]; + snprintf(trigger, 128, "some %d %d", CPU_TRIGGER_THRESHOLD_MS * 1000, CPU_TRACKING_WINDOW_SECS * 1000000); + printf("Trigger: %s\n", trigger); + if (write(fds[FD_CPU_IDX].fd, trigger, strlen(trigger) + 1) < 0) + fatal_error("write(): " CPU_PRESSURE_FILE); + snprintf(trigger, 128, "some %d %d", IO_TRIGGER_THRESHOLD_MS * 1000, IO_TRACKING_WINDOW_SECS * 1000000); + printf("Trigger: %s\n", trigger); + if (write(fds[FD_IO_IDX].fd, trigger, strlen(trigger) + 1) < 0) + fatal_error("write(): " IO_PRESSURE_FILE); +} + + +/* + * This is the main function where we wait for notifications from + * PSI. We increment 2 separate variables that track CPU and I/O + * notification counts separately and print them. + * */ + +void wait_for_notification() { + int cpu_event_counter = 1; + int io_event_counter = 1; + + while (1) { + int n = poll(fds, 2, -1); + if (n < 0) { + fatal_error("poll()"); + } + + for (int i = 0; i < 2; i++) { + + /* If the fd of the current iteration does not have any + * events, move on to the next fd. + * */ + if (fds[i].revents == 0) + continue; + + if (fds[i].revents & POLLERR) { + fprintf(stderr, "Error: poll() event source is gone.\n"); + exit(1); + } + if (fds[i].revents & POLLPRI) { + if (i == FD_CPU_IDX) + printf("CPU PSI event %d triggered.\n", cpu_event_counter++); + else + printf("I/O PSI event %d triggered.\n", io_event_counter++); + } else { + fprintf(stderr, "Unrecognized event: 0x%x.\n", fds[i].revents); + exit(1); + } + } + } +} + +/* + * We check for tell-tale signs of the running kernel supporting PSI. + * Else, we print a friendly message and exit. + * */ + +void check_basics() { + struct stat st; + int sret = stat(CPU_PRESSURE_FILE, &st); + if (sret == -1) { + fprintf(stderr, "Error! Your kernel does not expose pressure stall information.\n"); + fprintf(stderr, "You may want to check if you have Linux Kernel v5.2+ with PSI enabled.\n"); + exit(1); + } +} + +int main() { + check_basics(); + setup_polling(); + wait_for_notification(); + return 0; +} \ No newline at end of file