解析meminfo

2,257 阅读10分钟

前言

在排查线上服务器出现可用内存不足的问题时,首要做的就是掌握当前服务器内存的使用现状。了解服务器内存使用现状的主要接口之一就是/proc/meminfo文件,因此详细了解文件中列出的指标的含义很有必要。本文的目的就是梳理meminfo文件中的所有指标的含义、作用,并通过实验加深对指标的理解。

MemTotal:        7679032 kB
MemFree:         7060444 kB
MemAvailable:    7258732 kB
Buffers:            2668 kB
Cached:           400492 kB
SwapCached:            0 kB
Active:           109364 kB
Inactive:         371440 kB
Active(anon):        312 kB
Inactive(anon):    79276 kB
Active(file):     109052 kB
Inactive(file):   292164 kB
Unevictable:           0 kB
Mlocked:               0 kB
SwapTotal:             0 kB
SwapFree:              0 kB
Dirty:                 0 kB
Writeback:             0 kB
AnonPages:         75476 kB
Mapped:           106096 kB
Shmem:              1944 kB
KReclaimable:      33652 kB
Slab:              63068 kB
SReclaimable:      33652 kB
SUnreclaim:        29416 kB
KernelStack:        2736 kB
PageTables:         6764 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     3839516 kB
Committed_AS:     343444 kB
VmallocTotal:   34359738367 kB
VmallocUsed:           0 kB
VmallocChunk:          0 kB
Percpu:             2336 kB
HardwareCorrupted:     0 kB
AnonHugePages:     16384 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
FileHugePages:         0 kB
FilePmdMapped:         0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
DirectMap4k:       92864 kB
DirectMap2M:     4870144 kB
DirectMap1G:     3145728 kB

meminfo

系统内存大致可以分成如下几类,其中:

  • 内核使用的:linux 内核使用的内存
  • 用户进程使用的: 用户进程使用的内存
  • MemFree: 剩余内存
  • 内存黑洞:未能感知的内存

image.png

MemTotal

MemTotal就是可由linux kernel支配的内存总数,该值在系统运行期间一般是固定不变的。你可能有这样的疑问,我的内存条是8G的,为什么MemTotal却是7点多G。

这是因为硬件和BIOS会保留一部分的内存,linux kernel本身也要使用一部分内存来存储自身镜像,因此MemTotal要比系统物理总内存要少些。

执行dmesg | grep Memory命令可查看系统在引导过程中的内存初始化信息。

# dmesg | grep Memory
[    0.000000] Memory: 3789320k/4915200k available (6243k kernel code, 795332k absent, 330548k reserved, 4180k data, 1604k init)

MemFree

MemFree 就是系统真正尚未使用的内存。linux使用伙伴系统分配器管理空闲页的,因此MemFree等于伙伴系统空闲页。

[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cat /proc/buddyinfo 
Node 0, zone      DMA      0      0      0      0      0      0      0      0      0      1      3 
Node 0, zone    DMA32      2      2      0      2      1      1      2      3      2      4    692 
Node 0, zone   Normal    127    119     73     34     18     24     10     20     12      9    972

MemAvailable

MemAvailable 表示系统当前可用内存,这和MemFree有什么不一样?MemFree是系统真正未被使用的内存,这部分内存当然是可用的,但系统有些内存虽然被使用了,但可以被回收,因此可以被回收的内存也可用。比如系统的cache、buffer、slab中的部分内存都可以被回收,因此这部分内存也可被计入MemAvailable。

MemAvailable是kernel使用特定的算法估算出来的,不是精确值。估算的具体算法如下:

long si_mem_available(void)
{
	long available;
	unsigned long pagecache;
	unsigned long wmark_low = 0;
	unsigned long pages[NR_LRU_LISTS];
	unsigned long reclaimable;
	struct zone *zone;
	int lru;

	for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++)
		pages[lru] = global_node_page_state(NR_LRU_BASE + lru);

	for_each_zone(zone)
		wmark_low += low_wmark_pages(zone);

	/*
	 * Estimate the amount of memory available for userspace allocations,
	 * without causing swapping.
	 */
	available = global_zone_page_state(NR_FREE_PAGES) - totalreserve_pages;

	/*
	 * Not all the page cache can be freed, otherwise the system will
	 * start swapping. Assume at least half of the page cache, or the
	 * low watermark worth of cache, needs to stay.
	 */
	pagecache = pages[LRU_ACTIVE_FILE] + pages[LRU_INACTIVE_FILE];
	pagecache -= min(pagecache / 2, wmark_low);
	available += pagecache;

	/*
	 * Part of the reclaimable slab and other kernel memory consists of
	 * items that are in use, and cannot be freed. Cap this estimate at the
	 * low watermark.
	 */
	reclaimable = global_node_page_state_pages(NR_SLAB_RECLAIMABLE_B) +
		global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);
	available += reclaimable - min(reclaimable / 2, wmark_low);

	if (available < 0)
		available = 0;
	return available;
}

Buffers

Buffers 表示块设备占用的缓存页。kernel对Buffers的计算如下:

long nr_blockdev_pages(void)
{
	struct inode *inode;
	long ret = 0;

	spin_lock(&blockdev_superblock->s_inode_list_lock);
	list_for_each_entry(inode, &blockdev_superblock->s_inodes, i_sb_list)
		ret += inode->i_mapping->nrpages;
	spin_unlock(&blockdev_superblock->s_inode_list_lock);

	return ret;
}

使用dd命令读写磁盘,可以看到Buffers增加,而Cached没怎么变化。

[root@iZ2zeam9jrqo0a6a7a6387Z dev]# cat /proc/meminfo | grep "Buffers\|Cached"
Buffers:               0 kB
Cached:           111916 kB
[root@iZ2zeam9jrqo0a6a7a6387Z dev]# dd if=/dev/vda1 of=/dev/null count=20000
20000+0 records in
20000+0 records out
10240000 bytes (10 MB, 9.8 MiB) copied, 0.0207874 s, 493 MB/s
[root@iZ2zeam9jrqo0a6a7a6387Z dev]# cat /proc/meminfo | grep "Buffers\|Cached"
Buffers:           16208 kB
Cached:           111932 kB

Cached

Cached 表示纯粹意义上的文件系统的文件缓存页,kernel对Cached的计算公式如下:

cached = global_node_page_state(NR_FILE_PAGES) - total_swapcache_pages() - i.bufferram;
  • global_node_page_state(NR_FILE_PAGES):所有缓存页。
  • total_swapcache_pages(): 当发生交换时,匿名页被swap-out到交换设备(磁盘),当swap-in时,匿名页先放在swap cache中,直到页面中的内容被修改。swap cache可以理解为和交换设备对应的page cache。
  • i.bufferram: Buffers。

SwapCached

用户进程的内存页分为两种:file-backed pages(与文件对应的内存页)和 anonymous pages(匿名页)。匿名页(anonymous pages)是没有关联任何文件的,比如用户进程通过malloc()申请的内存页,如果发生swapping换页,它们没有关联的文件进行回写,所以只能写入到交换区里。

交换区可以包括一个或多个交换区设备(裸盘、逻辑卷、文件都可以充当交换区设备),每一个交换区设备在内存里都有对应的swap cache,可以把swap cache理解为交换区设备的”page cache”。

并不是每一个匿名页都在swap cache中,只有以下情形之一的匿名页才在:

  • 匿名页即将被swap-out时会先被放进swap cache,但通常只存在很短暂的时间,因为紧接着在pageout完成之后它就会从swap cache中删除,毕竟swap-out的目的就是为了腾出空闲内存;
  • 曾经被swap-out现在又被swap-in的匿名页会在swap cache中,直到页面中的内容发生变化、或者原来用过的交换区空间被回收为止。

Active Inactive Active(anon) Active(file) Inactive(file) Unevictable

Active Inactive Active(anon) Active(file) Inactive(file) Unevictable都是和LRU相关的指标,kernel统计的代码如下:

unsigned long pages[NR_LRU_LISTS];
for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++)
    pages[lru] = global_node_page_state(NR_LRU_BASE + lru);
                
show_val_kb(m, "Active:         ", pages[LRU_ACTIVE_ANON] + pages[LRU_ACTIVE_FILE]);
show_val_kb(m, "Inactive:       ", pages[LRU_INACTIVE_ANON] + pages[LRU_INACTIVE_FILE]);
show_val_kb(m, "Active(anon):   ", pages[LRU_ACTIVE_ANON]);
show_val_kb(m, "Inactive(anon): ", pages[LRU_INACTIVE_ANON]);
show_val_kb(m, "Active(file):   ", pages[LRU_ACTIVE_FILE]);
show_val_kb(m, "Inactive(file): ", pages[LRU_INACTIVE_FILE]);
show_val_kb(m, "Unevictable:    ", pages[LRU_UNEVICTABLE]);

LRU是linux kernel的内存页面回收算法。所谓页面回收是指当系统可用内存不足时,将适合的文件页pageout到文件,将合适的匿名页swapout到交换区,关键是选择适合pageout和swapout的内存页。

LRU算法的核心思想就是回收的页面应该是最近使用得最少的。LRU算法最理想实现:每个页面维护一个最近一次访问时间标志,每次回收的页面应该是最近一次访问时间最远的。但x86 CPU硬件并不支持这个特性,x86 CPU只能做到在访问页面时设置一个标志位Access Bit,无法记录时间,所以Linux Kernel使用了一个折衷的方法:它采用了LRU list列表,把刚访问过的页面放在列首,越接近列尾的就是越长时间未访问过的页面,这样,虽然不能记录访问时间,但利用页面在LRU list中的相对位置也可以轻松找到年龄最长的页面。Linux kernel设计了两种LRU list: active list 和 inactive list, 刚访问过的页面放进active list,长时间未访问过的页面放进inactive list,这样从inactive list回收页面就变得简单了。内核线程kswapd会周期性地把active list中符合条件的页面移到inactive list中,这项转移工作是由refill_inactive_zone()完成的。

  • Active:active LRU list中的页面大小。
  • Inactive:inactive LRU list中的页面大小。Inactive值越大,表明可以回收的页面越多。
  • Active(anon): active LRU list中的匿名页。匿名页就是和文件无关的内存,如使用malloc申请的内存,该内存页发生换页时是通过写入交换区完成。
  • Inactive(anon): inactive LRU list中的匿名页。
  • Active(file): active LRU list中的文件页。文件页就是和文件(如程序文件、数据文件)关联的内存,该内存页发生换页时是通过写回文件完成。
  • Inactive(file):inactive LRU list中的文件页。
  • Unevictable:不能pageout/swapout的内存页。LRU的目的是快速找到系统中最适合被pageout/swapout的内存页,但系统中有一些内存页因为某些原因不能被pageout/swapout,如果此内存页也放在active LRU list或者inactive LRU list中,一旦该部分内存页变大,扫描active LRU list会变得耗时,但却做无用功。因此linux kernel设计了Unevictable LRU list,专门用于存放无法pageout/swapout的内存页。参见:www.kernel.org/doc/Documen…

Unevictable LRU list存放不能pageout/swapout的内存页,主要有如下类型:

  • ramfs持有的内存页;
  • SHM_LOCK的共享内存页;
  • VM_LOCKED的内存页。

Mlocked

Mlocked 统计使用mlock()系统调用锁定的内存大小。使用mlock()锁定的内存不允许被pageout/swapout,这在有些场景下可提高应用的性能。上面我们提到,Unevictable LRU list存放不能pageout/swapout的内存页,因此当Mlocked增加,Unevictable也会相应的增加。

一段示例程序如下:

#include<stdio.h>
#include<stdlib.h>
#include<sys/mman.h>

const int alloc_size = 32 * 1024 * 1024;  // 申请32MB内存
int main()
{
        char *memory = malloc(alloc_size);
        if(mlock(memory,alloc_size) == -1) {
                perror("mlock");
                return (-1);
        }
        size_t i;
        size_t page_size = getpagesize();
        for(i=0;i<alloc_size;i+=page_size) {
                printf("i=%zd\n",i);
                memory[i] = 0;
        }

        printf("lock memory 成功,点击回车unlock memory\n");
        getchar();

        if(munlock(memory,alloc_size) == -1) {
                perror("munlock");
                return (-1);
        }

        return 0;
}

执行前,Unevictable和Mlocked如下:

Unevictable:           0 kB
Mlocked:               0 kB

mlock()执行后,Unevictable和Mlocked如下,刚好为锁住的32M内存。

Unevictable:       32772 kB
Mlocked:           32772 kB

munlock执行后,Unevictable和Mlocked又变为0.

Unevictable:           0 kB
Mlocked:               0 kB

SwapTotal SwapFree

关于swap的介绍参见:draveness.me/whys-the-de…
SwapTotal 表示swap分区的总大小
SwapFree 表示swap分区当前剩余的大小

Dirty

Dirty 统计系统的脏页,即需要写入磁盘的内存页。

使用dd命令进行文件写入,发现在执行完命令的一小段时间内,Dirty的值和写入文件的大小相同,都为10M,Cached的值也相应的增加了10M。

[root@iZ2zeam9jrqo0a6a7a6387Z meminfo_code]# dd if=/dev/zero of=dirty_test.file bs=1M count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.00354709 s, 3.0 GB/s
Dirty:             10244 kB

过了大约30秒后,Dirty的值又变为0,并且Cached的值也下降了10M,这是因为kernel有定时脏页写回磁盘操作,因此回收了这部分内存页。

关于cache调优的参数参见:zhuanlan.zhihu.com/p/136237953

Writeback

Writeback 统计系统当前正在写入磁盘的内存页。

我们利用dd命令生成10M大小的文件,并调用sync触发数据同步到磁盘操作,观测Writeback指标变化。

# 实验之前的指标
MemFree:         5980448 kB
Cached:          1395024 kB
Dirty:                16 kB
Writeback:             0 kB

[root@iZ2zeam9jrqo0a6a7a6387Z meminfo_code]# dd if=/dev/zero of=~/X.img bs=1M count=10 ; sync & for i in 1 2 3; do grep -E '^(Dirty:|Writeback:|MemFree:|Cached:)' /proc/meminfo ; done
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.00375089 s, 2.8 GB/s
[1] 164520
MemFree:         5970164 kB
Cached:          1405176 kB
Dirty:              8104 kB
Writeback:          2064 kB

MemFree:         5970148 kB
Cached:          1405176 kB
Dirty:              8104 kB
Writeback:          2064 kB

MemFree:         5970144 kB
Cached:          1405176 kB
Dirty:              8104 kB
Writeback:          2064 kB

# sync后台执行完之后的指标
MemFree:         5970208 kB
Cached:          1405272 kB
Dirty:                12 kB
Writeback:             0 kB

实验结果显示在sync执行过程中,Writeback的值为2064KB,表明当前正在进行数据同步操作。sync执行结束,Writeback的值为0,Dirty的值也和实验之前一致。

AnonPages

AnonPages 统计用户进程的匿名页。AnonPages大致等于Active(anon) + Inactive(anon) 。

利用如下一段程序观测AnonPages指标。

#include<stdio.h>
#include<stdlib.h>
#include<sys/mman.h>

const int alloc_size = 32 * 1024 * 1024;
int main()
{
        char *memory = malloc(alloc_size);
        
        size_t i;
        size_t page_size = getpagesize();
        for(i=0;i<alloc_size;i+=page_size) {
                printf("i=%zd\n",i);
                memory[i] = 0;
        } 

        printf("分配内存成功,点击回车释放内存\n");
        getchar();

        free(memory);
}

程序运行前的AnonPages为101100 kB,内存分配成功之后的AnonPages为 139360 kB,内存释放之后的AnonPages为 105296 kB。

Mapped

Mapped 统计用户进程的文件页,是Cached的子集。

Cached包含了文件的缓存页,其中有些文件当前已不在使用,但cache仍然可能保留着它们的缓存页面;而另一些文件正被用户进程关联,比如shared libraries、可执行程序的文件、mmap的文件等,这些文件的缓存页就称为mapped。

Shmem

Shmem 统计共享内存和tmpfs使用大小。

tmpfs的实验如下:

[root@iZ2zeam9jrqo0a6a7a6387Z shm]# cat /proc/meminfo | grep Shmem
Shmem:              1944 kB
[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cd /dev/shm
[root@iZ2zeam9jrqo0a6a7a6387Z shm]# dd if=/dev/zero of=/dev/shm/test bs=1M count=10
10+0 records in
10+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.00352609 s, 3.0 GB/s
[root@iZ2zeam9jrqo0a6a7a6387Z shm]# cat /proc/meminfo | grep Shmem
Shmem:             12184 kB

在/dev/shm目录下生成一个10M的文件,Shmem大小刚好增加10M

利用shmget等函数进行共享内存实验,实验程序如下:

// 创建一块共享内存
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>

int main(int argc, const char * argv[]) {
    
    //生成一个key
    key_t key = ftok("./", 88);
    
    //创建共享内存,返回一个id
    //数字 4 、2 和 1表示读、写、执行权限
    //用户、所属组、其他组都有读写权限
    int shmid = shmget(key, 20*1024*1024, IPC_CREAT|0666); // IPC_CREAT: Create entry if key does not exist
    printf("共享内存的shmid is %d \n", shmid);
    
    if (shmid == -1) {
        perror("shmget failed");
        //exit(0) 表示程序正常退出,exit⑴/exit(-1)表示程序异常退出。
        exit(1);
    }
    
    //映射共享内存,得到虚拟地址
    //shmaddr:指定共享内存出现在进程内存地址的什么位置,直接指定为NULL让内核自己决定一个合适的地址位置
    //shmflg: SHM_RDONLY:为只读模式,其他为读写模式
    void *p = shmat(shmid, NULL, 0);
    if (p == (void *)-1) {
        perror("shmat failed");
        exit(2);
    }
    
    //写共享内存
    int *pp = p;
    int i;
    for(i=0;i<20*1024*1024/4;i++){
        *(pp + i) = i;
    }
    printf("写入共享内存成功,点击回车解除内存映射\n");
    getchar();
    
    //解除映射
    if (shmdt(p) == -1) {
        printf("shmdt failed");
        exit(3);
    }
    
    printf("解除映射成功,点击回车销毁共享内存\n");
    getchar();
    
    //IPC_RMID:删除这片共享内存
    if (shmctl(shmid, IPC_RMID, NULL)) {
        perror("shmctl failed");
        exit(4);
    }
    
    return 0;
}

// 读取共享内存中的数
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdlib.h>

int main() {
    
    //生成key
    key_t key = ftok("./", 88);
    
    //获取共享内存,返回id
    //0:只获取共享内存时指定为0
    //shmflg0:取共享内存标识符,若不存在则函数会报错
    int shmid = shmget(key, 0, 0);
    if (shmid == -1) {
        perror("shmget failed");
        exit(1);
    }
    
    //映射共享内存,得到虚拟地址
    void *p = shmat(shmid, 0, 0);
    if (p == (void *)-1) {
        perror("shmat failed");
        exit(2);
    }
    
    //读取共享内存
    int data1 = *(int *)p;
    int data2 = *((int *)p + 1);
    printf("共享内存第1000个的数为 %d", *((int *)p + 999));
    
    //printf("从共享内存中读取了,%x 和 %x\n", data1, data2);

    printf("点击回车解除共享内存映射\n");
    getchar();
    
    //解除映射
    if(shmdt(p) == -1) {
        perror("shmdt failed");
        exit(3);
    }
    
    
    return 0;
}

实验开始之前,shmem的值为 12192,创建共享内存成功之后,shmem的值为 32672,刚好增加了20M的共享内存,删除共享内存之后,shmem的值又变为12192。

[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cat /proc/meminfo | grep Shmem
Shmem:             12192 kB    
[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cat /proc/meminfo | grep Shmem
Shmem:             32672 kB
[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cat /proc/meminfo | grep Shmem
Shmem:             12192 kB

slab SReclaimable SUnreclaim:

关于slab分配器的介绍参见: zhuanlan.zhihu.com/p/105582468 www.jianshu.com/p/95d68389f… slab的核心作用就是提高内核object的分配速度,提高内存利用率。

image.png

slab 统计slab分配器使用的内存数。 SReclaimable 统计slab分配器中可被回收的内存 SUnreclaim 统计slab分配器中不可被回收的内存

KernelStack

每一个用户线程拥有两个独立的栈,分别是位于用户虚拟空间的用户栈和内核虚拟空间的内核栈,而 KernelStack 统计内核栈的大小。

创建多线程程序如下:


#include <stdio.h>
#include <pthread.h>
/*
 * pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,
 * 在使用pthread_create创建线程时,在编译中要加-lpthread参数:
 * gcc createThread.c -lpthread -o createThread.o
 * ./createThread.o
 * 加上上面两句以后编译成功,并输出结果
 * */
#define NUM_Threads 100
 
// 线程的运行函数
void *PrintHello(void *arg)
{
    printf("Hello,World of Thread in C!\n");
    sleep(100);
    return 0;
}
 
int main()
{
    int i;
    int ret;
    // 定义线程的id变量,多个变量使用数组
    pthread_t tids[NUM_Threads];
 
    for (i=0; i<NUM_Threads; i++)
    {
        // 参数依次是: 创建的线程id,线程参数,调用的函数,传入的函数参数
       ret = pthread_create(&tids[i], NULL, PrintHello, NULL);
       if (ret != 0)
      {
          printf("pthread_create error: error_code = \n");
      }
    }
    // 等各个线程推出后,进程才结束
    pthread_exit(NULL);
    printf("exit\n");
    return 0;
}
[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cat /proc/meminfo | grep KernelStack
KernelStack:        2800 kB   # 线程未创建的时候
[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cat /proc/meminfo | grep KernelStack
KernelStack:        4400 kB   # 线程运行的时候
[root@iZ2zeam9jrqo0a6a7a6387Z ~]# cat /proc/meminfo | grep KernelStack
KernelStack:        2800 kB   # 线程提出的时候

每一个内核栈的大小固定为16K,因此在创建了100个线程的时候,KernelStack大小增加了1600k。

PageTables

Page Table用于将内存的虚拟地址翻译成物理地址,随着内存地址分配得越来越多,Page Table会增大。PageTables统计了Page Table所占用的内存大小。

NFS_Unstable

NFS_Unstable 统计发给 NFS server 但尚未写入硬盘的缓存页。

Bounce

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

WritebackTmp

CommitLimit Committed_AS

linux中的 memory overcommit机制详解参见:linuxperf.com/?p=102

memory overcommit机制的核心思想就是:操作系统允许进程申请大于实际可用的内存。一个进程实际使用的内存比它申请的内存要少,内存实际分配是在使用中通过缺页中断分配的,因此操作系统能够允许进程进行 memory overcommit。

当然,内核提供参数 vm.overcommit_memory 对memory overcommit机制进行开关操作。内核参数 vm.overcommit_memory 接受三种取值:

  • 0 – Heuristic overcommit handling. 这是缺省值,它允许overcommit,但过于明目张胆的overcommit会被拒绝,比如malloc一次性申请的内存大小就超过了系统总内存。Heuristic的意思是“试探式的”,内核利用某种算法(对该算法的详细解释请看文末)猜测你的内存申请是否合理,它认为不合理就会拒绝overcommit。
  • 1 – Always overcommit. 允许overcommit,对内存申请来者不拒。
  • 2 – Don’t overcommit. 禁止overcommit。

其中,当vm.overcommit_memory为2时,操作系统禁止memory overcommit机制,那么就需要知道申请内存时怎么算 overcommit。

  • CommitLimit:当申请的内存总数大于CommitLimit,内存申请判定为 overcommit机制。
  • Committed_AS: 所有进程已经申请的内存总大小。

VmallocTotal VmallocUsed VmallocChunk

在linux内核中,内核空间内存分配函数主要有如下:

  • kmalloc。kmalloc用于分配小块(小于page size)的物理地址上连续的内存,kmalloc基于slab分配器进行内存分配,效率较高。
  • vmalloc。vmalloc用于分配大块的虚拟地址上连续的内存。

虚拟地址空间详解参见:www.jianshu.com/p/52d4b2502… zhuanlan.zhihu.com/p/66794639

  • VmallocTotal:内核空间可动态分配的虚拟内存总大小
  • VmallocUsed: 内核空间已经使用的虚拟内存大小
  • VmallocChunk:

Percpu

HardwareCorrupted

HardwareCorrupted 统计系统检测到的故障页

AnonHugePages

AnonHugePages 统计系统透明大页的使用值
有关透明大页和标准大页的介绍参见:cloud.tencent.com/developer/a…

ShmemHugePages ShmemPmdMapped FileHugePages FilePmdMapped

ShmemHugePages 统计用于共享内存或者tmpfs的大页使用

HugePages_Total HugePages_Free HugePages_Rsvd HugePages_Surp Hugepagesize Hugetlb

该部分的指标都和大页相关,大页介绍参见:draveness.me/whys-the-de…

大页内存是独立于通常的4K页内存管理方式的,因此有关大页内存的统计指标和meminfo文件中的其他指标是独立统计。

# 未设置大页的指标
MemFree:         5867304 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

# 设置1000个大页
echo 1000 > /proc/sys/vm/nr_hugepages

# 设置了大页之后的指标
MemFree:         3819860 kB
HugePages_Total:    1000
HugePages_Free:     1000
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

可以看到,在设置了1000个大页之后,MemFree减少了2000M,这说明大页内存是独立统计的。

大页内存的使用主要有如下三种方式:

  1. 调用 mmap()函数时指定MAP_HUGETLB标志。 实验程序如下:
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<sys/time.h>

int main(int argc, char **argv) {
  char *m;
  size_t s = (800 * 1024 * 1024);
  
  m =(char* ) mmap(NULL, s, PROT_READ | PROT_WRITE,
        MAP_PRIVATE /*MAP_SHARED*/ | MAP_ANONYMOUS | MAP_HUGETLB, -1, 0);
  
  if (m == MAP_FAILED) {
    perror("map mem");
    m = NULL;
    return 1;
  }

  printf("申请800M大页成功!");
  
  printf("点击回车写入数据\n");
  getchar();


  for(int i=0;i<s/2;i+=4096){
    m[i]=0;
  }

  printf("点击回车释放大页\n");
  getchar();
  
 
  munmap(m, s);
  
  return 0;
}
# 实验前的指标
HugePages_Total:    1000
HugePages_Free:     1000
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

# 申请800M大页之后的指标
HugePages_Total:    1000
HugePages_Free:     1000
HugePages_Rsvd:      400
HugePages_Surp:        0
Hugepagesize:       2048 kB

# 使用大页之后的指标
HugePages_Total:    1000
HugePages_Free:      800
HugePages_Rsvd:      200
HugePages_Surp:        0
Hugepagesize:       2048 kB

## 释放大页之后的指标
HugePages_Total:    1000
HugePages_Free:     1000
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

通过观测发现,程序在申请了大页的时候,HugePages_Free并不会减少,只是HugePages_Rsvd的值增加了相应的申请数。当程序真实的使用大页时,HugePages_Free和HugePages_Rsvd都减少相应的使用数,在本例中,虽然申请了400个大页,但实际使用200个大页,因此HugePages_Free和HugePages_Rsvd都减少了200。释放大页之后,HugePages_Free和HugePages_Rsvd都变为实验之前的值。

  1. 通过shmget/shmat也可以使用Hugepages,调用shmget申请共享内存时要加上 SHM_HUGETLB 标志。

因此,大页内存相关的指标含义如下:

  • Hugepagesize:大页内存的页大小,默认情况下是2M,但有些系统支持1G的超大页
  • HugePages_Total:大页内存的总页数
  • HugePages_Free: 未被实际使用的页数
  • HugePages_Rsvd: 申请的但还未被使用的页数
  • HugePages_Surp: 申请数超过系统设定的常驻HugePages数目的数目
  • Hugetlb: 系统所有大页的统计值 kb为单位

关于 HugePages_Surp指标,这里再介绍下。一般来说,通过设置nr_hugepages参数来设置系统大页个数,该个数为常驻大页数目,当程序申请的大页数目超过常驻大页数目时,内存申请出错。但内核还提供nr_overcommit_hugepages参数,该参数表示程序申请的大页数目可超过常驻大页数目的最大数,nr_overcommit_hugepages默认为0。当设置了nr_overcommit_hugepages > 0时,如果程序申请的大页数目 >nr_hugepages,但< nr_hugepages + nr_overcommit_hugepages时,申请就会成功,此时HugePages_Surp的值就记录申请的大页数目超过nr_hugepages的值。

下面通过实验来观测HugePages_Surp,首先设置nr_hugepages为3,nr_overcommit_hugepages为2,并利用程序申请4个大页。

# 实验之前的指标
HugePages_Total:       3
HugePages_Free:        3
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

# 申请4个大页后的指标
HugePages_Total:       4
HugePages_Free:        4
HugePages_Rsvd:        4
HugePages_Surp:        1
Hugepagesize:       2048 kB

# 使用4个大页后的指标
HugePages_Total:       4
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        1
Hugepagesize:       2048 kB

# 释放4个大页后的指标
HugePages_Total:       3
HugePages_Free:        3
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB

DirectMap4k DirectMap2M DirectMap1G

DirectMap所统计的不是关于内存的使用,而是一个反映TLB效率的指标。TLB(Translation Lookaside Buffer)是位于CPU上的缓存,用于将内存的虚拟地址翻译成物理地址,由于TLB的大小有限,不能缓存的地址就需要访问内存里的page table来进行翻译,速度慢很多。为了尽可能地将地址放进TLB缓存,新的CPU硬件支持比4k更大的页面从而达到减少地址数量的目的, 比如2MB,4MB,甚至1GB的内存页,视不同的硬件而定。”DirectMap4k”表示映射为4kB的内存数量, “DirectMap2M”表示映射为2MB的内存数量,以此类推。所以DirectMap其实是一个反映TLB效率的指标。

参考资料

/PROC/MEMINFO之谜

UNEVICTABLE LRU INFRASTRUCTURE

为什么 Linux 需要 Swapping

Linux cache参数调优