root/tools/testing/selftests/mm/hugepage_settings.c
// SPDX-License-Identifier: GPL-2.0
#include <dirent.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "vm_util.h"
#include "hugepage_settings.h"

#define THP_SYSFS "/sys/kernel/mm/transparent_hugepage/"
#define MAX_SETTINGS_DEPTH 4
static struct thp_settings settings_stack[MAX_SETTINGS_DEPTH];
static int settings_index;
static struct thp_settings saved_settings;
static char dev_queue_read_ahead_path[PATH_MAX];
static bool thp_settings_saved;

static const char * const thp_enabled_strings[] = {
        "never",
        "always",
        "inherit",
        "madvise",
        NULL
};

static const char * const thp_defrag_strings[] = {
        "always",
        "defer",
        "defer+madvise",
        "madvise",
        "never",
        NULL
};

static const char * const shmem_enabled_strings[] = {
        "never",
        "always",
        "within_size",
        "advise",
        "inherit",
        "deny",
        "force",
        NULL
};

int thp_read_string(const char *name, const char * const strings[])
{
        char path[PATH_MAX];
        char buf[256];
        char *c;
        int ret;

        ret = snprintf(path, PATH_MAX, THP_SYSFS "%s", name);
        if (ret >= PATH_MAX) {
                printf("%s: Pathname is too long\n", __func__);
                exit(EXIT_FAILURE);
        }

        if (!read_file(path, buf, sizeof(buf))) {
                perror(path);
                exit(EXIT_FAILURE);
        }

        c = strchr(buf, '[');
        if (!c) {
                printf("%s: Parse failure\n", __func__);
                exit(EXIT_FAILURE);
        }

        c++;
        memmove(buf, c, sizeof(buf) - (c - buf));

        c = strchr(buf, ']');
        if (!c) {
                printf("%s: Parse failure\n", __func__);
                exit(EXIT_FAILURE);
        }
        *c = '\0';

        ret = 0;
        while (strings[ret]) {
                if (!strcmp(strings[ret], buf))
                        return ret;
                ret++;
        }

        printf("Failed to parse %s\n", name);
        exit(EXIT_FAILURE);
}

void thp_write_string(const char *name, const char *val)
{
        char path[PATH_MAX];
        int ret;

        ret = snprintf(path, PATH_MAX, THP_SYSFS "%s", name);
        if (ret >= PATH_MAX) {
                printf("%s: Pathname is too long\n", __func__);
                exit(EXIT_FAILURE);
        }
        write_file(path, val, strlen(val) + 1);
}

unsigned long thp_read_num(const char *name)
{
        char path[PATH_MAX];
        int ret;

        ret = snprintf(path, PATH_MAX, THP_SYSFS "%s", name);
        if (ret >= PATH_MAX) {
                printf("%s: Pathname is too long\n", __func__);
                exit(EXIT_FAILURE);
        }
        return read_num(path);
}

void thp_write_num(const char *name, unsigned long num)
{
        char path[PATH_MAX];
        int ret;

        ret = snprintf(path, PATH_MAX, THP_SYSFS "%s", name);
        if (ret >= PATH_MAX) {
                printf("%s: Pathname is too long\n", __func__);
                exit(EXIT_FAILURE);
        }
        write_num(path, num);
}

void thp_read_settings(struct thp_settings *settings)
{
        unsigned long orders = thp_supported_orders();
        unsigned long shmem_orders = thp_shmem_supported_orders();
        char path[PATH_MAX];
        int i;

        *settings = (struct thp_settings) {
                .thp_enabled = thp_read_string("enabled", thp_enabled_strings),
                .thp_defrag = thp_read_string("defrag", thp_defrag_strings),
                .shmem_enabled =
                        thp_read_string("shmem_enabled", shmem_enabled_strings),
                .use_zero_page = thp_read_num("use_zero_page"),
        };
        settings->khugepaged = (struct khugepaged_settings) {
                .defrag = thp_read_num("khugepaged/defrag"),
                .alloc_sleep_millisecs =
                        thp_read_num("khugepaged/alloc_sleep_millisecs"),
                .scan_sleep_millisecs =
                        thp_read_num("khugepaged/scan_sleep_millisecs"),
                .max_ptes_none = thp_read_num("khugepaged/max_ptes_none"),
                .max_ptes_swap = thp_read_num("khugepaged/max_ptes_swap"),
                .max_ptes_shared = thp_read_num("khugepaged/max_ptes_shared"),
                .pages_to_scan = thp_read_num("khugepaged/pages_to_scan"),
        };
        if (dev_queue_read_ahead_path[0])
                settings->read_ahead_kb = read_num(dev_queue_read_ahead_path);

        for (i = 0; i < NR_ORDERS; i++) {
                if (!((1 << i) & orders)) {
                        settings->hugepages[i].enabled = THP_NEVER;
                        continue;
                }
                snprintf(path, PATH_MAX, "hugepages-%ukB/enabled",
                        (getpagesize() >> 10) << i);
                settings->hugepages[i].enabled =
                        thp_read_string(path, thp_enabled_strings);
        }

        for (i = 0; i < NR_ORDERS; i++) {
                if (!((1 << i) & shmem_orders)) {
                        settings->shmem_hugepages[i].enabled = SHMEM_NEVER;
                        continue;
                }
                snprintf(path, PATH_MAX, "hugepages-%ukB/shmem_enabled",
                        (getpagesize() >> 10) << i);
                settings->shmem_hugepages[i].enabled =
                        thp_read_string(path, shmem_enabled_strings);
        }
}

void thp_write_settings(struct thp_settings *settings)
{
        struct khugepaged_settings *khugepaged = &settings->khugepaged;
        unsigned long orders = thp_supported_orders();
        unsigned long shmem_orders = thp_shmem_supported_orders();
        char path[PATH_MAX];
        int enabled;
        int i;

        thp_write_string("enabled", thp_enabled_strings[settings->thp_enabled]);
        thp_write_string("defrag", thp_defrag_strings[settings->thp_defrag]);
        thp_write_string("shmem_enabled",
                        shmem_enabled_strings[settings->shmem_enabled]);
        thp_write_num("use_zero_page", settings->use_zero_page);

        thp_write_num("khugepaged/defrag", khugepaged->defrag);
        thp_write_num("khugepaged/alloc_sleep_millisecs",
                        khugepaged->alloc_sleep_millisecs);
        thp_write_num("khugepaged/scan_sleep_millisecs",
                        khugepaged->scan_sleep_millisecs);
        thp_write_num("khugepaged/max_ptes_none", khugepaged->max_ptes_none);
        thp_write_num("khugepaged/max_ptes_swap", khugepaged->max_ptes_swap);
        thp_write_num("khugepaged/max_ptes_shared", khugepaged->max_ptes_shared);
        thp_write_num("khugepaged/pages_to_scan", khugepaged->pages_to_scan);

        if (dev_queue_read_ahead_path[0])
                write_num(dev_queue_read_ahead_path, settings->read_ahead_kb);

        for (i = 0; i < NR_ORDERS; i++) {
                if (!((1 << i) & orders))
                        continue;
                snprintf(path, PATH_MAX, "hugepages-%ukB/enabled",
                        (getpagesize() >> 10) << i);
                enabled = settings->hugepages[i].enabled;
                thp_write_string(path, thp_enabled_strings[enabled]);
        }

        for (i = 0; i < NR_ORDERS; i++) {
                if (!((1 << i) & shmem_orders))
                        continue;
                snprintf(path, PATH_MAX, "hugepages-%ukB/shmem_enabled",
                        (getpagesize() >> 10) << i);
                enabled = settings->shmem_hugepages[i].enabled;
                thp_write_string(path, shmem_enabled_strings[enabled]);
        }
}

struct thp_settings *thp_current_settings(void)
{
        if (!settings_index) {
                printf("Fail: No settings set");
                exit(EXIT_FAILURE);
        }
        return settings_stack + settings_index - 1;
}

void thp_push_settings(struct thp_settings *settings)
{
        if (settings_index >= MAX_SETTINGS_DEPTH) {
                printf("Fail: Settings stack exceeded");
                exit(EXIT_FAILURE);
        }
        settings_stack[settings_index++] = *settings;
        thp_write_settings(thp_current_settings());
}

void thp_pop_settings(void)
{
        if (settings_index <= 0) {
                printf("Fail: Settings stack empty");
                exit(EXIT_FAILURE);
        }
        --settings_index;
        thp_write_settings(thp_current_settings());
}

void thp_restore_settings(void)
{
        if (thp_settings_saved)
                thp_write_settings(&saved_settings);
}

static void __thp_save_settings(void)
{
        if (!thp_available())
                return;

        if (thp_settings_saved)
                return;

        thp_read_settings(&saved_settings);
        thp_settings_saved = true;
}

void thp_set_read_ahead_path(char *path)
{
        if (!path) {
                dev_queue_read_ahead_path[0] = '\0';
                return;
        }

        strncpy(dev_queue_read_ahead_path, path,
                sizeof(dev_queue_read_ahead_path));
        dev_queue_read_ahead_path[sizeof(dev_queue_read_ahead_path) - 1] = '\0';
}

static unsigned long __thp_supported_orders(bool is_shmem)
{
        unsigned long orders = 0;
        char path[PATH_MAX];
        char buf[256];
        int ret, i;
        char anon_dir[] = "enabled";
        char shmem_dir[] = "shmem_enabled";

        for (i = 0; i < NR_ORDERS; i++) {
                ret = snprintf(path, PATH_MAX, THP_SYSFS "hugepages-%ukB/%s",
                               (getpagesize() >> 10) << i, is_shmem ? shmem_dir : anon_dir);
                if (ret >= PATH_MAX) {
                        printf("%s: Pathname is too long\n", __func__);
                        exit(EXIT_FAILURE);
                }

                ret = read_file(path, buf, sizeof(buf));
                if (ret)
                        orders |= 1UL << i;
        }

        return orders;
}

unsigned long thp_supported_orders(void)
{
        return __thp_supported_orders(false);
}

unsigned long thp_shmem_supported_orders(void)
{
        return __thp_supported_orders(true);
}

bool thp_available(void)
{
        if (access(THP_SYSFS, F_OK) != 0)
                return false;
        return true;
}

bool thp_is_enabled(void)
{
        if (!thp_available())
                return false;

        int mode = thp_read_string("enabled", thp_enabled_strings);

        /* THP is considered enabled if it's either "always" or "madvise" */
        return mode == 1 || mode == 3;
}

#define HUGETLB_MAX_NR_PAGESIZES 10
struct hugetlb_settings {
        unsigned long nr_hugepages[HUGETLB_MAX_NR_PAGESIZES];
        unsigned long sizes[HUGETLB_MAX_NR_PAGESIZES];
        unsigned long default_size;
        int nr_sizes;
};

static struct hugetlb_settings hugetlb_saved_settings;
static bool hugetlb_settings_saved;

int detect_hugetlb_page_sizes(unsigned long sizes[], int max)
{
        static struct hugetlb_settings *settings = &hugetlb_saved_settings;
        DIR *dir;
        int count = 0;

        if (settings->nr_sizes) {
                if (settings->nr_sizes < max)
                        max = settings->nr_sizes;
                for (count = 0; count < max; count++)
                        sizes[count] = settings->sizes[count];
                return count;
        }

        dir = opendir("/sys/kernel/mm/hugepages/");
        if (!dir)
                return 0;

        while (count < max) {
                struct dirent *entry = readdir(dir);
                size_t kb;

                if (!entry)
                        break;
                if (entry->d_type != DT_DIR)
                        continue;
                if (sscanf(entry->d_name, "hugepages-%zukB", &kb) != 1)
                        continue;
                sizes[count++] = kb * 1024;
                ksft_print_msg("[INFO] detected hugetlb page size: %zu KiB\n",
                               kb);
        }
        closedir(dir);
        return count;
}

unsigned long default_huge_page_size(void)
{
        static struct hugetlb_settings *settings = &hugetlb_saved_settings;
        unsigned long hps = 0;
        char *line = NULL;
        size_t linelen = 0;
        FILE *f;

        if (settings->default_size)
                return settings->default_size;

        f = fopen("/proc/meminfo", "r");
        if (!f)
                return 0;
        while (getline(&line, &linelen, f) > 0) {
                if (sscanf(line, "Hugepagesize:       %lu kB", &hps) == 1) {
                        hps <<= 10;
                        break;
                }
        }

        free(line);
        fclose(f);
        return hps;
}

static void hugetlb_sysfs_path(char *buf, size_t buflen,
                               unsigned long size, const char *attr)
{
        snprintf(buf, buflen, "/sys/kernel/mm/hugepages/hugepages-%lukB/%s",
                 size / 1024, attr);
}

unsigned long hugetlb_nr_pages(unsigned long size)
{
        char path[PATH_MAX];

        hugetlb_sysfs_path(path, sizeof(path), size, "nr_hugepages");

        return read_num(path);
}

void hugetlb_set_nr_pages(unsigned long size, unsigned long nr)
{
        char path[PATH_MAX];

        hugetlb_sysfs_path(path, sizeof(path), size, "nr_hugepages");

        write_num(path, nr);
}

unsigned long hugetlb_free_pages(unsigned long size)
{
        char path[PATH_MAX];

        hugetlb_sysfs_path(path, sizeof(path), size, "free_hugepages");

        return read_num(path);
}

static bool __hugetlb_setup(unsigned long size, unsigned long nr)
{
        unsigned long free = hugetlb_free_pages(size);
        unsigned long total = hugetlb_nr_pages(size);

        if (free >= nr)
                return true;

        hugetlb_set_nr_pages(size, total + (nr - free));

        return hugetlb_free_pages(size) >= nr;
}

bool hugetlb_setup_default(unsigned long nr)
{
        unsigned long size;

        hugetlb_save_settings();
        size = default_huge_page_size();
        if (!size)
                return false;

        return __hugetlb_setup(size, nr);
}

bool hugetlb_setup_default_exact(unsigned long nr)
{
        unsigned long size;

        hugetlb_save_settings();
        size = default_huge_page_size();
        if (!size)
                return false;

        hugetlb_set_nr_pages(size, nr);

        return hugetlb_free_pages(size) == nr;
}

unsigned long hugetlb_setup(unsigned long nr, unsigned long sizes[],
                            int max)
{
        unsigned long enabled[10];
        int nr_sizes = 0;
        int nr_enabled;

        hugetlb_save_settings();

        nr_enabled = detect_hugetlb_page_sizes(enabled, ARRAY_SIZE(enabled));
        if (!nr_enabled)
                return 0;

        if (nr_enabled > max) {
                ksft_print_msg("detected %d huge page sizes, will only test %d\n", nr_enabled, max);
                nr_enabled = max;
        }

        /* request nr HugeTLB pages of every size. */
        for (int i = 0; i < nr_enabled; i++) {
                if (!__hugetlb_setup(enabled[i], nr))
                        continue;
                sizes[nr_sizes++] = enabled[i];
        }

        return nr_sizes;
}

static void __hugetlb_save_settings(void)
{
        struct hugetlb_settings *settings = &hugetlb_saved_settings;
        int nr_sizes;

        if (hugetlb_settings_saved)
                return;

        settings->default_size = default_huge_page_size();
        if (!settings->default_size)
                return;

        nr_sizes = detect_hugetlb_page_sizes(settings->sizes,
                                             HUGETLB_MAX_NR_PAGESIZES);
        if (!nr_sizes) {
                settings->default_size = 0;
                return;
        }

        for (int i = 0; i < nr_sizes; i++) {
                unsigned long sz = settings->sizes[i];

                if (!sz)
                        continue;
                settings->nr_hugepages[i] = hugetlb_nr_pages(sz);
        }

        settings->nr_sizes = nr_sizes;
        hugetlb_settings_saved = true;
}

void hugetlb_restore_settings(void)
{
        struct hugetlb_settings *settings = &hugetlb_saved_settings;

        if (!hugetlb_settings_saved || !settings->default_size)
                return;

        for (int i = 0; i < HUGETLB_MAX_NR_PAGESIZES; i++) {
                unsigned long sz = settings->sizes[i];

                if (!sz)
                        continue;

                hugetlb_set_nr_pages(sz, settings->nr_hugepages[i]);
        }
}

static void hugepage_restore_settings_atexit(void)
{
        if (thp_settings_saved)
                thp_restore_settings();
        if (hugetlb_settings_saved)
                hugetlb_restore_settings();
}

static void hugepage_restore_settings_sighandler(int sig)
{
        /* exit() will invoke the hugepage_restore_settings_atexit handler. */
        exit(KSFT_FAIL);
}

void hugepage_save_settings(bool thp, bool hugetlb)
{
        if (!thp && !hugetlb)
                return;

        if (thp)
                __thp_save_settings();
        if (hugetlb)
                __hugetlb_save_settings();

        /*
         * setup exit hooks to make sure THP and HugeTLB settings are
         * restored on graceful and error exits and signals
         */
        atexit(hugepage_restore_settings_atexit);
        signal(SIGTERM, hugepage_restore_settings_sighandler);
        signal(SIGINT, hugepage_restore_settings_sighandler);
        signal(SIGHUP, hugepage_restore_settings_sighandler);
        signal(SIGQUIT, hugepage_restore_settings_sighandler);
}