【问题标题】:how to get the page size of a specific address programmatically?如何以编程方式获取特定地址的页面大小?
【发布时间】:2023-03-28 18:32:01
【问题描述】:

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

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

【问题讨论】:

  • /proc/PID/smaps 不是文件,它是内核读取时提供的伪文件。 (open()/read()/close() 系统调用导致内核在需要时生成数据,它不经过任何文件系统的东西。事实上,它是令人惊讶的轻量级系统。)它不是“非常慢”,特别是如果您使用unistd.h I/O 和强大的解析器功能。 stdio.h 功能齐全但速度较慢,因此请避免使用。
  • 你说得对,但我并不是说慢的解决方案慢是由于文件I/O,而是由于字符串处理。在我做的一个测试中,一个 smap “文件”可以有超过 500K 行,所以最坏情况下的运行时间很长(坏)。
  • 不同地址有不同的页面大小吗?如果没有,也许这就是你需要的:sysconf(_SC_PAGESIZE)
  • @leeduhem:是的,他们有。例如,在 x86-64 上,正常页面大小为 4 KB(4096 字节),但大页面为 2048 KB(2097152 字节)。
  • @NominalAnimal 好吧,在这种情况下,sysconf() 无法提供您需要的信息。

标签: c linux huge-pages


【解决方案1】:

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

但是,对于特定工作负载,性能 优势非常显着。这就是开发透明大页面支持(参见Documentation/vm/transhuge.txt)的原因。这在虚拟环境中尤其明显,即工作负载在来宾环境中运行。用于madvise() 的新advice 标志MADV_HUGEPAGEMADV_NOHUGEPAGE 允许应用程序告诉内核它的偏好,因此mmap(...MAP_HUGETLB...) 不是获得这些性能优势的唯一方法。

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

让我们通过一个真实的例子来消除所有误解,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;
}

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

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

sudo sh -c 'echo 10 > /proc/sys/vm/nr_hugepages'

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

编译运行上述程序,

gcc -W -Wall -O3 huge.c -o huge && ./huge

您将获得一个缩写的/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

明显不同于典型的部分,例如

01830000-01851000 rw-p 00000000 00:00 0   [heap]
Size:                132 kB
AnonHugePages:         0 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB

完整的/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;
};

只有当一个线程检查区域数组而另一个线程正在更新或替换它时才需要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 的弱版本,您的预加载库可以覆盖它,您无需担心预加载库是否已加载——如果没有,函数只会返回一个错误。)

问题?

【讨论】:

  • 谢谢。维护解析的 smaps 的解决方案不是我需要的。这并不简单并且需要分配和维护数据库。我的最终解决方案是实际解决这个问题。还是谢谢。
  • @Eldad:听起来很明智。就像我在回答中所说的那样,实际上很少需要知道页面大小。即使为了性能优势,只给内核提示(优雅地回退到正常页面)几乎总是足够的。再说一次,您只问了如何获取页面大小;为什么你希望任何人知道你的实际问题和背景是什么?您可能应该描述并询问您实际的原始问题,而不是就您决定的解决方案寻求帮助。
  • 对不起,我没有完全清楚,我不想要一个涉及读取和解析“地图”的解决方案,因为它效率不高。
  • @Eldad:没有现有的系统调用,或任何其他方法来推断属于该进程的任意地址的内核页面大小。这是当前 Linux 内核中唯一可用的解决方案。您唯一的其他选择是创建一个提供必要信息的 Linux 内核模块。为此导出一个简单的ioctl() 的字符设备驱动程序应该不会太难。棘手的部分是正确地遍历内核数据结构,以找到必要的信息。但这是一个完全不同的问题,即使这样也可能对您的需求来说太慢了。
  • 谢谢。事实上,看起来人们必须深入了解 vma 才能获得该信息。这不是我需要的 O(1) 解决方案,所以我解决了整个问题。还是谢谢。
猜你喜欢
  • 2012-10-20
  • 1970-01-01
  • 2015-02-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多