Linux 原来是这么管理内存的

linux 内存管理模型非常直接明了,因为 Linux 的这种机制使其具有可移植性并且能够在内存管理单元相差不大的机器下实现 Linux,下面我们就来认识一下 Linux 内存管理是如何实现的 。
基本概念每个 Linux 进程都会有地址空间,这些地址空间由三个段区域组成:text 段、data 段、stack 段 。下面是进程地址空间的示例 。

Linux 原来是这么管理内存的

文章插图
 
数据段(data segment) 包含了程序的变量、字符串、数组和其他数据的存储 。数据段分为两部分,已经初始化的数据和尚未初始化的数据 。其中尚未初始化的数据就是我们说的 BSS 。数据段部分的初始化需要编译就期确定的常量以及程序启动就需要一个初始值的变量 。所有 BSS 部分中的变量在加载后被初始化为 0。
和 代码段(Text segment) 不一样,data segment 数据段可以改变 。程序总是修改它的变量 。而且,许多程序需要在执行时动态分配空间 。Linux 允许数据段随着内存的分配和回收从而增大或者减小 。为了分配内存,程序可以增加数据段的大小 。在 C 语言中有一套标准库 malloc 经常用于分配内存 。进程地址空间描述符包含动态分配的内存区域称为 堆(heap) 。
第三部分段是 栈段(stack segment) 。在大部分机器上,栈段会在虚拟内存地址顶部地址位置处,并向低位置处(向地址空间为 0 处)拓展 。举个例子来说,在 32 位 x86 架构的机器上,栈开始于 0xC0000000,这是用户模式下进程允许可见的 3GB 虚拟地址限制 。如果栈一直增大到超过栈段后,就会发生硬件故障并把页面下降一个页面 。
当程序启动时,栈区域并不是空的,相反,它会包含所有的 shell 环境变量以及为了调用它而向 shell 输入的命令行 。举个例子,当你输入
cp cxuan lx时,cp 程序会运行并在栈中带着字符串 cp cxuan lx,这样就能够找出源文件和目标文件的名称 。
当两个用户运行在相同程序中,例如编辑器(editor),那么就会在内存中保持编辑器程序代码的两个副本,但是这种方式并不高效 。Linux 系统支持共享文本段作为替代 。下面图中我们会看到 A 和 B 两个进程,它们有着相同的文本区域 。
Linux 原来是这么管理内存的

文章插图
 
数据段和栈段只有在 fork 之后才会共享,共享也是共享未修改过的页面 。如果任何一个都需要变大但是没有相邻空间容纳的话,也不会有问题,因为相邻的虚拟页面不必映射到相邻的物理页面上 。
除了动态分配更多的内存,Linux 中的进程可以通过内存映射文件来访问文件数据 。这个特性可以使我们把一个文件映射到进程空间的一部分而该文件就可以像位于内存中的字节数组一样被读写 。把一个文件映射进来使得随机读写比使用 read 和 write 之类的 I/O 系统调用要容易得多 。共享库的访问就是使用了这种机制 。如下所示
Linux 原来是这么管理内存的

文章插图
 
我们可以看到两个相同文件会被映射到相同的物理地址上,但是它们属于不同的地址空间 。
映射文件的优点是,两个或多个进程可以同时映射到同一文件中,任意一个进程对文件的写操作对其他文件可见 。通过使用映射临时文件的方式,可以为多线程共享内存提供高带宽,临时文件在进程退出后消失 。但是实际上,并没有两个相同的地址空间,因为每个进程维护的打开文件和信号不同 。
Linux 内存管理系统调用下面我们探讨一下关于内存管理的系统调用方式 。事实上,POSIX 并没有给内存管理指定任何的系统调用 。然而,Linux 却有自己的内存系统调用,主要系统调用如下
Linux 原来是这么管理内存的

文章插图
 
如果遇到错误,那么 s 的返回值是 -1,a 和 addr 是内存地址,len 表示的是长度,prot 表示的是控制保护位,flags 是其他标志位,fd 是文件描述符,offset 是文件偏移量 。
brk 通过给出超过数据段之外的第一个字节地址来指定数据段的大小 。如果新的值要比原来的大,那么数据区会变得越来越大,反之会越来越小 。
mmap 和 unmap 系统调用会控制映射文件 。mmp 的第一个参数 addr 决定了文件映射的地址 。它必须是页面大小的倍数 。如果参数是 0,系统会分配地址并返回 a 。第二个参数是长度,它告诉了需要映射多少字节 。它也是页面大小的倍数 。prot 决定了映射文件的保护位,保护位可以标记为 可读、可写、可执行或者这些的结合 。第四个参数 flags 能够控制文件是私有的还是可读的以及 addr 是必须的还是只是进行提示 。第五个参数 fd 是要映射的文件描述符 。只有打开的文件是可以被映射的,因此如果想要进行文件映射,必须打开文件;最后一个参数 offset 会指示文件从什么时候开始,并不一定每次都要从零开始 。


推荐阅读