“linux进程地址空间”是如何存储的?

thu*_*ium 1 linux malloc process virtual-memory

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

假设它是从 0x0 到 0xMAX

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

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

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

谁能解释这个 0x0 - 0xMAX 是如何存储在乞讨中的?

rka*_*ach 5

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

void main {}
Run Code Online (Sandbox Code Playgroud)

用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)
Run Code Online (Sandbox Code Playgroud)

第一个 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............
Run Code Online (Sandbox Code Playgroud)

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

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

cat /proc/pid_of_process/maps
Run Code Online (Sandbox Code Playgroud)

如果我运行上述简单程序的修改版本(通过添加对 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]
Run Code Online (Sandbox Code Playgroud)

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

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

在此处输入图片说明

因此,考虑到这些信息并回到您最初的问题,

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

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

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

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