ESearch:Linux进程内存用量分析之内存映射篇

一、背景

搜索引擎中,使用内存映射的方式加载庞大的索引数据文件已是常态,58自研的搜索引擎ESearch也是如此。ESearch检索节点中,索引数据是分段存储的,段上正排倒排等数据又分别存储在不同文件上,所有索引数据都使用内存映射的方式读取或更新。这样做有许多好处,比如简化了数据加载逻辑,由操作系统管理磁盘缓存等。但是也存在一些问题,比如映射的内存用量不容易控制等。

ESearch检索节点索引数据的常驻内存用量占服务总常驻内存的60%左右,所以分析和监控内存映射文件的内存用量很有必要的,一方面有助于了解索引的内存使用情况,辅助进行内存优化,提高资源利用率;另一方面也有助于排查I/O相关问题,比如当发生I/O过高问题时,通过各文件映射的内存变化量来分析服务在读取哪些文件等。

本文将介绍两种与内存映射文件相关的内存用量分析方法:1.分析磁盘文件内容加载到物理内存中的总量,可辅助定位I/O相关问题等。2.分析进程使用到的文件数据的内存用量,可帮助了解进程的内存资源、文件的热数据分布等。下面会结合Linux内存管理机制进行介绍。

二、原理分析

为了更加系统的了解内存统计工具和方法,在这一章节对内存映射、Page Cache、页表(Page Table)等相关原理做简单描述。

首先介绍下Page Cache,它把一部分内存用于缓存磁盘数据,减少访问磁盘时间。它与文件系统和内存管理系统有密切关联。

接下来介绍页表,页表是进程中不可或缺的一部分,用于把虚拟地址空间映射到物理地址空间。页表需要存储在连续的物理内存上,X86机器单层页表的情况下,想映射3G用户虚拟空间,需要连续768个大小为4k的物理页。所以同时运行N个进程时,需要N块大小为768页帧的连续内存,在x64中这种情况变得更加严重。

绝大多数进程只会使用整个虚拟地址空间的一小部分,并且这一小部分还是非常离散地分布在整个地址空间范围内,根据进程运行的这个特点,Linux内核设计了四层页表,分别为Page Global Directory(PGD)、Page UpperDirectory(PUD)、Page MiddleDirector(PMD)、Page Table Entry(PTE)。多层页表对比一层页表的一个优势在于对虚拟地址空间中不必要的区域,在PMD或者PTE层中不必创建没用的PMD节点或者PTE节点,多级页表节省了大量内存。另一个优点在于x64及其中适配四级硬件页表,可以增大寻址范围。但是对于不同架构,页表的层次也不同,有的架构只需要实现二层或三层页表。在这里我们以三层举例,如图:

image.png

接下来通过内存映射将页表、PageCache、内存映射相关知识串联起来。

2.1 内存映射

ESearch使用mmap的方式打开磁盘文件,将磁盘文件映射到虚拟内存中,如下图所示:

image

加载磁盘页主要经过页表和Page Cache两层结构,关系图如下:

image

根据上图模拟访问内存映射中的一段虚拟内存的过程。

1. 先将虚拟地址0x00445500转换成虚拟页,再根据地址映射原理在页表寻找所对应的页帧中是否存在物理页地址。假设页表中0x0044页帧(page frame)内没有指向物理页的指针(具体表现为对应PTE有效位为0);

2.1. 如果存在有效物理页地址,将物理页对应数据返回给用户空间;

2.2. 如果不存在有效物理页地址,内核将会到Page Cache查找;

3. 进程根据打开的文件描述符找到文件的inode数据结构;

4. inode包含了i_mapping域,i_mapping存放一个指向address_space对象的指针,address_space对象是承接文件到基数树的桥梁,能让进程通过inode快速定位到需要查找的物理内存页;基数树是多叉树的一种,Page Cache使用基数树快速定位物理页的位置以及判断物理页是否被加载到内存;

5. 根据文件偏移量,定位到需要访问的页,通过address_space查找该页是否被缓存;

6. address_space中包含了指向基数树根节点(rnode)的指针page_tree,根据rnode可以找到基数树的最高层节点,然后快速查找需要访问的页是否被缓存在物理内存中。基数树结构如下图所示,快速查询方法类似于页表虚拟地址映射物理页的方法;

image.png

7.1. 如果定位到,则说明已经缓存,直接将指向该物理页的指针加载到页表中对应的页帧中即可;

7.2 如果没有找到,则发生缺页中断,创建一个页缓存页(假设为0xff007700),同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;重复查找页缓存。

2.2 Page Cache ****和****页表的不同

内存映射中加载页的统计分为两个层级:

1.内核层级:Page Cache保存了多少该文件的磁盘页在内存;

2.进程层级:进程使用了多少该文件的磁盘页,也就是页表中有多少关于该文件的有效物理页。

对于某个进程使用到的内存映射文件,使用页表统计得到的就是当前进程使用到的页;使用Page Cache统计,还会包括预读带入的页、其他进程缓存的页等。对于同一文件二者的加载量一般不相同,一般情况下进程页表加载页的数量都小于等于PageCache加载页的数量。

接下来介绍用mincore系统调用统计Page Cache物理内存用量和用/proc目录下的文件来统计页表中内存映射区间内物理内存加载情况。

2.3 mincore系统调用

mincore系统调用原型是int mincore(void *addr, size_tlength, unsigned char *vec),第一个参数是mmap返回出来的指针,第二个参数是文件长度,第三个参数是uint8数组,该数组是返回值。mincore会统计从addr开始长度为 length字节的虚拟内存页,通过vec返回这些页所映射的数据是否被加载到物理内存中。该页数据在内存中,vec对应的元素就为1,否则为0。mincore系统调用能够很方便的拿到该页是否在PageCache中。接下来介绍页表用量的统计,就使用了/proc目录下的文件。

2.4 /proc目录下的文件

Linux通过/proc目录,以虚拟文件系统的方式提供了访问内核数据的途经。/proc/目录下提供了每个进程的各种信息,其中我们用到与内存相关的主要有smaps,maps,pagemap三个文件。以下一一介绍它们的用法和不同之处,以及它们之间的优缺点比较。注意一点:只有在这三个文件被访问的时候,内核才会进行统计并在内存中生成文件内容,因此它们平时不会占用内核的计算资源和存储空间。

2.4.1 smaps

/proc//smaps其中pid是运行进程的进程号,smaps里面表示着所有内存映射的信息,比如动态链接库,new/malloc使用mmap打开的内存空间,内存映射的文件和栈空间等。下图为smaps输出的一部分,这部分是ESearch服务正排索引文件加载情况的示意图。

image

其中Size表示所占虚拟内存大小(总大小),Rss表示加载到物理内存的大小。让进程分析自己进程号的smaps文件,得到对应文件真实加载到页表的Rss。利用这样的方式可以获取文件部分加载到页表中的大小。

但是需要注意的是如果在高频统计内存段和其他线程访问的内存段重叠的情况下,对smaps文件进行读操作甚至外部去cat smaps 都会对其他线程造成较大影响。阅读源码时发现生成smaps有大粒度的内存锁的操作,具体一起看代码,以下源码做了精简,生成smaps文件关键源码如下:

image.png

代码大致含义如下:

1. 使用ptr_offset_map_lock加锁addr开始到end结束的内存段,该函数会独占内存区间对应的页表;

2. 返回addr对应的页表偏移量的pte,pte变量中保存的是当前addr对应的那一页;

3. for循环内存地址,每次addr加PageSize大小,pte++表示下一个页,for循环内部是给mss赋值,mss内累加了addr到end范围内的每页的状态;

4. 解除对应区间页表占用。

下图为每个mmap打开地址段的标准化输出函数:

image.png

每次show_smap都会调用smaps_walk,smaps_walk将会调用smaps_ptr_range来得到addr到end范围内所有页的状况,再用seq_printf格式化。

循环调用show_smap函数就能生成完整的smaps文件内容。以上就是smaps统计一段用内存映射打开的文件中所有页的方法。

总结:

优势:smaps文件易于观察,统计信息丰富,并且容易获得;

缺点:因为生成smaps过程中会多次调用smaps_ptr_range,该过程中都独占该进程页表的一部分,别的线程不能访问。所以读取smaps文件不适用于频繁的内存读写场景。

2.4.2 pagemap

在/proc//pagemap中保存着pid进程对应的页表,可以读取该文件的页表信息来计算进程需要的页有多少。首先需要查看源码来判断pagemap文件是否跟smaps文件一样,在生成过程中都需要加锁。以下为关键源码:

image

1. 从walk中拿出上层定义的pm对象,该对象为pagemapread型,该数据结构中保存了一个指向u64数组的指针,该数组用来保存最终结果;

2. for循环以PageSize为步长,遍历整个从addr到end的内存区间,for循环中ptr_offset_map和pte_to_pagemap_entry都是在填充pfn这个u64类型的变量;

3. 由add_to_pagemap把pfn对象的内容填充到保存最终结果的pm结构体中。根据源码可以知道生成pagemap过程是不加锁的,所以读取该文件比较适合于统计内存占用量这种对数据一致性要求不高的场景。pagemap中每64位代表着当前pid进程页表中对应页的状态信息,这 64位状态信息如下图所示:

image

在统计内存用量这个场景下,只用到了第63位,该位表示对应的页是否被该进程使用。因为/proc/下的文件都是特殊文件,mmap打开pagemap、smaps等文件会失败,并且errno会置ENODEV。所以只可以用read/fread打开,打开后使用lseek改变当前文件偏移量(cfo),然后read读取需要的字节数,再展开分析即可。

对比smaps文件,因为pagemap在生成的过程中不加锁,所以对频繁内存读写没有影响,但是一致性没有smaps强。

因为ESearch服务内部可以拿到磁盘文件内存映射返回的指针和长度,所以很方便能找到页表中对应的页。但如果不能拿到映射的地址范围,可以使用maps得到映射地址范围。

/proc//maps文件描述了pid进程所有mmap打开文件或者开辟的内存段所在的地址区域,可以根据maps里的内容拿到感兴趣的地址段再进行操作。

2.5 系统命令

以下介绍的两种系统命令也能在一定程度上反应内核Page Cache或页表使用量的情况。

1. free –m会展示 Page Cache的大小。

2. vmstat也会展示Page Cache的大小,而且会展示PageCache中活跃页(active)和非活跃页(inactive)的量。

3.top会展示进程SHR内存占用,包括栈,大于128kb的内存分配,动态链接库,mmap打开的文件等。

4. sar 展示历史系统情况,-B参数展示页情况,其中pgfree展示系统中的空闲页数量,pgsteal表示从Page Cache中清理页的个数,在ESearch集群中该值和kswapd0进程的活跃度成正比。

三、总结

统计页表的工具有top、smaps、pagemap;统计文件占用PageCache的工具有mincore等;

文件映射下的内存统计主要分为两个方面:

1. 页表:可以使用smaps、top中的SHR、 pagemap来查看进程使用的内存;

2. Page Cache:可以使用free、vmstat、mincore系统调用来查看文件映射到物理内存页面所占用的内存。

进程层面和系统层面间的不同主要在于预读(read-ahead)等操作造成额外内存页会计算在Page Cache中,进程页表是不计算预读等操作所读入的内存页,页表用量只表示该进程需要页的数量。

你可能感兴趣的:(ESearch:Linux进程内存用量分析之内存映射篇)