进程如何共享虚拟内存(Linux)

3
我不确定我的问题是关于Linux还是与操作系统无关的。
如果我有三个进程正在运行(我们称它们为P0、P1和P2),并且它们对用户来说似乎是同时运行的,那么它们如何共享?
它们是否在用户空间内各自维护自己的堆栈、堆等?

Scenario A

他们是否只是拥有整个堆栈、堆等,直到下一个进程出现并抢占它?

Scenario B


1
请看这里:https://www.softprayog.in/programming/interprocess-communication-using-posix-shared-memory-in-linux - Ôrel
2
物理计算机内存用于情景A。然而,进程看不到物理地址。每个进程都有自己的虚拟地址空间,CPU在每次内存访问时将虚拟地址转换为物理地址。从进程的角度来看,它看起来好像可以使用整个地址空间,就像在情景B中一样。参见:https://dev59.com/1WEh5IYBdhLWcg3wjD_n - Marian
1个回答

6
在Linux和大多数其他当前使用的通用操作系统中,内存根本不是单个线性数组:底层物理内存使用虚拟内存进行页面级别管理。
基本上,每个进程都有自己的虚拟地址空间。其中大部分为空、未映射的,试图访问它会导致分段错误或一般保护违规,通常会杀死该进程;进程只能访问内核已明确设置为对该进程可访问的内存。
在大多数情况下,进程也无法直接访问内核内存。要执行系统调用——例如打开、读取或写入文件或设备——处理器核心基本上需要进行上下文切换到内核模式,在此模式下,内核数据结构以及当前进程在用户空间中使用的内存同时可访问(但在内核空间和用户空间中的虚拟地址不一定相同)。
这意味着现在每个进程可以访问的内存实际上相当分散和不连续。
    ╔════════╗   ╔════════╗   ╔═══════╗
    ║  Code  ║   ║  Data  ║   ║ Stack ║
    ╚════════╝   ╟────────╢   ╚═══════╝
    ╔════════╗   ║  BSS   ║
    ║ ROdata ║   ╟────────╢
    ╚════════╝   ║  Heap  ║
    ╔════════╗   ╚════════╝
    ║  Libs  ║
    ╚════════╝

如果启用地址空间随机化,上述每个段的地址甚至可能在下一次运行时都会有所变化。通常,代码(只读且可执行)和只读数据加载到固定地址,但动态链接库、堆栈和数据的地址会变化。
此外,以上任何一个部分的地址都没有更高或更低的原因,因此我故意将它们画在一起,而不是单独列成一列!
初始化数据和未初始化数据通常位于连续的段中,仅从可执行文件(数据部分)中加载初始化数据部分。在类Unix和POSIX系统中,堆跟随未初始化数据(可以使用 brk()sbrk() 系统调用进行扩展)。在类POSIX的系统(如Linux),以及大多数其他系统中,进程可以通过(匿名)内存映射获得额外的“堆”。
进程中的初始线程还会获得单独的堆栈段。任何其他线程也将获得自己的堆栈。
一个典型的学习使用POSIX线程的练习是找出一个进程可以创建多少并发线程。在Linux中,典型的结果只有一百个或几百个,并且许多学习者觉得这非常奇怪。这样低的数字实际上是由于默认堆栈大小造成的,在当前GNU / Linux桌面发行版上大约为8兆字节; 仅一百个线程的堆栈就需要近1GB的内存,因此并发线程的数量主要受限于可用于其堆栈的内存。一个非递归线程工作函数最多只需要几十KB的堆栈,并且只需要几行代码来显式设置新创建的pthread的堆栈大小。然后,单个进程中的最大并发线程数通常在一千个或更多,通常取决于系统管理员或分发商默认设置的进程限制。
正如您在上面的图表中所看到的,没有“操作系统”。
实际上,我们确实需要将“操作系统”分成两个完全独立的部分:内核(提供系统调用中实现的功能)和库(实现可用于用户空间处理器的非系统调用接口,通常从标准C库开始)。
我只画了一个“Libs”(代表库)的框,但实际上,每个库的代码往往会获得自己单独的内存段。
让我们看一个特定的例子,在Linux中(因为这是我现在使用的操作系统); cat命令。在Linux中,/sys和/proc文件系统是特殊的伪文件系统树,它们不对应于任何存储介质上的文件,而是在访问时由内核构造--本质上,它们是由内核提供的实时数据视图。/proc/self子树包含有关“当前进程”的信息--也就是说,无论哪个进程正在检查该目录。 (如果同时有多个进程检查它,他们各自只看到自己的数据,因为这不是正常的文件系统,而是内核创建并根据需要提供的。)
"

/proc/self/maps(或 /proc/PID/maps 对于进程的进程 ID 为 PID)伪文件描述了进程拥有的所有内存映射。如果我们运行 cat /proc/self/maps,我们可以看到 cat 进程本身的映射。在我的机器上(一个运行在 x86-64 架构上的 64 位 Linux),它显示:

"
00400000-0040c000 r-xp 00000000 08:05 2359392             /bin/cat
0060b000-0060c000 r--p 0000b000 08:05 2359392             /bin/cat
0060c000-0060d000 rw-p 0000c000 08:05 2359392             /bin/cat
0215f000-02180000 rw-p 00000000 00:00 0                   [heap]
7f735b70f000-7f735c237000 r--p 00000000 08:05 658950      /usr/lib/locale/locale-archive
7f735c237000-7f735c3f6000 r-xp 00000000 08:05 1179825     /lib/x86_64-linux-gnu/libc-2.23.so
7f735c3f6000-7f735c5f6000 ---p 001bf000 08:05 1179825     /lib/x86_64-linux-gnu/libc-2.23.so
7f735c5f6000-7f735c5fa000 r--p 001bf000 08:05 1179825     /lib/x86_64-linux-gnu/libc-2.23.so
7f735c5fa000-7f735c5fc000 rw-p 001c3000 08:05 1179825     /lib/x86_64-linux-gnu/libc-2.23.so
7f735c5fc000-7f735c600000 rw-p 00000000 00:00 0 
7f735c600000-7f735c626000 r-xp 00000000 08:05 1179826     /lib/x86_64-linux-gnu/ld-2.23.so
7f735c7fe000-7f735c823000 rw-p 00000000 00:00 0 
7f735c823000-7f735c825000 rw-p 00000000 00:00 0 
7f735c825000-7f735c826000 r--p 00025000 08:05 1179826     /lib/x86_64-linux-gnu/ld-2.23.so
7f735c826000-7f735c827000 rw-p 00026000 08:05 1179826     /lib/x86_64-linux-gnu/ld-2.23.so
7f735c827000-7f735c828000 rw-p 00000000 00:00 0 
7ffeea455000-7ffeea476000 rw-p 00000000 00:00 0           [stack]
7ffeea48b000-7ffeea48d000 r--p 00000000 00:00 0           [vvar]
7ffeea48d000-7ffeea48f000 r-xp 00000000 00:00 0           [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0   [vsyscall]

前三个是进程本身的代码(r-xp),只读数据(r--p)和已初始化数据(rw-p)。进程可以使用sbrk()扩展的数据段(或"堆")是第三个(即sbrk(0)将返回0x60d000)。
进程有一些堆,从地址0x215f000到0x2180000(不包括0x2180000)。
下一个段是当前区域设置数据的只读映射。C库用它来处理区域设置相关的接口。
接下来的四个段是C库本身:代码(r-xp)、一个通常无法访问的映射,某种方式被C库使用/需要(---p),只读数据(r--p)和已初始化数据(rw-p)。
下一段和最后一列没有名称的其他段,保护模式为(rw-p),是单独的数据段或堆。
下面的三个段落是Linux中使用的动态链接器ld.so。同样,有一个代码段(r-xp),只读数据段(r--p)和已初始化数据段(rw-p)。

[stack]段是初始线程的堆栈。 (cat是单线程的,所以它只有一个线程。) [vvar]段由内核提供(允许进程直接访问某些内核提供的数据,而不必承担系统调用的开销)。[vdso][vsyscall]段由内核提供,用于加速不需要完整上下文切换即可完成的系统调用。

因此,正如您所看到的,完整的图像要更加分散,但也更加自由(更加自由形式),而不是老式C和操作系统书籍所描述的那样。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接