# 内存

:Memory ,又称为内部存储器、主存储器、物理内存。

# 存储技术

# ROM

常见技术:

  • 只读存储器(Read Only Memory ,ROM)

    • 只能读取数据,不能写入数据。
    • 断电后数据不会丢失,因此能持久保存数据。
  • 可编程 ROM(Programmable ROM ,PROM)

    • 出厂时,存储的每个 bit 都为 1 ,用户可以擦除为 0 ,从而写入数据,但只能擦除一次。
  • 可擦除可编程 ROM(Erasable Programmable ROM ,EPROM)

    • 通过照射紫外线来擦除,可以多次擦除。
  • 电可擦除可编程 ROM(EEPROM)

    • 可以用电信号多次擦除,但是以 Byte 为单位擦除,效率低。
  • 闪存(FLash Memory)

    • 属于改进型的 EEPROM ,基于浮栅晶体管存储电荷。
    • 主要分类:
      • NOR
        • :基于与或门(NOT-OR)。
        • 容量小,一般为几十 MB 。擦写速度慢,支持随机访问。
        • 常用于 BIOS 等存储数据少的嵌入式设备。
      • NAND
        • :基于与非门(NOT-AND)。
        • 容量大。擦写速度快,以 Block 为单位访问。
        • 常用于 U 盘、SD 卡、SSD 硬盘。
    • 传统的嵌入式设备(比如 BIOS )一般采用 EPROM 存储数据,目前改用 Flash 。

# RAM

:随机存储器(Random Access Memory),泛指可以在任意位置读、写数据的存储器。

常见技术:

  • 动态 RAM(DRAM)

    • :基于电容存储电荷,用电容的充、放电之后的电压高低表示二进制的 1、0 。不过电容的电荷会缓慢耗散,需要定期刷新电容。
    • 结构简单,容易做到高密度、大存储容量。
    • 一个内存条包含几个内存芯片以及外围电路,每个内存芯片中都集成了大量的内存颗粒。
    • 常用于制作计算机的内存,已演变出多代技术:
      • SDRAM :同步 DRAM ,时钟频率与 CPU 一致。
      • DDR SDRAM :提高时钟频率、读写速度。
      • DDR2 SDRAM :读写速度大约为 5 GB/s 。
      • DDR3 SDRAM :读写速度大约为 10 GB/s 。
      • DDR4 SDRAM :读写速度大约为 30 GB/s 。
  • 静态 RAM(SRAM)

    • :用两个 CMOS 晶体管组成非门,在保持供电的情况下会保持电信号。
    • SRAM 比 DRAM 的成本更高,但读写速度快很多。
    • 常用于制作 CPU Cache 。
  • 非易失性 RAM(Non-Volatile ,NVRAM)

    • :基于浮栅晶体管存储电荷,即使断电也可以持久保持电信号。
    • 非易失性是指断电后数据不会丢失,而 DRAM、SRAM 都是易失性,需要保持供电。
  • ECC(Error Checking and Correcting)

    • :一种数据纠错技术。是在内存条中增加一颗 ECC 芯片,用于检查数据并纠正错误的二进制位,从而降低出错率,比如抵抗电磁干扰。
    • ECC 内存比普通内存贵了 1 倍多,常用于服务器,而家用电脑一般不兼容。

# 内存管理

# Page

  • 内核基于 buddy system 以 page 为单位管理内存,单个 page 大小默认为 4 KB 。

  • 内核为每个 page 维护了一个 struct page 结构体。

    • 相关代码:
      struct page {
          unsigned long flags;
          union {
            struct address_space *mapping;    // page 指向的物理内存首地址
            void *s_mem;
            atomic_t compound_mapcount;
          };
          ...
      }
      
    • 一个 struct page 结构体占用 64 bytes 内存。
      • 假设主机总物理内存为 M ,则有 M/4KB 个 page 。保存 struct page 结构体需要 (M/4KB)*64B ,占总内存的 1.56% 。
    • page 常见的 flags :
      PG_locked       # page 是否被锁定。一个进程可以申请锁定某个 page ,以免同时有其它进程读写 page
      PG_dirty        # page 是否被修改过。脏页需要同步到磁盘
      PG_lru          # page 是否位于 lru 列表。lru 列表分为 active、inactive 两个
      PG_active       # page 是否位于 active lru 列表
      PG_referenced   # page 是否最近被访问过。 lru 算法会优先释放 inactive 且 unreferenced 的 page
      PG_workingset   # page 是否用于某个进程的工作集
      PG_slab         # page 是否用于 slab
      PG_error        # page 是否损坏,不能访问
      
    • _refcount :page 的引用计数,表示该 page 被引用的次数。
      • 如果等于 0 ,则视作空闲内存。
    • _mapcount :page 的映射计数,表示该 page 被多少个进程的 Page Table 映射了。
      • 如果等于 -1 ,则表示没有映射。
      • 如果等于 0 ,则表示只有一个进程映射,是私有内存(private)。
      • 如果大于 0 ,则表示有多个进程映射,是共享内存(share)。
  • Huge Page :指大于 4 KB 的内存页,比如 2MB、1GB 。

    • 优点:
      • 减少进程存储数据时使用的 Page 数,从而减少查询 Page Table 的次数和耗时。
      • 支持锁定 Page ,禁止交换到 Swap 分区。

# Page Cache

:简称为 Cache ,是在内存中选出一组地址不连续的 page ,用于内核读写磁盘时的缓存、缓冲区。

  • 注意与 CPU Cache 区分。

  • 功能:缓存从磁盘读取到内存的数据。

    • 优点:
      • 减少重复读取的耗时。
  • 功能:缓冲从内存写入磁盘的数据。

    • 优点:
      • 累计一定数据再写入磁盘,提高写磁盘的效率。
      • 让内存与磁盘异步工作,减少等待磁盘 IO 的耗时。
    • 缺点:
      • 进程读取磁盘时,所有数据都要先被内核拷贝到 Page Cache ,再拷贝到进程内存空间。写磁盘的流程反之。
        • 因此与不用 Cache 相比,耗时更久,占用内存更多(不过 Cache 会自动释放)。
      • 写数据时,需要保证将脏页同步到磁盘。
  • 修改缓存中的数据时,需要保证缓存与磁盘的数据一致性,主要有三种策略:

    • 不缓存(no-cache)
      • :删除缓存中的原数据,直接将数据写入磁盘。
    • 写穿透缓存(write through cache)
      • :修改缓存中的数据,并立即同步到磁盘。
    • 写回(write back)
      • :修改缓存中的数据,并将被修改的页面标记为脏页(dirty page)。由内核定期将脏页同步到磁盘。
      • 如果从磁盘读取一段数据到内存中,修改内存中的数据之后没有同步写入磁盘,则称为脏页(dirty page)。
      • 与写透缓存相比,减少了磁盘 IO 次数,效率更高。
        • 但实现难度更大。比如主机突然宕机,脏页可能来不及同步到磁盘,导致数据丢失。
      • Linux 的 Cache 默认采用该策略。
  • Buffer Cache

    • Linux v2.4 以前,Page Cache 用于读写文件时,Buffer Cache 用于读写磁盘时。
    • Linux v2.4 开始,Buffer Cache 与 Page Cache 合并,成为后者的一部分。
  • 系统运行一段时间之后,Cache 会越来越大,而 free 内存越来越少。

    • 此时提高了 RAM 内存的利用率,并不需要主动清理 Cache 。
    • 可以执行以下命令,主动让内核清理一次缓存,不过之后内核依然会重新建立缓存:
      echo 1 > /proc/sys/vm/drop_caches   # 清除 Page Cache
      echo 2 > /proc/sys/vm/drop_caches   # 清除 Reclaimable slab ,包括 dentries、inodes
      echo 3 > /proc/sys/vm/drop_caches   # 清除 Page Cache + Reclaimable slab
      

# 虚拟内存空间

  • Linux 中,每个进程都运行在一个独立的虚拟内存空间中,就像系统中只有它一个进程。

    • 注意它属于一个逻辑概念,并没有实际占用物理内存。与 Windows 的虚拟内存不同,后者类似于 Linux 的 Swap 分区。
  • 例如:在一个 32 位、4G 内存的系统中,每个进程独享一个 4G 的虚拟内存空间。

    • 高地址的 1G 空间为内核内存空间,只能被内核态进程访问。
    • 低地址的 3G 空间为用户内存空间,可以被用户态进程访问。
      • 用户空间又分为五个部分:文件映射区、数据区(存储全局变量等)、只读区(存储代码、常量等)、堆区、栈区。
      • 不同进程之间访问不到对方的用户内存空间,只能通过内核内存空间进行通信。

# MMU

:Linux 内核中的内存管理单元(Memory Management Unit)。

  • 当物理内存的 page 数不足时,MMU 可能采取三种措施:
    • 回收最近不用的 page 。
    • 将不常用的内存数据从 page 移到 swap 分区。
    • 通过 OOM 杀死某些占用大量内存的进程。
  • MMU 为每个进程维护了一张页表(Page Table),将进程的虚拟内存空间中被使用的一些 pages ,映射到物理内存的一些 pages 。
    • 当进程调用 malloc() 申请分配一块内存时,内核并不会立即分配物理内存,而是先分配一块虚拟内存 pages ,返回内存指针。
      • 等进程首次访问该虚拟内存 pages 时,CPU 会找不到对应的物理内存而报出缺页异常。此时 MMU 才修改 Page Table ,将物理内存 pages 映射到虚拟内存 pages 。
    • 多个进程可以同时映射物理内存中的同一个 page ,即共享内存,采用写时复制(Copy On Write ,COW)策略:
      • 如果进程只是读取,则直接访问该 page 。
      • 如果进程需要写入,则将该 page 拷贝一份,供该进程单独访问。
  • 当进程想读写某块虚拟内存时,CPU 会先获取其虚拟内存 page 地址,然后到 Page Table 中找到对应的物理内存 page 地址,从而访问该 page 。
    • 如果 CPU 没有找到对应的物理内存 pages,则抛出缺页异常(page fault),并调用缺页异常处理程序。
    • 缺页异常分为多种:
      • 主缺页异常(major):page 不在内存中,需要从磁盘载入。
        • 需要等待读取磁盘,耗时比次缺页异常久。
      • 次缺页异常(minor):page 在内存中,但是没有分配给当前进程。
        • 例如当前进程需要读取一个共享库文件,发现其它进程已经将该库载入内存。
      • segment fault :进程要访问的虚拟内存地址超出了它的虚拟内存空间,属于越界访问。

# OOM

:Out of Memory ,Linux 内核提供的一种服务,用于在系统内存不足时自动杀死某些进程,通过发送 SIGKILL 信号。

  • OOM 会给每个进程评一个 oom_score 分数,取值范围为 0~1000 ,表示杀死该进程的可能性。
    • oom_score 的取值等于以下两项之和:
      • 系统评分:主要与进程占用的内存量呈正比。
      • 用户评分:称为 oom_score_adj ,取值范围为 -1000~1000 ,默认为 0 。
        • 用户可以将某些进程的 oom_score_adj 设置为负数,从而降低其 oom_score ,但最低为 0 。
  • 例:调整 oom_score
    [[email protected] ~]# echo 10 > /proc/self/oom_score_adj
    [[email protected] ~]# cat /proc/self/oom_score   /proc/self/oom_score_adj
    10
    10
    [[email protected] ~]# echo -10 > /proc/self/oom_score_adj
    [[email protected] ~]# cat /proc/self/oom_score   /proc/self/oom_score_adj
    0
    -10
    
  • 例:查看 OOM 日志
    [[email protected] ~]# grep -i 'out of memory' /var/log/messages
    Jan 17 19:48:48 CentOS kernel: Out of memory: Kill process 8120 (java) score 313 or sacrifice child
    Jan 19 21:10:21 CentOS kernel: Out of memory: Kill process 20607 (java) score 372 or sacrifice child
    Jan 24 05:11:08 CentOS kernel: Memory cgroup out of memory: Kill process 15230 (run.py) score 251 or sacrifice child
    
    • Memory cgroup 是 Docker 容器的 OOM 日志。

# 内存碎片

  • 内存碎片(memory fragmentation):指一些无法被使用的空闲内存。分为两类:

    • 内部碎片
      • :已分配给进程的内存碎片。
      • buddy system 以 page 为单位管理内存,如果进程申请的内存空间不是 page 的整数倍,则分配的最后一个 page 不会被完全使用。
      • 如果进程需要申请大量小于 page 的内存,比如 bool、int ,可以事先申请一大块内存作为内存池,避免产生内部碎片。
      • 如果进程不停产生内部碎片,则会占用越来越多 RSS 内存,看起来像内存泄漏。
    • 外部碎片
      • :没有分配给进程的内存碎片。
      • buddy system 以链表结构管理内存,要求每块内存空间的地址连续。多次分配、释放内存之后,空闲内存可能比较离散,找不到一块足够大、地址连续的空闲内存分配给进程。
      • 进程内存被页表管理,可以将物理内存的分散地址映射到虚拟内存空间的连续地址,因此不用担心外部碎片,除非申请分配 Huge Page 。
      • 当外部碎片导致不能分配内存时,内核会自动压缩内存,将空闲内存移动到连续的地址空间。用户也可以手动触发:
        echo 3 > /proc/sys/vm/drop_caches     # 删除缓存,有助于减少碎片
        echo 1 > /proc/sys/vm/compact_memory  # 内存压缩,需要几秒耗时
        
  • 例:查看本机空闲的内存块数量

    [[email protected] ~]# cat /proc/buddyinfo
    Node 0, zone      DMA      1      0      1      0      2      1      1      0      1      1      3
    Node 0, zone    DMA32    372    808    446    358    113    170     54     12      7      0      0
    Node 0, zone   Normal    286    123    324    348    137     94     16     20     79      0      0
    
    • Node 0 表示 MUMA 架构的节点编号,每个节点中划分多种内存区域 zone :
      • DMA :直接内存访问(Direct Memory Access)。
      • DMA32 :用于在 64bits 系统中,按 32bits 模式访问内存。
      • Normal :普通内存。
    • 右侧有 11 列数字,从 0 开始编号,第 n 列数字表示体积为 2^n 个 page 的空闲内存块的数量。如果最右端的 0 较多,则说明大内存块不足。
  • 为了减少内部碎片,Linux 采用了 Slab 分配器:为一些经常分配的、占内存小的对象专门划分内存空间,管理单位为 Bytes 。

    • 例如:进程描述符需要经常创建,因此事先为该类对象划分一组内存空间,称为一个 Slab ,存储时占用一个或多个 Page 。
    • 每种对象需要定义一个结构体 struct kmem_cache ,用于创建 Slab 、划分内存空间。
    • Slab 分为两种:
      • Unreclaim :不可回收的。
      • Reclaimable :可回收的。

# 内存分配器

  • 内存分配器(memory allocator):提供 malloc()、free() 等 API 供用户调用,并与内核交互,控制底层的内存分配。
  • 常见的几种内存分配器:
    • ptmalloc :由 Glibc 库提供,默认采用。只有一个内存分配区,多线程时需要频繁加锁,占用较多 CPU 。
    • jemalloc :由 FreeBSD libc 库提供。根据 CPU 核数划分多个内存分配区,大幅减少多线程时的加锁。
    • tcmalloc :由 Google 开发。
  • 进程调用 malloc() 申请分配内存时,有两种实现方式:
    • 调用 brk() ,改变数据段栈顶指针的位置,从而增加数据段的长度。
      • 调用 free() 时不会立即释放内存,而是继续被当前进程占用,方便以后分配,因此下次申请内存时不会引发缺页异常。
        • 用 brk() 申请内存一段时间之后,该内存通常不再位于栈顶,不能通过移动栈顶指针来释放。
      • 适合分配小块内存,但可能产生进程内部的内存碎片。
    • 调用 mmap() ,映射一块内存区域。
      • 调用 free() 时会立即释放内存,因此每次申请内存都会引发缺页异常。
      • 适合分配大块内存。
      • 默认当 malloc() 申请的内存小于 MMAP_THRESHOLD=128KB 时,采用 brk() 方式,否则采用 mmap() 方式。

# 内存开销

  • 计算机的物理内存,一般少部分被内核占用,大部分被进程占用。

# 内核内存

  • 内核占用的内存举例:

    Kernel Modules    # 内核模块,可用 lsmod 命令查看
    Page Cache        # 用于内核读写磁盘时的缓存、缓冲区。通常体积较大,但是当主机 free 内存紧张时会被自动回收
    Slab
    Page Tables       # MMU 为每个进程维护了一张页表
    Kernel Stack      # 内核给每个线程分配了一个栈区,每个大小默认为 16KB
    Sockets           # 每个 Socket 大约占用 3 KB 内存,对应的 IO 缓冲区最多占用 200 KB 内存
    
  • 通过 /proc/meminfo 文件可以获取系统内存的使用情况。

    • 但它不会统计以下内存:
      • Socket 内存
      • 通过 alloc_pages() 分配的内存

# 进程内存

  • Linux 会准确记录每个进程占用的 CPU 时长,但统计一个进程占用的内存比较麻烦、有误差,常见的算法有 RSS、WSS 。

    • 内核会记录每个内存 Page 是否被进程占用,但不知道被哪个进程占用。
  • 常驻集(Resident Set Size ,RSS)

    • :进程长期驻留的内存,是指进程的 Page Table 中,引发过缺页异常的 Pages 数。
    • 假设进程申请了 100M 内存,实际只使用了 10M 。则虚拟内存(Virtual Memory ,VIRT)为 100M , RSS 为 10M 。
      • 进程释放一些已用内存时, RSS 不会减少,因此会比实际占用内存虚高。
    • RSS = SHR + 进程独自占用的非共享内存。
      • RSS 包括堆、栈、共享内存,不包括 Swap ,也不包括 page tables、huge page、kernel stack、struct thread_info、struct task_struct 等。
      • 用如下命令可以统计所有进程的 RSS 内存之和,但这样会重复累计 SHR 内存,因此比所有进程实际占用的内存量偏大。应该统计 PSS 内存之和。
        ps -eo rss | awk 'NR>1' | awk '{sum+=$1} END {print sum/1024}'
        
  • SHR

    • :Shared Memory ,进程使用的共享内存。
    • 比如多个进程可能导入同一个共享库 glibc 。
  • 比例集(Proportional Set Size,PSS)

    • :按比例估算出进程的常驻内存。
    • PSS = 进程独占的非共享内存 + 进程平均占用的 SHR
    • 例如:进程 A 独自占用的非共享内存为 8M ,与其它 N 个进程共享 2M 的共享内存,则进程 A 的 PSS = 9 + 2/(N+1) MB 。
  • 工作集(Working Set Size ,WSS)

    • :进程保持工作所需的内存,是估算进程最近访问过的 Pages 数,包括物理内存、内核内存、脏页。
    • 一个进程的 RSS 大于等于 WSS ,因为 RSS 可能虚高。

# 相关命令

# free

$ free            # 显示主机内存的使用情况
      -k          # 采用 KB 作为显示单位(默认)
      -m          # 采用 MB 作为显示单位
      -h          # --human ,自动调整显示单位
      -w          # 拉宽显示,将 buffers 与 cache 列分别显示
      -s 1 -c 10  # 每隔一秒显示一次,最多显示 10 次
  • 例:
    [[email protected] ~]# free -wh
                  total        used        free      shared  buff/cache   available
    Mem:           7.6G        5.9G        187M        496K        1.6G        673M
    Swap:          4.0G        1.8G        2.2G
    
    • total :内存的总可用量。
    • used :已被占用的内存。
      • free 命令会先从 /proc/meminfo 读取 MemTotal、MemFree、Buffers、Cached、Slab 信息,然后按 used = total - free - buffer - cache 计算出已用内存。
    • free :空闲内存。表示既没有被进程使用,也没有被划分为缓存的内存。
    • shared :共享内存,属于 used 内存。
    • buff/cache :指 Page Cache + Slab 。
    • available :可用内存,等于 free + buff/cache 内存。该值是估算的,并不准确。
      • free 内存较少并不代表内存紧张,因为内核会自动回收 buff/cache 内存,分配给进程使用,成为 used 内存。
      • 当 available 内存较少、甚至 Swap 分区被使用时,才说明内存紧张,需要增加主机的物理内存。
  • /proc/meminfo 记录的 MemTotal 表示内存的总可用量,比实际容量大概少 2% ,因为要减去 absent、reserved 内存。
    • 例:查看内存设备
      [r[email protected] ~]# lsmem
      ...
      Memory block size:       128M
      Total online memory:       8G
      Total offline memory:      0B
      
    • 例:查看内核保留的内存
      [[email protected] ~]# dmesg | grep reserved
      ...
      [    0.000000] Memory: 4972572k/9437184k available (7788k kernel code, 1049104k absent, 402384k reserved, 5954k data, 1984k init)
      
      • absent :不受内核管理的内存,比如被 BIOS 占用。
      • reserved :内核 Boot 阶段会保留一些内存,不会分配出去。
        • 这包括 kernel code、data、init 占用的内存。
        • 内核 Boot 完成之后,会释放少量 reserved 内存。

# sync

$ sync            # 将脏页立即同步到磁盘

# pmap

$ pmap  <pid>     # 显示一个进程的虚拟内存空间的内存映射表
        -x        # 增加显示 RSS、Dirty Page 列
  • 例:
    [[email protected] ~]# pmap 1 -x
    1:   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
    Address           Kbytes     RSS   Dirty    Mode    Mapping
    0000557a0dbf2000    1424    1208       0    r-x--   systemd
    0000557a0df56000     140     132     132    r----   systemd
    0000557a0df79000       4       4       4    rw---   systemd
    0000557a0f80b000    1224    1124    1124    rw---     [ anon ]
    00007f1878000000     164      12      12    rw---     [ anon ]
    00007f1878029000   65372       0       0    -----     [ anon ]
    
    表中每行描述一块内存空间,大小不一。
    • Address :表示该内存空间的首地址。
    • Mapping :表示该内存空间的用途。
      • 取值为 systemd 表示映射了一个文件。
      • 取值为 [ anon ] 表示程序申请分配的内存。
      • 取值为 [ stack ] 表示堆栈。