/proc/meminfo之謎

/proc/meminfo是瞭解Linux系統內存使用狀況的主要接口,我們最常用的”free”、”vmstat”等命令就是通過它獲取數據的 ,/proc/meminfo所包含的信息比”free”等命令要豐富得多,然而真正理解它並不容易,比如我們知道”Cached”統計的是文件緩存頁,manpage上說是“In-memory  cache  for  files read from the disk (the page cache)”,那爲什麼它不等於[Active(file)+Inactive(file)]?AnonHugePages與AnonPages、HugePages_Total有什麼聯繫和區別?很多細節在手冊中並沒有講清楚,本文對此做了一點探究。

負責輸出/proc/meminfo的源代碼是:
fs/proc/meminfo.c : meminfo_proc_show()

MemTotal: 3809036 kB MemFree: 282012 kB MemAvailable: 865620 kB Buffers: 0 kB Cached: 854972 kB SwapCached: 130900 kB Active: 1308168 kB Inactive: 1758160 kB Active(anon): 1010416 kB Inactive(anon): 1370480 kB Active(file): 297752 kB Inactive(file): 387680 kB Unevictable: 0 kB Mlocked: 0 kB SwapTotal: 4063228 kB SwapFree: 3357108 kB Dirty: 0 kB Writeback: 0 kB AnonPages: 2104412 kB Mapped: 40988 kB Shmem: 169540 kB Slab: 225420 kB SReclaimable: 134220 kB SUnreclaim: 91200 kB KernelStack: 5936 kB PageTables: 35628 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 5967744 kB Committed_AS: 5626436 kB VmallocTotal: 34359738367 kB VmallocUsed: 351900 kB VmallocChunk: 34359363652 kB HardwareCorrupted: 0 kB AnonHugePages: 139264 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB DirectMap4k: 204484 kB DirectMap2M: 3915776 kB

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

MemTotal:        3809036 kB

MemFree:          282012 kB

MemAvailable:     865620 kB

Buffers:               0 kB

Cached:           854972 kB

SwapCached:       130900 kB

Active:          1308168 kB

Inactive:        1758160 kB

Active(anon):    1010416 kB

Inactive(anon):  1370480 kB

Active(file):     297752 kB

Inactive(file):   387680 kB

Unevictable:           0 kB

Mlocked:               0 kB

SwapTotal:       4063228 kB

SwapFree:        3357108 kB

Dirty:                 0 kB

Writeback:             0 kB

AnonPages:       2104412 kB

Mapped:            40988 kB

Shmem:            169540 kB

Slab:             225420 kB

SReclaimable:     134220 kB

SUnreclaim:        91200 kB

KernelStack:        5936 kB

PageTables:        35628 kB

NFS_Unstable:          0 kB

Bounce:                0 kB

WritebackTmp:          0 kB

CommitLimit:     5967744 kB

Committed_AS:    5626436 kB

VmallocTotal:   34359738367 kB

VmallocUsed:      351900 kB

VmallocChunk:   34359363652 kB

HardwareCorrupted:     0 kB

AnonHugePages:    139264 kB

HugePages_Total:       0

HugePages_Free:        0

HugePages_Rsvd:        0

HugePages_Surp:        0

Hugepagesize:       2048 kB

DirectMap4k:      204484 kB

DirectMap2M:     3915776 kB

 

MemTotal

系統從加電開始到引導完成,firmware/BIOS要保留一些內存,kernel本身要佔用一些內存,最後剩下可供kernel支配的內存就是MemTotal。這個值在系統運行期間一般是固定不變的。可參閱解讀DMESG中的內存初始化信息

MemFree

表示系統尚未使用的內存。[MemTotal-MemFree]就是已被用掉的內存。

MemAvailable

有些應用程序會根據系統的可用內存大小自動調整內存申請的多少,所以需要一個記錄當前可用內存數量的統計值,MemFree並不適用,因爲MemFree不能代表全部可用的內存,系統中有些內存雖然已被使用但是可以回收的,比如cache/buffer、slab都有一部分可以回收,所以這部分可回收的內存加上MemFree纔是系統可用的內存,即MemAvailable。/proc/meminfo中的MemAvailable是內核使用特定的算法估算出來的,要注意這是一個估計值,並不精確。

內存黑洞

追蹤Linux系統的內存使用一直是個難題,很多人試着把能想到的各種內存消耗都加在一起,kernel text、kernel modules、buffer、cache、slab、page table、process RSS…等等,卻總是與物理內存的大小對不上,這是爲什麼呢?因爲Linux kernel並沒有滴水不漏地統計所有的內存分配,kernel動態分配的內存中就有一部分沒有計入/proc/meminfo中。

我們知道,Kernel的動態內存分配通過以下幾種接口:

  • alloc_pages/__get_free_page: 以頁爲單位分配
  • vmalloc: 以字節爲單位分配虛擬地址連續的內存塊
  • slab allocator
    • kmalloc: 以字節爲單位分配物理地址連續的內存塊,它是以slab爲基礎的,使用slab層的general caches — 大小爲2^n,名稱是kmalloc-32、kmalloc-64等(在老kernel上的名稱是size-32、size-64等)。

通過slab層分配的內存會被精確統計,可以參見/proc/meminfo中的slab/SReclaimable/SUnreclaim;

通過vmalloc分配的內存也有統計,參見/proc/meminfo中的VmallocUsed 和 /proc/vmallocinfo(下節中還有詳述);

而通過alloc_pages分配的內存不會自動統計,除非調用alloc_pages的內核模塊或驅動程序主動進行統計,否則我們只能看到free memory減少了,但從/proc/meminfo中看不出它們具體用到哪裏去了。比如在VMware guest上有一個常見問題,就是VMWare ESX宿主機會通過guest上的Balloon driver(vmware_balloon module)佔用guest的內存,有時佔用得太多會導致guest無內存可用,這時去檢查guest的/proc/meminfo只看見MemFree很少、但看不出內存的去向,原因就是Balloon driver通過alloc_pages分配內存,沒有在/proc/meminfo中留下統計值,所以很難追蹤。

內存都到哪裏去了?

使用內存的,不是kernel就是用戶進程,下面我們就分類討論。

注:page cache比較特殊,很難區分是屬於kernel還是屬於進程,其中被進程mmap的頁面自然是屬於進程的了,而另一些頁面沒有被mapped到任何進程,那就只能算是屬於kernel了。

1. 內核

內核所用內存的靜態部分,比如內核代碼、頁描述符等數據在引導階段就分配掉了,並不計入MemTotal裏,而是算作Reserved(在dmesg中能看到)。而內核所用內存的動態部分,是通過上文提到的幾個接口申請的,其中通過alloc_pages申請的內存有可能未納入統計,就像黑洞一樣。

下面討論的都是/proc/meminfo中所統計的部分。

1.1 SLAB

通過slab分配的內存被統計在以下三個值中:

  • SReclaimable: slab中可回收的部分。調用kmem_getpages()時加上SLAB_RECLAIM_ACCOUNT標記,表明是可回收的,計入SReclaimable,否則計入SUnreclaim。
  • SUnreclaim: slab中不可回收的部分。
  • Slab: slab中所有的內存,等於以上兩者之和。

1.2 VmallocUsed

通過vmalloc分配的內存都統計在/proc/meminfo的 VmallocUsed 值中,但是要注意這個值不止包括了分配的物理內存,還統計了VM_IOREMAP、VM_MAP等操作的值,譬如VM_IOREMAP是把IO地址映射到內核空間、並未消耗物理內存,所以我們要把它們排除在外。從物理內存分配的角度,我們只關心VM_ALLOC操作,這可以從/proc/vmallocinfo中的vmalloc記錄看到:

# grep vmalloc /proc/vmallocinfo ... 0xffffc90004702000-0xffffc9000470b000 36864 alloc_large_system_hash+0x171/0x239 pages=8 vmalloc N0=8 0xffffc9000470b000-0xffffc90004710000 20480 agp_add_bridge+0x2aa/0x440 pages=4 vmalloc N0=4 0xffffc90004710000-0xffffc90004731000 135168 raw_init+0x41/0x141 pages=32 vmalloc N0=32 0xffffc90004736000-0xffffc9000473f000 36864 drm_ht_create+0x55/0x80 [drm] pages=8 vmalloc N0=8 0xffffc90004744000-0xffffc90004746000 8192 dm_table_create+0x9e/0x130 [dm_mod] pages=1 vmalloc N0=1 0xffffc90004746000-0xffffc90004748000 8192 dm_table_create+0x9e/0x130 [dm_mod] pages=1 vmalloc N0=1 ...

1

2

3

4

5

6

7

8

9

# grep vmalloc /proc/vmallocinfo

...

0xffffc90004702000-0xffffc9000470b000   36864 alloc_large_system_hash+0x171/0x239 pages=8 vmalloc N0=8

0xffffc9000470b000-0xffffc90004710000   20480 agp_add_bridge+0x2aa/0x440 pages=4 vmalloc N0=4

0xffffc90004710000-0xffffc90004731000  135168 raw_init+0x41/0x141 pages=32 vmalloc N0=32

0xffffc90004736000-0xffffc9000473f000   36864 drm_ht_create+0x55/0x80 [drm] pages=8 vmalloc N0=8

0xffffc90004744000-0xffffc90004746000    8192 dm_table_create+0x9e/0x130 [dm_mod] pages=1 vmalloc N0=1

0xffffc90004746000-0xffffc90004748000    8192 dm_table_create+0x9e/0x130 [dm_mod] pages=1 vmalloc N0=1

...

注:/proc/vmallocinfo中能看到vmalloc來自哪個調用者(caller),那是vmalloc()記錄下來的,相應的源代碼可見:
mm/vmalloc.c: vmalloc > __vmalloc_node_flags > __vmalloc_node > __vmalloc_node_range > __get_vm_area_node > setup_vmalloc_vm

通過vmalloc分配了多少內存,可以統計/proc/vmallocinfo中的vmalloc記錄,例如:

# grep vmalloc /proc/vmallocinfo | awk '{total+=$2}; END {print total}' 23375872

1

2

# grep vmalloc /proc/vmallocinfo | awk '{total+=$2}; END {print total}'

23375872

一些driver以及網絡模塊和文件系統模塊可能會調用vmalloc,加載內核模塊(kernel module)時也會用到,可參見 kernel/module.c。

1.3 kernel modules (內核模塊)

系統已經加載的內核模塊可以用 lsmod 命令查看,注意第二列就是內核模塊所佔內存的大小,通過它可以統計內核模塊所佔用的內存大小,但這並不準,因爲”lsmod”列出的是[init_size+core_size],而實際給kernel module分配的內存是以page爲單位的,不足 1 page的部分也會得到整個page,此外每個module還會分到一頁額外的guard page。下文我們還會細說。

# lsmod | less Module Size Used by rpcsec_gss_krb5 31477 0 auth_rpcgss 59343 1 rpcsec_gss_krb5 nfsv4 474429 0 dns_resolver 13140 1 nfsv4 nfs 246411 1 nfsv4 lockd 93977 1 nfs sunrpc 295293 5 nfs,rpcsec_gss_krb5,auth_rpcgss,lockd,nfsv4 fscache 57813 2 nfs,nfsv4 ...

1

2

3

4

5

6

7

8

9

10

11

# lsmod | less

Module                  Size  Used by

rpcsec_gss_krb5        31477  0

auth_rpcgss            59343  1 rpcsec_gss_krb5

nfsv4                 474429  0

dns_resolver           13140  1 nfsv4

nfs                   246411  1 nfsv4

lockd                  93977  1 nfs

sunrpc                295293  5 nfs,rpcsec_gss_krb5,auth_rpcgss,lockd,nfsv4

fscache                57813  2 nfs,nfsv4

...

lsmod的信息來自/proc/modules,它顯示的size包括init_size和core_size,相應的源代碼參見:

// kernel/module.c static int m_show(struct seq_file *m, void *p) { ... seq_printf(m, "%s %u", mod->name, mod->init_size + mod->core_size); ... }

1

2

3

4

5

6

7

8

// kernel/module.c

static int m_show(struct seq_file *m, void *p)

{

...

        seq_printf(m, "%s %u",

                   mod->name, mod->init_size + mod->core_size);

...

}

注:我們可以在 /sys/module/<module-name>/ 目錄下分別看到coresize和initsize的值。

kernel module的內存是通過vmalloc()分配的(參見下列源代碼),所以在/proc/vmallocinfo中會有記錄,也就是說我們可以不必通過”lsmod”命令來統計kernel module所佔的內存大小,通過/proc/vmallocinfo就行了,而且還比lsmod更準確,爲什麼這麼說呢?

// kernel/module.c static int move_module(struct module *mod, struct load_info *info) { ... ptr = module_alloc_update_bounds(mod->core_size); ... if (mod->init_size) { ptr = module_alloc_update_bounds(mod->init_size); ... } // 注:module_alloc_update_bounds()最終會調用vmalloc_exec()

1

2

3

4

5

6

7

8

9

10

11

12

// kernel/module.c

static int move_module(struct module *mod, struct load_info *info)

{

...

        ptr = module_alloc_update_bounds(mod->core_size);

...

        if (mod->init_size) {

                ptr = module_alloc_update_bounds(mod->init_size);

...

}

 

// 注:module_alloc_update_bounds()最終會調用vmalloc_exec()

因爲給kernel module分配內存是以page爲單位的,不足 1 page的部分也會得到整個page,此外,每個module還會分到一頁額外的guard page。
詳見:mm/vmalloc.c: __get_vm_area_node()

而”lsmod”列出的是[init_size+core_size],比實際分配給kernel module的內存小。我們做個實驗來說明:

# 先卸載floppy模塊 $ modprobe -r floppy # 確認floppy模塊已經不在了 $ lsmod | grep floppy # 記錄vmallocinfo以供隨後比較 $ cat /proc/vmallocinfo > vmallocinfo.1 # 加載floppy模塊 $ modprobe -a floppy # 注意floppy模塊的大小是69417字節: $ lsmod | grep floppy floppy 69417 0 $ cat /proc/vmallocinfo > vmallocinfo.2 # 然而,我們看到vmallocinfo中記錄的是分配了73728字節: $ diff vmallocinfo.1 vmallocinfo.2 68a69 > 0xffffffffa03d7000-0xffffffffa03e9000 73728 module_alloc_update_bounds+0x14/0x70 pages=17 vmalloc N0=17 # 爲什麼lsmod看到的內存大小與vmallocinfo不同呢? # 因爲給kernel module分配內存是以page爲單位的,而且外加一個guard page # 我們來驗證一下: $ bc -q 69417%4096 3881 <--- 不能被4096整除 69417/4096 16 <--- 相當於16 pages,加上面的3881字節,會分配17 pages 18*4096 <--- 17 pages 加上 1個guard page 73728 <--- 正好是vmallocinfo記錄的大小

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

# 先卸載floppy模塊

$ modprobe -r floppy

# 確認floppy模塊已經不在了

$ lsmod | grep floppy

# 記錄vmallocinfo以供隨後比較

$ cat /proc/vmallocinfo > vmallocinfo.1

 

# 加載floppy模塊

$ modprobe -a floppy

# 注意floppy模塊的大小是69417字節:

$ lsmod | grep floppy

floppy                 69417  0

$ cat /proc/vmallocinfo > vmallocinfo.2

# 然而,我們看到vmallocinfo中記錄的是分配了73728字節:

$ diff vmallocinfo.1 vmallocinfo.2

68a69

> 0xffffffffa03d7000-0xffffffffa03e9000   73728 module_alloc_update_bounds+0x14/0x70 pages=17 vmalloc N0=17

 

# 爲什麼lsmod看到的內存大小與vmallocinfo不同呢?

# 因爲給kernel module分配內存是以page爲單位的,而且外加一個guard page

# 我們來驗證一下:

$ bc -q

69417%4096

3881    <--- 不能被4096整除

69417/4096

16      <--- 相當於16 pages,加上面的3881字節,會分配17 pages

18*4096 <--- 17 pages 加上 1個guard page

73728   <--- 正好是vmallocinfo記錄的大小

所以結論是kernel module所佔用的內存包含在/proc/vmallocinfo的統計之中,不必再去計算”lsmod”的結果了,而且”lsmod”也不準。

1.4 HardwareCorrupted

當系統檢測到內存的硬件故障時,會把有問題的頁面刪除掉,不再使用,/proc/meminfo中的HardwareCorrupted統計了刪除掉的內存頁的總大小。相應的代碼參見 mm/memory-failure.c: memory_failure()。

 1.5 PageTables

Page Table用於將內存的虛擬地址翻譯成物理地址,隨着內存地址分配得越來越多,Page Table會增大,/proc/meminfo中的PageTables統計了Page Table所佔用的內存大小。

注:請把Page Table與Page Frame(頁幀)區分開,物理內存的最小單位是page frame,每個物理頁對應一個描述符(struct page),在內核的引導階段就會分配好、保存在mem_map[]數組中,mem_map[]所佔用的內存被統計在dmesg顯示的reserved中,/proc/meminfo的MemTotal是不包含它們的。(在NUMA系統上可能會有多個mem_map數組,在node_data中或mem_section中)。
而Page Table的用途是翻譯虛擬地址和物理地址,它是會動態變化的,要從MemTotal中消耗內存。

1.6 KernelStack

每一個用戶線程都會分配一個kernel stack(內核棧),內核棧雖然屬於線程,但用戶態的代碼不能訪問,只有通過系統調用(syscall)、自陷(trap)或異常(exception)進入內核態的時候纔會用到,也就是說內核棧是給kernel code使用的。在x86系統上Linux的內核棧大小是固定的8K或16K(可參閱我以前的文章:內核棧溢出)。

Kernel stack(內核棧)是常駐內存的,既不包括在LRU lists裏,也不包括在進程的RSS/PSS內存裏,所以我們認爲它是kernel消耗的內存。統計值是/proc/meminfo的KernelStack。

1.7 Bounce

有些老設備只能訪問低端內存,比如16M以下的內存,當應用程序發出一個I/O 請求,DMA的目的地址卻是高端內存時(比如在16M以上),內核將在低端內存中分配一個臨時buffer作爲跳轉,把位於高端內存的緩存數據複製到此處。這種額外的數據拷貝被稱爲“bounce buffering”,會降低I/O 性能。大量分配的bounce buffers 也會佔用額外的內存。

2. 用戶進程

/proc/meminfo統計的是系統全局的內存使用狀況,單個進程的情況要看/proc/<pid>/下的smaps等等。

2.1 Hugepages

Hugepages在/proc/meminfo中是被獨立統計的,與其它統計項不重疊,既不計入進程的RSS/PSS中,又不計入LRU Active/Inactive,也不會計入cache/buffer。如果進程使用了Hugepages,它的RSS/PSS不會增加。

注:不要把 Transparent HugePages (THP)跟 Hugepages 搞混了,THP的統計值是/proc/meminfo中的”AnonHugePages”,在/proc/<pid>/smaps中也有單個進程的統計,這個統計值與進程的RSS/PSS是有重疊的,如果用戶進程用到了THP,進程的RSS/PSS也會相應增加,這與Hugepages是不同的。

在/proc/meminfo中與Hugepages有關的統計值如下:

MemFree: 570736 kB ... HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB

1

2

3

4

5

6

7

MemFree: 570736 kB

...

HugePages_Total: 0

HugePages_Free: 0

HugePages_Rsvd: 0

HugePages_Surp: 0

Hugepagesize: 2048 kB

HugePages_Total 對應內核參數 vm.nr_hugepages,也可以在運行中的系統上直接修改 /proc/sys/vm/nr_hugepages,修改的結果會立即影響空閒內存 MemFree的大小,因爲HugePages在內核中獨立管理,只要一經定義,無論是否被使用,都不再屬於free memory。在下例中我們設置256MB(128頁)Hugepages,可以立即看到Memfree立即減少了262144kB(即256MB):

# echo 128 > /proc/sys/vm/nr_hugepages # cat /proc/meminfo ... MemFree: 308592 kB ... HugePages_Total: 128 HugePages_Free: 128 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB

1

2

3

4

5

6

7

8

9

10

# echo 128 > /proc/sys/vm/nr_hugepages

# cat /proc/meminfo

...

MemFree: 308592 kB

...

HugePages_Total: 128

HugePages_Free: 128

HugePages_Rsvd: 0

HugePages_Surp: 0

Hugepagesize: 2048 kB

使用Hugepages有三種方式:
(詳見 https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt)

  1. mount一個特殊的 hugetlbfs 文件系統,在上面創建文件,然後用mmap() 進行訪問,如果要用 read() 訪問也是可以的,但是 write() 不行。
  2. 通過shmget/shmat也可以使用Hugepages,調用shmget申請共享內存時要加上 SHM_HUGETLB 標誌。
  3. 通過 mmap(),調用時指定MAP_HUGETLB 標誌也可以使用Huagepages。

用戶程序在申請Hugepages的時候,其實是reserve了一塊內存,並未真正使用,此時/proc/meminfo中的 HugePages_Rsvd 會增加,而 HugePages_Free 不會減少。

HugePages_Total: 128 HugePages_Free: 128 HugePages_Rsvd: 128 HugePages_Surp: 0 Hugepagesize: 2048 kB

1

2

3

4

5

HugePages_Total: 128

HugePages_Free: 128

HugePages_Rsvd: 128

HugePages_Surp: 0

Hugepagesize: 2048 kB

等到用戶程序真正讀寫Hugepages的時候,它才被消耗掉了,此時HugePages_Free會減少,HugePages_Rsvd也會減少。

HugePages_Total: 128 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB

1

2

3

4

5

HugePages_Total: 128

HugePages_Free: 0

HugePages_Rsvd: 0

HugePages_Surp: 0

Hugepagesize: 2048 kB

我們說過,Hugepages是獨立統計的,如果進程使用了Hugepages,它的RSS/PSS不會增加。下面舉例說明,一個進程通過mmap()申請並使用了Hugepages,在/proc/<pid>/smaps中可以看到如下內存段,VmFlags包含的”ht”表示Hugepages,kernelPageSize是2048kB,注意RSS/PSS都是0:

... 2aaaaac00000-2aaabac00000 rw-p 00000000 00:0c 311151 /anon_hugepage (deleted) Size: 262144 kB Rss: 0 kB Pss: 0 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 0 kB Referenced: 0 kB Anonymous: 0 kB AnonHugePages: 0 kB Swap: 0 kB KernelPageSize: 2048 kB MMUPageSize: 2048 kB Locked: 0 kB VmFlags: rd wr mr mw me de ht ...

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

2aaaaac00000-2aaabac00000 rw-p 00000000 00:0c 311151 /anon_hugepage (deleted)

Size: 262144 kB

Rss: 0 kB

Pss: 0 kB

Shared_Clean: 0 kB

Shared_Dirty: 0 kB

Private_Clean: 0 kB

Private_Dirty: 0 kB

Referenced: 0 kB

Anonymous: 0 kB

AnonHugePages: 0 kB

Swap: 0 kB

KernelPageSize: 2048 kB

MMUPageSize: 2048 kB

Locked: 0 kB

VmFlags: rd wr mr mw me de ht

...

 

2.2 AnonHugePages

AnonHugePages統計的是Transparent HugePages (THP),THP與Hugepages不是一回事,區別很大。

上一節說過,Hugepages在/proc/meminfo中是被獨立統計的,與其它統計項不重疊,既不計入進程的RSS/PSS中,又不計入LRU Active/Inactive,也不會計入cache/buffer。如果進程使用了Hugepages,它的RSS/PSS不會增加。

而AnonHugePages完全不同,它與/proc/meminfo的其他統計項有重疊,首先它被包含在AnonPages之中,而且在/proc/<pid>/smaps中也有單個進程的統計,與進程的RSS/PSS是有重疊的,如果用戶進程用到了THP,進程的RSS/PSS也會相應增加,這與Hugepages是不同的。下例截取自/proc/<pid>/smaps中的一段:

7efcf0000000-7efd30000000 rw-p 00000000 00:00 0 Size: 1048576 kB Rss: 313344 kB Pss: 313344 kB Shared_Clean: 0 kB Shared_Dirty: 0 kB Private_Clean: 0 kB Private_Dirty: 313344 kB Referenced: 239616 kB Anonymous: 313344 kB AnonHugePages: 313344 kB Swap: 0 kB KernelPageSize: 4 kB MMUPageSize: 4 kB Locked: 0 kB VmFlags: rd wr mr mw me dc ac hg mg

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

7efcf0000000-7efd30000000 rw-p 00000000 00:00 0

Size:            1048576 kB

Rss:              313344 kB

Pss:              313344 kB

Shared_Clean:          0 kB

Shared_Dirty:          0 kB

Private_Clean:         0 kB

Private_Dirty:    313344 kB

Referenced:       239616 kB

Anonymous:        313344 kB

AnonHugePages:    313344 kB

Swap:                  0 kB

KernelPageSize:        4 kB

MMUPageSize:           4 kB

Locked:                0 kB

VmFlags: rd wr mr mw me dc ac hg mg

THP也可以用於shared memory和tmpfs,缺省是禁止的,打開的方法如下(詳見 https://www.kernel.org/doc/Documentation/vm/transhuge.txt):

  • mount時加上”huge=always”等選項
  • 通過/sys/kernel/mm/transparent_hugepage/shmem_enabled來控制

因爲缺省情況下shared memory和tmpfs不使用THP,所以進程之間不會共享AnonHugePages,於是就有以下等式:
【/proc/meminfo的AnonHugePages】==【所有進程的/proc/<pid>/smaps中AnonHugePages之和】
舉例如下:

# grep AnonHugePages /proc/[1-9]*/smaps | awk '{total+=$2}; END {print total}' 782336 # grep AnonHugePages /proc/meminfo AnonHugePages: 782336 kB

1

2

3

4

# grep AnonHugePages /proc/[1-9]*/smaps | awk '{total+=$2}; END {print total}'

782336

# grep AnonHugePages /proc/meminfo

AnonHugePages:    782336 kB

 

2.3 LRU

LRU是Kernel的頁面回收算法(Page Frame Reclaiming)使用的數據結構,在解讀vmstat中的Active/Inactive memory一文中有介紹。Page cache和所有用戶進程的內存(kernel stack和huge pages除外)都在LRU lists上。

LRU lists包括如下幾種,在/proc/meminfo中都有對應的統計值:

LRU_INACTIVE_ANON  –  對應 Inactive(anon)
LRU_ACTIVE_ANON  –  對應 Active(anon)
LRU_INACTIVE_FILE  –  對應 Inactive(file)
LRU_ACTIVE_FILE  –  對應 Active(file)
LRU_UNEVICTABLE  –  對應 Unevictable

注:

  • Inactive list裏的是長時間未被訪問過的內存頁,Active list裏的是最近被訪問過的內存頁,LRU算法利用Inactive list和Active list可以判斷哪些內存頁可以被優先回收。
  • 括號中的 anon 表示匿名頁(anonymous pages)。
    用戶進程的內存頁分爲兩種:file-backed pages(與文件對應的內存頁),和anonymous pages(匿名頁),比如進程的代碼、映射的文件都是file-backed,而進程的堆、棧都是不與文件相對應的、就屬於匿名頁。file-backed pages在內存不足的時候可以直接寫回對應的硬盤文件裏,稱爲page-out,不需要用到交換區(swap);而anonymous pages在內存不足時就只能寫到硬盤上的交換區(swap)裏,稱爲swap-out。
  • 括號中的 file 表示 file-backed pages(與文件對應的內存頁)。
  • Unevictable LRU list上是不能pageout/swapout的內存頁,包括VM_LOCKED的內存頁、SHM_LOCK的共享內存頁(又被統計在”Mlocked”中)、和ramfs。在unevictable list出現之前,這些內存頁都在Active/Inactive lists上,vmscan每次都要掃過它們,但是又不能把它們pageout/swapout,這在大內存的系統上會嚴重影響性能,設計unevictable list的初衷就是避免這種情況,參見:
    https://www.kernel.org/doc/Documentation/vm/unevictable-lru.txt

LRU與/proc/meminfo中其他統計值的關係:

  • LRU中不包含HugePages_*。
  • LRU包含了 Cached 和 AnonPages。

2.4 Shmem

/proc/meminfo中的Shmem統計的內容包括:

  • shared memory
  • tmpfs和devtmpfs。

注:所有tmpfs類型的文件系統佔用的空間都計入共享內存,devtmpfs是/dev文件系統的類型,/dev/下所有的文件佔用的空間也屬於共享內存。可以用ls和du命令查看。如果文件在沒有關閉的情況下被刪除,空間仍然不會釋放,shmem不會減小,可以用 “lsof -a +L1 /<mount_point>” 命令列出這樣的文件。

此處所講的shared memory又包括:

  • SysV shared memory [shmget etc.]
  • POSIX shared memory [shm_open etc.]
  • shared anonymous mmap [ mmap(…MAP_ANONYMOUS|MAP_SHARED…)]

因爲shared memory在內核中都是基於tmpfs實現的,參見:
https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt
也就是說它們被視爲基於tmpfs文件系統的內存頁,既然基於文件系統,就不算匿名頁,所以不被計入/proc/meminfo中的AnonPages,而是被統計進了:

  • Cached (i.e. page cache)
  • Mapped (當shmem被attached時候)

然而它們背後並不存在真正的硬盤文件,一旦內存不足的時候,它們是需要交換區才能swap-out的,所以在LRU lists裏,它們被放在:

  • Inactive(anon) 或 Active(anon)
    注:雖然它們在LRU中被放進了anon list,但是不會被計入 AnonPages。這是shared memory & tmpfs比較擰巴的一個地方,需要特別注意。
  • 或 unevictable (如果被locked的話)

注意:
當shmget/shm_open/mmap創建共享內存時,物理內存尚未分配,要直到真正訪問時才分配。/proc/meminfo中的 Shmem 統計的是已經分配的大小,而不是創建時申請的大小。

2.5 AnonPages

前面提到用戶進程的內存頁分爲兩種:file-backed pages(與文件對應的內存頁),和anonymous pages(匿名頁)。Anonymous pages(匿名頁)的數量統計在/proc/meminfo的AnonPages中。

以下是幾個事實,有助於瞭解Anonymous Pages:

  • 所有page cache裏的頁面(Cached)都是file-backed pages,不是Anonymous Pages。”Cached”與”AnoPages”之間沒有重疊。
    注:shared memory 不屬於 AnonPages,而是屬於Cached,因爲shared memory基於tmpfs,所以被視爲file-backed、在page cache裏,上一節解釋過。
  • mmap private anonymous pages屬於AnonPages(Anonymous Pages),而mmap shared anonymous pages屬於Cached(file-backed pages),因爲shared anonymous mmap也是基於tmpfs的,上一節解釋過。
  • Anonymous Pages是與用戶進程共存的,一旦進程退出,則Anonymous pages也釋放,不像page cache即使文件與進程不關聯了還可以緩存。
  • AnonPages統計值中包含了Transparent HugePages (THP)對應的 AnonHugePages 。參見:

 

fs/proc/meminfo.c: static int meminfo_proc_show(struct seq_file *m, void *v) { ... #ifdef CONFIG_TRANSPARENT_HUGEPAGE K(global_page_state(NR_ANON_PAGES) + global_page_state(NR_ANON_TRANSPARENT_HUGEPAGES) * HPAGE_PMD_NR), ...

1

2

3

4

5

6

7

8

9

10

fs/proc/meminfo.c:

 

static int meminfo_proc_show(struct seq_file *m, void *v)

{

...

#ifdef CONFIG_TRANSPARENT_HUGEPAGE

                K(global_page_state(NR_ANON_PAGES)

                  + global_page_state(NR_ANON_TRANSPARENT_HUGEPAGES) *

                  HPAGE_PMD_NR),

...

 

2.6 Mapped

上面提到的用戶進程的file-backed pages就對應着/proc/meminfo中的”Mapped”。Page cache中(“Cached”)包含了文件的緩存頁,其中有些文件當前已不在使用,page cache仍然可能保留着它們的緩存頁面;而另一些文件正被用戶進程關聯,比如shared libraries、可執行程序的文件、mmap的文件等,這些文件的緩存頁就稱爲mapped。

/proc/meminfo中的”Mapped”就統計了page cache(“Cached”)中所有的mapped頁面。”Mapped”是”Cached”的子集。

因爲Linux系統上shared memory & tmpfs被計入page cache(“Cached”),所以被attached的shared memory、以及tmpfs上被map的文件都算做”Mapped”。

進程所佔的內存頁分爲anonymous pages和file-backed pages,理論上應該有:
【所有進程的PSS之和】 == 【Mapped + AnonPages】。
然而我實際測試的結果,雖然兩者很接近,卻總是無法精確相等,我猜也許是因爲進程始終在變化、採集的/proc/[1-9]*/smaps以及/proc/meminfo其實不是來自同一個時間點的緣故。

2.7 Cached

Page Cache裏包括所有file-backed pages,統計在/proc/meminfo的”Cached”中。

  • Cached是”Mapped”的超集,就是說它不僅包括mapped,也包括unmapped的頁面,當一個文件不再與進程關聯之後,原來在page cache中的頁面並不會立即回收,仍然被計入Cached,還留在LRU中,但是 Mapped 統計值會減小。【ummaped = (Cached – Mapped)】
  • Cached包含tmpfs中的文件,POSIX/SysV shared memory,以及shared anonymous mmap。
    注:POSIX/SysV shared memory和shared anonymous mmap在內核中都是基於tmpfs實現的,參見:
    https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt

“Cached”和”SwapCached”兩個統計值是互不重疊的,源代碼參見下一節。所以,Shared memory和tmpfs在不發生swap-out的時候屬於”Cached”,而在swap-out/swap-in的過程中會被加進swap cache中、屬於”SwapCached”,一旦進了”SwapCached”,就不再屬於”Cached”了。

2.8 SwapCached

我們說過,匿名頁(anonymous pages)要用到交換區,而shared memory和tmpfs雖然未統計在AnonPages裏,但它們背後沒有硬盤文件,所以也是需要交換區的。也就是說需要用到交換區的內存包括:”AnonPages”和”Shmem”,我們姑且把它們統稱爲匿名頁好了。

交換區可以包括一個或多個交換區設備(裸盤、邏輯卷、文件都可以充當交換區設備),每一個交換區設備都對應自己的swap cache,可以把swap cache理解爲交換區設備的”page cache”:page cache對應的是一個個文件,swap cache對應的是一個個交換區設備,kernel管理swap cache與管理page cache一樣,用的都是radix-tree,唯一的區別是:page cache與文件的對應關係在打開文件時就確定了,而一個匿名頁只有在即將被swap-out的時候才決定它會被放到哪一個交換區設備,即匿名頁與swap cache的對應關係在即將被swap-out時才確立。

並不是每一個匿名頁都在swap cache中,只有以下情形之一的匿名頁纔在:

  • 匿名頁即將被swap-out時會先被放進swap cache,但通常只存在很短暫的時間,因爲緊接着在pageout完成之後它就會從swap cache中刪除,畢竟swap-out的目的就是爲了騰出空閒內存;
    【注:參見mm/vmscan.c: shrink_page_list(),它調用的add_to_swap()會把swap cache頁面標記成dirty,然後它調用try_to_unmap()將頁面對應的page table mapping都刪除,再調用pageout()回寫dirty page,最後try_to_free_swap()會把該頁從swap cache中刪除。】
  • 曾經被swap-out現在又被swap-in的匿名頁會在swap cache中,直到頁面中的內容發生變化、或者原來用過的交換區空間被回收爲止。
    【注:當匿名頁的內容發生變化時會刪除對應的swap cache,代碼參見mm/swapfile.c: reuse_swap_page()。】

/proc/meminfo中的SwapCached背後的含義是:系統中有多少匿名頁曾經被swap-out、現在又被swap-in並且swap-in之後頁面中的內容一直沒發生變化。也就是說,如果這些匿名頁需要被swap-out的話,是無需進行I/O write操作的。

“SwapCached”不屬於”Cached”,兩者沒有交叉。參見:

fs/proc/meminfo.c: static int meminfo_proc_show(struct seq_file *m, void *v) { ... cached = global_page_state(NR_FILE_PAGES) - total_swapcache_pages() - i.bufferram; ... }

1

2

3

4

5

6

7

8

fs/proc/meminfo.c:

static int meminfo_proc_show(struct seq_file *m, void *v)

{

...

        cached = global_page_state(NR_FILE_PAGES) -

                        total_swapcache_pages() - i.bufferram;

...

}

“SwapCached”內存同時也在LRU中,還在”AnonPages”或”Shmem”中,它本身並不佔用額外的內存。

2.9 Mlocked

“Mlocked”統計的是被mlock()系統調用鎖定的內存大小。被鎖定的內存因爲不能pageout/swapout,會從Active/Inactive LRU list移到Unevictable LRU list上。也就是說,當”Mlocked”增加時,”Unevictable”也同步增加,而”Active”或”Inactive”同時減小;當”Mlocked”減小的時候,”Unevictable”也同步減小,而”Active”或”Inactive”同時增加。

“Mlocked”並不是獨立的內存空間,它與以下統計項重疊:LRU Unevictable,AnonPages,Shmem,Mapped等。

2.10 Buffers

“Buffers”表示塊設備(block device)所佔用的緩存頁,包括:直接讀寫塊設備、以及文件系統元數據(metadata)比如SuperBlock所使用的緩存頁。它與“Cached”的區別在於,”Cached”表示普通文件所佔用的緩存頁。參見我的另一篇文章http://linuxperf.com/?p=32

“Buffers”所佔的內存同時也在LRU list中,被統計在Active(file)或Inactive(file)。

注:通過閱讀源代碼可知,塊設備的讀寫操作涉及的緩存被納入了LRU,以讀操作爲例,do_generic_file_read()函數通過 mapping->a_ops->readpage() 調用塊設備底層的函數,並調用 add_to_page_cache_lru() 把緩存頁加入到LRU list中。參見:
filemap.c: do_generic_file_read > add_to_page_cache_lru

其它問題

DirectMap

/proc/meminfo中的DirectMap所統計的不是關於內存的使用,而是一個反映TLB效率的指標。TLB(Translation Lookaside Buffer)是位於CPU上的緩存,用於將內存的虛擬地址翻譯成物理地址,由於TLB的大小有限,不能緩存的地址就需要訪問內存裏的page table來進行翻譯,速度慢很多。爲了儘可能地將地址放進TLB緩存,新的CPU硬件支持比4k更大的頁面從而達到減少地址數量的目的, 比如2MB,4MB,甚至1GB的內存頁,視不同的硬件而定。”DirectMap4k”表示映射爲4kB的內存數量, “DirectMap2M”表示映射爲2MB的內存數量,以此類推。所以DirectMap其實是一個反映TLB效率的指標。

Dirty pages到底有多少?

/proc/meminfo 中有一個Dirty統計值,但是它未能包括系統中全部的dirty pages,應該再加上另外兩項:NFS_Unstable 和 Writeback,NFS_Unstable是發給NFS server但尚未寫入硬盤的緩存頁,Writeback是正準備回寫硬盤的緩存頁。即:

系統中全部dirty pages = ( Dirty + NFS_Unstable + Writeback )

注1:NFS_Unstable的內存被包含在Slab中,因爲nfs request內存是調用kmem_cache_zalloc()申請的。

注2:anonymous pages不屬於dirty pages。
參見mm/vmscan.c: page_check_dirty_writeback()
“Anonymous pages are not handled by flushers and must be written from reclaim context.”

爲什麼【Active(anon)+Inactive(anon)】不等於AnonPages?

因爲Shmem(即Shared memory & tmpfs) 被計入LRU Active/Inactive(anon),但未計入 AnonPages。所以一個更合理的等式是:

【Active(anon)+Inactive(anon)】 = 【AnonPages + Shmem】

但是這個等式在某些情況下也不一定成立,因爲:

  • 如果shmem或anonymous pages被mlock的話,就不在Active(non)或Inactive(anon)裏了,而是到了Unevictable裏,以上等式就不平衡了;
  • 當anonymous pages準備被swap-out時,分幾個步驟:先被加進swap cache,再離開AnonPages,然後離開LRU Inactive(anon),最後從swap cache中刪除,這幾個步驟之間會有間隔,而且有可能離開AnonPages就因某些情況而結束了,所以在某些時刻以上等式會不平衡。
    【注:參見mm/vmscan.c: shrink_page_list():
    它調用的add_to_swap()會把swap cache頁面標記成dirty,然後調用try_to_unmap()將頁面對應的page table mapping都刪除,再調用pageout()回寫dirty page,最後try_to_free_swap()把該頁從swap cache中刪除。】

爲什麼【Active(file)+Inactive(file)】不等於Mapped?

  1. 因爲LRU Active(file)和Inactive(file)中不僅包含mapped頁面,還包含unmapped頁面;
  2. Mapped中包含”Shmem”(即shared memory & tmpfs),這部分內存被計入了LRU Active(anon)或Inactive(anon)、而不在Active(file)和Inactive(file)中。

爲什麼【Active(file)+Inactive(file)】不等於 Cached?

  1. 因爲”Shmem”(即shared memory & tmpfs)包含在Cached中,而不在Active(file)和Inactive(file)中;
  2. Active(file)和Inactive(file)還包含Buffers。
  • 如果不考慮mlock的話,一個更符合邏輯的等式是:
    【Active(file) + Inactive(file) + Shmem】== 【Cached + Buffers】
  • 如果有mlock的話,等式應該如下(mlock包括file和anon兩部分,/proc/meminfo中並未分開統計,下面的mlock_file只是用來表意,實際並沒有這個統計值):
    【Active(file) + Inactive(file) + Shmem + mlock_file】== 【Cached + Buffers】

注:
測試的結果以上等式通常都成立,但內存發生交換的時候以上等式有時不平衡,我猜可能是因爲有些屬於Shmem的內存swap-out的過程中離開Cached進入了Swapcached,但沒有立即從swap cache刪除、仍算在Shmem中的緣故。

 Linux的內存都用到哪裏去了?

儘管不可能精確統計Linux系統的內存,但大體瞭解還是可以的。

kernel內存的統計方式應該比較明確,即

【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】

  • 注1:VmallocUsed其實不是我們感興趣的,因爲它還包括了VM_IOREMAP等並未消耗物理內存的IO地址映射空間,我們只關心VM_ALLOC操作,(參見1.2節),所以實際上應該統計/proc/vmallocinfo中的vmalloc記錄,例如(此處單位是byte):

 

# grep vmalloc /proc/vmallocinfo | awk '{total+=$2}; END {print total}' 23375872

1

2

# grep vmalloc /proc/vmallocinfo | awk '{total+=$2}; END {print total}'

23375872

 

  • 注2:kernel module的內存被包含在VmallocUsed中,見1.3節。
  • 注3:X表示直接通過alloc_pages/__get_free_page分配的內存,沒有在/proc/meminfo中統計,不知道有多少,就像個黑洞。

用戶進程的內存主要有三種統計口徑:

  1. 圍繞LRU進行統計
    【(Active + Inactive + Unevictable) + (HugePages_Total * Hugepagesize)】
  2. 圍繞Page Cache進行統計
    當SwapCached爲0的時候,用戶進程的內存總計如下:
    【(Cached + AnonPages + Buffers) + (HugePages_Total * Hugepagesize)】
    當SwapCached不爲0的時候,以上公式不成立,因爲SwapCached可能會含有Shmem,而Shmem本來被含在Cached中,一旦swap-out就從Cached轉移到了SwapCached,可是我們又不能把SwapCached加進上述公式中,因爲SwapCached雖然不與Cached重疊卻與AnonPages有重疊,它既可能含有Shared memory又可能含有Anonymous Pages。
  3. 圍繞RSS/PSS進行統計
    把/proc/[1-9]*/smaps 中的 Pss 累加起來就是所有用戶進程佔用的內存,但是還沒有包括Page Cache中unmapped部分、以及HugePages,所以公式如下:
    ΣPss + (Cached – mapped) + Buffers + (HugePages_Total * Hugepagesize)

所以系統內存的使用情況可以用以下公式表示:

  • MemTotal = MemFree +【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】+【Active + Inactive + Unevictable + (HugePages_Total * Hugepagesize)】
  • MemTotal = MemFree +【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】+【Cached + AnonPages + Buffers + (HugePages_Total * Hugepagesize)】
  • MemTotal = MemFree +【Slab+ VmallocUsed + PageTables + KernelStack + HardwareCorrupted + Bounce + X】+【ΣPss + (Cached – mapped) + Buffers + (HugePages_Total * Hugepagesize)】
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章