首頁 > 軟體

Linux記憶體管理機制簡單分析

2020-06-16 16:51:05

本文對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)機制,將存取較多頁表快取在CPU的cache裡。因此CPU的效能統計裡很重要的一項就是L1/L2 cache的TLB miss統計項。在記憶體較大的系統裡,如256GB記憶體全量的頁表項有256GB/4KB=67108864條,每個條目佔用16位元組的話,需要1GB,顯然是CPU cache無法全量快取的。這時候如果存取的記憶體範圍較廣很容易出現TLB miss導致存取延時的增加。

Hugepages

為了降低TLB miss的概率,Linux引入了Hugepages機制,可以設定Page大小為2MB或者1GB。2MB的Hugepages機制下,同樣256GB記憶體需要的頁表項降低為256GB/2MB=131072,僅需要2MB。因此Hugepages的頁表可以全量快取在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            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
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            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
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(檔案系統快取)機制會導致大量的記憶體被用於檔案系統快取,包括資料快取和後設資料(dentry、inode)快取。當記憶體不足時,我們通過該引數可以快速釋放檔案系統快取:

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用於決定記憶體低於多少時啟動記憶體回收機制(包括上面提到的檔案系統快取和下面會提到的可回收的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記憶體管理機制和幾個常用的記憶體管理核心引數。

參考資料

Understanding The Linux Kernel 3rd Edition
[Linux Physical Memory Description]](http://www.ilinuxkernel.com/files/Linux_Physical_Memory_Description.pdf)

本文永久更新連結地址https://www.linuxidc.com/Linux/2018-05/152624.htm


IT145.com E-mail:sddin#qq.com