如何以编程方式获取特定地址的页面大小?

Eld*_*dad 5 c linux huge-pages

我正在寻找一种方法来实现获取地址并告知该地址中使用的页面大小的函数。一种解决方案在 /proc//smaps 中的段中查找地址并返回“KernelPageSize:”的值。该解决方案非常慢,因为它涉及线性读取文件,该文件可能很长。我需要一个更快、更有效的解决方案。

有系统调用吗?(int getpagesizefromaddr(void *addr);) 如果没有,有没有办法推断页面大小?

Nom*_*mal 4

许多 Linux 架构都支持“大页面”,请参阅Documentation/vm/hugetlbpage.txt了解详细信息。例如,在 x86-64 上,sysconf(_SC_PAGESIZE)报告页面大小为 4096,但也可以使用 2097152 字节的大页面。从应用程序的角度来看,这并不重要。内核完全能够根据需要从一种页面类型转换为另一种页面类型,而用户空间应用程序不必担心它。

然而,对于特定的工作负载,性能优势是显着的。这就是开发透明大页面支持(请参阅Documentation/vm/transhuge.txt )的原因。这在虚拟环境中尤其明显,即工作负载在来宾环境中运行。新的建议标志MADV_HUGEPAGEMADV_NOHUGEPAGEmadvise ()允许应用程序告诉内核其首选项,因此这mmap(...MAP_HUGETLB...)并不是获得这些性能优势的唯一方法。

我个人认为Eldad的guion与在来宾环境中运行的工作负载有关,重点是在基准测试时观察页面映射类型(正常或大页面),以找出针对特定工作负载的最有效配置。

让我们通过展示一个现实世界的例子来消除所有误解huge.c

#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#define  PAGES 1024

int main(void)
{
    FILE   *in;
    void   *ptr;
    size_t  page;

    page = (size_t)sysconf(_SC_PAGESIZE);

    ptr = mmap(NULL, PAGES * page, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_HUGETLB, -1, (off_t)0);
    if (ptr == MAP_FAILED) {
        fprintf(stderr, "Cannot map %ld pages (%ld bytes): %s.\n", (long)PAGES, (long)PAGES * page, strerror(errno));
        return 1;
    }

    /* Dump /proc/self/smaps to standard out. */
    in = fopen("/proc/self/smaps", "rb");
    if (!in) {
        fprintf(stderr, "Cannot open /proc/self/smaps: %s.\n", strerror(errno));
        return 1;
    }
    while (1) {
        char *line, buffer[1024];

        line = fgets(buffer, sizeof buffer, in);
        if (!line)
            break;

        if ((line[0] >= '0' && line[0] <= '9') ||
            (line[0] >= 'a' && line[0] <= 'f') ||
            (strstr(line, "Page")) ||
            (strstr(line, "Size")) ||
            (strstr(line, "Huge"))) {
            fputs(line, stdout);
            continue;
        }
    }

    fclose(in);
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

如果可能的话,上面使用大页面分配 1024 个页面。(在 x86-64 上,一个大页面为 2 MiB 或 512 个普通页面,因此这应该分配两个大页面的价值,即 4 MiB 的私有匿名内存。PAGES如果您在不同的体系结构上运行,请调整该常量。)

/proc/sys/vm/nr_hugepages通过验证大于零来确保启用大页面。在大多数系统上它默认为零,因此您需要提高它,例如使用

sudo sh -c 'echo 10 > /proc/sys/vm/nr_hugepages'
Run Code Online (Sandbox Code Playgroud)

它告诉内核保留 10 个大页面(x86-64 上为 20 MiB)的可用池。

编译并运行上面的程序,

gcc -W -Wall -O3 huge.c -o huge && ./huge
Run Code Online (Sandbox Code Playgroud)

您将获得一个缩写的/proc/PID/smaps输出。在我的机器上,有趣的部分包含

2aaaaac00000-2aaaab000000 rw-p 00000000 00:0c 21613022   /anon_hugepage (deleted)
Size:               4096 kB
AnonHugePages:         0 kB
KernelPageSize:     2048 kB
MMUPageSize:        2048 kB
Run Code Online (Sandbox Code Playgroud)

这与典型零件明显不同,例如

01830000-01851000 rw-p 00000000 00:00 0   [heap]
Size:                132 kB
AnonHugePages:         0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Run Code Online (Sandbox Code Playgroud)

完整文件的确切格式/proc/self/smaps在 中进行了描述man 5 proc,并且解析起来非常简单。请注意,这是由内核生成的伪文件,因此它从未本地化;空白字符是 HT(代码 9)和 SP(代码 32),换行符是 LF(代码 10)。


我推荐的方法是维护一个描述映射的结构,例如

struct region {
    size_t  start;    /* first in region at (void *)start */
    size_t  length;   /* last in region at (void *)(start + length - 1) */
    size_t  pagesize; /* KernelPageSize field */
};

struct maps {
    size_t           length;   /* of /proc/self/smaps */
    unsigned long    hash;     /* fast hash, say DJB XOR */
    size_t           count;    /* number of regions */
    pthread_rwlock_t lock;     /* region array lock */
    struct region   *region;
};
Run Code Online (Sandbox Code Playgroud)

其中,lock仅当一个线程可能检查区域数组而另一个线程正在更新或替换它时才需要该成员。

这个想法是,以所需的时间间隔/proc/self/smaps读取伪文件,并计算快速、简单的散列(或 CRC)。如果长度和哈希匹配,则假设映射未更改,并重用现有信息。否则,将获取写锁(记住,信息已经过时),解析映射信息,并region生成一个新数组。

如果是多线程的,该lock成员允许多个并发读取器,但防止使用废弃的region数组。

注意:在计算哈希值时,您还可以计算地图条目的数量,因为属性线均以大写 ASCII 字母(A- Z,代码 65 至 90)开头。换句话说,以小写十六进制数字(0- 9,代码 48 至 57 ,或a- f,代码 97 至 102 )开头的行数是所描述的存储区域的数量。


在 C 库提供的函数中,mmap()munmap()mremap()madvise()(和posix_madvise())、mprotect()malloc()calloc()realloc()free()brk()、 和sbrk()可能会更改内存映射(尽管我不确定此列表包含所有这些函数)。可以插入这些库调用,并在每次(成功)调用后更新内存区域列表。这应该允许应用程序依赖内存区域结构来获取准确的信息。

就我个人而言,我会将此工具创建为预加载库(使用 加载LD_PRELOAD)。这样只需几行代码即可轻松插入上述函数:插入的函数调用原始函数,如果成功,则调用一个内部函数,该函数从/proc/self/smaps. 注意调用原来的内存管理函数,并保持errno不变;否则它应该非常简单。我个人也会避免使用库函数(包括string.h)来解析字段,但无论如何我都过于小心了。

插入的库显然还会提供查询特定地址处的页面大小的功能,例如pagesizeat()。(如果您的应用程序导出一个始终返回 的弱版本-1errno==ENOTSUP则您的预加载库可以覆盖它,并且您无需担心预加载库是否已加载 - 如果没有加载,该函数将仅返回错误。 )

问题?