内存详解

meminfo内存结构

root@limin:/home/limin/program/linux# cat /proc/meminfo
MemTotal:       16337488 kB
MemFree:          973760 kB
Buffers:           21012 kB
Cached:          1712636 kB
SwapCached:       532092 kB
Active:         12371568 kB
Inactive:        2357880 kB
Active(anon):   11696784 kB
Inactive(anon):  2041404 kB
Active(file):     674784 kB
Inactive(file):   316476 kB
Unevictable:        1128 kB
Mlocked:            1128 kB
SwapTotal:      16681980 kB
SwapFree:        7845068 kB
Dirty:              2492 kB
Writeback:             0 kB
AnonPages:      12539716 kB
Mapped:           451788 kB
Shmem:            742380 kB
Slab:             216492 kB
SReclaimable:     110684 kB
SUnreclaim:       105808 kB
KernelStack:       15680 kB
PageTables:       192280 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    24850724 kB
Committed_AS:   43477708 kB
VmallocTotal:   34359738367 kB
VmallocUsed:      391344 kB
VmallocChunk:   34359333984 kB
HardwareCorrupted:     0 kB
AnonHugePages:   2500608 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
DirectMap4k:      141296 kB
DirectMap2M:    12345344 kB
DirectMap1G:     4194304 kB

MemTotal

当我们在linux上查看内存状况的时候发现上面的打印
一般的是 ** MemTotal = MemFree+MemUsed **
MemTotal 是去掉kenel占用的剩下的

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中留下统计值,所以很难追踪

SLAB

slab是内核分配的内存, SReclaimable 是可以回收的,SUnreclaim是不可以回收的, ** Slab = SReclaimable + SUnreclaim ***

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中消耗内存。

KernelStack

每一个用户线程都会分配一个kernel stack(内核栈),内核栈虽然属于线程,但用户态的代码不能访问,只有通过系统调用(syscall)、自陷(trap)或异常(exception)进入内核态的时候才会用到,也就是说内核栈是给kernel code使用的。在x86系统上Linux的内核栈大小是固定的8K或16K(可参阅我以前的文章:内核栈溢出)。

Kernel stack(内核栈)是常驻内存的,既不包括在LRU lists里,也不包括在进程的RSS/PSS内存里,所以我们认为它是kernel消耗的内存。统计值是/proc/meminfo的KernelStack。

Bounce

有些老设备只能访问低端内存,比如16M以下的内存,当应用程序发出一个I/O 请求,DMA的目的地址却是高端内存时(比如在16M以上),内核将在低端内存中分配一个临时buffer作为跳转,把位于高端内存的缓存数据复制到此处。这种额外的数据拷贝被称为“bounce buffering”,会降低I/O 性能。大量分配的bounce buffers 也会占用额外的内存。

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

Cached

Cached” 表示除去 “buffers” 和 “swap cache” 之外,剩下的也就是普通文件的缓存页的数量:
global_page_state(NR_FILE_PAGES) – total_swapcache_pages – i.bufferram

SwapCached

SwapCached 主要是cache被swap-out出去的swap cache

我们说过,匿名页(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”内存同时也在LRU中,还在”AnonPages”或”Shmem”中,它本身并不占用额外的内存。

Active Inactive anon -page file-page

Active: 12371568 kB
Inactive: 2357880 kB
Active(anon): 11696784 kB
Inactive(anon): 2041404 kB
Active(file): 674784 kB
Inactive(file): 316476 kB
Unevictable: 1128 kB
Mlocked: 1128 kB
先通过下面几个等式了解下
** Active = Active(anon)+ Active(file) **
** Inactive = Inactive(anon) + Inactive(file) **
** Mlocked = Unevictable **

anon -page

Active(anon)和Inactive(anon)除了包含AnonPages: 12539716 kB还有一部分Shmem: 742380 kB共享的内存

前面提到用户进程的内存页分为两种: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),

Cached

Cached(page cache) 包含了Shmem ,tmps和devtmps已经shard内存
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”了。

file-page

Active(file)和Inactive(file) 除了包含cache还有一部分buffer的部分,但是Active(file)和Inactive(file) 没有包含Shmem

Mlocked Unevictable

“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等。

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。

AnonHugePages

AnonHugePages 包含再Anonpage中,但是不包含在HugePages_Total

HugePages

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

HugePages 单独计算,不包含在pss/rss中, 系统的free会减少

Hugepages在/proc/meminfo中是被独立统计的,与其它统计项不重叠,既不计入进程的RSS/PSS中,又不计入LRU Active/Inactive,也不会计入cache/buffer。如果进程使用了Hugepages,它的RSS/PSS不会增加。

注:不要把 Transparent HugePages (THP)跟 Hugepages 搞混了,THP的统计值是/proc/meminfo中的”AnonHugePages”,在/proc//smaps中也有单个进程的统计,这个统计值与进程的RSS/PSS是有重叠的,如果用户进程用到了THP,进程的RSS/PSS也会相应增加,这与Hugepages是不同的
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

使用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 不会减少。

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

我们说过,Hugepages是独立统计的,如果进程使用了Hugepages,它的RSS/PSS不会增加。下面举例说明,一个进程通过mmap()申请并使用了Hugepages,在/proc//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
...

问题总结

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?

  • 因为LRU Active(file)和Inactive(file)中不仅包含mapped页面,还包含unmapped页面;
  • Mapped中包含”Shmem”(即shared memory & tmpfs),这部分内存被计入了LRU Active(anon)或Inactive(anon)、而不在Active(file)和Inactive(file)中。

为什么【Active(file)+Inactive(file)】不等于 Cached?

  • 因为”Shmem”(即shared memory & tmpfs)包含在Cached中,而不在Active(file)和Inactive(file)中;

  • Active(file)和Inactive(file)还包含Buffers。

  • 如果不考虑mlock的话,一个更符合逻辑的等式是:
    【Active(file) + Inactive(file) + Shmem】== 【Cached + Buffers】

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
  • 注2:kernel module的内存被包含在VmallocUsed中,见1.3节。
  • 注3:X表示直接通过alloc_pages/__get_free_page分配的内存,没有在/proc/meminfo中统计,不知道有多少,就像个黑洞。

用户进程的内存主要有三种统计口径

  • 围绕LRU进行统计
    【(Active + Inactive + Unevictable) + (HugePages_Total * Hugepagesize)】
  • 围绕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。
  • 围绕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)】

free

root@limin:/home/limin/program/linux# free
             total       used       free     shared    buffers     cached
Mem:      16337488   14176472    2161016     718176       5644     905368
-/+ buffers/cache:   13265460    3072028
Swap:     16681980    8865932    7816048

cached 就是上文提到的pagecache 其中包含shmem也就是shared

Page cache用于缓存文件里的数据,不仅包括普通的磁盘文件,还包括了tmpfs文件,tmpfs文件系统是将一部分内存空间模拟成文件系统,由于背后并没有对应着磁盘,无法进行paging(换页),只能进行swapping(交换),在执行drop_cache操作的时候tmpfs对应的page cache并不会回收。

这个释放内存是有节点的

  • 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

tmpfs占用的page cache是不能通过drop_caches操作回收的,tmpfs占用的page cache同时也算进了”shared”中,也就是说,被视为共享内存。

Linux kernel利用tmpfs实现共享内存(shared memory),参见:
https://www.kernel.org/doc/Documentation/filesystems/tmpfs.txt
所以共享内存也和tmpfs一样,属于page cache,但又不能被drop_caches回收。这里所说的共享内存包括:

  • SysV shared memory
    是通过shmget申请的共享内存,用”ipcs -m”或”cat /proc/sysvipc/shm”查看;

  • POSIX shared memory
    是通过shm_open申请的共享内存,用”ls /dev/shm”查看;

  • tmpfs文件系统和devtmpfs文件系统
    所有tmpfs文件系统占用的空间都计入共享内存,devtmpfs是/dev文件系统的类型,/dev/下所有的文件占用的空间也属于共享内存。可以用ls和du命令查看。如果文件没有关闭的情况下被删除,空间仍然不会释放,可以用 “lsof -a +L1 /” 命令查看。

  • shared anonymous mmap
    通过mmap(…MAP_ANONYMOUS|MAP_SHARED…)申请的内存,可以用”pmap -x”或者”cat /proc//maps”查看;

    注:mmap调用参数如果不是MAP_ANONYMOUS|MAP_SHARED,则不属于tmpfs,比如MAP_ANONYMOUS|MAP_PRIVATE根本不属于page cache而是属于AnonPages,MAP_SHARED属于普通文件,对应的page cache可以回写硬盘并回收。

我们通过一个实验来观察共享内存对free命令的影响。以下程序通过shared anonymous mmap方式申请256MB大小的内存:

#include 
#include 
#include 
#include 
#include 
#include 
 
#define MAP_SIZE 268435456
 
int main()
{
    char *addr;
    ssize_t s;
 
    addr = mmap(NULL, MAP_SIZE, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0);
    if (addr == MAP_FAILED) {
        fprintf(stderr, "mmap failed\n");
        exit(EXIT_FAILURE);
    }
    memset(addr, 9, MAP_SIZE);
 
    printf("mmap done, memset done, check free output. Press any key to exit...\n");
    getchar();
    exit(EXIT_SUCCESS);
}

开始操作:

先回收cache,设置初始状态:
$ sync; sudo sh -c 'echo 1 > /proc/sys/vm/drop_caches'; free
             total       used       free     shared    buffers     cached
Mem:      65942736    2401684   63541052       9576       3648      54820
-/+ buffers/cache:    2343216   63599520 
Swap:     33038332          0   33038332 
 
然后执行我们的测试程序,申请256MB共享内存:
$ ./mmap_test
mmap done, memset done, check free output. Press any key to exit...
 
在另一个窗口里观察,看到cache值增加了256MB,注意"shared"也同时增加了256MB:
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    2652796   63289940     271720       6380     321760
-/+ buffers/cache:    2324656   63618080 
Swap:     33038332          0   33038332 
 
执行drop_caches,发现刚才新增的256MBcache值并未被回收:
$ sync; sudo sh -c 'echo 1 > /proc/sys/vm/drop_caches'; free
             total       used       free     shared    buffers     cached
Mem:      65942736    2666452   63276284     271720       3628     316532
-/+ buffers/cache:    2346292   63596444 
Swap:     33038332          0   33038332 
 
退出mmap_test程序,再看cache值就减少了256MB,注意"shared"也同时减少256MB:
$ free
             total       used       free     shared    buffers     cached
Mem:      65942736    2400268   63542468       9576       4796      59148
-/+ buffers/cache:    2336324   63606412 
Swap:     33038332          0   33038332

结论:cache值包含了共享内存的大小,然而drop_caches是不能回收它的。
注:上例是shared anonymous mmap,如果是SysV shared memory或POSIX shared memory,结果是一样的。

总结:为什么drop_caches操作不能回收所有的page cache?因为

  • dirty pages不能回收;
  • 共享内存和tmpfs不能回收(注意观察free命令显示的shared值);
  • 不同版本的free命令有不同的计算cache值的方法,有的包含了slab或SReclaimable,”echo 1 > /proc/sys/vm/drop_caches”是不能回收slab的,”echo 3 > /proc/sys/vm/drop_caches”也只是回收slab中的SReclaimable部分。

vmstat

root@limin:/home/limin/program/linux# vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 3  0 8722636 649524  78844 1970980    2    2    26    45    1    2  5  6 88  1  0
root@limin:/home/limin/program/linux# vmstat -a
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free  inact active   si   so    bi    bo   in   cs us sy id wa st
 2  0 8722636 649408 1965728 13081776    2    2    26    45    1    2  5  6 88  1  0


root@limin:/home/limin/program/linux# cat /proc/meminfo 
MemTotal:       16337488 kB
MemFree:          636164 kB
Buffers:           78968 kB
Cached:          1975000 kB
SwapCached:       398684 kB
Active:         13090396 kB
Inactive:        1970104 kB
Active(anon):   12201508 kB

root@limin:/home/limin/program/linux# cat /proc/vmstat 
nr_free_pages 255882
nr_alloc_batch 2307
nr_inactive_anon 415437
nr_active_anon 2966453
nr_inactive_file 70489
nr_active_file 215808
nr_unevictable 282
nr_mlock 282
nr_anon_pages 3113254
nr_mapped 133402
nr_file_pages 573901
nr_dirty 52

用kmemleak检测内核内存泄漏

所谓内存泄漏(memory leak),是指分配出去的内存在用完之后忘了释放,造成内存浪费,可用的内存越来越少。内存泄漏是程序设计的错误导致的,既可能发生在用户程序里,也可能发生在内核中。

诊断内存泄漏问题的目标是定位为什么内存用完之后会忘了释放,最终都是需要阅读源代码,理解内在的逻辑,找出其中的错误。作为最基本的分析线索,我们至少需要观察内存的分配与释放操作,还有一些更高级的工具可以帮助我们找出无人引用的内存块以及最初分配它的backtrace,这就更有针对性了。诊断用户态程序内存泄漏最流行的工具是Valgrind,对于内核,类似的工具是kmemleak。
kmemleak的原理

kmemleak通过追踪kmalloc(), vmalloc(), kmem_cache_alloc()等函数,把分配内存的指针和大小、时间、stack trace等信息记录在一个rbtree中,等到调用free释放内存时就把相应的记录从rbtree中删除,也就是说rbtree中的记录就是已经分配出去但尚未释放的内存,其中有些内存尚未释放是因为还在被使用,这属于正常情况,而不正常的情况即内存尚未释放但又不会再被使用,就是“泄漏”的内存,那么如何找出泄漏的内存呢?kmemleak缺省每10分钟对内存做一次扫描,在内存中寻找rbtree中记录的地址,如果某个rbtree记录的地址在内存中找不到,就认为这个地址是无人引用的,以后也不可能再被用到,是“泄漏”的内存,然后,把这些泄漏的内存地址以及rbtree中记录的时间、大小、strack trace等相关信息通过 /sys/kernel/debug/kmemleak 这个接口展现给我们。

注:
kmemleak的扫描算法存在误报的可能,比如内存中碰巧有一个数据与rbtree中的某个地址相同,但它只是数据而非指针,kmemleak是无法分辨的,会把它当作访问内存的指针;再比如rbtree中的某个地址在内存中找不到,但程序可能还在用它,只是因为程序并没有直接保存访问地址,而是通过某种方式临时计算访问地址,这种情况kmemleak也无法分辨,会认为是泄漏。但是请注意,kmemleak 这个工具的目的是为了给进一步分析提供线索,并不需要绝对精确,小概率的误报并不影响这个工具的实用性。
怎样启用kmemleak

要启用kmemleak,前提是内核编译时在“Kernel hacking”中开启了 CONFIG_DEBUG_KMEMLEAK 选项。怎样知道一个运行系统的内核是否支持kmemleak呢?可以查看 /boot/config-$(uname -r) 配置文件中 CONFIG_DEBUG_KMEMLEAK 是否等于”y”。以RHEL6为例,它的debug kernel是支持kmemleak的,我们看它的config文件:

$ grep CONFIG_DEBUG_KMEMLEAK /boot/config-2.6.32-642.4.2.el6.x86_64.debug
CONFIG_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE=20000
CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF=y

    
$ grep CONFIG_DEBUG_KMEMLEAK /boot/config-2.6.32-642.4.2.el6.x86_64.debug
CONFIG_DEBUG_KMEMLEAK=y
CONFIG_DEBUG_KMEMLEAK_EARLY_LOG_SIZE=20000
CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF=y
CONFIG_DEBUG_KMEMLEAK=y
表示内核支持kmemleak;
CONFIG_DEBUG_KMEMLEAK_DEFAULT_OFF=y
表示kmemleak默认是关闭的,需要显式启用,启用的方式是在kernel command line中添加以下选项:
kmemleak=on

怎样使用kmemleak

kmemleak的用户接口是:
/sys/kernel/debug/kmemleak
发送指令和输出信息都是通过以上文件进行的。要访问这个文件,必须先挂载以下文件系统:

mount -t debugfs nodev /sys/kernel/debug/

kmemleak缺省每10分钟扫描内存一次,找到可疑的内存泄漏会在syslog中写一条记录并提示通过/sys/kernel/debug/kmemleak可以看到更详细的信息:

# tail /var/log/messages
Aug 30 14:10:32 bj71s060-vm1 kernel: kmemleak: 1 new suspected memory leaks (see /sys/kernel/debug/kmemleak)

# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff88003cff1260 (size 32):
  comm "softirq", pid 0, jiffies 4297625839
  hex dump (first 32 bytes):
    f0 a1 9b 35 00 00 00 00 0c 00 00 00 01 00 01 00  ...5............
    da c0 70 3a 00 00 00 00 2a 00 00 00 00 00 00 00  ..p:....*.......
  backtrace:
    [] kmemleak_alloc+0x5e/0xe0
    [] __kmalloc+0x1f2/0x330
    [] virtqueue_add_buf+0x2c0/0x5e0 [virtio_ring]
    [] start_xmit+0x1a7/0x460 [virtio_net]
    [] dev_hard_start_xmit+0x22c/0x4a0
    [] sch_direct_xmit+0x166/0x1d0
    [] dev_queue_xmit+0x268/0x380
    [] arp_xmit+0x58/0x60
    [] arp_send+0x43/0x50
    [] arp_solicit+0x236/0x2b0
    [] neigh_timer_handler+0xf1/0x370
    [] run_timer_softirq+0x20e/0x3e0
    [] __do_softirq+0xff/0x260
    [] call_softirq+0x1c/0x30
    [] do_softirq+0xad/0xe0
    [] irq_exit+0x95/0xa0
    
# tail /var/log/messages
Aug 30 14:10:32 bj71s060-vm1 kernel: kmemleak: 1 new suspected memory leaks (see /sys/kernel/debug/kmemleak)
 
# cat /sys/kernel/debug/kmemleak
unreferenced object 0xffff88003cff1260 (size 32):
  comm "softirq", pid 0, jiffies 4297625839
  hex dump (first 32 bytes):
    f0 a1 9b 35 00 00 00 00 0c 00 00 00 01 00 01 00  ...5............
    da c0 70 3a 00 00 00 00 2a 00 00 00 00 00 00 00  ..p:....*.......
  backtrace:
    [] kmemleak_alloc+0x5e/0xe0
    [] __kmalloc+0x1f2/0x330
    [] virtqueue_add_buf+0x2c0/0x5e0 [virtio_ring]
    [] start_xmit+0x1a7/0x460 [virtio_net]
    [] dev_hard_start_xmit+0x22c/0x4a0
    [] sch_direct_xmit+0x166/0x1d0
    [] dev_queue_xmit+0x268/0x380
    [] arp_xmit+0x58/0x60
    [] arp_send+0x43/0x50
    [] arp_solicit+0x236/0x2b0
    [] neigh_timer_handler+0xf1/0x370
    [] run_timer_softirq+0x20e/0x3e0
    [] __do_softirq+0xff/0x260
    [] call_softirq+0x1c/0x30
    [] do_softirq+0xad/0xe0
    [] irq_exit+0x95/0xa0

也可以手工发起kmemleak的内存扫描:
# echo scan > /sys/kernel/debug/kmemleak

还可以调整kmemleak内存扫描的频率等参数,更多的操作详见手册:
https://www.kernel.org/doc/html/latest/dev-tools/kmemleak.html

参考资料:
http://tekkamanninja.blog.chinaunix.net/uid-26859697-id-5758036.html

VSS/RSS/PSS/USS

  • VSS- Virtual Set Size 虚拟耗用内存(包含共享库占用的内存)
  • RSS- Resident Set Size 实际使用物理内存(包含共享库占用的内存)
  • PSS- Proportional Set Size 实际使用的物理内存(比例分配共享库占用的内存)
  • USS- Unique Set Size 进程独自占用的物理内存(不包含共享库占用的内存)

一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

参考

http://linuxperf.com/?cat=7

你可能感兴趣的:(内存详解)