linux kernel内存管理之/proc/meminfo下参数介绍

一、前言

        /proc/meminfo是了解Linux系统内存状态的主要接口,里面统计了当前系统各类内存的使用状况,需要注意的是:这是从内核的角度来统计。我们常用的free,vmstat等指令都是通过/proc/meminfo来获取数据,并返还给指令输入者。/proc/meminfo统计的是系统全局的内存使用状况,如果需要看单个进程的内存情况可以在/proc//下。

二、meminfo打印函数meminfo_proc_show

2.1 meminfo_proc_show函数实现详解

//include/uapi/linux/sysinfo.h
struct sysinfo {
	__kernel_long_t uptime;		/* Seconds since boot 启动以来的时间,单位:s*/
 	__kernel_ulong_t loads[3];	/* 1, 5, and 15 minute load averages 过去1分钟,3分钟,15分钟的系统的平均负载*/
 	__kernel_ulong_t totalram;	/* Total usable main memory size 除去系统本身占用的内存,系统可支配的物理内存*/
 	__kernel_ulong_t freeram;	/* Available memory size 系统尚未使用的内存*/
 	__kernel_ulong_t sharedram;	/* Amount of shared memory 共享内存大小*/
 	__kernel_ulong_t bufferram;	/* Memory used by buffers 块设备所占用的内存*/
 	__kernel_ulong_t totalswap;	/* Total swap space size 系统swap空间总大小*/
 	__kernel_ulong_t freeswap;	/* swap space still available 系统swap空间空闲大小*/
 	__u16 procs;		   	/* Number of current processes */
 	__u16 pad;		   	/* Explicit padding for m68k */
 	__kernel_ulong_t totalhigh;	/* Total high memory size high zone的物理内存大小,在64位系统中由于不存在high zone,故为0,32位系统中根据实际情况*/
 	__kernel_ulong_t freehigh;	/* Available high memory size high zone可以使用的空闲页数目*/
 	__u32 mem_unit;			/* Memory unit size in bytes 1 page的大小,一般是4k,注意上述内存单位统一都是page*/
 	char _f[20-2*sizeof(__kernel_ulong_t)-sizeof(__u32)];	/* Padding: libc5 uses this.. */
 };

//mm/page_alloc.c
void si_meminfo(struct sysinfo *val)
{
	val->totalram = totalram_pages();//kernel-5.4,kernel-4.19直接是totalram_pages变量,实际上内核通过_totalram_pages原子变量记录总的内存
	val->sharedram = global_node_page_state(NR_SHMEM);//得到共享内存
	val->freeram = global_zone_page_state(NR_FREE_PAGES);//得到系统尚未使用的内存
	val->bufferram = nr_blockdev_pages();//块设备所占用的内存
	val->totalhigh = totalhigh_pages();//high zone的物理内存大小,在64位系统中由于不存在high zone,故为0,32位系统中根据实际情况
	val->freehigh = nr_free_highpages();//high zone部分可以使用的空闲页数目
	val->mem_unit = PAGE_SIZE;//4k
}

//include/linux/mmzone.h
/*
 * We do arithmetic on the LRU lists in various places in the code,
 * so it is important to keep the active lists LRU_ACTIVE higher in
 * the array than the corresponding inactive lists, and to keep
 * the *_FILE lists LRU_FILE higher than the corresponding _ANON lists.
 *
 * This has to be kept in sync with the statistics in zone_stat_item
 * above and the descriptions in vmstat_text in mm/vmstat.c
 */
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
	LRU_UNEVICTABLE,
	NR_LRU_LISTS
};

//fs/proc/meminfo.c
static void show_val_kb(struct seq_file *m, const char *s, unsigned long num)
{
//num << (PAGE_SHIFT - 10)左移2位,乘以4,将page转换为KB
	seq_put_decimal_ull_width(m, s, num << (PAGE_SHIFT - 10), 8);
	seq_write(m, " kB\n", 4);
}

//正主在这里
//meminfo信息的主函数,这个函数里面会调用show_val_k来输出各种内存状况信息
static int meminfo_proc_show(struct seq_file *m, void *v)
{
	struct sysinfo i;
	unsigned long committed;
	long cached;
	long available;
	unsigned long pages[NR_LRU_LISTS];
	unsigned long sreclaimable, sunreclaim;
	int lru;
//对结构体sysinfo里面的主要参数进行赋值,后面调用show_val_kb输出内存状态信息需要使用
	si_meminfo(&i);
	si_swapinfo(&i);

//保存了当前系统中已经申请(包括本次)的virtual memory的数目
	committed = percpu_counter_read_positive(&vm_committed_as);

//得到普通文件所占用的内存大小,NR_FILE_PAGES表示所有page cache的总和,需要减去块设备占用的page cache和swapcache(这是内存与交换区设备的"中间层",类似pagecache)
	cached = global_node_page_state(NR_FILE_PAGES) -
			total_swapcache_pages() - i.bufferram;
	if (cached < 0)
		cached = 0;
//遍历LRU list(如前面enum 枚举结构体),得到对应类型的LRU的page数目
	for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++)
		pages[lru] = global_node_page_state(NR_LRU_BASE + lru);

//得到系统可用内存大小
	available = si_mem_available();
//得到系统slab可回收和不可回收内存的内存大小
	sreclaimable = global_node_page_state(NR_SLAB_RECLAIMABLE);
	sunreclaim = global_node_page_state(NR_SLAB_UNRECLAIMABLE);

//开始打印输出内存状态信息
	show_val_kb(m, "MemTotal:       ", i.totalram);
	show_val_kb(m, "MemFree:        ", i.freeram);
	show_val_kb(m, "MemAvailable:   ", available);
	show_val_kb(m, "Buffers:        ", i.bufferram);
	show_val_kb(m, "Cached:         ", cached);
	show_val_kb(m, "SwapCached:     ", total_swapcache_pages());
//在前面的遍历LRU list时,已经计算得到,保存在局部数组page中
	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]);
	show_val_kb(m, "Mlocked:        ", global_zone_page_state(NR_MLOCK));

 #ifdef CONFIG_HIGHMEM
	show_val_kb(m, "HighTotal:      ", i.totalhigh);
	show_val_kb(m, "HighFree:       ", i.freehigh);
	show_val_kb(m, "LowTotal:       ", i.totalram - i.totalhigh);
	show_val_kb(m, "LowFree:        ", i.freeram - i.freehigh);
 #endif

 #ifndef CONFIG_MMU
	show_val_kb(m, "MmapCopy:       ",
		    (unsigned long)atomic_long_read(&mmap_pages_allocated));
 #endif

	show_val_kb(m, "SwapTotal:      ", i.totalswap);
 	show_val_kb(m, "SwapFree:       ", i.freeswap);
 	show_val_kb(m, "Dirty:          ",
 		    global_node_page_state(NR_FILE_DIRTY));
 	show_val_kb(m, "Writeback:      ",
 		    global_node_page_state(NR_WRITEBACK));
 	show_val_kb(m, "AnonPages:      ",
 		    global_node_page_state(NR_ANON_MAPPED));
 	show_val_kb(m, "Mapped:         ",
 		    global_node_page_state(NR_FILE_MAPPED));
 	show_val_kb(m, "Shmem:          ", i.sharedram);
 	show_val_kb(m, "KReclaimable:   ", sreclaimable +
 		    global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE));
 	show_val_kb(m, "Slab:           ", sreclaimable + sunreclaim);
 	show_val_kb(m, "SReclaimable:   ", sreclaimable);
 	show_val_kb(m, "SUnreclaim:     ", sunreclaim);
 	seq_printf(m, "KernelStack:    %8lu kB\n",
 		   global_zone_page_state(NR_KERNEL_STACK_KB));
 #ifdef CONFIG_SHADOW_CALL_STACK
 	seq_printf(m, "ShadowCallStack:%8lu kB\n",
 		   global_zone_page_state(NR_KERNEL_SCS_BYTES) / 1024);
 #endif
 	show_val_kb(m, "PageTables:     ",
 		    global_zone_page_state(NR_PAGETABLE));
 #ifdef CONFIG_QUICKLIST
 	show_val_kb(m, "Quicklists:     ", quicklist_total_size());
 #endif
 
 	show_val_kb(m, "NFS_Unstable:   ",
 		    global_node_page_state(NR_UNSTABLE_NFS));
 	show_val_kb(m, "Bounce:         ",
 		    global_zone_page_state(NR_BOUNCE));
 	show_val_kb(m, "WritebackTmp:   ",
 		    global_node_page_state(NR_WRITEBACK_TEMP));
 	show_val_kb(m, "CommitLimit:    ", vm_commit_limit());
 	show_val_kb(m, "Committed_AS:   ", committed);
 	seq_printf(m, "VmallocTotal:   %8lu kB\n",
 		   (unsigned long)VMALLOC_TOTAL >> 10);
 	show_val_kb(m, "VmallocUsed:    ", vmalloc_nr_pages());
 	show_val_kb(m, "VmallocChunk:   ", 0ul);
 	show_val_kb(m, "Percpu:         ", pcpu_nr_pages());
 
 #ifdef CONFIG_MEMORY_FAILURE
 	seq_printf(m, "HardwareCorrupted: %5lu kB\n",
 		   atomic_long_read(&num_poisoned_pages) << (PAGE_SHIFT - 10));
 #endif
 
 #ifdef CONFIG_TRANSPARENT_HUGEPAGE
 	show_val_kb(m, "AnonHugePages:  ",
 		    global_node_page_state(NR_ANON_THPS) * HPAGE_PMD_NR);
 	show_val_kb(m, "ShmemHugePages: ",
 		    global_node_page_state(NR_SHMEM_THPS) * HPAGE_PMD_NR);
 	show_val_kb(m, "ShmemPmdMapped: ",
 		    global_node_page_state(NR_SHMEM_PMDMAPPED) * HPAGE_PMD_NR);
 #endif
 
 #ifdef CONFIG_CMA
 	show_val_kb(m, "CmaTotal:       ", totalcma_pages);
 	show_val_kb(m, "CmaFree:        ",
 		    global_zone_page_state(NR_FREE_CMA_PAGES));
 #endif
 
 #if defined(FEATURE_HEALTHINFO) && defined(CONFIG_ION)
 	show_val_kb(m, "IonTotalCache:   ", global_zone_page_state(NR_IONCACHE_PAGES));;
 	show_val_kb(m, "IonTotalUsed:   ", ion_total() >> PAGE_SHIFT);
 #endif 
 #ifdef FEATURE_HEALTHINFO
 	show_val_kb(m, "GPUTotalUsed:  ", gpu_total() >> PAGE_SHIFT);
 #endif 
//这里打印hugepage的信息
 	hugetlb_report_meminfo(m);
 
 	arch_report_meminfo(m);
 
 	return 0;
}

//proc_meminfo的初始化,可以看到调用了meminfo_proc_show
 static int __init proc_meminfo_init(void)
{
	proc_create_single("meminfo", 0, NULL, meminfo_proc_show);
	return 0;
}
fs_initcall(proc_meminfo_init);

三、meminfo下各相关参数介绍

        如下是手机里面cat /proc/meminfo输出的内存状况信息。由于有些内存信息打印未使能,所以未打印,同时增加了Android手机特有的ion内存状况的打印。

User:/ # cat /proc/meminfo
cat /proc/meminfo
MemTotal:        7570916 kB
MemFree:          221820 kB
MemAvailable:    4699384 kB
Buffers:           14280 kB
Cached:          4655300 kB
SwapCached:        48232 kB
Active:          2825544 kB
Inactive:        2688788 kB
Active(anon):     802732 kB
Inactive(anon):   196828 kB
Active(file):    2022812 kB
Inactive(file):  2491960 kB
Unevictable:      142216 kB
Mlocked:          142216 kB
SwapTotal:       5242876 kB
SwapFree:        4317088 kB
Dirty:               512 kB
Writeback:             0 kB
AnonPages:        968372 kB
Mapped:          1245556 kB
Shmem:             14464 kB
KReclaimable:     314272 kB
Slab:             422852 kB
SReclaimable:     118064 kB
SUnreclaim:       304788 kB
KernelStack:       70992 kB
PageTables:       105056 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:    12174056 kB
Committed_AS:   140385476 kB
VmallocTotal:   263061440 kB
VmallocUsed:      163212 kB
VmallocChunk:          0 kB
Percpu:            10592 kB
CmaTotal:         262144 kB
CmaFree:              76 kB
IonTotalCache:     174480 kB
IonTotalUsed:     275708 kB
GPUTotalUsed:     137996 kB

MemTotal

        上面提到,/proc/meminfo给出的是内核统计的内存信息,其和实际的物理内存信息是有差异的,MemTotal表示内存管理系统管理的总物理内存大小,比如当前设备配置的是8G的内存,但是其输出的总内存只有7570916 kB,这是因为系统从上电开始到引导完成,firmware/BIOS要保留一些内存,kernel本身要占用一些内存,最后剩下可供kernel支配的内存就是MemTotal。这个值在系统运行期间一般是固定不变的。如果设备发生重启,这个值可能会发生变化。

MemFree

        表示系统尚未使用的内存大小。【MemTotal-MemFree】就是已被用掉的内存。

MemAvailable

        表示系统可用内存大小,该参数跟MemFree是有区别的。MemFree表示的是系统尚未使用的内存,但不代表全部可用的内存。系统中有些内存虽然已经被使用,但是可以回收的,这些内存也算是可用的内存,比如cache、buffer、slab都有一部分内存可以回收,将这部分可回收的内存+MemFree才是MemAvailable。内核采用一种可用内存估算算法来得到MemAvailable。由于是个估计值,因此并不精确,不过考虑到系统内存本身就是一个动态调整的过程,实际上也不需要特别精确。下面这个是估算的函数si_mem_available。

//include/linux/mmzone.h
/*
 * We do arithmetic on the LRU lists in various places in the code,
 * so it is important to keep the active lists LRU_ACTIVE higher in
 * the array than the corresponding inactive lists, and to keep
 * the *_FILE lists LRU_FILE higher than the corresponding _ANON lists.
 *
 * This has to be kept in sync with the statistics in zone_stat_item
 * above and the descriptions in vmstat_text in mm/vmstat.c
 */
#define LRU_BASE 0
#define LRU_ACTIVE 1
#define LRU_FILE 2

enum lru_list {
	LRU_INACTIVE_ANON = LRU_BASE,
	LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
	LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
	LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
	LRU_UNEVICTABLE,
	NR_LRU_LISTS
};

//mm/page_alloc.c
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;
//遍历LRU list,得到对应类型的LRU的page数目,单位:page
	for (lru = LRU_BASE; lru < NR_LRU_LISTS; lru++)
		pages[lru] = global_node_page_state(NR_LRU_BASE + lru);

//遍历zonelist,累加每个zone的Low watermark值
	for_each_zone(zone)
		wmark_low += low_wmark_pages(zone);

	/*
	 * Estimate the amount of memory available for userspace allocations,
	 * without causing swapping.
	 */
//对于totalreserve_pages的计算方式可以看mm/page_alloc.c的calculate_totalreserve_pages函数
//global_zone_page_state(NR_FREE_PAGES)实际就是MemFree
//1、计算有多少空闲page frame,同时要减去在系统严重内存不足时,自己尝试解决内存不足时需要预留的page frame的数目
	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.
	 */
//得到page cache,包括:cached,swapcached, buffers,单位:page
	pagecache = pages[LRU_ACTIVE_FILE] + pages[LRU_INACTIVE_FILE];
//并不是所有的pagecache都可以被释放,否则若全部释放,系统只有swap in/swap out操作,没有page in/page out了,
//必须保留一半或者low watermark值,所以这里要减去
	pagecache -= min(pagecache / 2, wmark_low);
//2、加上在减去必须的文件页后,剩余的文件页
	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可回收部分包括slab中可回收的page数目和非slab中可回收的page数目,实际上就对应KReclaimable
	reclaimable = global_node_page_state(NR_SLAB_RECLAIMABLE) +
			global_node_page_state(NR_KERNEL_MISC_RECLAIMABLE);
//同样要考虑预留一部分
//3、加上在减去预留部分后,可回收的slab和非slab内核页,最终得到总的available
	available += reclaimable - min(reclaimable / 2, wmark_low);

//特殊情况处理,若小于0,则直接返回0
	if (available < 0)
		available = 0;
	return available;
}
EXPORT_SYMBOL_GPL(si_mem_available);

Buffers

      表示块设备所占用的内存大小,包括:直接读写块设备、以及文件系统元数据(metadata),比如SuperBlock所使用的缓存页(page cache)。因为Buffers所占的内存属于page cache的一种,所以也在LRU list中,被统计在Active(file)或Inactive(file)。

Cached

        表示系统用户空间所有普通文件页(file page,也被成为file-backed page)的大小,包括用户进程正在使用(Mapped)和未在使用(ummaped)的文件页。

SwapCached

        表示内存与交换区设备的”中间层“的内存,用于匿名页换入换出时的缓存。内存中匿名页在放入到交换区之前,会在SwapCached中建立一个临时存储的地方,不过这个匿名页很快的就刷入到交换区设备,然后从SwapCached中删除。另外,当匿名页从交换区设备放入内存时,也会临时放入到SwapCached中。一般来说交换区可以包括一个或多个交换区设备,比如:裸盘、逻辑卷、文件等。每个交换区设备都对应自己的swap cache,跟page cache类似,page cache对应的是一个个文件,swap cache对应的是一个个交换区设备,kernel管理swap cache与管理page cache一样,用的都是radix-tree,唯一的区别是:page cache与文件的对应关系在打开文件时就已经确定,而一个匿名页只有在即将被swap-out(即将匿名页回写到交换区设备的操作)的时候才决定它会被放到哪一个交换区设备,即匿名页与swap cache的对应关系在即将被swap-out时才确立。SwapCached跟Cached的统计不会重叠,对于Shmem,未进行swap in/swap out时,是属于Cached的。一旦进行,就属于SwapCached。

        需要注意的是:Linux中存在两种形式的swap分区:swap disk和swap file。前者是一个专用于做swap的块设备,作为裸设备提供给swap机制操作;后者则是存放在文件系统上的一个特定文件,其实现依赖于不同的文件系统。

        手机现在一般都有内存拓展功能(即设备存储空间充足时,将部分存储空间拓展为同等大小的运行内存),这个功能一般都是先将运行内存中一些非关键应用和进程进行zram压缩,然后再swap到这个拓展的存储空间中。这个被用来拓展的存储空间相当就是个swap分区。因为是专用的,属于swap disk。

linux kernel内存管理之/proc/meminfo下参数介绍_第1张图片

         如下是统计swapcache的代码实现,需要注意的是:MAX_SWAPFILES包括了swap disk和swap file。

unsigned long total_swapcache_pages(void)
{
	unsigned int i, j, nr;
	unsigned long ret = 0;
	struct address_space *spaces;

	rcu_read_lock();
//遍历系统所有的swap file,找出对应交换区设备对应的swap cache的大小,累计到ret中,然后返回
	for (i = 0; i < MAX_SWAPFILES; i++) {
		/*
		 * The corresponding entries in nr_swapper_spaces and
		 * swapper_spaces will be reused only after at least
		 * one grace period.  So it is impossible for them
		 * belongs to different usage.
		 */
		nr = nr_swapper_spaces[i];
		spaces = rcu_dereference(swapper_spaces[i]);
		if (!nr || !spaces)
			continue;
		for (j = 0; j < nr; j++)
			ret += spaces[j].nrpages;
	}
	rcu_read_unlock();
	return ret;
}

Active

        表示Active list中所有page占用的内存大小(包括active(file)和active(anon))。Active list里面包含的都是最近被访问过的内存页。这里大致描述一下LRU。LRU是kernel的页面回收算法(Page Frame Reclaiming Algorithm)所使用的数据结构,LRU是Least Recently Used的缩写词,这个算法的核心思想就是:kernel要回收的页面应是最近使用的最少的。为实现这个目的,kernel利用list,把刚访问过的页面放在list头部,越接近list尾部,说明未被访问的时间越长,这样通过在list中相对位置就可以知道页面上次被访问时间距离现在的长短。kernel 设计了三种LRU list:Active list,Inactive list和Unevictable list。其中,刚访问过的页面放进Active list,长时间未访问过的页面放进Inactive list,这样从Inactive list回收页面就变得简单了,同时内核线程kswapd会周期性地把Active list中符合条件的页面移到Inactive list中,这项转移工作是由refill_inactive_zone()完成,如下图所示。Unevictable list里面page因为被上锁了,不能page out/swap out(即不能回收),包括VM_LOCKED的内存页、SHM_LOCK的共享内存页(又被统计在”Mlocked”中)和ramfs。

        对于LRU list这三种类型的里面的page,主要分为file page(文件页)和anon page(匿名页)。因而内核中,实际上管理着5种链表:LRU_INACTIVE_ANON,LRU_ACTIVE_ANON,LRU_INACTIVE_FILE,LRU_ACTIVE_FILE和LUR_UNEVITABLE,定义在include/linux/mmzone.h,感兴趣的看《Linux 物理内存管理涉及的三大结构体之struct pglist_data》的2.23节和《https://mp.csdn.net/mp_blog/creation/editor/130512059?not_checkout=1》的6.2节,关于LRU有相对更详细的介绍

        其中file page是进程的代码、映射的文件对应的page,而这些代码,映射的文件直接就保存在磁盘,被加载到内存,所以可回收后,需要的话直接从磁盘重新读取即可。anon page是应用程序动态分配的heap,stack对应的page,由于可能再次被访问,当然就不能直接回收,直接释放。

        file page在内存不足的时候可以直接写回对应的硬盘文件里,称为page-out,不需要用到交换区(swap);而anon page在内存不足时就只能写到硬盘上的交换区(swap)里,称为swap-out。由于anon page主要是用户进程的heap,stack对应的page,所以用户进程exit,那么对应的anon page就会被释放,不像file page,即使跟进程不关联了,也还可以缓存,保存Cached中。

        注意:(1)Active/inactive memory是针对用户进程所占用的内存而言的,内核占用的内存(包括slab)不在其中。(2)Page cache和所有用户进程的内存(kernel stack和huge pages除外)都在LRU lists上。

Inactive

        表示Inactive list中所有page占用的内存大小(包括Inactive(file)和Inactive(anon))。

Active(anon)

        表示Active list中所有匿名页占用的内存大小。

Inactive(anon)

         表示Inactive list中所有匿名页占用的内存大小。

Active(file)

        表示Active list中所有文件页占用的内存大小。

Inactive(file)

         表示Inactive list中所有文件页占用的内存大小。

Unevictable

        表示Unevictable list中所有page占用的内存大小(包括匿名页和文件页)。

Mlocked

        表示被系统调用函数mlock()锁住的物理内存大小。被锁定的内存因为不能page out/swap out,会从Active/Inactive list移到Unevictable list上。注意:Mlocked并不是独立的内存空间,它与以下统计项重叠:Unevictable,AnonPages,Shmem,Mapped,Cached等。在msm-kernel/mm/mlock.c中有相应的mlock系列函数。

        注意点:1、我们如果要使用mlock成功锁住内存,需至于仅分配内存并立即调用 mlock 并不会为调用进程锁定这些内存,因为对应的分页可能是写时复制(copy-on-write)的。因此,你应该在每个页面中写入一个假的值,此时,才会给进程分配实际的物理内存,这次mlock才起作用。参考代码如下。

const int alloc_size = 32 * 1024 * 1024; 
char* memory = malloc (alloc_size); 
//mlock (memory, alloc_size); 直接在这里mlock是存在无法锁住内存的,当是写时赋值时

size_t i; 
size_t page_size = getpagesize (); 
for (i = 0; i < alloc_size; i += page_size)
{
	memory[i] = 0;
}

mlock (memory, alloc_size);//在这里就可以100%锁住指定物理内存范围内,对应的page frame

        2、 Pinned Memory和Page-Locked Memory区别:Pinned Memory指物理地址不可变的pages,相当于虚拟地址对应的物理地址不变。Page-Locked Memory指通过mlock()系统调用锁定的物理内存,相当于进程要的page一定在物理内存中,不能swap out/page out,但是这些page是可以迁移,相当于虚拟地址保持不变,物理地址可变(因为可迁移),但一定在物理内存中,不可写回到硬盘中。

HighTotal/HighFree/LowTotal/LowFree

        在32位系统中,存在内核虚拟内存不足(只有1G)的情况,从而有了highmem/lowmem定义。但是在64位系统中,不存在这种情况,不会有这种定义,且CONFIG_HIGHMEM默认不使能,故这里不讨论。

MmapCopy

        表示在未使能MMU情况下,系统分配给用于memory map的内存大小。不过CONFIG_MMU默认是使能的,而且正常系统也是都使用MMU的。

SwapTotal

        表示系统swap总的内存大小。swap的两个内存状态信息通过meminfo_proc_show->si_swapinfo函数得到。每次初始化一个新的swap时, 就会累计式更新nr_swap_pages和total_swap_pages两个全局变量。当swap in/swap out时会更新nr_swap_pages。对于局部变量nr_to_be_unused,根据代码实现,是flag状态为SWP_USED且不是SWP_WRITEOK情况下的inuse_pages(正在使用的page数目)。nr_to_be_unused变量名中可以看出,这统计的是将要变成unused的swap page,这种情况是存在的,比如,在sys_swapoff()函数中(此函数是关闭swap区时有系统调用触发),首先会将swap状态置为SWP_USED,而在关闭swap区时要调用try_to_unuse()函数将此区中所有的inuse page换入RAM,这就需要从init_mm内存描述符开始,访问所有内核线程和进程的地址空间。这是一个相当耗时的操作,因此如果在try_to_unuse()执行期间,si_swapinfo()函数进行采集swap信息,那么swap的状态必然为SWP_USED。困惑的是:这个inuse_pages参数里面的page为什么不在total_swap_pages里面,需要单独计算??

SwapFree

        表示系统swap空闲内存大小。等于atomic_long_read(&nr_swap_pages) + nr_to_be_unused。

Dirty

        表示等待被写回磁盘的脏页占用的内存大小,但并没有包含系统中全部dirty page。

Writeback

        表示正在被写回磁盘的脏页占用的内存大小,这些页也是脏页。从这两个参数也可以看出,系统中的总的dirty page应该包括:Dirty,NFS_Unstable和Writeback。Dirty表示等待被写回磁盘的page,NFS_Unstable表示发给NFS server但尚未写入硬盘的page,Writeback表示正在写回硬盘的page。注意:(1)NFS_Unstable的内存被包含在Slab中,因为nfs request内存是调用kmem_cache_zalloc()申请的。(2)anon page不属于dirty page。根据mm/vmscan.c: page_check_dirty_writeback()里面的条件判断,可知anon page不属于Dirty和Writeback,所以anon page不是脏页。

AnonPages

        前面说过用户进程所占内存页可以分为文件页和匿名页,这里表示用户进程正在使用的所有匿名页占用内存的大小,由此也可知,其跟Cached是不相交的。注意:(1)shared memory 不属于 Anonpages,而是属于Cached,因为shared memory基于tmpfs文件系统实现的。(2)mmap private anon page属于AnonPages,而mmap shared anonymous pages属于Cached(file-backed pages),因为shared anonymous mmap也是基于tmpfs的。

Mapped

        表示用户进程正在使用的普通文件页占用的内存大小,比如:进程对应的so、shared libraries、可执行程序的文件和mmap的文件等,一旦进程内存被回收,就不会算到Mapped。Mapped算是Cached的子集。因为Linux系统上shared memory & tmpfs被计入Cached,所以被attached的shared memory、以及tmpfs上被map的文件都算做Mapped。结合AnonPages,系统所有进程的PSS内存之和理论上应该等于AnonPages + Mapped。

Shmem

        表示shared memory(共享内存)、tmpfs文件系统和devtmpfs文件系统占用的内存大小(统计的是已经分配的大小而不是进程申请的大小)文件系统中的文件,由于不是直接与硬盘映射,所以对其进行换入换出时需要swap分区,这行为和匿名页很类似,故这些页也被放入到Active(anon)/Inactive(anon)中。注意:虽然要用到swap,但是依然是文件页,它们不会被记录到AnonPages。因此,Shmem与Mapped、Cached统计有重叠,与AnonPages没有重叠。同时devtmpfs是/dev文件系统的类型,/dev/下所有的文件占用的空间也属于Shmem。

KReclaimable

        表示部分内核态可被回收的内存大小,包括slab和非slab可回收内核page。从前面知道,在计算MemAvailable时使用了这部分内存的一部分。

Slab

        表示用于slab的所有内存大小,包括slab可回收部分和不可回收部分。

SReclaimable

        表示slab中可回收部分的内存大小。调用kmem_getpages()时,判断kmem_cache的flag是否是SLAB_RECLAIM_ACCOUNT标记,如果是,则会将对应struct page标记为可回收的。

SUnreclaim

        表示slab中不可回收部分的内存大小。

KernelStack

        表示内核栈内存大小,包括内核态进程自身使用的栈空间和线程的内核栈空间。内核栈是内存是常驻的,这部分内存不计算到LRU中,也不包括进程的PSS/RSS内存中。每个用户线程都会分配一个kernel stack(内核栈),内核栈虽然属于线程,但是用户态的代码无法直接访问,只能通过syscall,trap或者exception进入内核态才能使用,相当于内核栈是给kernel层代码使用的,所以我们一般认为这部分是kernel消耗的内存。

PageTables

        表示page table(PTE)占用的内存大小。PTE用于将内存的虚拟地址翻译成物理地址,随着内存地址分配的越多,PTE会增大。注意:Page Table(页表)与Page Frame(页帧)是不一样的。物理内存的最小单位是page frame,每个物理页对应一个描述符(struct page),而且在内核的启动引导阶段就会配好,保存在mam_map数组中,这个数组占用的内存最后是会被统计在dmesg显示的reserved中,不包含在MemTotal中。在NUMA系统上,可能会有多个mem_map数组,在node_data中或mem_section中。而对于Page Table,内存大小是会动态变化的,要从MemTotal中消耗内存。

NFS_Unstable

         表示已经写入NFS server但尚未写入硬盘的page cache占用的内存大小,由于NFS request的内存是调用kmem_cache_zalloc()申请的,因此,NFS_Unstable的内存被包含在Slab中。默认是0,因为正常是没有NFS server的。

Bounce

        出于保持兼容性,适配老设备的内存,考虑到部分老设备只能访问低端内存,比如16M以下的内存。如果用户进程发出一个I/O请求,但是目的地址是16M以上的,此时内核会在低端内存中分配一个临时buffer作为跳转,将数据拷贝到此处,多个请求就会有多个临时buffer。这种额外的数据拷贝被称为“bounce buffering”,显然这个操作会降低I/O 性能。因此,Bounce表示在低端内存中针对高端内存访问用于跳转的临时buffer占用的内存大小。不过当前的设备,一般默认是0,不用过多考量这个。

WritebackTmp

        表示FUSE用于temporary writeback buffers而占用的内存大小。FUSE是Filesystem in Userspace的缩写,是一种用户空间文件框架,其文件系统的核心逻辑是在用户空间实现的,由内核模块(fuse.ko),用户空间库(libfuse.*)和挂载实用程序(fusermount)组成。由于其主要实现代码位于用户空间中,因而不需要重新编译内核,相对便利,但同样存在访问路径长,需要两个态之间频繁切换;IO吞吐量较低;增加了数据的copy。Android 12是已经支持该功能了,但是默认不会开启,因此为0。

CommitLimit

        表示系统允许所有用户进程申请的最大内存大小,实际上就是判断overcommit的阈值,在Linux kernel内存管理之overcommit相关参数中有详细介绍。

Committed_AS

        表示当前系统中已经申请(包括本次)的虚拟内存大小,跟CommitLimit是配对的。由于手机设置了默认可以overcommit,所以Committed_AS是大于CommitLimit。

VmallocTotal

        表示内核空间中vmalloc 虚拟地址空间的大小,值为(VMALLOC_END - VMALLOC_START)。当前64位内核系统,这里有260多G。

VmallocUsed

        表示内核通过vmalloc申请的虚拟内存大小,注意:仅仅涉及申请或者映射,并未涉及到物理内存的具体分配,因此该数值不代表实际物理内存消耗。

VmallocChunk

        表示vmalloc区域最大连续空闲块的大小。这个默认直接是0.

Percpu

        表示用于percpu分配的的内存大小,不包括metadata。

HardwareCorrupted

        当系统检测到内存的硬件故障时,会把有问题的页面删除掉,不再使用,HardwareCorrupted表示kernel标记为已经损坏的且被删除掉的内存页的总大小。默认不使能CONFIG_MEMORY_FAILURE,所以不会显示这部分。相应的代码参见 mm/memory-failure.c: memory_failure()。

AnonHugePages

        使能了CONFIG_TRANSPARENT_HUGEPAGE,才有包括下面两个,共三个关乎hugepage的内存参数显示。AnonHugePages表示透明大页中匿名页占用的内存大小。正常默认是关闭TRANSPARENT_HUGEPAGE的,特别是在嵌入式系统中。透明大页允许内核尽可能对应用程序透明地使用大页和大TLB。 此功能可以通过加速内存分配期间的page fault、减少TLB未命中的数量和加快页表遍历来提高某些应用程序的计算性能。

        注意:AnonHugePages统计的是Transparent HugePages (THP),THP与Hugepages不是一回事,区别很大。Hugepages在/proc/meminfo中是被独立统计的(调用函数hugetlb_report_meminfo来统计),与其它统计项不重叠,既不计入进程的RSS/PSS中,又不计入LRU Active/Inactive,也不会计入cache/buffer。如果进程使用了Hugepages,它的RSS/PSS不会增加。而AnonHugePages与/proc/meminfo的其他统计项有重叠,首先它被包含在AnonPages之中,而且在/proc//smaps中也有单个进程的统计,与进程的RSS/PSS是有重叠的。如果用户进程用到了THP,进程的RSS/PSS也会相应增加。

        THP也可用于shared memory和tmpfs,缺省是禁止的,打开方法有两种(详见 https://www.kernel.org/doc/Documentation/vm/transhuge.txt):1、mount时加上”huge=always”等选项;2、通过/sys/kernel/mm/transparent_hugepage/shmem_enabled来控制。因为缺省情况下shared memory和tmpfs不使用THP,所以进程之间不会共享AnonHugePages,于是就有以下等式:【/proc/meminfo的AnonHugePages】==【所有进程的/proc//smaps中AnonHugePages之和】

ShmemHugePages

        表示用于shared memory或tmpfs的透明大页。

ShmemPmdMapped

        表示用于用户态shared memory映射的透明大页。

CmaTotal

        表示系统预留给CMA(Contiguous Memory Allocator,连续内存分配器)用于分配的内存大小。CMA是内存管理子系统中的一个模块,负责物理地址连续的内存分配。一般系统会在启动过程中,从整个memory中配置一段连续物理内存用于CMA,然后内核其他的模块可以通过CMA的接口API进行连续内存的分配。CMA的核心并不是设计精巧的算法来管理地址连续的内存块,实际上它的底层还是依赖伙伴系统,或者说CMA是处于需要连续内存块的其他内核模块(例如DMA mapping framework)和内存管理模块之间的一个中间层模块。

CmaFree

        表示在CMA reserve中剩余还未被分配的内存大小。

IonTotalCache

        表示系统分配给ION用于分配和共享内存时,可使用的cache大小。ION是Google在Android 4.0开始引入的一种内存管理机制,在多媒体部分中使用的最多。它可以在用户空间的进程之间或者内核空间的模块之间进行内存共享,而且这种共享可以是零拷贝的。

IonTotalUsed

        表示已经使用的cache大小。

GPUTotalUsed

        表示GPU使用的内存大小。

四、Linux内存分配地图

        根据上面介绍和分析,可以大概得到如下一张内存分布的地图,显示了/proc/meminfo下,各类型内存的分布和对应关系。

linux kernel内存管理之/proc/meminfo下参数介绍_第2张图片

          下面是总结的一些内存类型之间的关系,可供参考。

MemAvailable:
1、MemFree + (cache、buffer和slab中可回收的内存)

Buffers:
1、属于page cache的一种,所以肯定会被统计在Active(file)或Inactive(file)中
2、它与Cached不同,所以与Cached不会重叠

Cached:
1、表示系统所有普通文件页(file page,也被成为file-backed page),包括shmem
2、Cached包括Mapped+unmaped 

SwapCached:
1、跟Cached的统计不会重叠
2、对于Shmem,未进行swap in/swap out的部分,是属于Cached的。进行的部分,就属于SwapCached。
3、SwapCached依托swap,而swap是给匿名页使用的,所以当发生swap out/swap in时,SwapCached会与AnonPages有重叠

VmallocUsed
1、通过vmalloc分配的内存,还统计了VM_IOREMAP、VM_MAP等操作的值

Kernel stack(内核栈)
1、是常驻内存的,既不包括在LRU lists里,也不包括在进程的RSS/PSS内存里

AnonHugePages
1、包含在AnonPages中,
2、如果进程使用了THP,则与进程的RSS/PSS是有重叠的

LRU:
1、LRU中不包含HugePages_*。
2、LRU包含了 Cached 和 AnonPages。
3、Unevictable包括VM_LOCKED的内存页、SHM_LOCK的共享内存页,其中SHM_LOCK的共享内存页也是包含在Mlocked

Active:
1、Active list中所有page占用的内存大小(包括Active(anon)和Active(file))
2、所以显然这部分跟Buffers、Cached、Shmem、AnonPages、Mapped等都有重叠

Inactive:
1、Inactive list中所有page占用的内存大小(包括Inactive(anon)和Inactive(file))
2、显然这部分跟Buffers、Cached、Shmem、AnonPages、Mapped等都有重叠

Unevictable:
1、Unevictable list中所有page占用的内存大小(包括匿名页和文件页)
2、显然这部分跟Buffers、Cached、Shmem、AnonPages、Mapped等都有重叠

SwapTotal:
1、表示系统swap总的内存大小
2、除了跟SwapFree有联系,跟其他内存没有重叠

SwapFree:
1、表示系统swap空闲内存大小
2、显然SwapFree包含在SwapTotal里面

Dirty:
1、等待被写回磁盘的脏页占用的内存大小
2、跟NFS_Unstable和Writeback不重叠
3、这个脏页并不包括系统全部dirty page,真正得系统全部dirty page = Dirty+NFS_Unstable+Writeback
4、anon page不属于脏页

Writeback:
1、表示正在被写回磁盘的脏页占用的内存大小

NFS_Unstable:
1、已经写入NFS server但尚未写入硬盘的page cache占用的内存大小
2、NFS_Unstable的内存被包含在Slab中

Shmem:
1、shared memory、tmpfs和devtmpfs
2、虽然会使用到swap,行为类似匿名页,所以kernel也会把用了swap的shmem记录到 Active(anon)和Inactive(anon)中
3、但是他只是行为类似,可依然是文件页,所以不会与AnonPages重叠
4、由于属于文件页,毫无疑问包含在Cached里面,然后如果发生map(当shmem被attached时候),则也包含在Mapped里面
5、如果它被locked的话,毫无疑问被locked的部分是属于Unevictable

AnonPages
1、所有page cache里属于Cached都是file-backed pages,不是匿名页,所以Cached与AnoPages不重叠
2、AnonPages表示的用户进程使用的匿名页占用内存大小
2、如前面Shmem所述,Shmem属于文件页,所以不与AnonPages重叠
3、如前面AnonHugePages所述,可知如果进程使用了THP,AnonPages与进程的RSS/PSS也是有重叠的

Mapped:
1、用户进程使用的文件页,则属于Mapped
2、如前面Cached所述,Mapped包含在Cached里面
3、因为Shmem也是属于Cached的,所以被进程使用部分的Shmem也是属于Mapped
4、结合AnonPages,系统所有进程的PSS内存之和理论上应该等于AnonPages + Mapped。

Mlocked:
1、如前面LRU所述,Mlocked的内存包含在Unevictable
2、同时因为它可以mlock匿名页和文件页,所以当然跟Cached,AnonPages,Shmem,Mapped是有部分重叠的。

KReclaimable:
1、表示部分内核态可被回收的内存大小,包括slab和非slab可回收内核page
2、跟MemAvailable有部分重叠

Slab:
1、表示用于slab的所有内存大小,包括slab可回收部分(SReclaimable)和不可回收部分(SUnreclaim)

KernelStack:
1、表示内核栈内存大小,包括内核态进程自身使用的栈空间和线程的内核栈空间
2、因为在内核里面,显然不与LRU类和进程的PSS/RSS有重叠

PageTables:
1、表示page table(PTE)占用的内存大小

Bounce:
1、表示在低端内存中针对高端内存访问用于跳转的临时buffer占用的内存大小

WritebackTmp:
1、表示FUSE用于temporary writeback buffers而占用的内存大小

AnonHugePages:
1、表示透明大页中匿名页占用的内存大小
2、包含在AnonPages之中
3、与进程的RSS/PSS是有重叠的

CmaTotal:
1、表示系统预留给CMA(Contiguous Memory Allocator,连续内存分配器)用于分配的内存大小
2、一般系统会在启动过程中,从整个memory中配置一段连续物理内存用于CMA
3、包含CmaFree

IonTotalCache:
1、表示系统分配给ION用于分配和共享内存时,可使用的cache大小
2、包含IonTotalUsed

4.1 内存黑洞

        如前面内存地图所描绘的,里面还有一个内存黑洞,这个内存黑洞在/proc/meminfo里面是看不到。一般kernel的动态内存分配涉及的接口大概就如下三种:以页为分配单位的alloc_page/_get_free_page,以字节为单位分配连续虚拟地址内存块的vmalloc和以字节为单位分配连续物理地址内存块的slab allocator(kmalloc)。对于slab,我们可以用/proc/meminfo里面的slab/SReclaimable/SUnreclaim看到。对于vmalloc,我们可以用/proc/meminfo里面的VmallocUsed或者/proc/vmallocinfo看到。但是对于alloc_page,一般是不会主动去统计的,因此,一旦是某些进程以alloc_page的方式分配内存,在/proc/meminfo只看到MemFree减少,但是具体去向不知道。

        对于上面存在的问题,我们可以利用kernel的PageOwner来跟踪页被谁分配走了,从而找出谁分配的页最多,占用的内存最多。首先,需要在内核中使能PageOwner编译选项(CONFIG_PAGE_OWNER=y),然后,我们需要将“page_owner=on”添加到开机启动的cmdline中,有三种方式,第一,对于高通和MTK平台,设置如下:CONFIG_PAGE_OWNER_SLIM=y;第二,进入手机的fastboot模式,进行设置,指令大概如下:adb reboot bootloader,fastboot oem append-cmdline page_owner=on,fasstboot continue;第三,直接修改代码,mm/page_owner.c,将static bool page_owner_enabled置为true,对于kernel版本相对低的,可能就是将static bool page_owner_disabled置为false。

        最后编译成功以后,手机的刷机成功后,可以压测复现这类问题。当复现后,我们可以cat /sys/kernel/debug/page_owner >  /data/page_owner.txt获取page owner信息,然后adb pull /data/page_owner.txt  <指定文件夹>。从指定文件夹,就可以看到对应的txt文件。

        对于其他如何定位内存黑洞的方式,后续会继续更新。

参考资料

内存统计

/proc/meminfo之谜

linux/proc.rst at master

解读vmstat中的active/inactive memory

深入理解Linux内存管理(十)meminfo详解

关于Meminfo中MemAvailable 理解

你可能感兴趣的:(linux内存管理,内存管理,kernel,linux)