/proc/$pid/maps 文件中未出现的许多地址怎么办?

What about the many addresses unpresented in the /proc/$pid/maps file?

简要版本: 地图文件中未显示的地址是什么状态?它们属于未分配的虚拟页面还是从匿名文件或其他文件中分配的?

详细版本 我正在学习 VM。在我的书(CS:APP)中,了解到所有的虚拟页面都可以分为三组:未分配的、已分配但未缓存的、已分配的以及cached.I有一些关于“什么是分配页面和未分配页面?什么时候分配”的问题页面分配了吗?”而且,堆栈和堆是属于已分配页面还是未分配或仅在使用时分配? 为了解决这些问题,我阅读了 /proc/$pid/maps 文件,同时我认为我可以从中得到任何我想要的东西。在我看来,该文件包含所有内存映射关系。但是没有关于它是否被缓存的信息(我知道它可能无法从用户模式中看到...),未显示的页面是否未分配?

老实说,我不太了解地图文件。我所知道的是,每个页面上的信息始终存储在 page 结构中。我将以x86-64为例。

对于 Linux 上的 x86-64,您有页面全局目录 (PGD)、页面上层目录 (PUD)、页面中间目录 (PMD) 和页面目录 (PD)。 PGD​​table底部地址存放在CR3寄存器中。 PGD​​包含PUD的地址,PUD包含PMD的地址,PMD包含PD的地址,PD包含物理页的地址。

一个只使用了48位的虚拟地址被分成了5个部分。 12 个最低有效位是物理页中的偏移量。下一个 9 位块是 PD 中的偏移量。下一个块是 PMD 等中的偏移量。例如,假设您有虚拟地址 0x0000000000000123。该虚拟地址将由 MMU 在 CPU 中通过查看 PGD 的条目(偏移量)0、PUD 的条目 0、PMD 的条目 0、PD 的条目 0 以及最终的偏移量 0x123 进行转换RAM 中的实际物理页。每个虚拟地址都是 64 位,其中只有 48 个最低有效位将被使用。

在启动时,内核会检查以确定有多少内存可用。然后它相应地构建其内核结构。

当内核启动时,它会在自己的结构中将所有页面标记为未分配(内核需要除外)。 page 结构对此很重要。内核对系统中的每个页面都有一个 page C 结构 (https://linux-kernel-labs.github.io/refs/heads/master/labs/memory_mapping.html and https://elixir.bootlin.com/linux/v4.6/source/include/linux/mm_types.h)。该结构通知内核页面是否分配。

Each physical page in the system has a struct page associated with it to keep track of whatever it is we are using the page for at the moment. Note that we have no way to track which tasks are using a page, though if it is a pagecache page, rmap structures can tell us who is mapping it.

起初页面大部分是未分配的。当您通过以系统用户身份启动 executable 来启动新进程时,会为您的进程分配页面。在 Linux 上,executables 是 ELF 文件。 ELF 是一种常规格式,它将代码分隔为可加载段。每个段都有一个虚拟地址,它将被加载到虚拟地址 space.

假设您有一个 elf 文件,其中一个段应该加载到虚拟地址 0x400000。当您启动 ELF executable 时,Linux 内核将调用某些函数,这些函数将查看代码的大小并相应地分配页面。内核将查看其结构并确定使用算法将进程分配到 RAM 中的位置。然后它将根据该进程的虚拟地址在实际物理内存中的位置设置页面 tables。

重要的是要了解系统中的每个 CPU 核心一次只有一个进程 运行。每个核心都有自己的一组页面 tables。当一个核心发生进程切换时,页面 table 将完全交换以指向 RAM 中的其他位置。相同的虚拟地址可以指向 RAM 中的任何位置,具体取决于页面 table 的设置方式。

内核为系统中的每个进程运行持有一个task_struct。 task_struct 包含一个名为 pgd 的字段,它是指向进程 PGD 的指针。每个过程都有自己的 PGD。如果取消引用指向 PGD 的指针,您将获得 PGD 第一个条目的实际值。第一个条目是 PUD 的地址。有了这个唯一的指针,内核就可以到达属于进程的每一个table,随意修改。

当进程 运行 时,它可以请求更多内存。这称为动态内存分配。内核无法知道进程将提前请求多少内存,因为它是动态的(在代码执行时完成)。当进程请求更多内存时,内核根据算法确定将哪个页面提供给进程。然后它将此页面标记为已分配给该进程。 task_struct 包含一个类型为 mm_struct (https://manybutfinite.com/post/how-the-kernel-manages-your-memory/) 的 mm 字段。它是该进程的内存描述符,以便内核可以知道该进程正在使用什么内存。该进程本身不需要该信息,因为该进程应该仅依靠自身向操作系统正确请求内存,而不是跳转到 RAM 中它不属于的某个地方。

你问的是堆和栈。进程的堆栈是在进程开始时分配的,我认为它的大小是固定的。如果堆栈溢出,您将抛出 CPU 异常,提示内核终止您的进程。每个 CPU 内核都有一个称为 RSP 的特殊寄存器。这是堆栈指针。它指向堆栈的顶部(堆栈向下增长到低内存)。当内核为您启动的进程分配堆栈时,它会将此寄存器设置为指向它的顶部。堆栈指针包含一个虚拟地址。因此它将像任何地址一样使用页面 tables 进行翻译。

堆完全由OS分配和管理。它没有像堆栈那样的特殊寄存器。只有当进程在代码执行期间请求更多内存时才会分配它。内核预先知道进程需要多少内存。都是在ELF executable里面写的。所有静态内存都是在编译期间分配的,因此内核知道有关静态内存大小的所有信息。它需要为进程分配新内存的唯一时刻是进程实际请求它的时候。在 C++ 中,您使用关键字 new 动态请求堆内存。如果你不使用这个关键字,那么内核会提前知道你的变量将被分配到哪里(它们在内存中的什么地方)。只有堆栈会被静态内存使用。