【问题标题】:How "linux process address space" is stored?“linux进程地址空间”是如何存储的?
【发布时间】:2015-09-23 18:34:38
【问题描述】:

从书中读到,当一个进程启动时,它的私有进程地址空间就被创建了

假设它是从 0x0 到 0xMAX

其中一部分空间是堆,我们编写了一个 for 循环来继续 malloc(1k 日期),直到它返回 false。它分配了 3GB 日期。

那么,问题来了,如果一开始就分配了0x0到0xMAX,是不是一开始就分配了0x0到0xMAX大于3GB(因为有栈,控制...)?

如果一个进程一开始可以占用超过3GB,那一定是我理解错了。

谁能解释一下这个 0x0 - 0xMAX 是如何存储在 begging 中的?

【问题讨论】:

  • 没有。运行一个进程不会神奇地为应用程序分配整个可能的内存空间。编译器可以计算出存储代码和静态数据需要多少内存,并且该内存将在进程启动时分配。
  • @MarcB 像我的进程一样,它可以接受参数来创建编译器几乎不知道的 3GB 或 3MB 堆。那么,在那种情况下,操作系统会动态更新整个内存空间吗?因为堆从 0xXX 继续到 0xMax,堆栈从 0xMax 到向下。它的操作系统要动态更新它,整个空间都会更新。
  • @marcb ,可能有点切题,但是当分叉发生时,父进程会有一些写入时复制。
  • @MattJoyce 我尝试使用linux内核api打印地图,我可以看到代码/数据地址没有改变。当堆增加到 3GB 时,开始和 0xMAX 没有改变。所以我相信这个地址空间的地址不是真正的硬盘/交换/甚至内存地址。
  • 如果你有时间和金钱,我建议你买一本《Linux 编程接口》:man7.org/tlpi它相当于 Stevens 高级 Unix 编程的 linux。

标签: linux process malloc virtual-memory


【解决方案1】:

通常,将可执行文件加载到内存是由 linux 加载程序 ld 驱动的进程。例如,如果我创建一个非常简单的 C 程序,让我们说:

void main {}

用gcc编译这个程序后,我们得到一个可执行文件(ELF格式)a.out。如果我们通过运行 ldd 来分析这个非常简单的程序所具有的依赖关系,我们会发现:

linux-gate.so.1 =>  (0x00545000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0x00ccb000)
/lib/ld-linux.so.2 (0x00594000)

第一个 linux-gate.so 被内核公开以进行系统调用。 ld-linux.so 实际上是 linux 加载器。它负责在内存中加载任何可执行文件并运行它。如果我们查看我们生成的 a.out(例如通过使用 hexedit 工具),我们可以看到它的标头包含对 ld-linux 所在位置的引用:

 .ELF........................4...8.......
 4. ...(.........4...4...4... ... .......
 ........T...T...T.......................
 ........................................
 ........................(...(...(.......
 ................h...h...h...D...D.......
 ....P.td............4...4...........Q.td
 ............................R.td........
 ..................../lib/ld-linux.so.2..
 ............GNU.........................
 ....GNU....F*QLk$,.....)..Yl............

一旦您启动该进程,ld-linux 加载程序首先会检查您需要(依赖)哪些共享库以及它们是否可用。如果您依赖某些不可用的共享库,ld-linux 将不会加载进程(ld-linux 会查看您的 LD_LIBRARY_PATH 环境变量、/etc/ld.so.cache 文件,最后查看默认路径:/lib 和/usr/lib(man ld-linux 了解更多信息)。

一旦 ld-linux 确保所有的库都在那里,它就会分配内存来加载进程。通常一个可执行文件有几个段,为简单起见,我们可以将它们简化为文本(代码)、bss(未初始化数据)、数据(初始化和静态数据)。当进程被加载到内存中时,加载器保留保存所有这些部分所需的内存量,并将进程所依赖的所有共享库映射到进程的虚拟空间。可以通过咨询查看linux中某个进程的map列表:

cat /proc/pid_of_process/maps

如果我运行上面简单程序的修改版本(通过添加对 usleep 的调用以获取进程 pid)并检查其映射,我们会得到以下内容(_ 只是为了隐藏真正的路径我的家出现了):

003a5000-003a6000 r-xp 00000000 00:00 0          [vdso]
0075a000-008fd000 r-xp 00000000 08:03 2137894    /lib/i386-linux-gnu/libc-2.15.so
008fd000-008ff000 r--p 001a3000 08:03 2137894    /lib/i386-linux-gnu/libc-2.15.so
008ff000-00900000 rw-p 001a5000 08:03 2137894    /lib/i386-linux-gnu/libc-2.15.so
00900000-00903000 rw-p 00000000 00:00 0 
00e4a000-00e6a000 r-xp 00000000 08:03 2137906    /lib/i386-linux-gnu/ld-2.15.so
00e6a000-00e6b000 r--p 0001f000 08:03 2137906    /lib/i386-linux-gnu/ld-2.15.so
00e6b000-00e6c000 rw-p 00020000 08:03 2137906    /lib/i386-linux-gnu/ld-2.15.so
08048000-08049000 r-xp 00000000 08:05 3589145    /______________/test/a.out
08049000-0804a000 r--p 00000000 08:05 3589145    /______________/test/a.out
0804a000-0804b000 rw-p 00001000 08:05 3589145    /______________/test/a.out
b771f000-b7720000 rw-p 00000000 00:00 0 
b7745000-b7747000 rw-p 00000000 00:00 0 
bf884000-bf8a5000 rw-p 00000000 00:00 0          [stack]

这实际上是进程的虚拟内存映射。这些页面映射到物理内存,每个进程都有自己的 PMT(程序映射表),用于在虚拟地址和物理地址之间进行转换。一般来说,进程内存的布局如下:

(来自http://duartes.org/gustavo/blog/post/anatomy-of-a-program-in-memory/

所以,记住这些信息并回到你原来的问题,

那么,问题来了,如果一开始就分配了0x0到0xMAX,是不是一开始就分配了0x0到0xMAX大于3GB(因为有栈,控制...)?

答案是没有这样的保留。加载程序保留运行进程所需的物理内存。之后,根据进程需要(动态内存分配)及其行为,它的堆和堆栈区域可能会增长和缩小。每次进程需要访问一些实际不存在于物理内存中的内存(虚拟)时,就会发出一个页面错误,并将该页面从磁盘加载到物理内存中的保留位置。有时为了做到这一点,内核必须将属于另一个进程的一些页面换出到磁盘。物理内存是一种有限的资源,操作系统必须正确处理它才能负担所有正在运行的进程。

使用这种策略,Linux 内核能够运行多个进程,其中每个进程通常在物理内存中(特别是在过去)中具有 4GB(32 位系统)的虚拟内存。通常,即使您动态保留内存(例如通过使用 malloc),调用也会成功,但实际上您还没有保留此物理内存。一旦尝试使用它(通过读取或写入此内存),您的进程就会得到它。

这可能是一个很长的答案。我希望我没有遗漏很多细节,希望它可以帮助您了解 linux 中进程内存的解剖结构。

【讨论】:

  • 我认为问题是,如果系统中实际上有那么多物理内存可用,进程分配 100G 会怎样。但是 32 位系统有 4G 虚拟内存限制。
  • 你不能在一个进程中管理 32 位系统中 100Gb 的内存,你需要一个 64 位系统。
  • 好的,所以对于 64 位系统内核空间的范围会大得多,而 100G mmaped 分配将位于堆栈下方?
【解决方案2】:

这里有一些误解。首先,进程地址dpace和分配给进程的内存是有区别的。

其次,地址空间内的有效地址范围不太可能是线性的。它很可能位于多个不连贯的内存区域中。

如果您在循环中执行 malloc,则会导致 (a) 导致将有效页面添加到进程中 (b) 除了分配 malloc 返回的内存之外,还导致分配开销内存。

第三,在一个进程的开始,有页表。 (忽略克隆)这些表在进程开始时不涉及任何内容。您的进程必须导致页面被分配并分配给表。

【讨论】:

    猜你喜欢
    • 2012-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-09-29
    • 1970-01-01
    • 2012-01-22
    • 1970-01-01
    相关资源
    最近更新 更多