| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Basic tests for PR_GET/SET_THP_DISABLE prctl calls |
| * |
| * Author(s): Usama Arif <usamaarif642@gmail.com> |
| */ |
| #include <stdio.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <unistd.h> |
| #include <sys/mman.h> |
| #include <linux/mman.h> |
| #include <sys/prctl.h> |
| #include <sys/wait.h> |
| |
| #include "kselftest_harness.h" |
| #include "thp_settings.h" |
| #include "vm_util.h" |
| |
| #ifndef PR_THP_DISABLE_EXCEPT_ADVISED |
| #define PR_THP_DISABLE_EXCEPT_ADVISED (1 << 1) |
| #endif |
| |
| enum thp_collapse_type { |
| THP_COLLAPSE_NONE, |
| THP_COLLAPSE_MADV_NOHUGEPAGE, |
| THP_COLLAPSE_MADV_HUGEPAGE, /* MADV_HUGEPAGE before access */ |
| THP_COLLAPSE_MADV_COLLAPSE, /* MADV_COLLAPSE after access */ |
| }; |
| |
| /* |
| * Function to mmap a buffer, fault it in, madvise it appropriately (before |
| * page fault for MADV_HUGE, and after for MADV_COLLAPSE), and check if the |
| * mmap region is huge. |
| * Returns: |
| * 0 if test doesn't give hugepage |
| * 1 if test gives a hugepage |
| * -errno if mmap fails |
| */ |
| static int test_mmap_thp(enum thp_collapse_type madvise_buf, size_t pmdsize) |
| { |
| char *mem, *mmap_mem; |
| size_t mmap_size; |
| int ret; |
| |
| /* For alignment purposes, we need twice the THP size. */ |
| mmap_size = 2 * pmdsize; |
| mmap_mem = (char *)mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, |
| MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); |
| if (mmap_mem == MAP_FAILED) |
| return -errno; |
| |
| /* We need a THP-aligned memory area. */ |
| mem = (char *)(((uintptr_t)mmap_mem + pmdsize) & ~(pmdsize - 1)); |
| |
| if (madvise_buf == THP_COLLAPSE_MADV_HUGEPAGE) |
| madvise(mem, pmdsize, MADV_HUGEPAGE); |
| else if (madvise_buf == THP_COLLAPSE_MADV_NOHUGEPAGE) |
| madvise(mem, pmdsize, MADV_NOHUGEPAGE); |
| |
| /* Ensure memory is allocated */ |
| memset(mem, 1, pmdsize); |
| |
| if (madvise_buf == THP_COLLAPSE_MADV_COLLAPSE) |
| madvise(mem, pmdsize, MADV_COLLAPSE); |
| |
| /* HACK: make sure we have a separate VMA that we can check reliably. */ |
| mprotect(mem, pmdsize, PROT_READ); |
| |
| ret = check_huge_anon(mem, 1, pmdsize); |
| munmap(mmap_mem, mmap_size); |
| return ret; |
| } |
| |
| static void prctl_thp_disable_completely_test(struct __test_metadata *const _metadata, |
| size_t pmdsize, |
| enum thp_enabled thp_policy) |
| { |
| ASSERT_EQ(prctl(PR_GET_THP_DISABLE, NULL, NULL, NULL, NULL), 1); |
| |
| /* tests after prctl overrides global policy */ |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_NONE, pmdsize), 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_NOHUGEPAGE, pmdsize), 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_HUGEPAGE, pmdsize), 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_COLLAPSE, pmdsize), 0); |
| |
| /* Reset to global policy */ |
| ASSERT_EQ(prctl(PR_SET_THP_DISABLE, 0, NULL, NULL, NULL), 0); |
| |
| /* tests after prctl is cleared, and only global policy is effective */ |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_NONE, pmdsize), |
| thp_policy == THP_ALWAYS ? 1 : 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_NOHUGEPAGE, pmdsize), 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_HUGEPAGE, pmdsize), |
| thp_policy == THP_NEVER ? 0 : 1); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_COLLAPSE, pmdsize), 1); |
| } |
| |
| FIXTURE(prctl_thp_disable_completely) |
| { |
| struct thp_settings settings; |
| size_t pmdsize; |
| }; |
| |
| FIXTURE_VARIANT(prctl_thp_disable_completely) |
| { |
| enum thp_enabled thp_policy; |
| }; |
| |
| FIXTURE_VARIANT_ADD(prctl_thp_disable_completely, never) |
| { |
| .thp_policy = THP_NEVER, |
| }; |
| |
| FIXTURE_VARIANT_ADD(prctl_thp_disable_completely, madvise) |
| { |
| .thp_policy = THP_MADVISE, |
| }; |
| |
| FIXTURE_VARIANT_ADD(prctl_thp_disable_completely, always) |
| { |
| .thp_policy = THP_ALWAYS, |
| }; |
| |
| FIXTURE_SETUP(prctl_thp_disable_completely) |
| { |
| if (!thp_available()) |
| SKIP(return, "Transparent Hugepages not available\n"); |
| |
| self->pmdsize = read_pmd_pagesize(); |
| if (!self->pmdsize) |
| SKIP(return, "Unable to read PMD size\n"); |
| |
| if (prctl(PR_SET_THP_DISABLE, 1, NULL, NULL, NULL)) |
| SKIP(return, "Unable to disable THPs completely for the process\n"); |
| |
| thp_save_settings(); |
| thp_read_settings(&self->settings); |
| self->settings.thp_enabled = variant->thp_policy; |
| self->settings.hugepages[sz2ord(self->pmdsize, getpagesize())].enabled = THP_INHERIT; |
| thp_write_settings(&self->settings); |
| } |
| |
| FIXTURE_TEARDOWN(prctl_thp_disable_completely) |
| { |
| thp_restore_settings(); |
| } |
| |
| TEST_F(prctl_thp_disable_completely, nofork) |
| { |
| prctl_thp_disable_completely_test(_metadata, self->pmdsize, variant->thp_policy); |
| } |
| |
| TEST_F(prctl_thp_disable_completely, fork) |
| { |
| int ret = 0; |
| pid_t pid; |
| |
| /* Make sure prctl changes are carried across fork */ |
| pid = fork(); |
| ASSERT_GE(pid, 0); |
| |
| if (!pid) { |
| prctl_thp_disable_completely_test(_metadata, self->pmdsize, variant->thp_policy); |
| return; |
| } |
| |
| wait(&ret); |
| if (WIFEXITED(ret)) |
| ret = WEXITSTATUS(ret); |
| else |
| ret = -EINVAL; |
| ASSERT_EQ(ret, 0); |
| } |
| |
| static void prctl_thp_disable_except_madvise_test(struct __test_metadata *const _metadata, |
| size_t pmdsize, |
| enum thp_enabled thp_policy) |
| { |
| ASSERT_EQ(prctl(PR_GET_THP_DISABLE, NULL, NULL, NULL, NULL), 3); |
| |
| /* tests after prctl overrides global policy */ |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_NONE, pmdsize), 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_NOHUGEPAGE, pmdsize), 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_HUGEPAGE, pmdsize), |
| thp_policy == THP_NEVER ? 0 : 1); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_COLLAPSE, pmdsize), 1); |
| |
| /* Reset to global policy */ |
| ASSERT_EQ(prctl(PR_SET_THP_DISABLE, 0, NULL, NULL, NULL), 0); |
| |
| /* tests after prctl is cleared, and only global policy is effective */ |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_NONE, pmdsize), |
| thp_policy == THP_ALWAYS ? 1 : 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_NOHUGEPAGE, pmdsize), 0); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_HUGEPAGE, pmdsize), |
| thp_policy == THP_NEVER ? 0 : 1); |
| |
| ASSERT_EQ(test_mmap_thp(THP_COLLAPSE_MADV_COLLAPSE, pmdsize), 1); |
| } |
| |
| FIXTURE(prctl_thp_disable_except_madvise) |
| { |
| struct thp_settings settings; |
| size_t pmdsize; |
| }; |
| |
| FIXTURE_VARIANT(prctl_thp_disable_except_madvise) |
| { |
| enum thp_enabled thp_policy; |
| }; |
| |
| FIXTURE_VARIANT_ADD(prctl_thp_disable_except_madvise, never) |
| { |
| .thp_policy = THP_NEVER, |
| }; |
| |
| FIXTURE_VARIANT_ADD(prctl_thp_disable_except_madvise, madvise) |
| { |
| .thp_policy = THP_MADVISE, |
| }; |
| |
| FIXTURE_VARIANT_ADD(prctl_thp_disable_except_madvise, always) |
| { |
| .thp_policy = THP_ALWAYS, |
| }; |
| |
| FIXTURE_SETUP(prctl_thp_disable_except_madvise) |
| { |
| if (!thp_available()) |
| SKIP(return, "Transparent Hugepages not available\n"); |
| |
| self->pmdsize = read_pmd_pagesize(); |
| if (!self->pmdsize) |
| SKIP(return, "Unable to read PMD size\n"); |
| |
| if (prctl(PR_SET_THP_DISABLE, 1, PR_THP_DISABLE_EXCEPT_ADVISED, NULL, NULL)) |
| SKIP(return, "Unable to set PR_THP_DISABLE_EXCEPT_ADVISED\n"); |
| |
| thp_save_settings(); |
| thp_read_settings(&self->settings); |
| self->settings.thp_enabled = variant->thp_policy; |
| self->settings.hugepages[sz2ord(self->pmdsize, getpagesize())].enabled = THP_INHERIT; |
| thp_write_settings(&self->settings); |
| } |
| |
| FIXTURE_TEARDOWN(prctl_thp_disable_except_madvise) |
| { |
| thp_restore_settings(); |
| } |
| |
| TEST_F(prctl_thp_disable_except_madvise, nofork) |
| { |
| prctl_thp_disable_except_madvise_test(_metadata, self->pmdsize, variant->thp_policy); |
| } |
| |
| TEST_F(prctl_thp_disable_except_madvise, fork) |
| { |
| int ret = 0; |
| pid_t pid; |
| |
| /* Make sure prctl changes are carried across fork */ |
| pid = fork(); |
| ASSERT_GE(pid, 0); |
| |
| if (!pid) { |
| prctl_thp_disable_except_madvise_test(_metadata, self->pmdsize, |
| variant->thp_policy); |
| return; |
| } |
| |
| wait(&ret); |
| if (WIFEXITED(ret)) |
| ret = WEXITSTATUS(ret); |
| else |
| ret = -EINVAL; |
| ASSERT_EQ(ret, 0); |
| } |
| |
| TEST_HARNESS_MAIN |