41-18 linux内存


前言:
Linux中如何查看进程消耗、占用的内存情况,如何进行Linux的内存管理。与之需求对应的,Linux 也提供了许多的方法来监控内存资源的使用情况。linux下使用top,atop,htop,free,memstat,vmstat,dstat,ps,pmap,smem等命令查看系统或者进程的内存使用情况。

查看内存命令

1./proc/meminfo
查看 RAM 使用情况最简单的方法是通过 /proc/meminfo。这个动态更新的虚拟文件实际上是许多其他内存相关工具 (如:free / ps / top) 等的组合显示。/proc/meminfo 列出了所有你想了解的内存的使用情况。进程的内存使用信息也可以通过 /proc//statm 和 /proc//status 来查看。

[root@cos7 ~ ]#cat /proc/meminfo 
MemTotal:        1423964 kB
MemFree:          593384 kB
MemAvailable:     760344 kB
Buffers:            2120 kB
Cached:           289940 kB
SwapCached:            0 kB
Active:           385640 kB
Inactive:         251592 kB
Active(anon):     346096 kB
Inactive(anon):     9636 kB
Active(file):      39544 kB
Inactive(file):   241956 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:       1048572 kB
SwapFree:        1048572 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:        345056 kB
Mapped:            94004 kB
Shmem:             10560 kB
Slab:              72856 kB
SReclaimable:      29664 kB
SUnreclaim:        43192 kB
KernelStack:        7776 kB
PageTables:        22376 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     1760552 kB
Committed_AS:    2464560 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      183424 kB
VmallocChunk:   34359310332 kB
HardwareCorrupted:     0 kB
AnonHugePages:     96256 kB
CmaTotal:              0 kB
CmaFree:               0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:      100224 kB
DirectMap2M:     1382400 kB
DirectMap1G:           0 kB

2.atop
atop 命令是一个终端环境的监控命令。它显示的是各种系统资源(CPU, memory, network, I/O, kernel)的综合,并且在高负载的情况下进行了彩色标注。

$ sudo atop

3.free
free 命令是一个快速查看内存使用情况的方法,它是对 /proc/meminfo 收集到的信息的一个概述, free 命令中的信息都来自于 /proc/meminfo 文件。/proc/meminfo 文件包含了更多更原始的信息,只是看起来不太直观。其显示系统内存的使用情况,包括物理内存、交换内存(swap)和内核缓冲区内存。

 内存空间使用状态:
free [OPTION]
-b 以字节为单位
-m 以MB为单位
-g 以GB为单位
-h 易读格式
-o 不显示-/+buffers/cache行
-t 显示RAM + swap的总和
-s n 刷新间隔为n秒
-c n 刷新n次后即退出
在这里插入图片描述

[root @redhat6 :logs]$ free -g  
             total       used       free     shared    buffers     cached
Mem:            62         55          7          0          0         22
-/+ buffers/cache:         31         30
Swap:            9          2          6

[root@cos7 ~ ]#free -g
              total        used        free      shared  buff/cache   available
Mem:              1           0           0           0           0           0
Swap:             0           0           0

输出简介
Mem 行(第二行)是内存的使用情况。
Swap 行(第三行)是交换空间的使用情况。

  • total 列显示系统总的可用物理内存和交换空间大小。

  • used 列显示已经被使用的物理内存和交换空间。

  • free 列显示还有多少物理内存和交换空间可用使用。

  • shared 列显示被共享使用的物理内存大小。

  • buff/cache 列显示被 buffer 和 cache 使用的物理内存大小。

  • buff/cache
    先来提一个问题: buffer 和 cache 应该是两种类型的内存,但是 free 命令为什么会把它们放在一起呢?要回答这个问题需要我们做些准备工作。让我们先来搞清楚 buffer 与 cache 的含义。 buffer 在操作系统中指 buffer cache, 中文一般翻译为 “缓冲区”。要理解缓冲区,必须明确另外两个概念:“扇区” 和 “块”。扇区是设备的最小寻址单元,也叫 “硬扇区” 或 “设备块”。块是操作系统中文件系统的最小寻址单元,也叫 “文件块” 或 “I/O 块”。每个块包含一个或多个扇区,但大小不能超过一个页面,所以一个页可以容纳一个或多个内存中的块。当一个块被调入内存时,它要存储在一个缓冲区中。每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示(下图来自互联网):
    在这里插入图片描述

注意,buffer cache 只有块的概念而没有文件的概念,它只是把磁盘上的块直接搬到内存中而不关心块中究竟存放的是什么格式的文件。

buffers是指用来给块设备做的缓冲大小(块设备的读写缓冲区),它只记录文件系统的metadata以及 tracking in-flight pages.
Buffers are associated with a specific block device, and cover caching of filesystem metadata as well as tracking in-flight pages. The cache only contains parked file data. That is, the buffers remember what’s in directories, what file permissions are, and keep track of what memory is being written from or read to for a particular block device. The cache only contains the contents of the files themselves.

cache 在操作系统中一般指 page cache,中文一般翻译为 “页高速缓存”。页高速缓存是内核实现的磁盘缓存。它主要用来减少对磁盘的 I/O 操作。具体地讲,是通过把磁盘中的数据缓存到物理内存中,把对磁盘的访问变为对物理内存的访问。页高速缓存缓存的是内存页面。缓存中的页来自对普通文件、块设备文件(这个指的就是 buffer cache 呀)和内存映射文件的读写。 页高速缓存对普通文件的缓存我们可以这样理解:当内核要读一个文件(比如 /etc/hosts)时,它会先检查这个文件的数据是不是已经在页高速缓存中了。如果在,就放弃访问磁盘,直接从内存中读取。这个行为称为缓存命中。如果数据不在缓存中,就是未命中缓存,此时内核就要调度块 I/O 操作从磁盘去读取数据。然后内核将读来的数据放入页高速缓存中。这种缓存的目标是文件系统可以识别的文件(比如 /etc/hosts)。 页高速缓存对块设备文件的缓存就是我们在前面介绍的 buffer cahce。因为独立的磁盘块通过缓冲区也被存入了页高速缓存(缓冲区最终是由页高速缓存来承载的)。

cached是作为page cache的内存, 文件系统的cache。你读写文件的时候,Linux内核为了提高读写性能与速度,会将文件在内存中进行缓存,这部分内存就是Cache Memory(缓存内存)。即使你的程序运行结束后,Cache Memory也不会自动释放。这就会导致你在Linux系统中程序频繁读写文件后,你会发现可用物理内存会很少。其实这缓存内存(Cache Memory)在你需要使用内存的时候会自动释放,所以你不必担心没有内存可用
Cached is the size of the page cache. Buffers is the size of in-memory block I/O buffers. Cached matters; Buffers is largely irrelevant. Cached is the size of the Linux page cache, minus the memory in the swap cache, which is represented by SwapCached (thus the total page cache size is Cached + SwapCached). Linux performs all file I/O through the page cache. Writes are implemented as simply marking as dirty the corresponding pages in the page cache; the flusher threads then periodically write back to disk any dirty pages. Reads are implemented by returning the data from the page cache; if the data is not yet in the cache, it is first populated. On a modern Linux system, Cached can easily be several gigabytes. It will shrink only in response to memory pressure. The system will purge the page cache along with swapping data out to disk to make available more memory as needed. Buffers are in-memory block I/O buffers. They are relatively short-lived. Prior to Linux kernel version 2.4, Linux had separate page and buffer caches. Since 2.4, the page and buffer cache are unified and Buffers is raw disk blocks not represented in the page cache—i.e., not file data. The Buffers metric is thus of minimal importance. On most systems, Buffers is often only tens of megabytes.

到这里我们应该搞清楚了:无论是缓冲区还是页高速缓存,它们的实现方式都是一样的。缓冲区只不过是一种概念上比较特殊的页高速缓存罢了。 那么为什么 free 命令不直接称为 cache 而非要写成 buff/cache? 这是因为缓冲区和页高速缓存的实现并非天生就是统一的。在 linux 内核 2.4 中才将它们统一。更早的内核中有两个独立的磁盘缓存:页高速缓存和缓冲区高速缓存。前者缓存页面,后者缓存缓冲区。当你知道了这些故事之后,输出中列的名称可能已经不再重要了

Buffers are associated with a specific block device, and cover caching of filesystem metadata as well as tracking in-flight pages. The cache only contains parked file data. That is, the buffers remember what’s in directories, what file permissions are, and keep track of what memory is being written from or read to for a particular block device. The cache only contains the contents of the files themselves.

Linux共享内存

共享内存是进程间通信中最简单的方式之一。共享内存允许两个或更多进程访问同一块内存,就如同 malloc() 函数向不同进程返回了指向同一个物理内存区域的指针。当一个进程改变了这块地址中的内容的时候,其它进程都会察觉到这个。其实所谓共享内存,就是多个进程间共同地使用同一段物理内存空间,它是通过将同一段物理内存映射到不同进程的虚拟空间来实现的。由于映射到不同进程的虚拟空间中,不同进程可以直接使用,不需要像消息队列那样进行复制,所以共享内存的效率很高。共享内存可以通过mmap()映射普通文件机制来实现,也可以System V共享内存机制来实现,System V是通过映射特殊文件系统shm中的文件实现进程间的共享内存通信,也就是说每个共享内存区域对应特殊文件系统shm中的一个文件。

另外,我们还必须了解RSS、PSS、USS等相关概念:

VSS – Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)

RSS – Resident Set Size 实际使用物理内存(包含共享库占用的内存)

PSS – Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)

USS – Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

RSS(Resident set size),使用top命令可以查询到,是最常用的内存指标,表示进程占用的物理内存大小。但是,将各进程的RSS值相加,通常会超出整个系统的内存消耗,这是因为RSS中包含了各进程间共享的内存。

PSS(Proportional set size)所有使用某共享库的程序均分该共享库占用的内存时,每个进程占用的内存。显然所有进程的PSS之和就是系统的内存使用量。它会更准确一些,它将共享内存的大小进行平均后,再分摊到各进程上去。

USS(Unique set size )进程独自占用的内存,它是PSS中自己的部分,它只计算了进程独自占用的内存大小,不包含任何共享的部分。

所以下面介绍的命令,有些查看进程的虚拟内存使用,有些是查看进程的RSS或实际物理内存。在讲述的时候,我们会标注这些信息

  • free 与 available
    在 free 命令的输出中,有一个 free 列,同时还有一个 available 列。这二者到底有何区别? free 是真正尚未被使用的物理内存数量。至于 available 就比较有意思了,它是从应用程序的角度看到的可用内存数量。Linux 内核为了提升磁盘操作的性能,会消耗一部分内存去缓存磁盘数据,就是我们介绍的 buffer 和 cache。所以对于内核来说,buffer 和 cache 都属于已经被使用的内存。当应用程序需要内存时,如果没有足够的 free 内存可以用,内核就会从 buffer 和 cache 中回收内存来满足应用程序的请求。所以从应用程序的角度来说,available = free + buffer + cache。请注意,这只是一个很理想的计算方式,实际中的数据往往有较大的误差。

交换空间(swap space)
swap space 是磁盘上的一块区域,可以是一个分区,也可以是一个文件。所以具体的实现可以是 swap 分区也可以是 swap 文件。当系统物理内存吃紧时,Linux 会将内存中不常访问的数据保存到 swap 上,这样系统就有更多的物理内存为各个进程服务,而当系统需要访问 swap 上存储的内容时,再将 swap 上的数据加载到内存中,这就是常说的换出和换入。交换空间可以在一定程度上缓解内存不足的情况,但是它需要读写磁盘数据,所以性能不是很高。

现在的机器一般都不太缺内存,如果系统默认还是使用了 swap 是不是会拖累系统的性能?理论上是的,但实际上可能性并不是很大。并且内核提供了一个叫做 swappiness 的参数,用于配置需要将内存中不常用的数据移到 swap 中去的紧迫程度。这个参数的取值范围是 0~100,0 告诉内核尽可能的不要将内存数据移到 swap 中,也即只有在迫不得已的情况下才这么做,而 100 告诉内核只要有可能,尽量的将内存中不常访问的数据移到 swap 中。在 ubuntu 系统中,swappiness 的默认值是 60。如果我们觉着内存充足,可以在 /etc/sysctl.conf 文件中设置 swappiness:

vm.swappiness=10
[root@cos7 ~ ]#swap
swaplabel swapoff swapon

如果系统的内存不足,则需要根据物理内存的大小来设置交换空间的大小

4.GNOME System Monitor
GNOME System Monitor 是一个显示最近一段时间内的 CPU、内存、交换区及网络的使用情况的视图工具。它还提供了一种查看 CPU 及内存使用情况的方法。

$ gnome-system-monitor

5.htop
htop 命令显示了每个进程的内存实时使用率。它提供了所有进程的常驻内存大小、程序总内存大小、共享库大小等的报告。列表可以水平及垂直滚动。

$ htop

6.KDE System Monitor
功能同 4 中介绍的 GENOME 版本。

$ ksysguard

7.memstat
memstat 是一个有效识别 executable(s), process(es) and shared libraries 使用虚拟内存情况的命令。给定一个进程 ID,memstat 可以列出这个进程相关的可执行文件、数据和共享库。

$ memstat -p

8.nmon
nmon 是一个基于 ncurses 的系统基准测试工具,它可以监控 CPU、内存、I/O、文件系统及网络资源等的互动模式。对于内存的使用,它可以实时的显示总 / 剩余内存、交换空间等信息。

$ nmon

9.ps
ps 命令可以实时的显示各个进程的内存使用情况。Reported memory usage information includes %MEM (percent of physical memory used), VSZ (totalamount of virtual memory used), and RSS (total amount of physical memory used)。你可以使用 “–sort” 选项对进程进行排序,例如按 RSS 进行排序:

$ ps aux –sort -rss

10.smem
smem 命令允许你统计基于 / proc 信息的不同进程和用户的内存使用情况。内存使用情况的分析可以导出图表(如条形图和饼图)。

$ sudo smem –pie name -c “pss”

11.top
top 命令提供了实时的运行中的程序的资源使用统计。你可以根据内存的使用和大小来进行排序。

$ top

12.vmstat
vmstat 命令显示实时的和平均的统计,覆盖 CPU、内存、I/O 等内容。例如内存情况,不仅显示物理内存,也统计虚拟内存。
在这里插入图片描述
 vmstat命令:虚拟内存信息
        vmstat [options] [delay [count]]
        vmstat 2 5
 procs:
        r:可运行(正运行或等待运行)进程的个数,和核心数有关
        b:处于不可中断睡眠态的进程个数(被阻塞的队列的长度) 
 memory:
        swpd: 交换内存的使用总量
        free:空闲物理内存总量
        buffer:用于buffer的内存总量
        cache:用于cache的内存总量
 swap:
        si:从磁盘交换进内存的数据速率(kb/s)
        so:从内存交换至磁盘的数据速率(kb/s)
 io:
        bi:从块设备读入数据到系统的速率(kb/s)
        bo: 保存数据至块设备的速率
 system:
        in: interrupts 中断速率,包括时钟
        cs: context switch 进程切换速率 
 cpu:
        us:Time spent running non-kernel code 用户耗费时间
        sy: Time spent running kernel code 系统耗费时间
         id: Time spent idle. Linux 2.5.41前,包括IO-wait time. 空闲时间
        wa: Time spent waiting for IO. 2.5.41前,包括in idle. 等待时间
        st: Time stolen from a virtual machine. 2.6.11前, unknown. 被偷走时间
 选项:

  • -s: 显示内存的统计数据

内存管理中的虚拟内存

虚拟内存是Linxu管理内存的一种技术,它使得每个应用程序都认为自己拥有独立且连续完整的可用内存空间,而实际上,它通常是被映射到多个物理内存段,还有部分暂时存储在外部磁盘存储器上,在需要时再加载到内存中来。每个进程所能使用的虚拟地址大小和cpu位数有关,32位的系统上,虚拟地址空间大小为4g,64位为2^64,当然实际的物理内存大小可能远远小于虚拟地址空间的大小,虚拟地址空间大小并不等同于交换空间,交换空间只能算其中的一部分

  进程X                                                                      进程Y
+-------+                                                                  +-------+
| VPFN7 |--+                                                               | VPFN7 |
+-------+  |       进程X的                                 进程Y的           +-------+
| VPFN6 |  |      Page Table                              Page Table     +-| VPFN6 |
+-------+  |      +------+                                +------+       | +-------+
| VPFN5 |  +----->| .... |---+                    +-------| .... |<---+  | | VPFN5 |
+-------+         +------+   |        +------+    |       +------+    |  | +-------+
| VPFN4 |    +--->| .... |---+-+      | PFN4 |    |       | .... |    |  | | VPFN4 |
+-------+    |    +------+   | |      +------+    |       +------+    |  | +-------+
| VPFN3 |--+ |    | .... |   | | +--->| PFN3 |<---+  +----| .... |<---+--+ | VPFN3 |
+-------+  | |    +------+   | | |    +------+       |    +------+    |    +-------+
| VPFN2 |  +-+--->| .... |---+-+-+    | PFN2 |<------+    | .... |    |    | VPFN2 |
+-------+    |    +------+   | |      +------+            +------+    |    +-------+
| VPFN1 |    |               | +----->| FPN1 |                        +----| VPFN1 |
+-------+    |               |        +------+                             +-------+
| VPFN0 |----+               +------->| PFN0 |                             | VPFN0 |
+-------+                             +------+                             +-------+
 虚拟内存                               物理内存                               虚拟内存


PFN(the page frame number): 页编号

当一个程序开始运行时,需要先到内存中读取该进程的指令,获取指令是用到的就是虚拟地址,该地址是程序链接时确定的,为了获取到实际的指令和数据,cpu需要借助进程的页表(page table)将虚拟地址转换为物理地址,页表里面的数据由操作系统维护。注意linux内核代码访问内存用的都是实际的物理地址,不存在虚拟地址到物理地址的转换,只有应用程序才需要。 为了方便转换,Linux将虚拟内存和物理内存的page都拆分为固定大小的页,一般是4k,每个页都会分配一个唯一的编号,就是页编号PFN。
从上面的图可以看出,虚拟内存和物理内存的page之间通过page table进行映射。进程X和Y的虚拟内存是相互独立的,他们的页表也是相互独立的,不同进程共享物理内存。进程可以随便访问自己的虚拟地址空间,而页表和物理内存由内核维护,当进程需要访问内存时,cpu会根据进程的页表将虚拟地址翻译成物理地址,然后进行访问。:并不是每个虚拟地址空间的page都有对应的Page Table相关联,只有虚拟地址被分配给进程后,也即进程调用类似malloc函数之后,系统才会为相应的虚拟地址在Page Table中添加记录,如果进程访问一个没有和Page Table关联的虚拟地址,系统将会抛出SIGSEGV信号,导致进程退出,这也是为什么我们访问野指针时会经常出现segmentfault的原因。换句话说,虽然每个进程都有4G(32位系统)的虚拟地址空间,但只有向系统申请了的那些地址空间才能用,访问未分配的地址空间将会出segmentfault错误。Linux会将虚拟地址0不映射到任何地方,这样我们访问空指针就一定会报segmentfault错误。

虚拟内存的优点

  • 更大的地址空间:并且是连续的,使得程序编写、链接更加简单
  • 进程隔离:不同进程的虚拟地址之间没有关系,所以一个进程的操作不会对其它进程造成影响
  • 数据保护:每块虚拟内存都有相应的读写属性,这样就能保护程序的代码段不被修改,数据块不能被执行等,增加了系统的安全性
  • 内存映射:有了虚拟内存之后,可以直接映射磁盘上的文件(可执行文件或动态库)到虚拟地址空间,这样可以做到物理内存延时分配,只有在需要读相应的文件的时候,才将它真正的从磁盘上加载到内存中来,而在内存吃紧的时候又可以将这部分内存清空掉,提高物理内存利用效率,并且所有这些对应用程序来说是都透明的
  • 共享内存:比如动态库,只要在内存中存储一份就可以了,然后将它映射到不同进程的虚拟地址空间中,让进程觉得自己独占了这个文件。进程间的内存共享也可以通过映射同一块物理内存到进程的不同虚拟地址空间来实现共享
  • 其它:有了虚拟地址空间后,交换空间和COW(copy on write)等功能都能很方便的实现

page table

page table可以简单的理解为一个memory mapping的链表(当然实际结构很复杂),里面的每个memory mapping都将一块虚拟地址映射到一个特定的资源(物理内存或者外部存储空间)。每个进程拥有自己的page table,和其它进程的page table没有关系。

memory mapping

每个memory mapping就是对一段虚拟内存的描述,包括虚拟地址的起始位置,长度,权限(比如这段内存里的数据是否可读、写、执行), 以及关联的资源(如物理内存page,swap空间上的page,磁盘上的文件内容等)。当进程申请内存时,系统将返回虚拟内存地址,同时为相应的虚拟内存创建memory mapping并将它放入page table,但这时系统不一定会分配相应的物理内存,系统一般会在进程真正访问这段内存的时候才会分配物理内存并关联到相应的memory mapping,这就是所谓的延时分配/按需分配。每个memory mapping都有一个标记,用来表示所关联的物理资源类型,一般分两大类,那就是anonymous和file backed。

  • file backed这种类型表示对应的物理资源存放在磁盘上的文件中,它所包含的信息包括文件的位置、offset、rwx权限等。当进程第一次访问对应的虚拟page的时候,由于在memory mapping中找不到对应的物理内存,CPU会报page fault中断,然后操作系统就会处理这个中断并将文件的内容加载到物理内存中,然后更新memory mapping,这样下次CPU就能访问这块虚拟地址了。以这种方式加载到内存的数据一般都会放到page cache中。一般程序的可执行文件,动态库都是以这种方式映射到进程的虚拟地址空间的。
  • anonymous类型: 程序自己用到的数据段和堆栈空间,以及通过mmap分配的共享内存,它们在磁盘上找不到对应的文件,所以这部分内存页被叫做anonymous page。anonymous page和file backed最大的差别是当内存吃紧时,系统会直接删除掉file backed对应的物理内存,因为下次需要的时候还能从磁盘加载到内存,但anonymous page不能被删除,只能被swap out。
  • shared: 不同进程的page table里面的多个memory mapping可以映射到相同的物理地址,通过虚拟地址可以访问到相同的内容,当一个进程修改内存的内容后,在另一个进程可以立即读取到,这种方式一般用来实现进程间高速的共享数据。当标记为shared的memory mapping被删除回收时,需要更新物理page上的引用计数,当物理page的计数变0后被回收。
  • copy on write: 它是基于shared技术,当读这种类型的内存时,系统不需要做任何特殊的操作,而当要写这块内存时,系统将会生成一块新的内存并拷贝原来内存中的数据到新内存中,然后将新内存关联到相应的memory mapping,接着执行写操作,linux很多功能都依赖于copy on write技术来提高性能,最常见的是fork。
  1. 进程向系统发出内存申请请求
  2. 系统会检查进程的虚拟地址空间是否用完,如果有剩余,给进程分配虚拟地址
  3. 系统为这块虚拟地址创建相应的memory mapping,并把它放进进程的page table
  4. 系统返回虚拟地址给进程,进程开始访问虚拟地址
  5. cpu根据虚拟地址在该进程的page table找到对应的memory mapping,但是该mapping没有和物理内存关联,于是产生缺页中断
  6. 操作系统收到缺页中断后,分配真正的物理内存并将它关联到对应的memory mapping
  7. 中断处理完成后,cpu就可以访问内存了。
    当然缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才用的着, 也就是说上面第三步很多时候系统会分配真正的物理内存并关联memory mapping

其他概念

操作系统只要实现了虚拟内存和物理内存之间的映射关系就能正常工作了,但要使得内存访问更高效,还有很多需要考虑,下面我们来介绍一下与之相关的一些其他概念及其作用:

MMU(Memory Management Unit)
MMU是cpu的一个用来将进程的虚拟地址转换为物理地址的模块,它的输入是进程的page table和虚拟地址,输出是物理地址。将虚拟地址转换成物理地址的速度直接影响着系统的速度,所有cpu包含了这个硬件模块用来加速。

TLB (Translation Lookaside Buffer)
上面介绍到,MMU的输入是page table,而page table又存在内存里,和cpu的cache相比,内存的速度很慢,为了进一步加快虚拟地址到物理地址的转换速度,Linux发明了TLB,它存在于cpu的L1cache里面,用来缓存已经找到的虚拟地址和物理地址的映射,这样下次转换前先排查一下TLB,如果已经在里面了就不需要使用MMU进行转换了。

按需分配物理页

由于实际情况下,物理内存要比虚拟内存少很多,所以操作系统必须很小心的分配物理内存,以使内存的使用率达到最大化。一个节约物理内存的办法就是只加载当前正在使用的虚拟page对应的数据到内存。比如,一个很大的数据库程序,如果你只是用了查询操作,那么负责插入删除等部分的代码段就没必要加载到内存中,这样就能节约很多物理内存,这种方法就叫做物理内存页按需分配,也可以称作延时加载。

当CPU访问一个虚拟内存页的时候,如果这个虚拟内存页对应的数据还没加载到物理内存中,则CPU就会通知操作系统发生了page fault,然后由操作系统负责将数据加载进物理内存。由于将数据加载进内存比较耗时,所以CPU不会等在那里,而是去调度其它进程,当它下次再调度到该进程时,数据已经在物理内存上了。

Linux主要使用这种方式来加载可执行文件和动态库,当程序被内核开始调度执行时,内核将进程的可执行文件和动态库映射到进程的虚拟地址空间,并只加载马上要用到的那小部分数据到物理内存中,其它的部分只有当CPU访问到它们时才去加载。

访问控制

page table里面的每条虚拟内存到物理内存的映射记录(memory mapping)都包含一份控制信息,当进程要访问一块虚拟内存时,系统可以根据这份控制信息来检查当前的操作是否是合法的。

为什么需要做这个检查呢?比如有些内存里面放的是程序的可执行代码,那么就不应该去修改它;有些内存里面存放的是程序运行时用到的数据,那么这部分内存只能被读写,不应该被执行;有些内存里面存放的是内核的代码,那么在用户态就不应该去执行它;有了这些检查之后会大大增强系统的安全性

huge pages

由于CPU的cache有限,所以TLB里面缓存的数据也有限,而采用了huge page后,由于每页的内存变大(比如由原来的4K变成了4M),虽然TLB里面的纪录数没变,但这些纪录所能覆盖的地址空间变大,相当于同样大小的TLB里面能缓存的映射范围变大,从而减少了调用MMU的次数,加快了虚拟地址到物理地址的转换速度。

Caches

为了提高系统性能,Linux使用了一些跟内存管理相关的cache,并且尽量将空闲的内存用于这些cache。这些cache都是系统全局共享的:

  • Buffer Cache:用来缓冲块设备上的数据,比如磁盘,当读写块设备时,系统会将相应的数据存放到这个cache中,等下次再访问时,可以直接从cache中拿数据,从而提高系统效率。它里面的数据结构是一个块设备ID和block编号到具体数据的映射,只要根据块设备ID和块的编号,就能找到相应的数据。

  • Page Cache: 这个cache主要用来加快读写磁盘上文件的速度。它里面的数据结构是文件ID和offset到文件内容的映射,根据文件ID和offset就能找到相应的数据

从上面的定义可以看出,page cache和buffer cache有重叠的地方,不过实际情况是buffer cache只缓存page cache不缓存的那部分内容,比如磁盘上文件的元数据。所以一般情况下和page cache相比,Buffer Cache的大小基本可以忽略不计。

小结

学习了以上内容,我们结合top命令来看看各个字段表示的含义:

top - 22:56:37 up 5 days, 11:28,  1 user,  load average: 0.06, 0.05, 0.01
Tasks: 186 total,   1 running, 185 sleeping,   0 stopped,   0 zombie
%Cpu(s):  0.3 us,  0.3 sy,  0.0 ni, 99.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem :   997596 total,   134328 free,   132816 used,   730452 buff/cache
KiB Swap:  1046524 total,  1017976 free,    28548 used.   635824 avail Mem 

KiB Mem代表物理内存,KiB Swap表示交换空间,他们的单位都是KiB(1k)。buff/cached代表了buff和cache总共用了多少,buff代表buffer cache占了多少空间,由于它主要用来缓存磁盘上文件的元数据,所以一般都比较小,跟cache比可以忽略不计;cache代表page cache和其它一些占用空间比较小且大小比较固定的cache的总和,基本上cache就约等于page cache,page cache的准确值可以通过查看/proc/meminf中的Cached得到。由于page cache是用来缓存磁盘上文件内容的,所以占有空间很大,Linux一般会尽可能多的将空闲物理内存用于page cache。

参考资料
Linux内存管理


进程与内存初识

摘要:本章首先以应用程序开发者的角度审视Linux的进程内存管理,在此基础上逐步深入到内核中讨论系统物理内存管理和内核内存的使用方法。力求从外到内、水到渠成地引导网友分析Linux的内存管理与使用。在本章最后,我们给出一个内存映射的实例,帮助网友们理解内核内存管理与用户内存管理之间的关系,希望大家最终能驾驭Linux内存管理。

前言
内存管理一向是所有操作系统书籍不惜笔墨重点讨论的内容,无论市面上或是网上都充斥着大量涉及内存管理的教材和资料。因此,我们这里所要写的Linux内存管理采取避重就轻的策略,从理论层面就不去班门弄斧,贻笑大方了。我们最想做的和可能做到的是从开发者的角度谈谈对内存管理的理解,最终目的是把我们在内核开发中使用内存的经验和对Linux内存管理的认识与大家共享。

当然,这其中我们也会涉及到一些诸如段页等内存管理的基本理论,但我们的目的不是为了强调理论,而是为了指导理解开发中的实践,所以仅仅点到为止,不做深究。

遵循“理论来源于实践”的“教条”,我们先不必一下子就钻入内核里去看系统内存到底是如何管理,那样往往会让你陷入似懂非懂的窘境(我当年就犯了这个错误!)。所以最好的方式是先从外部(用户编程范畴)来观察进程如何使用内存,等到大家对内存的使用有了较直观的认识后,再深入到内核中去学习内存如何被管理等理论知识。最后再通过一个实例编程将所讲内容融会贯通。

进程与内存
进程如何使用内存?

毫无疑问,所有进程(执行的程序)都必须占用一定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。

对任何一个普通进程来讲,它都会涉及到5种不同的数据段。稍有编程知识的朋友都能想到这几个数据段中包含有“程序代码段”、“程序数据段”、“程序堆栈段”等。不错,这几种数据段都在其中,但除了以上几种数据段之外,进程还另外包含两种数据段。下面我们来简单归纳一下进程对应的内存空间中所包含的5种不同的数据区。

  • 代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。

  • 数据段:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配[1]的变量和全局变量。

  • BSS段[2]:BSS段包含了程序中未初始化的全局变量,在内存中 bss段全部置零。

  • 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)

  • 栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。

进程如何组织这些区域?

上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。

下图简要描述了进程内存区域的分布:
“事实胜于雄辩”,我们用一个小例子(原形取自《User-Level Memory Management》)来展示上面所讲的各种内存区的
在这里插入图片描述
程序内存使用


Linux 虚拟内存和物理内存的理解

关于Linux 虚拟内存和物理内存的理解。

首先,让我们看下虚拟内存:

第一层理解

  1. 每个进程都有自己独立的4G内存空间,各个进程的内存空间具有类似的结构

  2. 一个新进程建立的时候,将会建立起自己的内存空间,此进程的数据,代码等从磁盘拷贝到自己的进程空间,哪些数据在哪里,都由进程控制表中的task_struct记录,task_struct中记录中一条链表,记录中内存空间的分配情况,哪些地址有数据,哪些地址无数据,哪些可读,哪些可写,都可以通过这个链表记录

  3. 每个进程已经分配的内存空间,都与对应的磁盘空间映射

在这里插入图片描述问题:

计算机明明没有那么多内存(n个进程的话就需要n*4G)内存

建立一个进程,就要把磁盘上的程序文件拷贝到进程对应的内存中去,对于一个程序对应的多个进程这种情况,浪费内存!

第二层理解

  1. 每个进程的4G内存空间只是虚拟内存空间,每次访问内存空间的某个地址,都需要把地址翻译为实际物理内存地址

  2. 所有进程共享同一物理内存,每个进程只把自己目前需要的虚拟内存空间映射并存储到物理内存上。

  3. 进程要知道哪些内存地址上的数据在物理内存上,哪些不在,还有在物理内存上的哪里,需要用页表来记录

4.页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)

  1. 当进程访问某个虚拟地址,去看页表,如果发现对应的数据不在物理内存中,则缺页异常

6.缺页异常的处理过程,就是把进程需要的数据从磁盘上拷贝到物理内存中,如果内存已经满了,没有空地方了,那就找一个页覆盖,当然如果被覆盖的页曾经被修改过,需要将此页写回磁盘

在这里插入图片描述
总结:

优点:

1.既然每个进程的内存空间都是一致而且固定的,所以链接器在链接可执行文件时,可以设定内存地址,而不用去管这些数据最终实际的内存地址,这是有独立内存空间的好处

2.当不同的进程使用同样的代码时,比如库文件中的代码,物理内存中可以只存储一份这样的代码,不同的进程只需要把自己的虚拟内存映射过去就可以了,节省内存

3.在程序需要分配连续的内存空间的时候,只需要在虚拟内存空间分配连续空间,而不需要实际物理内存的连续空间,可以利用碎片。

另外,事实上,在每个进程创建加载时,内核只是为进程“创建”了虚拟内存的布局,具体就是初始化进程控制表中内存相关的链表,实际上并不立即就把虚拟内存对应位置的程序数据和代码(比如.text .data段)拷贝到物理内存中,只是建立好虚拟内存和磁盘文件之间的映射就好(叫做存储器映射),等到运行到对应的程序时,才会通过缺页异常,来拷贝数据。还有进程运行过程中,要动态分配内存,比如malloc时,也只是分配了虚拟内存,即为这块虚拟内存对应的页表项做相应设置,当进程真正访问到此数据时,才引发缺页异常。

补充理解:

虚拟存储器涉及三个概念: 虚拟存储空间,磁盘空间,内存空间
在这里插入图片描述可以认为虚拟空间都被映射到了磁盘空间中,(事实上也是按需要映射到磁盘空间上,通过mmap),并且由页表记录映射位置,当访问到某个地址的时候,通过页表中的有效位,可以得知此数据是否在内存中,如果不是,则通过缺页异常,将磁盘对应的数据拷贝到内存中,如果没有空闲内存,则选择牺牲页面,替换其他页面。

mmap是用来建立从虚拟空间到磁盘空间的映射的,可以将一个虚拟空间地址映射到一个磁盘文件上,当不设置这个地址时,则由系统自动设置,函数返回对应的内存地址(虚拟地址),当访问这个地址的时候,就需要把磁盘上的内容拷贝到内存了,然后就可以读或者写,最后通过manmap可以将内存上的数据换回到磁盘,也就是解除虚拟空间和内存空间的映射,这也是一种读写磁盘文件的方法,也是一种进程共享数据的方法 共享内存
在这里插入图片描述
接下来我们来讨论下物理内存:

在内核态申请内存比在用户态申请内存要更为直接,它没有采用用户态那种延迟分配内存技术。内核认为一旦有内核函数申请内存,那么就必须立刻满足该申请内存的请求,并且这个请求一定是正确合理的。相反,对于用户态申请内存的请求,内核总是尽量延后分配物理内存,用户进程总是先获得一个虚拟内存区的使用权,最终通过缺页异常获得一块真正的物理内存。

1.物理内存的内核映射

IA32架构中内核虚拟地址空间只有1GB大小(从3GB到4GB),因此可以直接将1GB大小的物理内存(即常规内存)映射到内核地址空间,但超出1GB大小的物理内存(即高端内存)就不能映射到内核空间。为此,内核采取了下面的方法使得内核可以使用所有的物理内存。

1).高端内存不能全部映射到内核空间,也就是说这些物理内存没有对应的线性地址。不过,内核为每个物理页框都分配了对应的页框描述符,所有的页框描述符都保存在mem_map数组中,因此每个页框描述符的线性地址都是固定存在的。内核此时可以使用alloc_pages()和alloc_page()来分配高端内存,因为这些函数返回页框描述符的线性地址。

2).内核地址空间的后128MB专门用于映射高端内存,否则,没有线性地址的高端内存不能被内核所访问。这些高端内存的内核映射显然是暂时映射的,否则也只能映射128MB的高端内存。当内核需要访问高端内存时就临时在这个区域进行地址映射,使用完毕之后再用来进行其他高端内存的映射。

由于要进行高端内存的内核映射,因此直接能够映射的物理内存大小只有896MB,该值保存在high_memory中。内核地址空间的线性地址区间如下图所示:
在这里插入图片描述从图中可以看出,内核采用了三种机制将高端内存映射到内核空间:永久内核映射,固定映射和vmalloc机制。

2.物理内存管理机制

基于物理内存在内核空间中的映射原理,物理内存的管理方式也有所不同。内核中物理内存的管理机制主要有伙伴算法,slab高速缓存和vmalloc机制。其中伙伴算法和slab高速缓存都在物理内存映射区分配物理内存,而vmalloc机制则在高端内存映射区分配物理内存。

伙伴算法

伙伴算法负责大块连续物理内存的分配和释放,以页框为基本单位。该机制可以避免外部碎片。

per-CPU页框高速缓存

内核经常请求和释放单个页框,该缓存包含预先分配的页框,用于满足本地CPU发出的单一页框请求。

slab缓存

slab缓存负责小块物理内存的分配,并且它也作为高速缓存,主要针对内核中经常分配并释放的对象。

vmalloc机制

vmalloc机制使得内核通过连续的线性地址来访问非连续的物理页框,这样可以最大限度的使用高端物理内存。

3.物理内存的分配

内核发出内存申请的请求时,根据内核函数调用接口将启用不同的内存分配器。

3.1 分区页框分配器

分区页框分配器 (zoned page frame allocator) ,处理对连续页框的内存分配请求。分区页框管理器分为两大部分:前端的管理区分配器和伙伴系统,如下图:
在这里插入图片描述
管理区分配器负责搜索一个能满足请求页框块大小的管理区。在每个管理区中,具体的页框分配工作由伙伴系统负责。为了达到更好的系统性能,单个页框的申请工作直接通过per-CPU页框高速缓存完成。该分配器通过几个函数和宏来请求页框,它们之间的封装关系如下图所示。

在这里插入图片描述
这些函数和宏将核心的分配函数__alloc_pages_nodemask()封装,形成满足不同分配需求的分配函数。其中,alloc_pages()系列函数返回物理内存首页框描述符,__get_free_pages()系列函数返回内存的线性地址。 3.2 slab分配器

slab 分配器最初是为了解决物理内存的内部碎片而提出的,它将内核中常用的数据结构看做对象。slab分配器为每一种对象建立高速缓存。内核对该对象的分配和释放均是在这块高速缓存中操作。一种对象的slab分配器结构图如下:

在这里插入图片描述可以看到每种对象的高速缓存是由若干个slab组成,每个slab是由若干个页框组成的。虽然slab分配器可以分配比单个页框更小的内存块,但它所需的所有内存都是通过伙伴算法分配的。slab高速缓存分专用缓存和通用缓存。专用缓存是对特定的对象,比如为内存描述符创建高速缓存。通用缓存则是针对一般情况,适合分配任意大小的物理内存,其接口即为kmalloc()。 3.3 非连续内存区内存的分配

内核通过vmalloc()来申请非连续的物理内存,若申请成功,该函数返回连续内存区的起始地址,否则,返回NULL。vmalloc()和kmalloc()申请的内存有所不同,kmalloc()所申请内存的线性地址与物理地址都是连续的,而vmalloc()所申请的内存线性地址连续而物理地址则是离散的,两个地址之间通过内核页表进行映射。vmalloc()的工作方式理解起来很简单:

1).寻找一个新的连续线性地址空间;

2).依次分配一组非连续的页框;

3).为线性地址空间和非连续页框建立映射关系,即修改内核页表;

vmalloc()的内存分配原理与用户态的内存分配相似,都是通过连续的虚拟内存来访问离散的物理内存,并且虚拟地址和物理地址之间是通过页表进行连接的,通过这种方式可以有效的使用物理内存。但是应该注意的是,vmalloc()申请物理内存时是立即分配的,因为内核认为这种内存分配请求是正当而且紧急的;相反,用户态有内存请求时,内核总是尽可能的延后,毕竟用户态跟内核态不在一个特权级。

转载出处
Linux 虚拟内存和物理内存的理解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值