本文对 Linux 内存管理机制做一个简单的分析,试图让你快速理解 Linux 一些内存管理的概念并有效的利用一些管理方法。
NUMA
Linux 2.6 开始支援 NUMA(Non-Uniform Memory Access)内存管理模式。在多个 CPU 的系统中,内存按 CPU 划分为不同的 Node,每个 CPU 挂一个 Node,其访问本地 Node 比访问其他 CPU 上的 Node 速度要快很多。
通过 numactl -H 检视 NUMA 硬体资讯,可以看到 2 个 node 的大小和对应的 CPU 核,以及 CPU 访问 node 的 distances 。如下所示 CPU 访问远端 node 的 distances 是本地 node 的 2 倍多。
[root@localhost ~]# numactl -H
available: 2 nodes (0-1)
node 0 cpus: 0 1 2 3 4 5 6 7 16 17 18 19 20 21 22 23
node 0 size: 15870 MB
node 0 free: 13780 MB
node 1 cpus: 8 9 10 11 12 13 14 15 24 25 26 27 28 29 30 31
node 1 size: 16384 MB
node 1 free: 15542 MB
node distances:
node  0  1
0:  10  21
1:  21  10
通过 numastat 检视 NUMA 的统计资讯,包括内存分配的命中次数、未命中次数、本地分配次数和远端分配次数等。
[root@localhost ~]# numastat
node0          node1
numa_hit              2351854045      3021228076
numa_miss              22736854        2976885
numa_foreign            2976885        22736854
interleave_hit            14144          14100
local_node            2351844760      3021220020
other_node              22746139        2984941
Zone
Node 下面划分为一个或多个 Zone,为啥要有 Zone,两个原因:1.DMA 装置能够访问的内存范围有限(ISA 装置只能访问 16MB);2.x86-32bit 系统地址空间有限(32 位最多只能 4GB),为了使用更大内存,需要使用 HIGHMEM 机制。
ZONE_DMA
地址段最低的一块内存区域,用于 ISA(Industry Standard Architecture) 装置 DMA 访问。在 x86 架构下,该 Zone 大小限制为 16MB 。
ZONE_DMA32
该 Zone 用于支援 32-bits 地址汇流排的 DMA 装置,只在 64-bits 系统里才有效。
ZONE_NORMAL
该 Zone 的内存被核心直接对映为线性地址并可以直接使用。在 X86-32 架构下,该 Zone 对应的地址范围为 16MB~896MB 。在 X86-64 架构下,DMA 和 DMA32 之外的内存全部在 NORMAL 的 Zone 里管理。
ZONE_HIGHMEM
该 Zone 只在 32 位系统才有,通过建立临时页表的方式对映超过 896MB 的内存空间。即在需要访问的时候建立地址空间和内存的对映关系,在访问结束后拆掉对映关系释放地址空间,该地址空间可以用于其他 HIGHMEM 的内存对映。
通过/proc/zoneinfo 可以检视 Zone 相关的资讯。如下所示 X86-64 系统上两个 Node,Node0 上有 DMA 、 DMA32 和 Normal 三个 Zone,Node1 上只有一个 Normal Zone 。
[root@localhost ~]# cat /proc/zoneinfo |grep -E “zone| free|managed”
Node 0, zone      DMA
pages free    3700
managed  3975
Node 0, zone    DMA32
pages free    291250
managed  326897
Node 0, zone  Normal
pages free    3232166
managed  3604347
Node 1, zone  Normal
pages free    3980110
managed  4128056
Page
Page 是 Linux 底层内存管理的基本单位,大小为 4KB 。一个 Page 对映为一段连续的实体内存,内存的分配和释放都要以 Page 为单位进行。程序虚拟地址到实体地址的对映也是通过 Page Table 页表进行,页表的每一项记录一个 Page 的虚拟地址对应的实体地址。
TLB
内存访问时需要查询地址对应的 Page 结构,这个资料记录在页表里。所有对内存地址的访问都要先查询页表,因此页表的访问次数是频率最高的。为了提高对页表的访问速度,引入了 TLB(Translation Lookaside Buffer)机制,将访问较多页表 WordPress 加速缓存在 CPU 的 cache 里。因此 CPU 的效能统计里很重要的一项就是 L1/L2 cache 的 TLB miss 统计项。在内存较大的系统里,如 256GB 内存全量的页表项有 256GB/4KB=67108864 条,每个条目占用 16 位元组的话,需要 1GB,显然是 CPU cache 无法全量 WordPress 加速缓存的。这时候如果访问的内存范围较广很容易出现 TLB miss 导致访问延时的增加。
Hugepages
为了降低 TLB miss 的概率,Linux 引入了 Hugepages 机制,可以设定 Page 大小为 2MB 或者 1GB 。 2MB 的 Hugepages 机制下,同样 256GB 内存需要的页表项降低为 256GB/2MB=131072,仅需要 2MB 。因此 Hugepages 的页表可以全量 WordPress 加速缓存在 CPU cache 中。
通过 sysctl -w vm.nr_hugepages=1024 可以设定 hugepages 的个数为 1024,总大小为 4GB 。需要注意是,设定 huagepages 会从系统申请连续 2MB 的内存块并进行保留(不能用于正常内存申请),如果系统执行一段时间导致内存碎片较多时,再申请 hugepages 会失败。
如下所示为 hugepages 的设定和 mount 方法,mount 之后应用程式需要在 mount 路径下通过 mmap 进行档案对映来使用这些 hugepages 。
sysctl -w vm.nr_hugepages=1024
mkdir -p /mnt/hugepages
mount -t hugetlbfs hugetlbfs /mnt/hugepages
Buddy System
Linux Buddy System 是为了解决以 Page 为单位的内存分配导致外内存碎片问题:即系统缺少连续的 Page 页导致需要连续 Page 页的内存申请无法得到满足。原理很简单,将不同个数的连续 Pages 组合成 Block 进行分配,Block 按 2 的幂次方个 Pages 划分为 11 个 Block 连结串列,分别对应 1,2,4,8,16,32,64,128,256,512 和 1024 个连续的 Pages 。呼叫 Buddy System 进行内存分配时,根据申请的大小找最合适的 Block 。
如下所示为各个 Zone 上的 Buddy System 基本资讯,后面 11 列为 11 个 Block 连结串列里可用的 Block 个数。
[root@localhost ~]# cat /proc/buddyinfo
Node 0, zone      DMA      0      0      1      0      1      1      1      0      0      1      3
Node 0, zone    DMA32    102    79    179    229    230    166    251    168    107    78    169
Node 0, zone  Normal  1328    900  1985  1920  2261  1388    798    972    539    324  2578
Node 1, zone  Normal    466  1476  2133  7715  6026  4737  2883  1532    778    490  2760
Slab
Buddy System 的内存都是大块申请,但是大多数应用需要的内存都很小,比如常见的几百个 Bytes 的资料结构,如果也申请一个 Page,将会非常浪费。为了满足小而不规则的内存分配需求,Linux 设计了 Slab 分配器。原理简单说就是为特定的资料结构建立 memcache,从 Buddy System 里申请 Pages,将每个 Page 按资料结构的大小划分为多个 Objects,使用者从 memcache 里申请资料结构时分配一个 Object 。
如下所示为 Linux 检视 slab 资讯的方法:
[root@localhost ~]# cat /proc/slabinfo
slabinfo – version: 2.1
# name            : tunables : slabdata
fat_inode_cache      90    90    720  45    8 : tunables    0    0    0 : slabdata      2      2      0
fat_cache              0      0    40  102    1 : tunables    0    0    0 : slabdata      0      0      0
kvm_vcpu              0      0  16576    1    8 : tunables    0    0    0 : slabdata      0      0      0
kvm_mmu_page_header      0      0    168  48    2 : tunables    0    0    0 : slabdata      0      0      0
ext4_groupinfo_4k  4440  4440    136  30    1 : tunables    0    0    0 : slabdata    148    148      0
ext4_inode_cache  63816  65100  1032  31    8 : tunables    0    0    0 : slabdata  2100  2100      0
ext4_xattr          1012  1012    88  46    1 : tunables    0    0    0 : slabdata    22    22      0
ext4_free_data    16896  17600    64  64    1 : tunables    0    0    0 : slabdata    275    275      0
通常我们都是通过 slabtop 命令检视排序后的 slab 资讯:
OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME
352014 352014 100%    0.10K  9026  39    36104K buffer_head
93492  93435  99%    0.19K  2226  42    17808K dentry
65100  63816  98%    1.01K  2100  31    67200K ext4_inode_cache
48128  47638  98%    0.06K    752  64  3008K kmalloc-64
47090  43684  92%    0.05K    554  85  2216K shared_policy_node
44892  44892 100%    0.11K  1247  36  4988K sysfs_dir_cache
43624  43177  98%    0.07K    779  56  3116K Acpi-ParseExt
43146  42842  99%    0.04K    423  102  1692K ext4_extent_status
kmalloc
和 glibc 的 malloc() 一样,核心也提供 kmalloc() 用于分配任意大小的内存空间。同样,如果放任应用程式随意从 Page 里申请任意大小的内存也会导致 Page 内���内存碎片化。为了解决内部碎片问题,Linux 使用 Slab 机制来实现 kmalloc 内存分配。原理和 Buddy System 类似,即建立 2 的幂次方的 Slab 池用于 kmalloc 根据大小适配最佳的 Slab 进行分配。
如下所示为用于 kmalloc 分配的 Slabs:
[root@localhost ~]# cat /proc/slabinfo
slabinfo – version: 2.1
# name            : tunables : slabdata
kmalloc-8192        196    200  8192    4    8 : tunables    0    0    0 : slabdata    50    50      0
kmalloc-4096        1214  1288  4096    8    8 : tunables    0    0    0 : slabdata    161    161      0
kmalloc-2048        2861  2928  2048  16    8 : tunables    0    0    0 : slabdata    183    183      0
kmalloc-1024        7993  8320  1024  32    8 : tunables    0    0    0 : slabdata    260    260      0
kmalloc-512        6030  6144    512  32    4 : tunables    0    0    0 : slabdata    192    192      0
kmalloc-256        7813  8576    256  32    2 : tunables    0    0    0 : slabdata    268    268      0
kmalloc-192        15542  15750    192  42    2 : tunables    0    0    0 : slabdata    375    375      0
kmalloc-128        16814  16896    128  32    1 : tunables    0    0    0 : slabdata    528    528      0
kmalloc-96        17507  17934    96  42    1 : tunables    0    0    0 : slabdata    427    427      0
kmalloc-64        48590  48704    64  64    1 : tunables    0    0    0 : slabdata    761    761      0
kmalloc-32          7296  7296    32  128    1 : tunables    0    0    0 : slabdata    57    57      0
kmalloc-16        14336  14336    16  256    1 : tunables    0    0    0 : slabdata    56    56      0
kmalloc-8          21504  21504      8  512    1 : tunables    0    0    0 : slabdata    42    42      0
核心引数
Linux 提供了一些内存管理相关的核心引数,在/proc/sys/vm 目录下可以检视或者通过 sysctl -a |grep vm 检视:
[root@localhost vm]# sysctl -a |grep vm
vm.admin_reserve_kbytes = 8192
vm.block_dump = 0
vm.dirty_background_bytes = 0
vm.dirty_background_ratio = 10
vm.dirty_bytes = 0
vm.dirty_expire_centisecs = 3000
vm.dirty_ratio = 20
vm.dirty_writeback_centisecs = 500
vm.drop_caches = 1
vm.extfrag_threshold = 500
vm.hugepages_treat_as_movable = 0
vm.hugetlb_shm_group = 0
vm.laptop_mode = 0
vm.legacy_va_layout = 0
vm.lowmem_reserve_ratio = 256  256 32
vm.max_map_count = 65530
vm.memory_failure_early_kill = 0
vm.memory_failure_recovery = 1
vm.min_free_kbytes = 1024000
vm.min_slab_ratio = 1
vm.min_unmapped_ratio = 1
vm.mmap_min_addr = 4096
vm.nr_hugepages = 0
vm.nr_hugepages_mempolicy = 0
vm.nr_overcommit_hugepages = 0
vm.nr_pdflush_threads = 0
vm.numa_zonelist_order = default
vm.oom_dump_tasks = 1
vm.oom_kill_allocating_task = 0
vm.overcommit_kbytes = 0
vm.overcommit_memory = 0
vm.overcommit_ratio = 50
vm.page-cluster = 3
vm.panic_on_oom = 0
vm.percpu_pagelist_fraction = 0
vm.stat_interval = 1
vm.swappiness = 60
vm.user_reserve_kbytes = 131072
vm.vfs_cache_pressure = 100
vm.zone_reclaim_mode = 0
vm.drop_caches
vm.drop_caches 是最常用到的引数,因为 Linux 的 Page cache(档案系统 WordPress 加速缓存)机制会导致大量的内存被用于档案系统 WordPress 加速缓存,包括资料 WordPress 加速缓存和后设资料(dentry 、 inode)WordPress 加速缓存。当内存不足时,我们通过该引数可以快速释放档案系统 WordPress 加速缓存:
To free pagecache:
echo 1 > /proc/sys/vm/drop_caches
To free reclaimable slab objects (includes dentries and inodes):
echo 2 > /proc/sys/vm/drop_caches
To free slab objects and pagecache:
echo 3 > /proc/sys/vm/drop_caches
vm.min_free_kbytes
vm.min_free_kbytes 用于决定内存低于多少时启动内存回收机制(包括上面提到的档案系统 WordPress 加速缓存和下面会提到的可回收的 Slab),该值预设值较小,在内存较多的系统设定为一个较大的值(如 1GB)可以在内存还不会太少时自动触发内存回收。但也不能设定太大,导致频繁应用程式经常被 OOM killed 。
sysctl -w vm.min_free_kbytes=1024000
vm.min_slab_ratio
vm.min_slab_ratio 用于决定 Slab 池里可回收的 Slab 空间在该 Zone 里的占比达到多少时进行回收,预设是 5% 。但经过笔者试验,当内存充足时根本不会触发 Slab 回收,也只有在内存水位线达到上面 min_free_kbytes 时才会触发 Slab 回收。该值最小可以设定为 1%:
sysctl -w vm.min_slab_ratio=1
总结
以上简单描述了 Linux 内存管理机制和几个常用的内存管理核心引数。