本文對 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 記憶體管理機制和幾個常用的記憶體管理核心引數。