9

在 Linux 中,当进程向系统请求一些(虚拟)内存时,它只是在 vma(进程虚拟内存的描述符)中注册,但每个虚拟的物理页面在调用时并未保留。之后,当进程访问该页时,会发生故障(访问会产生缺页中断),PF# handler 将分配物理页并更新进程页表。

有两种情况:读取时出错可能变成写保护的零页(特殊全局预置零页)的链接;写入错误(在零页和刚刚需要但未物理映射的页上)将导致实际的私有物理页分配。

对于 mmap(以及内部 mmap 的 brk/sbrk),此方法是每页的;所有 mmaped 区域都在 vma 中整体注册(它们具有开始和结束地址)。但是堆栈以其他方式处理,因为它只有起始地址(典型平台上较高的地址;增长到较低的地址)。

问题是:

当我在堆栈附近访问新的未分配内存时,它将获得 PF# 并增长。如果我访问的不是堆栈旁边的页面,而是距离堆栈 10 或 100 页的页面,如何处理这种增长?

例如

int main() {
  int *a = alloca(100); /* some useful data */
  int *b = alloca(50*4096); /* skip 49 pages */
  int *c = alloca(100);

  a[0]=1;
 /* no accesses to b - this is untouched hole of 49 pages */
  c[0]=1;

}

这个程序会为堆栈分配 2 或 50 个私有物理页吗?

我认为要求内核在单个页面错误中分配十个物理页面然后逐页分配十个页面错误是有利可图的(1个中断+ 1个上下文切换+简单,对N个页面分配请求的缓存友好循环与N个中断+ N 上下文切换 + N 页面分配,当 mm 代码可能从 Icache 中逐出时)。

4

2 回答 2

4

使用此代码:

int main() {
  int *a = alloca(100); /* some useful data */
  int *b = alloca(50*4096); /* skip 49 pages */
  int *c = alloca(100);
  int i;
#if TOUCH > 0
  a[0] = 1;               // [1]
#endif
#if TOUCH > 1
  c[0] = 1;               // [2]
#endif
#if TOUCH > 2
  for (i=0; i<25; i++)    // [3]
    b[i*1024] = 1;
#endif
#if TOUCH > 3
  for (i=25; i<50; i++)   // [4]
    b[i*1024] = 1;
#endif
  return 0;
}

这个脚本:

for i in 1 2 3 4; do
  gcc d.c -DTOUCH=$i
  echo "Upto [$i]" $(perf stat ./a.out 2>&1 | grep page-faults)
done

输出:

Upto [1] 105 page-faults # 0.410 M/sec
Upto [2] 106 page-faults # 0.246 M/sec
Upto [3] 130 page-faults # 0.279 M/sec
Upto [4] 154 page-faults # 0.290 M/sec
于 2012-12-21T02:12:52.007 回答
3

堆栈的自动增长可以被认为是自动调用以mremap调整算作“堆栈”的虚拟地址区域的大小。一旦处理完毕,堆栈区域或普通 mmap 区域的页面错误将被处理相同,即一次一页。

因此,您最终应该分配了 ~2 页,而不是 ~51。@perreal 的经验回答证实了这一点......

对于问题的最后一部分,连续页面错误的成本是导致“大页面”发展的因素之一。我认为 Linux 中没有其他方法可以“批量”处理页面错误。也许madvise可能会做一些事情,但我怀疑它主要优化了页面错误中真正昂贵的部分,即在存储上查找支持页面)。相比之下,映射到零页面的堆栈页面错误相对较轻。

于 2012-12-22T17:14:57.317 回答