我不确定我的问题是 Linux 问题还是与操作系统无关的问题。
如果我运行了三个进程(我们称它们为 P0、P1 和 P2),并且它们在用户看来是同时运行的,那么它们如何共享?
他们每个人都在用户空间内维护自己的堆栈、堆等吗?
或者他们只是拥有整个堆栈、堆等,直到下一个进程出现并抢占它?
在 Linux 和大多数其他当前使用的通用操作系统中,内存根本不是单个线性阵列:底层物理内存使用虚拟内存在页面级别进行管理。
本质上,每个进程都有自己的虚拟地址空间。其中大部分是空的、未映射的——尝试访问它会导致分段错误或一般保护违规,通常会杀死进程——;进程只能访问内核明确设置为进程可以访问的内存。
在大多数情况下,进程也不能直接访问内核内存。为了执行系统调用——例如,打开或读取或写入文件或设备——,处理器核心本质上将上下文切换到内核模式,内核数据结构和当前进程使用的内存在用户空间可以同时访问(但不一定在内核空间中与用户空间中的虚拟地址相同)。
这意味着现在每个进程可访问的内存实际上非常分散和不连续:
?????????? ?????????? ?????????
? Code ? ? Data ? ? Stack ?
?????????? ?????????? ?????????
?????????? ? BSS ?
? ROdata ? ??????????
?????????? ? Heap ?
?????????? ??????????
? Libs ?
??????????
Run Code Online (Sandbox Code Playgroud)
如果使用地址空间随机化,则上述每个段的地址甚至从一次运行到下一次运行都可能有所不同。通常,代码(只读且可执行)和只读数据加载到固定地址,但动态链接库、堆栈和数据的地址各不相同。
也没有理由为什么上面的一个地址应该比另一个高或低,所以我特意把它们放在一起,而不是在一个列中!
初始化数据和未初始化数据通常在一个连续的段中,只有初始化数据部分从可执行文件(数据段)加载。在 Unix 和 POSIX 类系统中,堆跟在未初始化的数据之后(并且可以使用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]
Run Code Online (Sandbox Code Playgroud)
前三个是进程本身的代码 ( 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 和操作系统书籍相比,完整的图片更加零散,但也更自由(如更自由的形式)。