前言
在排查线上服务器出现可用内存不足的问题时,首要做的就是掌握当前服务器内存的使用现状。了解服务器内存使用现状的主要接口之一就是/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: 剩余内存
- 内存黑洞:未能感知的内存
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的分配速度,提高内存利用率。
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,这说明大页内存是独立统计的。
大页内存的使用主要有如下三种方式:
- 调用 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都变为实验之前的值。
- 通过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效率的指标。