转至:http://linuxperf.com/?p=142
/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()
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记录看到:
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记录,例如:
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。下文我们还会细说。
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,相应的源代码参见:
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更准确,为什么这么说呢?
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的内存小。我们做个实验来说明:
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有关的统计值如下:
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):
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)
- mount一个特殊的 hugetlbfs 文件系统,在上面创建文件,然后用mmap() 进行访问,如果要用 read() 访问也是可以的,但是 write() 不行。
- 通过shmget/shmat也可以使用Hugepages,调用shmget申请共享内存时要加上 SHM_HUGETLB 标志。
- 通过 mmap(),调用时指定MAP_HUGETLB 标志也可以使用Huagepages。
用户程序在申请Hugepages的时候,其实是reserve了一块内存,并未真正使用,此时/proc/meminfo中的 HugePages_Rsvd 会增加,而 HugePages_Free 不会减少。
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也会减少。
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: