近期遇到一个问题:统计一个进程使用的物理内存居然远远超过了系统总内存大小。咋一看这个现象被下了一跳,怎么会这样呢?后来慢慢分析,终于了解到其中的奥秘.....
我的 "进程占用物理内存统计" 是通过累加 /proc/pid/smaps中的Rss项来计算的。以往对Rss"Resident Set Size"的理解就是实际占用的内存,但是并没有真正去区分和Pss的区别。
随后我仔细观察,才发现这次出问题的这个进程的Pss和Rss是不一样的:Pss累计统计出来的内存量是比较合理的。这是怎么回事呢?
网上又查找了一番Rss和Pss的区别,都谈到了Rss是包含了共享库而Pss则比例分配共享库占用的内存。带着这些信息线索我又分析了两个方面: 1) 出问题的进程是如何消耗/分配内存的 ; 2) 内核中究竟是如何统计Rss和Pss的。
方向1
代码中主要内存消耗的代码为:
mmap(NULL, LENTH, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
即通过mmap()函数对一个文件进行LENTH大小的多次映射,并最终写这段映射出来的地址来消耗内存。
通过实验发现,如果mmap()函数不使用MAP_SHARED标志作为参数,则Rss统计值和Pss值一致,同时其Rss的统计值不会超过系统内存总量。
方向2
内核统计smaps中Rss和Pss的主要代码逻辑:
for 遍历进程的vma
通过vma中各个的页的虚拟地址分解为对应的页表项
smaps_pte_entry()-->
smaps_account(mss, page, PAGE_SIZE, pte_young(*pte), pte_dirty(*pte))
smaps_pte_entry()函数首先找到pte对应物理页框的page结构,然后调用smaps_account()函数将此page统计到Rss和Pss中。具体一个page的统计方式如下:
static void smaps_account(struct mem_size_stats *mss, struct page *page,
unsigned long size, bool young, bool dirty)
{
int mapcount;
if (PageAnon(page))
mss->anonymous += size;
mss->resident += size; /* 将page size 统计到Rss */
/* Accumulate the size in pages that have been accessed. */
if (young || page_is_young(page) || PageReferenced(page))
mss->referenced += size;
mapcount = page_mapcount(page);
if (mapcount >= 2) {
u64 pss_delta;
if (dirty || PageDirty(page))
mss->shared_dirty += size;
else
mss->shared_clean += size;
pss_delta = (u64)size << PSS_SHIFT;
do_div(pss_delta, mapcount); /* 如果一个物理页映射多次则Pss均分 */
mss->pss += pss_delta;
} else {
if (dirty || PageDirty(page))
mss->private_dirty += size;
else
mss->private_clean += size;
mss->pss += (u64)size << PSS_SHIFT;
}
}
从上面代码可以看出:(1)只要进程虚拟地址映射到物理页就会统计到Rss中,不论这些物理页是否被多次映射; (2) Pss在统计进程虚拟地址空间每个映射的物理页时会div物理页映射的counts,即会均分多次映射的page。
而出问题的进程正好是以MAP_SHARED的方式多次映射同一个文件,这样多次映射的虚拟地址实际上只会对应到相同的物理页,因而也就有上述问题。
所以,以后再计算进程占用物理内存时,Rss和Pss如果不一样就需要考虑是否是有多次映射或者和共享映射的情况。