Initial commit
This commit is contained in:
parent
1b161dcf28
commit
14e32a6598
13
Makefile
Normal file
13
Makefile
Normal file
@ -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
|
||||
|
226
create_load.c
Normal file
226
create_load.c
Normal file
@ -0,0 +1,226 @@
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <pthread.h>
|
||||
#include <dirent.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
126
monitor.c
Normal file
126
monitor.c
Normal file
@ -0,0 +1,126 @@
|
||||
#include <stdio.h>
|
||||
#include <poll.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/stat.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
|
||||
#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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user