// SPDX-License-Identifier: GPL-2.0 /* * A test of splitting PMD THPs and PTE-mapped THPs from a specified virtual * address range in a process via /split_huge_pages interface. */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include "vm_util.h" #include "../kselftest.h" uint64_t pagesize; unsigned int pageshift; uint64_t pmd_pagesize; #define SPLIT_DEBUGFS "/sys/kernel/debug/split_huge_pages" #define SMAP_PATH "/proc/self/smaps" #define INPUT_MAX 80 #define PID_FMT "%d,0x%lx,0x%lx,%d" #define PATH_FMT "%s,0x%lx,0x%lx,%d" #define PFN_MASK ((1UL<<55)-1) #define KPF_THP (1UL<<22) int is_backed_by_thp(char *vaddr, int pagemap_file, int kpageflags_file) { uint64_t paddr; uint64_t page_flags; if (pagemap_file) { pread(pagemap_file, &paddr, sizeof(paddr), ((long)vaddr >> pageshift) * sizeof(paddr)); if (kpageflags_file) { pread(kpageflags_file, &page_flags, sizeof(page_flags), (paddr & PFN_MASK) * sizeof(page_flags)); return !!(page_flags & KPF_THP); } } return 0; } static void write_file(const char *path, const char *buf, size_t buflen) { int fd; ssize_t numwritten; fd = open(path, O_WRONLY); if (fd == -1) ksft_exit_fail_msg("%s open failed: %s\n", path, strerror(errno)); numwritten = write(fd, buf, buflen - 1); close(fd); if (numwritten < 1) ksft_exit_fail_msg("Write failed\n"); } static void write_debugfs(const char *fmt, ...) { char input[INPUT_MAX]; int ret; va_list argp; va_start(argp, fmt); ret = vsnprintf(input, INPUT_MAX, fmt, argp); va_end(argp); if (ret >= INPUT_MAX) ksft_exit_fail_msg("%s: Debugfs input is too long\n", __func__); write_file(SPLIT_DEBUGFS, input, ret + 1); } static char *allocate_zero_filled_hugepage(size_t len) { char *result; size_t i; result = memalign(pmd_pagesize, len); if (!result) { printf("Fail to allocate memory\n"); exit(EXIT_FAILURE); } madvise(result, len, MADV_HUGEPAGE); for (i = 0; i < len; i++) result[i] = (char)0; return result; } static void verify_rss_anon_split_huge_page_all_zeroes(char *one_page, int nr_hpages, size_t len) { unsigned long rss_anon_before, rss_anon_after; size_t i; if (!check_huge_anon(one_page, 4, pmd_pagesize)) { printf("No THP is allocated\n"); exit(EXIT_FAILURE); } rss_anon_before = rss_anon(); if (!rss_anon_before) { printf("No RssAnon is allocated before split\n"); exit(EXIT_FAILURE); } /* split all THPs */ write_debugfs(PID_FMT, getpid(), (uint64_t)one_page, (uint64_t)one_page + len, 0); for (i = 0; i < len; i++) if (one_page[i] != (char)0) { printf("%ld byte corrupted\n", i); exit(EXIT_FAILURE); } if (!check_huge_anon(one_page, 0, pmd_pagesize)) { printf("Still AnonHugePages not split\n"); exit(EXIT_FAILURE); } rss_anon_after = rss_anon(); if (rss_anon_after >= rss_anon_before) { printf("Incorrect RssAnon value. Before: %ld After: %ld\n", rss_anon_before, rss_anon_after); exit(EXIT_FAILURE); } } void split_pmd_zero_pages(void) { char *one_page; int nr_hpages = 4; size_t len = nr_hpages * pmd_pagesize; one_page = allocate_zero_filled_hugepage(len); verify_rss_anon_split_huge_page_all_zeroes(one_page, nr_hpages, len); printf("Split zero filled huge pages successful\n"); free(one_page); } void split_pmd_thp(void) { char *one_page; size_t len = 4 * pmd_pagesize; size_t i; one_page = memalign(pmd_pagesize, len); if (!one_page) ksft_exit_fail_msg("Fail to allocate memory: %s\n", strerror(errno)); madvise(one_page, len, MADV_HUGEPAGE); for (i = 0; i < len; i++) one_page[i] = (char)i; if (!check_huge_anon(one_page, 4, pmd_pagesize)) ksft_exit_fail_msg("No THP is allocated\n"); /* split all THPs */ write_debugfs(PID_FMT, getpid(), (uint64_t)one_page, (uint64_t)one_page + len, 0); for (i = 0; i < len; i++) if (one_page[i] != (char)i) ksft_exit_fail_msg("%ld byte corrupted\n", i); if (!check_huge_anon(one_page, 0, pmd_pagesize)) ksft_exit_fail_msg("Still AnonHugePages not split\n"); ksft_test_result_pass("Split huge pages successful\n"); free(one_page); } void split_pte_mapped_thp(void) { char *one_page, *pte_mapped, *pte_mapped2; size_t len = 4 * pmd_pagesize; uint64_t thp_size; size_t i; const char *pagemap_template = "/proc/%d/pagemap"; const char *kpageflags_proc = "/proc/kpageflags"; char pagemap_proc[255]; int pagemap_fd; int kpageflags_fd; if (snprintf(pagemap_proc, 255, pagemap_template, getpid()) < 0) ksft_exit_fail_msg("get pagemap proc error: %s\n", strerror(errno)); pagemap_fd = open(pagemap_proc, O_RDONLY); if (pagemap_fd == -1) ksft_exit_fail_msg("read pagemap: %s\n", strerror(errno)); kpageflags_fd = open(kpageflags_proc, O_RDONLY); if (kpageflags_fd == -1) ksft_exit_fail_msg("read kpageflags: %s\n", strerror(errno)); one_page = mmap((void *)(1UL << 30), len, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); if (one_page == MAP_FAILED) ksft_exit_fail_msg("Fail to allocate memory: %s\n", strerror(errno)); madvise(one_page, len, MADV_HUGEPAGE); for (i = 0; i < len; i++) one_page[i] = (char)i; if (!check_huge_anon(one_page, 4, pmd_pagesize)) ksft_exit_fail_msg("No THP is allocated\n"); /* remap the first pagesize of first THP */ pte_mapped = mremap(one_page, pagesize, pagesize, MREMAP_MAYMOVE); /* remap the Nth pagesize of Nth THP */ for (i = 1; i < 4; i++) { pte_mapped2 = mremap(one_page + pmd_pagesize * i + pagesize * i, pagesize, pagesize, MREMAP_MAYMOVE|MREMAP_FIXED, pte_mapped + pagesize * i); if (pte_mapped2 == MAP_FAILED) ksft_exit_fail_msg("mremap failed: %s\n", strerror(errno)); } /* smap does not show THPs after mremap, use kpageflags instead */ thp_size = 0; for (i = 0; i < pagesize * 4; i++) if (i % pagesize == 0 && is_backed_by_thp(&pte_mapped[i], pagemap_fd, kpageflags_fd)) thp_size++; if (thp_size != 4) ksft_exit_fail_msg("Some THPs are missing during mremap\n"); /* split all remapped THPs */ write_debugfs(PID_FMT, getpid(), (uint64_t)pte_mapped, (uint64_t)pte_mapped + pagesize * 4, 0); /* smap does not show THPs after mremap, use kpageflags instead */ thp_size = 0; for (i = 0; i < pagesize * 4; i++) { if (pte_mapped[i] != (char)i) ksft_exit_fail_msg("%ld byte corrupted\n", i); if (i % pagesize == 0 && is_backed_by_thp(&pte_mapped[i], pagemap_fd, kpageflags_fd)) thp_size++; } if (thp_size) ksft_exit_fail_msg("Still %ld THPs not split\n", thp_size); ksft_test_result_pass("Split PTE-mapped huge pages successful\n"); munmap(one_page, len); close(pagemap_fd); close(kpageflags_fd); } void split_file_backed_thp(void) { int status; int fd; ssize_t num_written; char tmpfs_template[] = "/tmp/thp_split_XXXXXX"; const char *tmpfs_loc = mkdtemp(tmpfs_template); char testfile[INPUT_MAX]; uint64_t pgoff_start = 0, pgoff_end = 1024; ksft_print_msg("Please enable pr_debug in split_huge_pages_in_file() for more info.\n"); status = mount("tmpfs", tmpfs_loc, "tmpfs", 0, "huge=always,size=4m"); if (status) ksft_exit_fail_msg("Unable to create a tmpfs for testing\n"); status = snprintf(testfile, INPUT_MAX, "%s/thp_file", tmpfs_loc); if (status >= INPUT_MAX) { ksft_exit_fail_msg("Fail to create file-backed THP split testing file\n"); } fd = open(testfile, O_CREAT|O_WRONLY, 0664); if (fd == -1) { ksft_perror("Cannot open testing file"); goto cleanup; } /* write something to the file, so a file-backed THP can be allocated */ num_written = write(fd, tmpfs_loc, strlen(tmpfs_loc) + 1); close(fd); if (num_written < 1) { ksft_perror("Fail to write data to testing file"); goto cleanup; } /* split the file-backed THP */ write_debugfs(PATH_FMT, testfile, pgoff_start, pgoff_end, 0); status = unlink(testfile); if (status) { ksft_perror("Cannot remove testing file"); goto cleanup; } status = umount(tmpfs_loc); if (status) { rmdir(tmpfs_loc); ksft_exit_fail_msg("Unable to umount %s\n", tmpfs_loc); } status = rmdir(tmpfs_loc); if (status) ksft_exit_fail_msg("cannot remove tmp dir: %s\n", strerror(errno)); ksft_print_msg("Please check dmesg for more information\n"); ksft_test_result_pass("File-backed THP split test done\n"); return; cleanup: umount(tmpfs_loc); rmdir(tmpfs_loc); ksft_exit_fail_msg("Error occurred\n"); } bool prepare_thp_fs(const char *xfs_path, char *thp_fs_template, const char **thp_fs_loc) { if (xfs_path) { *thp_fs_loc = xfs_path; return false; } *thp_fs_loc = mkdtemp(thp_fs_template); if (!*thp_fs_loc) ksft_exit_fail_msg("cannot create temp folder\n"); return true; } void cleanup_thp_fs(const char *thp_fs_loc, bool created_tmp) { int status; if (!created_tmp) return; status = rmdir(thp_fs_loc); if (status) ksft_exit_fail_msg("cannot remove tmp dir: %s\n", strerror(errno)); } int create_pagecache_thp_and_fd(const char *testfile, size_t fd_size, int *fd, char **addr) { size_t i; int dummy = 0; srand(time(NULL)); *fd = open(testfile, O_CREAT | O_RDWR, 0664); if (*fd == -1) ksft_exit_fail_msg("Failed to create a file at %s\n", testfile); for (i = 0; i < fd_size; i++) { unsigned char byte = (unsigned char)i; write(*fd, &byte, sizeof(byte)); } close(*fd); sync(); *fd = open("/proc/sys/vm/drop_caches", O_WRONLY); if (*fd == -1) { ksft_perror("open drop_caches"); goto err_out_unlink; } if (write(*fd, "3", 1) != 1) { ksft_perror("write to drop_caches"); goto err_out_unlink; } close(*fd); *fd = open(testfile, O_RDWR); if (*fd == -1) { ksft_perror("Failed to open testfile\n"); goto err_out_unlink; } *addr = mmap(NULL, fd_size, PROT_READ|PROT_WRITE, MAP_SHARED, *fd, 0); if (*addr == (char *)-1) { ksft_perror("cannot mmap"); goto err_out_close; } madvise(*addr, fd_size, MADV_HUGEPAGE); for (size_t i = 0; i < fd_size; i++) dummy += *(*addr + i); asm volatile("" : "+r" (dummy)); if (!check_huge_file(*addr, fd_size / pmd_pagesize, pmd_pagesize)) { ksft_print_msg("No large pagecache folio generated, please provide a filesystem supporting large folio\n"); munmap(*addr, fd_size); close(*fd); unlink(testfile); ksft_test_result_skip("Pagecache folio split skipped\n"); return -2; } return 0; err_out_close: close(*fd); err_out_unlink: unlink(testfile); ksft_exit_fail_msg("Failed to create large pagecache folios\n"); return -1; } void split_thp_in_pagecache_to_order(size_t fd_size, int order, const char *fs_loc) { int fd; char *addr; size_t i; char testfile[INPUT_MAX]; int err = 0; err = snprintf(testfile, INPUT_MAX, "%s/test", fs_loc); if (err < 0) ksft_exit_fail_msg("cannot generate right test file name\n"); err = create_pagecache_thp_and_fd(testfile, fd_size, &fd, &addr); if (err) return; err = 0; write_debugfs(PID_FMT, getpid(), (uint64_t)addr, (uint64_t)addr + fd_size, order); for (i = 0; i < fd_size; i++) if (*(addr + i) != (char)i) { ksft_print_msg("%lu byte corrupted in the file\n", i); err = EXIT_FAILURE; goto out; } if (!check_huge_file(addr, 0, pmd_pagesize)) { ksft_print_msg("Still FilePmdMapped not split\n"); err = EXIT_FAILURE; goto out; } out: munmap(addr, fd_size); close(fd); unlink(testfile); if (err) ksft_exit_fail_msg("Split PMD-mapped pagecache folio to order %d failed\n", order); ksft_test_result_pass("Split PMD-mapped pagecache folio to order %d passed\n", order); } int main(int argc, char **argv) { int i; size_t fd_size; char *optional_xfs_path = NULL; char fs_loc_template[] = "/tmp/thp_fs_XXXXXX"; const char *fs_loc; bool created_tmp; ksft_print_header(); if (geteuid() != 0) { ksft_print_msg("Please run the benchmark as root\n"); ksft_finished(); } if (argc > 1) optional_xfs_path = argv[1]; ksft_set_plan(3+9); pagesize = getpagesize(); pageshift = ffs(pagesize) - 1; pmd_pagesize = read_pmd_pagesize(); if (!pmd_pagesize) ksft_exit_fail_msg("Reading PMD pagesize failed\n"); fd_size = 2 * pmd_pagesize; split_pmd_zero_pages(); split_pmd_thp(); split_pte_mapped_thp(); split_file_backed_thp(); created_tmp = prepare_thp_fs(optional_xfs_path, fs_loc_template, &fs_loc); for (i = 8; i >= 0; i--) split_thp_in_pagecache_to_order(fd_size, i, fs_loc); cleanup_thp_fs(fs_loc, created_tmp); ksft_finished(); return 0; }