[英]How processes share virtual mem (Linux)
在 Linux 和大多数其他当前使用的通用操作系统中,内存根本不是单个线性阵列:底层物理内存使用虚拟内存在页面级别进行管理。
本质上,每个进程都有自己的虚拟地址空间。 其中大部分是空的、未映射的——尝试访问它会导致分段错误或一般保护违规,通常会杀死进程——; 进程只能访问内核明确设置为进程可以访问的内存。
在大多数情况下,进程也不能直接访问内核内存。 为了执行系统调用——例如,打开或读取或写入文件或设备——,处理器核心本质上将上下文切换到内核模式,内核数据结构和当前进程使用的内存在用户空间可以同时访问(但不一定在内核空间中与用户空间中的虚拟地址相同)。
这意味着现在每个进程可访问的内存实际上非常分散和不连续:
╔════════╗ ╔════════╗ ╔═══════╗
║ Code ║ ║ Data ║ ║ Stack ║
╚════════╝ ╟────────╢ ╚═══════╝
╔════════╗ ║ BSS ║
║ ROdata ║ ╟────────╢
╚════════╝ ║ Heap ║
╔════════╗ ╚════════╝
║ Libs ║
╚════════╝
如果使用地址空间随机化,则上述每个段的地址甚至从一次运行到下一次运行都可能有所不同。 通常,代码(只读且可执行)和只读数据加载到固定地址,但动态链接库、堆栈和数据的地址各不相同。
也没有理由为什么上面的一个地址应该比另一个高或低,所以我特意把它们放在一起,而不是在一个列中!
初始化数据和未初始化数据通常在一个连续的段中,只有初始化数据部分从可执行文件(数据段)加载。 在 Unix 和 POSIX-like 系统中,堆跟随未初始化的数据(并且可以使用brk()
或sbrk()
系统调用进行扩展)。 在像 Linux 这样的 POSIXy 系统中,实际上大多数其他系统中,进程也可以通过(匿名)内存映射拥有额外的“堆”。
进程中的初始线程也获得了一个单独的堆栈段。 任何额外的线程也将获得自己的堆栈。
(学习使用POSIX线程的一个典型练习是找出一个进程可以创建多少个并发线程。Linux中典型的结果只有一百或几百个,很多学习者觉得这很奇怪。造成这种情况的原因较低的数字实际上是默认的堆栈大小,在当前的 GNU/Linux 桌面发行版中大约为 8 兆字节;仅一百个线程的堆栈就需要近 1 GB 的内存,因此并发线程的数量主要受内存限制可用于他们的堆栈。一个非递归线程工作函数最多只需要几十千字节的堆栈,并且只需要几行代码就可以为新创建的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。
下一段是当前语言环境数据的只读映射。 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 和操作系统书籍相比,完整的图片更加零散,但也更自由(如更自由的形式)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.