我正在寻找一种方法来实现一个获取地址的函数,并告诉该地址中使用的页面大小。一种解决方案是在 /proc//smaps 的段中查找地址并返回“KernelPageSize:”的值。该解决方案非常慢,因为它涉及线性读取文件,该文件可能很长。我需要一个更快、更有效的解决方案。
有系统调用吗?(int getpagesizefromaddr(void *addr);) 如果没有,有没有办法推断页面大小?
我正在寻找一种方法来实现一个获取地址的函数,并告诉该地址中使用的页面大小。一种解决方案是在 /proc//smaps 的段中查找地址并返回“KernelPageSize:”的值。该解决方案非常慢,因为它涉及线性读取文件,该文件可能很长。我需要一个更快、更有效的解决方案。
有系统调用吗?(int getpagesizefromaddr(void *addr);) 如果没有,有没有办法推断页面大小?
许多 Linux 体系结构都支持“大页面”,有关详细信息,请参阅Documentation/vm/hugetlbpage.txt。例如,在 x86-64 上,sysconf(_SC_PAGESIZE)
报告 4096 作为页面大小,但也可以使用 2097152 字节的大页面。从应用程序的角度来看,这并不重要。内核完全能够根据需要从一种页面类型转换为另一种页面类型,而用户空间应用程序不必担心它。
但是,对于特定的工作负载,性能优势是显着的。这就是开发透明大页面支持(参见Documentation/vm/transhuge.txt)的原因。这在虚拟环境中尤其明显,即工作负载在来宾环境中运行。新的advice flagsMADV_HUGEPAGE
和MADV_NOHUGEPAGE
for madvise()允许应用程序告诉内核它的偏好,所以这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()
(and posix_madvise()
), mprotect()
, malloc()
, calloc()
, realloc()
, free()
, brk()
, 和sbrk()
可能会更改内存映射(尽管我不确定这个列表是否包含所有这些)。可以插入这些库调用,并在每次(成功)调用后更新内存区域列表。这应该允许应用程序依赖内存区域结构来获取准确的信息。
就个人而言,我会将此工具创建为预加载库(使用 加载LD_PRELOAD
)。只需几行代码即可轻松插入上述函数:插入的函数调用原始函数,如果成功,则调用一个内部函数,该函数从/proc/self/smaps
. 注意调用原来的内存管理函数,并保持errno
不变;否则它应该很简单。我个人也会避免使用库函数(包括string.h
)来解析字段,但无论如何我都过于小心了。
插入的库显然还会提供查询特定地址的页面大小的功能,例如pagesizeat()
. (如果你的应用程序导出一个总是返回的弱版本-1
,errno==ENOTSUP
你的预加载库可以覆盖它,你不需要担心预加载库是否被加载——如果没有,函数只会返回一个错误。 )
问题?