c - 通过/proc/$PID/smaps 在 Linux 内核中进行脏页统计

标签 c linux memory-management linux-kernel page-tables

TL;DR:内核究竟是如何在 /proc/$PID/smaps 中进行脏页统计的? ?

考虑以下 C 程序语句:

static char page1[PAGE_SIZE] __attribute__ ((aligned (PAGE_SIZE)));

现在未初始化的变量在开始时为零。我的理解是,在程序启动时,内核将未初始化的变量映射到零页,并对页面进行写时复制延迟分配。很好,有道理,这样内核就可以在发生页面错误时解决未初始化部分的脏页。

现在考虑语句:

static char page1[PAGE_SIZE] __attribute__ ((aligned (PAGE_SIZE))) = {'c'};

在这里,加载程序将加载 page1 的值在程序初始化时,和 将页面标记为 RW。所以程序所做的任何写入对内核来说都是不可见的 因为没有页面错误被触发。

下面是我写的实验程序:

#define PAGE_SIZE (4*1024)
static char page1[PAGE_SIZE] __attribute__ ((aligned (PAGE_SIZE))) = {'c'};
int main()
{
  char c; int i; int *d;
  scanf("%c", &c);                // --------- tag 1
  for(i = 0; i < PAGE_SIZE; i++)
    {
      page1[i] = c;               // --------- tag 2
    }
  d = malloc(sizeof(int));
  while(1);
  return 0;
}

现在在标记 1 之前和标记 2 之后(代码中的注释),/proc/$PID/smaps 的输出对于包含 page1 的部分粘贴在下面的表格中:

<表类="s-表"> <头> map TAG-1 之前 在 TAG-2 之后 <正文> 尺寸: 8 kB 8 kB 内核页面大小: 4 kB 4 kB MMUPageSize: 4 kB 4 kB RSS: 8 kB 8 kB 请问: 8 kB 8 kB Shared_Clean: 0 kB 0 kB Shared_Dirty: 0 kB 0 kB Private_Clean: 4 kB 0 kB Private_Dirty: 4 kB 8 kB 引用: 8 kB 8 kB 匿名: 4 kB 8 kB

如你所见,上面的粗体参数发生了变化。

问题:

  1. 内核到底是怎么知道我写了这个页面的?
  2. 这是什么Anonymous字段,为什么会发生变化?

任何其他详细解释所有工作的页面/博客/手册都会有所帮助。

我的猜测是,也许内核将页面标记为 RO,即使它是 RW,以便触发页面错误并且它可以进行统计。或者也许还有其他一些过程 不断遍历进程的页表,但这似乎太昂贵了。

最佳答案

Now consider the statement

static char page1[PAGE_SIZE] __attribute__ ((aligned (PAGE_SIZE))) = {'c'}

Here, the loader will load the values for page1 at init of the program, and mark the page as RW.

您似乎认为加载程序会为这条语句写入内存,但实际上并没有。

在这种情况下发生的不是 mmap RW + 写入字节 'c'。该字节已在编译时嵌入到您的可执行文件中,因此唯一发生的是 mmap RW,仅此而已。像这样:

mmap(0, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd_of_your_elf, offset_of_data_section);

或者,最有可能的是,只是 mmap(...entire file...) 后跟一系列具有不同部分的正确权限的 mprotect() Sprite 的。

实际上,在这种情况下,甚至不是加载程序执行此操作,而是内核本身将可执行文件映射到内存,假设您将程序作为 ./exe 启动。加载程序仅在以 /path/to/loader./exe 调用时自行映射程序。另见 this other answer of mine在这里我有更详细的解释。


How on earth did the kernel got to know I wrote the page?

您可能已经知道,当您的程序最初映射到内存中时(包括包含 page1 的页面),即使该页面的映射是 RW,也没有真正需要内核实际为页面分配内存,直到发生读取或写入。这种技术被称为 demand paging .最初(在映射之后)该页面甚至不存在于您进程的页表中:它仅作为众多 vm_area_struct 之一存在。任务内存映射中的条目。

当发生页面错误(由读或写引起)时,内核会根据映射的性质决定要做什么。在这种情况下,映射是文件支持的(整个 page1 数组的实际初始值在编译时写入您的 ELF 文件),因此两种可能的情况如下:

  1. 当发生内存读取时,会发生页面错误,页面内容会从文件中读取到内存中。新分配的页面现在被标记为只读,即使它被映射为 RW(内核仍然知道这个 VMA 是 RW)。

  2. 当发生内存写入时,有两种情况:要么 (A) 由于之前的读取,页面已经存在于内存中(并且是标记为 RO),或者 (B) 该页面根本不在内存中,因为这是对其的第一次内存访问。在这两种情况下,都会发生页面错误,内核会检查是否允许写入(是的),并且 copy-on-write发生。

    由于该页面是文件备份的,但共享(即未使用 MAP_SHARED 进行映射),因此不需要将数据写回到文件中,因此内核简单地分配一个新的匿名页面,然后从前一页复制内容(情况A)或从文件中读取页面到内存中(情况 B) 在应用写入之前。这就是您看到 Anonymous 从 0kB 变为 4kB 的原因。

    此外,由于旧页面在写时复制之前只有一个用户,它可以被释放,这就是您看到 Rss 保持不变的原因。最后,在写入之前页面是干净的(不脏),写入之后它变脏了(不干净),所以这就是为什么您看到 Private_CleanPrivate_Dirty 发生变化的原因值(value)观。

关于c - 通过/proc/$PID/smaps 在 Linux 内核中进行脏页统计,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70822002/

相关文章:

linux - golang应用如何启动upstart进程?

linux - 基于grep模式自定义文件内容显示

ios - 为什么在 viewDidUnload 里面有一个 release 语句?

c - 由于循环,RAM 正在爆炸

python - 在 Linux 到 Windows (Flask) 中将模式管道传输到 sqlite 的对应物是什么

c - 如何使用省略号 (...)

c++ - 从 C++ 宏创建字符串列表和枚举列表

c - 这个字符串连接函数有什么问题

java - 如果 ArrayList 的大小已传递给构造函数,则添加新元素时,Java 中 ArrayList 的元素是否会在内存中重新分配?

c - 列出所有排列的代码的时间复杂度?