内存工做原理

Reference:https://time.geekbang.org/column/article/74272算法

 

内存

内存主要用来存储系统和应用程序的指令、数据、缓存等。缓存

 

内存映射

一般所说的内存容量,好比笔记本电脑的8GB内存,其实指的是物理内存。物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM)。只有内核才能够直接访问物理内存。ssh

Linux 内核给每一个进程都提供了一个独立的虚拟地址空间,而且这个地址空间是连续的。这样,进程就能够很方便地访问内存,更确切地说是访问虚拟内存。ide

虚拟地址空间的内部又被分为内核空间和用户空间两部分,不一样字长(也就是单个CPU指令能够处理数据的最大长度)的处理器,地址空间的范围也不一样。好比最多见的 32 位和 64 位系统,它们的虚拟地址空间,以下所示:函数

经过这里能够看出,32位系统的内核空间占用 1G,位于最高处,剩下的3G是用户空间。而 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。工具

进程在用户态时,只能访问用户空间内存;只有进入内核态后,才能够访问内核空间内存。虽然每一个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就能够很方便地访问内核空间内存。性能

既然每一个进程都有一个这么大的地址空间,那么全部进程的虚拟内存加起来,天然要比实际的物理内存大得多。因此,并非全部的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,而且分配后的物理内存,是经过内存映射来管理的。url

内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每一个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系,以下图所示:spa

页表实际上存储在 CPU 的内存管理单元 MMU中,这样,正常状况下,处理器就能够直接经过硬件,找出要访问的内存。
而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。.net

另外,TLB(Translation Lookaside Buffer,转译后备缓冲器)会影响 CPU 的内存访问性能,TLB 其实就是 MMU 中页表的高速缓存。因为进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,因此,经过减小进程的上下文切换,减小TLB的刷新次数,就能够提升TLB 缓存的使用率,进而提升CPU的内存访问性能。

不过要注意,MMU 并不以字节为单位来管理内存,而是规定了一个内存映射的最小单位,也就是页,一般是 4 KB大小。这样,每一次内存映射,都须要关联 4 KB 或者 4KB 整数倍的内存空间。

页的大小只有4 KB ,致使的另外一个问题就是,整个页表会变得很是大。比方说,仅 32 位系统就须要 100 多万个页表项(4GB/4KB),才能够实现整个地址空间的映射。为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。

多级页表就是把内存分红区块来管理,将原来的映射关系改为区块索引和区块内的偏移。因为虚拟内存空间一般只用了不多一部分,那么,多级页表就只保存这些使用中的区块,这样就能够大大地减小页表的项数。

Linux用的正是四级页表来管理内存页,以下图所示,虚拟地址被分为5个部分,前4个表项用于选择页,而最后一个索引表示页内偏移。

大页,就是比普通页更大的内存块,常见的大小有 2MB 和 1GB。大页一般用在使用大量内存的进程上,好比 Oracle、DPDK等。
经过这些机制,在页表的映射下,进程就能够经过虚拟地址来访问物理内存了。

 

虚拟内存空间分布

最上方的内核空间不用多讲,下方的用户空间内存,其实又被分红了多个不一样的段。以32 位系统为例,以下图:

经过这张图能够看到,用户空间内存,从低到高分别是五种不一样的内存段。

  1. 只读段,包括代码和常量等。
  2. 数据段,包括全局变量等。
  3. 堆,包括动态分配的内存,从低地址开始向上增加。
  4. 文件映射段,包括动态库、共享内存等,从高地址开始向下增加。
  5. 栈,包括局部变量和函数调用的上下文等。栈的大小是固定的,通常是 8 MB。

在这五个内存段中,堆和文件映射段的内存是动态分配的。好比说,使用 C 标准库的 malloc() 或者 mmap() ,就能够分别在堆和文件映射段动态分配内存。
其实64位系统的内存分布也相似,只不过内存空间要大得多。

 

内存分配与回收

malloc() 是 C 标准库提供的内存分配函数,对应到系统调用上,有两种实现方式,即 brk() 和 mmap()。

  • 对小块内存(小于128K),C 标准库使用 brk() 来分配,也就是经过移动堆顶的位置来分配内存。这些内存释放后并不会马上归还系统,而是被缓存起来,这样就能够重复使用。
  • 而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。

这两种方式,天然各有优缺点。

  • brk() 方式的缓存,能够减小缺页异常的发生,提升内存访问效率。不过,因为这些内存没有归还系统,在内存工做繁忙时,频繁的内存分配和释放会形成内存碎片。
  • mmap() 方式分配的内存,会在释放时直接归还系统,因此每次 mmap 都会发生缺页异常。在内存工做繁忙时,频繁的内存分配会致使大量的缺页异常,使内核的管理负担增大。这也是malloc 只对大块内存使用 mmap 的缘由。

了解这两种调用方式后,还须要清楚一点,那就是,当这两种调用发生后,其实并无真正分配内存。这些内存,都只在首次访问时才分配,也就是经过缺页异常进入内核中,再由内核来分配内存。

总体来讲,Linux 使用伙伴系统来管理内存分配。这些内存在MMU中以页为单位进行管理,伙伴系统也同样,以页为单位来管理内存,而且会经过相邻页的合并,减小内存碎片化(好比brk方式形成的内存碎片)。

在用户空间,malloc 经过 brk() 分配的内存,在释放时并不当即归还系统,而是缓存起来重复利用。
在内核空间,Linux 则经过 slab 分配器来管理小内存。能够把slab 当作构建在伙伴系统上的一个缓存,主要做用就是分配并释放内核中的小对象。

对内存来讲,若是只分配而不释放,就会形成内存泄漏,甚至会耗尽系统内存。因此,在应用程序用完内存后,还须要调用 free() 或 unmap(),来释放这些不用的内存。

固然,系统也不会任由某个进程用完全部内存。在发现内存紧张时,系统就会经过一系列机制来回收内存,好比下面这三种方式:

  • 回收缓存,好比使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;
  • 回收不常访问的内存,把不经常使用的内存经过交换分区直接写到磁盘中;
  • 杀死进程,内存紧张时系统还会经过 OOM(Out of Memory),直接杀掉占用大量内存的进程。

其中,第二种方式回收不常访问的内存时,会用到交换分区(如下简称 Swap)。Swap 其实就是把一块磁盘空间当成内存来用。它能够把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。
因此,能够发现,Swap 把系统的可用内存变大了。不过要注意,一般只在内存不足时,才会发生 Swap 交换。而且因为磁盘读写的速度远比内存慢,Swap 会致使严重的内存性能问题。

第三种方式提到的 OOM(Out of Memory),实际上是内核的一种保护机制。它监控进程的内存使用状况,而且使用 oom_score 为每一个进程的内存使用状况进行评分:

  • 一个进程消耗的内存越大,oom_score 就越大;
  • 一个进程运行占用的 CPU 越多,oom_score 就越小。

这样,进程的 oom_score 越大,表明消耗的内存越多,也就越容易被 OOM 杀死,从而能够更好保护系统。

固然,为了实际工做的须要,管理员能够经过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。
oom_adj 的范围是 [-17, 15],数值越大,表示进程越容易被 OOM 杀死;数值越小,表示进程越不容易被 OOM 杀死,其中 -17 表示禁止OOM。

好比用下面的命令,就能够把 sshd 进程的 oom_adj 调小为 -16,这样, sshd 进程就不容易被 OOM 杀死。

1 echo -16 > /proc/$(pidof sshd)/oom_adj

 

小结

  • 对普通进程来讲,它能看到的实际上是内核提供的虚拟内存,这些虚拟内存还须要经过页表,由系统映射为物理内存。
  • 当进程经过 malloc() 申请内存后,内存并不会当即分配,而是在首次访问时,才经过缺页异常陷入内核中分配内存。
  • 因为进程的虚拟地址空间比物理内存大不少,Linux 还提供了一系列的机制,应对内存不足的问题,好比缓存的回收、交换分区 Swap 以及OOM 等。
  • 当须要了解系统或者进程的内存使用状况时,能够用 free 和 top 、ps 等性能工具。它们都是分析性能问题时最经常使用的性能工具。
相关文章
相关标签/搜索