0%

从存储器到内存映射布局

从存储器到内存映射布局

在日常工作中,经常会有同学提出这样的问题,

  • “我的程序运行时究竟占用了多大的内存?”
  • “我的程序占用的虚拟内存非常大,这个正常么?”

本文主要从以下几个方向,深入浅出的说明内存的基本布局,后续会更新内存的管理的文章。

  • 计算机体系结构
  • 存储器缓存策略
  • 虚拟内存
  • 虚拟地址
  • 进程的内存划分方案

计算机体系结构

计算机技术领域的变化日新月异,集成电路技术带来的元件不仅种类繁多,而且在功能方面还在不断改进增强。按照摩尔定律,集成电路上可容纳的晶体管数目大约每两年便会增加一倍。而与晶体管数据量密切相关的处理能力也将提升一倍。

经验告诉我们,想要应对这种快速的变化,就是在经常变动的实现层次之上,利用抽象和泛华的方法为计算机系统定义全局目标和结构体系。这种方法的核心在于描述抽象的方式,该方式要确保在去除相对无关的实现细节后,任何新的实现与核心定义都能保持一致。

整个计算机体系结构可用下图表示:

在计算机系统中,有一些和存储器相关的趣事:

  • 人们对存储器容量总是无法满足,而且存储器容量总是供不应求。
  • 存储器技术似乎是导致处理器性能障碍的主要原因
    – 这种被称为”处理器和存储器之间的速度鸿沟(The processor-memory gap)”
  • 存储器的访问能力与其存储容量成反比。

存储器缓存策略

我们从程序员、设计师和工程师的角度,我们希望系统能够以最快的速度访问所有可用存储器。
但是这个基本上是不可能的,实际上系统并不是使用所有的存储器,而是仅仅是在某些时段内使用某一部分存储器。这个情况下,只需要为程序预留相对最快的存储器,而让那些并非立即执行的代码或者数据使用相对较慢的存储器。当CPU立即需要执行指令时,在执行到存储在较慢的存储器上的代码之前,这些代码会转存在较快的存储器中,这种策略称为”缓存”。

缓存策略无处不在,横跨多个级别的存储器,如下图所示:

虚拟内存

我们用名为”进程”的抽象概念来表示正在运行的程序。现在多任务操作系统的设计允许一个或者多个用户并发地运行多个程序,对于一个普通用户来说,同时运行多个应用程序(比如听歌、看网页、编辑文档)在正常不过了。

通过虚拟内存的概念,可以很好的解决内存需求与有限的内存容量之间的矛盾,运行时的物理内存会被划分成数个小的分段(页),每个页都可以用来同步执行程序。正在运行的程序的完整内存布局会被保存在低速存储器(磁盘)中。只有那些当前即将被执行的一部分内存(代码和数据)才会被加载到物理内存页中。

首先需要强调的是虚拟内存不同于物理内存,二者属于不同层面的东西。
进程占用虚拟内存空间大并非意味着程序的物理内存也一定占用很大。虚拟内存是操作系统内核为了对进程地址空间进行管理(process address space management)而设计的一个逻辑意义上的内存空间概念,我们程序中的指针其实都是这个虚拟内存空间中的地址。

虚拟地址(virtual address):

  • CPU启动保护模式后,程序运行在虚拟地址空间中。
  • 注意,并不是所有的“程序”都是运行在虚拟地址中。
  • CPU在启动的时候是运行在实模式的,Bootloader以及内核在初始化页表之前并不使用虚拟地址,而是直接使用物理地址的。
  • 虚拟地址范围:编程模型假定地址空间的范围在0到$2^N$,其中N是32或者64。

比如我们写完一个C/C++项目之后,采用gcc/g++进行编译,这个时候编译器采用的地址其实就是虚拟内存空间的地址。因为这时程序都没有开始运行,所以何谈物理内存空间地址?

凡是程序运行过程中可能需要用到的指令或者数据都必须在虚拟内存空间中。既然说虚拟内存是一个逻辑意义上(逻辑存在)的内存空间,为了能够让程序在物理机器上运行起来,那就需要有一套机制可以让这些虚拟内存空间映射到物理内存空间(真实内存条上的空间)。

在Linux中,解决这个问题的关键是一个叫做 page table (PT页面转换表) 的结构。Linux把物理内存分为了固定统一大小的块,称为page(页),一般为4KB,并且每个页都有一个编号”page frame number”。这样一个512M大小的内存将包括128K个页。这种方式称为paging,使得操作系统对内存的管理更方便。page table的作用就是将进程操作的地址(虚拟地址)转换成物理地址。

关于 page table 内容原理,可以参考以下文章。

内核会为系统中每一个进程维护一份相互独立的页映射表,页映射表的基本原理是将程序运行过程中需要访问的一段虚拟内存空间通过页映射表映射到一段物理内存空间上,这样CPU访问对应虚拟内存地址的时候就可以通过这种查找页映射表的机制访问物理内存上的某个对应的地址。

“页(page)”是虚拟内存空间向物理内存空间映射的基本单元。

虚拟内存的概念如下图所示:

上图演示了虚拟内存空间和物理内存空间的相互关系。它们通过Page Table关联起来。

其中虚拟内存空间中着色部分分别被映射到了物理内存空间对应相同的着色的部分。而虚拟内存空间中灰色的部分表示在物理内存空间中没有与之对应的部分,也就是说灰色部分没有被映射到物理内存空间中。因为虚拟内存空间很大,可能其中很多部分在一次程序运行中根本不需要访问,所以也就没有必要讲虚拟内存空间中的这些部分映射到物理内存空间上。

那么,总结一下,虚拟内存就是一个逻辑存在的内存空间,在程序运行过程中虚拟内存空间中需要被访问的部分会被映射到物理内存空间。虚拟内存空间大只能表示程序运行过程中可访问的空间比较大,不代表物理内存空间占用也大。

驻留内存

顾名思义是指那些被映射到进程虚拟内存空间的物理内存。上图3中,在系统物理内存空间中被着色的部分都是驻留内存。

比如,A1、A2、A3和A4是进程A的驻留内存;B1、B2和B3是进程B的驻留内存。

进程的驻留内存就是进程实实在在占用的物理内存。一般我们所讲的进程占用了多少内存,其实就是说的占用了多少驻留内存而不是多少虚拟内存。因为虚拟内存大并不意味着占用的物理内存大。

以下为具体示例:

操作系统:Linux(Redhat 7.4)

内存信息

1
2
3
4
[root(jihaodong)@redhat work]# free
total used free shared buff/cache available
Mem: 131455940 54627048 12895764 178028 63933128 76268692
Swap: 32767996 134996 32633000
Key Desc
Mem.total 物理内存的总量
Mem.used 使用的物理内存的总量
Mem.free 空闲内存总量
Mem.shared 共享内存
Mem.buff 内核缓冲区使用的内存
Mem.cache 页面缓存和Slab分配器使用的内存
Mem.available 系统空闲内存 Mem.available = Mem.free + Mem.buff + Mem.cache
Swap.total 交换分区总大小,系统物理内存不够用时,与swap进行交换
Swap.used 已经被使用的交换分区大小
Swap.free 未被使用的交换分区大小

进程的TOP信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root(jihaodong)@redhat work]# top -d 1 
top - 14:30:32 up 28 days, 2:25, 1 user, load average: 0.22, 0.31, 0.25
Tasks: 398 total, 1 running, 397 sleeping, 0 stopped, 0 zombie
%Cpu(s): 3.5 us, 0.3 sy, 0.0 ni, 96.2 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 13145593+total, 12901476 free, 54629708 used, 63924756 buff/cache
KiB Swap: 32767996 total, 32632996 free, 135000 used. 76267056 avail Mem
scroll coordinates: y = 1/398 (tasks), x = 1/12 (fields)
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
5746 elastic+ 20 0 0.233t 0.060t 0.011t S 100.0 48.8 7155:16 /opt/soft/jdk/jdk1.8.0_66/bin/java -Xms48g -Xmx48g -XX:+UseConcMa+
31299 root 20 0 4283448 305600 2368 S 0.0 0.2 51:26.88 java -XX:PermSize=128m -XX:MaxPermSize=256m -Xms512m -Xmx512m -cp+
7104 elastic+ 20 0 1277408 99848 6260 S 0.0 0.1 93:41.92 ./bin/../node/bin/node --no-warnings ./bin/../src/cli
18809 root 20 0 2921264 47312 4168 S 10.0 0.0 294:00.58 /usr/local/bin/xcdc run s1.js
18811 root 20 0 2663044 45584 4124 S 5.0 0.0 286:10.11 /usr/local/bin/xcdc run s3.js
18810 root 20 0 2724032 45428 3980 S 5.0 0.0 298:59.47 /usr/local/bin/xcdc run s2.js
18813 root 20 0 2930092 44012 4116 S 5.0 0.0 288:44.38 /usr/local/bin/xcdc run s5.js
18812 root 20 0 2924452 43312 4024 S 0.0 0.0 291:19.32 /usr/local/bin/xcdc run s4.js
10189 root 20 0 2350900 29800 2444 S 0.0 0.0 38:41.39 ./falcon-agent -c cfg.json
Key Desc
PID 进程ID
PPID 父进程ID
UID 进程所有者的用户ID
USER 进程所有者的用户名
PR 优先级
NI NICE值,负值表示搞优先级,正值表示优先级底
VIRT 进程所用的虚拟内存总量, VIRT = SWAP + RES
SWAP 进程使用的虚拟内存中,被置换出去的大小
RES 进程使用的,未被置换出去的物理内存,(目前占用物理内存的值)
SHR 共享内存大小

搞清楚了虚拟内存的概念之后解释VIRT就很简单了,VIRT表示虚拟内存空间大小结合图1(计算机体系结构抽象),对应到图3(虚拟内存空间到物理内存空间的映射)中来说就是A1、A2、A3、A4以及灰色部分所有空间的总和。也就是说虚拟包含了在已经映射到物理内存空间的部分和尚未映射到物理内存空间的部分的总和。

RES的含义是指进程虚拟内存空间中已经映射到物理内存空间的那部分的大小。对应到图1中的进程A来说就是A1、A2、A3以及A4几个部分空间的总和。所以说,看进程在运行过程中占用了多少内存应该看RES的值而不是VIRT的值。

最后来看看SHR所表示的含义。SHR是share(共享)的缩写,它表示的是进程占用的共享内存大小。在上图1中我们看到进程A虚拟内存空间中的A4和进程B虚拟内存空间中的B3都映射到了物理内存空间的A4/B3部分。咋一看很奇怪。为什么会出现这样的情况呢?其实我们写的程序会依赖于很多外部的动态库(.so),比如libc.so、libld.so等等。这些动态库在内存中仅仅会保存/映射一份,如果某个进程运行时需要这个动态库,那么动态加载器会将这块内存映射到对应进程的虚拟内存空间中。多个进展之间通过共享内存的方式相互通信也会出现这样的情况。这么一来,就会出现不同进程的虚拟内存空间会映射到相同的物理内存空间。这部分物理内存空间其实是被多个进程所共享的,所以我们将他们称为共享内存,用SHR来表示。某个进程占用的内存除了和别的进程共享的内存之外就是自己的独占内存了。所以要计算进程独占内存的大小只要用RES的值减去SHR值即可。

这片文章主要简单讲解Linux下进程的内存映射布局。

内存管理是操作系统的核心; 这对于编程和系统管理都至关重要。

进程的内存划分方案

本节主要讨论进程内存映射的内部组织细节,我们假定的程序地址空间的范围在0到$2^N$,其中N是32或者64。

不同的多任务或者多用户操作系统拥有不同的内存映射布局。对于Linux进程的虚拟内存映射来说,其遵循图4所示的映射方案。

无论平台的进程内存划分方案多么特殊,下面几个内存映射的节(section)都是必须支持的:

  • 代码节:该段包含了供CPU执行的机器码指令(.text节)。
  • 数据节:该段包含了供CPU操作的数据。通常来说,初始化数据(.data节)、为初始化数据(.bss节)和只读数据(.rdata节)会保存在分离的节中。
  • 堆:动态内存分配的区域。
  • 栈:为各个函数提供了独立的存储空间。
  • 最上层部分属于内核区域,特定进程的环境变量就存放在该区域。

二进制文件、编译器、连接器与装载器的作用

粗略地讲:

  • 程序的二进制文件中包含了程序运行过程中的内存映射布局的细节。
  • 链接器创建了二进制文件的整体框架。要实现这项功能,链接器要对编译器生成的二进制文件进行合并,然后想各个内存映射节填充信息(代码和数据等信息)。
  • 进程内存映射的初始化建立工作是由程序装载器这一系统工具完成的。在最简单的情况下,装载器会打开二进制可执行文件,读取节的相关信息,然后将这些信息载入进程内存映射结构中。

所有现代操作系统都是按照这种角色分离的方式设计的。

需要注意的是,以上描述是一个粗略地描述。

文章参考:

1
2
[1]: 探索 Linux 内存模型: https://www.ibm.com/developerworks/cn/linux/l-memmod/
[2]: 理解VIRT/RES/SHR之间的关系: http://orchome.com/298