一种确定进程“真实”内存使用量的方法,即私有脏数据占用的常驻内存集(private dirty RSS)。

63

'ps'和'top'等工具会报告各种内存使用情况,例如虚拟内存大小和Resident Set Size。但是,这些都不是“真实”的内存使用情况:

  • 程序代码在同一程序的多个实例之间共享。
  • 共享库程序代码在使用该库的所有进程之间共享。
  • 一些应用程序派生出进程并与它们共享内存(例如通过共享内存段)。
  • 虚拟内存系统使得虚拟内存大小报告几乎没用。
  • 当进程被交换出时,RSS为0,因此不是很有用。
  • 等等。

我发现Linux上由私有脏页占用的RSS(Private dirty RSS)最接近“真实”内存使用情况。可以通过在/proc/somepid/smaps中汇总所有Private_Dirty值来获取此值。

然而,其他操作系统是否提供类似的功能?如果没有,有哪些替代方案?特别是,我对FreeBSD和OS X感兴趣。


“真实内存使用量”究竟是什么?根据您的列表,单个进程的内存使用情况要么无用,要么是任意的概念。 - BCS
6
我会将“实际内存使用量”定义为如果我使用“kill -9”命令终止给定进程,将释放的物理内存数量(但不包括交换空间)。我认为该数字应该介于报告给定进程的RSS和PSS值之间。 - Mikko Rantalainen
@Hongli:虽然这是一个旧的帖子,但我很惊讶为什么在FreeBSD中挂载linprocfs不是任何人建议的解决方案的一部分。这是否有特定的原因?无论如何,我已经添加了该答案以便完整性。 - Arvind
10个回答

53

在Mac OSX中,活动监视器实际上可以很好地猜测。

私有内存肯定是只被您的应用程序使用的内存。例如,堆栈内存和所有使用malloc()和类似函数/方法(Objective-C的alloc方法)动态保留的内存是私有内存。如果您进行分叉,则私有内存将与您的子进程共享,但标记为写时复制。这意味着只要一个页面未被任何进程(父进程或子进程)修改,它就会在它们之间共享。一旦任何一个进程修改了任何页面,该页面将在修改之前被复制。即使这个内存与fork子进程共享(并且只能与fork子进程共享),它仍然显示为“private”内存,因为在最坏的情况下,它的每一页都会被修改(迟早),然后它再次对于每个进程都是私有的。

共享内存是当前共享的内存(相同的页面在不同进程的虚拟进程空间中可见)或者未来可能共享的内存(例如只读内存,因为没有理由不共享只读内存)。至少这是我从苹果公司的一些命令行工具的源代码中读到的。因此,如果您使用mmap(或将相同内存映射到多个进程的类似调用)在进程之间共享内存,这将是共享内存。但可执行代码本身也是共享内存,因为如果启动另一个应用程序实例,则没有理由它不会共享已经加载到内存中的代码(可执行代码页面默认情况下是只读的,除非您在调试器中运行应用程序)。因此,共享内存实际上是您的应用程序使用的内存,就像私有内存一样,但可能还与另一个进程共享(或者可能不共享,但如果它被共享,为什么不算作您的应用程序的一部分?)

实际内存是当前“分配”给您的进程的RAM量,无论是私有的还是共享的。这可能恰好等于私有和共享的总和,但通常不是这样。您的进程可能被分配比它当前所需的更多的内存(这样可以加快未来请求更多内存的速度),但这对系统来说并不是损失。如果另一个进程需要内存而没有可用的空闲内存,在系统开始交换之前,它将从您的进程中拿走这些额外的内存,并将其分配给另一个进程(这是一个快速且无痛的操作);因此,您的下一个malloc调用可能会稍慢一些。实际内存也可能比私有和物理内存小;这是因为如果您的进程从系统请求内存,它将只收到“虚拟内存”。只要您不使用它(因此malloc 10 MB的内存,只使用其中一个字节,您的进程将被分配一个单独页面,4096字节的内存-其余仅在您实际需要时分配)。交换的内存也可能不计入实际内存(对此不确定),但它将计入共享和私有内存。

虚拟内存是指在应用程序进程空间中被认为有效的所有地址块的总和。这些地址可能与物理内存链接(再次是私有或共享),也可能没有,但是在这种情况下,只要使用该地址,它们就会与物理内存链接。访问已知地址范围之外的内存地址将导致SIGBUS错误并使应用程序崩溃。当内存被交换时,该内存的虚拟地址空间仍然有效,并且访问这些地址将导致内存被重新交换回来。
结论: 如果您的应用程序不明确或隐式地使用共享内存,则私有内存是由于堆栈大小(或多线程的大小)以及由于您为动态内存分配而进行过的malloc()调用所需的内存量。在这种情况下,您不必过多关心共享内存或实际内存。
如果您的应用程序使用共享内存,包括图形用户界面,在此类情况下,内存在应用程序和WindowServer之间共享,那么您也可以查看共享内存。非常高的共享内存数字可能意味着您当前在内存中加载了太多图形资源。
实际内存对应用程序开发没有太大兴趣。如果它比共享和私有内存的总和更大,那么这意味着系统对从您的进程中取走内存有点懒惰。如果它更小,则意味着您的进程请求了比实际需要的更多的内存,这也不是坏事,因为只要您没有使用所有请求的内存,就不会“窃取”系统的内存。如果它远小于共享和私有内存的总和,您可能只需要在可能的情况下请求更少的内存,因为您有点过度请求内存(再次说明,这不是坏事,但这告诉我,您的代码未经过优化,以实现最小内存使用量,如果它跨平台,其他平台可能没有如此复杂的内存处理,因此您可能更喜欢分配许多小块而不是几个大块,或者更早地释放内存等)。
如果您对所有这些信息仍不满意,可以获得更多信息。打开终端并运行:
sudo vmmap <pid>

其中,pid 是您进程的进程 ID。这将向您显示进程空间中每个内存块的统计信息,包括起始地址和结束地址。它还会告诉您此内存来自哪里(映射文件?堆栈内存?malloc分配的内存?可执行文件的__DATA或__TEXT部分?),以 KB 为单位的大小,访问权限以及它是私有的、共享的还是写时复制的。如果它是从文件映射而来,它甚至会给出文件的路径。

如果您只想要“实际”的 RAM 使用情况,请使用

sudo vmmap -resident <pid>

现在它将显示每个内存块的虚拟大小和实际存在于物理内存中的大小。

每个转储结束时还会有一个概述表格,列出了不同内存类型的总和。这张表格在我的系统上现在看起来像这样:

REGION TYPE             [ VIRTUAL/RESIDENT]
===========             [ =======/========]
ATS (font support)      [   33.8M/   2496K]
CG backing stores       [   5588K/   5460K]
CG image                [     20K/     20K]
CG raster data          [    576K/    576K]
CG shared images        [   2572K/   2404K]
Carbon                  [   1516K/   1516K]
CoreGraphics            [      8K/      8K]
IOKit                   [  256.0M/      0K]
MALLOC                  [  256.9M/  247.2M]
Memory tag=240          [      4K/      4K]
Memory tag=242          [     12K/     12K]
Memory tag=243          [      8K/      8K]
Memory tag=249          [    156K/     76K]
STACK GUARD             [  101.2M/   9908K]
Stack                   [   14.0M/    248K]
VM_ALLOCATE             [   25.9M/   25.6M]
__DATA                  [   6752K/   3808K]
__DATA/__OBJC           [     28K/     28K]
__IMAGE                 [   1240K/    112K]
__IMPORT                [    104K/    104K]
__LINKEDIT              [   30.7M/   3184K]
__OBJC                  [   1388K/   1336K]
__OBJC/__DATA           [     72K/     72K]
__PAGEZERO              [      4K/      0K]
__TEXT                  [  108.6M/   63.5M]
__UNICODE               [    536K/    512K]
mapped file             [  118.8M/   50.8M]
shared memory           [    300K/    276K]
shared pmap             [   6396K/   3120K]
这告诉我们什么?例如,Firefox二进制文件和它加载的所有库在它们的__TEXT部分中共有108 MB的数据,但当前只有63 MB的数据实际上是常驻内存的。字体支持(ATS)需要33 MB,但只有大约2.5 MB真正在内存中。它使用了略多于5 MB的CG后备存储,CG = Core Graphics,这些最可能是窗口内容、按钮、图像和其他缓存以快速绘制的数据。它通过malloc调用请求了256 MB,并且当前有247 MB实际上映射到内存页。它有14 MB空间保留用于堆栈,但现在只有248 KB的堆栈空间实际上在使用中。 vmmap还在表格上方提供了一个很好的总结。
ReadOnly portion of Libraries: Total=139.3M resident=66.6M(48%) swapped_out_or_unallocated=72.7M(52%)
Writable regions: Total=595.4M written=201.8M(34%) resident=283.1M(48%) swapped_out=0K(0%) unallocated=312.3M(52%)

这个展示了 OS X 的一个有趣的方面:对于来自库的只读内存,它是否被交换出去或者只是未分配并不重要;只有驻留和不驻留之分。但对于可写内存,这是有区别的(在我的情况下,52% 的所请求内存从未被使用,因此是未分配的;0% 的内存被交换到磁盘上)。

原因很简单:来自映射文件的只读内存不会被交换出去。如果系统需要该内存,当前的页面将被从进程中删除,因为内存已经“被交换”。它只由直接从文件映射的内容组成,每当需要时,这些内容可以重新映射,因为文件仍然存在。这样,这些内存也不会在交换文件中浪费空间。只有可写内存在被删除之前必须先被交换到文件中,因为它的内容之前并没有存储在磁盘上。


11

在Linux中,您可能希望查看/proc/self/smaps中的PSS(比例集大小)数字。映射的PSS是其RSS除以使用该映射的进程数。


2
在Linux(Ubuntu 18.04)上,是否有一个命令可以返回进程的PSS? - Roman Gaufman
@RomanGaufman pmap +x ${PID_OF_THE_PROCESS}请将上述代码翻译为中文。 - Jeoker

8

Top知道如何做到这一点。在Debian Linux上,默认情况下显示VIRT、RES和SHR。VIRT = SWAP + RES。RES = CODE + DATA。SHR是可能与另一个进程共享的内存(共享库或其他内存)。

此外,“脏”内存仅是已使用和/或未交换的RES内存。

很难判断,但最好的方法是查看不进行交换的系统。然后,RES-SHR是进程独占内存。然而,这并不是一个好的观察方式,因为您不知道SHR中的内存是否被另一个进程使用。它可能代表着只被进程使用的未写入的共享对象页面。


我认为那不正确。请参考http://pastie.org/277766这将300 MB映射到地址空间,但只写入了最后一个字节。该块的实际内存使用量应为4 KB(页面大小)。进程的实际内存使用量应该只有几KB。 - Hongli
在您的示例中未使用的任何内存仍将显示在VIRT总计中。而RES总计将反映任何未使用的内存(即不显示它)。至少,在Debian x86上似乎是这样工作的。 - Chris
2
我同意你的观点。对于大多数进程来说,RES-SHR将是评估进程内存使用情况的好方法。通常,内存泄漏发生在私有内存中,这也是你需要调查的地方。如果想要了解完整的内存使用情况,不应该将进程相加,而是应该查看top/htop的总体情况。 - norekhov

7

你真的做不到。

我的意思是,进程之间的共享内存...你要计算它吗?如果你不计算它,那么你就错了;所有进程内存使用量的总和将不会等于总内存使用量。如果你计算它,你会把它算两次——总和将不正确。

我很满意 RSS。而且知道你不能完全依赖它...


6

6

你可以从/proc/pid/smaps获取私有脏页和私有清洁页的RSS。


4
重新设计了这个代码,让它更加简洁,并展示了一些bash的最佳实践方法,特别是使用awk代替bc
find /proc/ -maxdepth 1 -name '[0-9]*' -print0 | while read -r -d $'\0' pidpath; do
  [ -f "${pidpath}/smaps" ] || continue
  awk '!/^Private_Dirty:/ {next;}
       $3=="kB" {pd += $2 * (1024^1); next}
       $3=="mB" {pd += $2 * (1024^2); next}
       $3=="gB" {pd += $2 * (1024^3); next}
       $3=="tB" {pd += $2 * (1024^4); next}
       $3=="pB" {pd += $2 * (1024^5); next}
       {print "ERROR!!  "$0 >"/dev/stderr"; exit(1)}
       END {printf("%10d: %d\n", '"${pidpath##*/}"', pd)}' "${pidpath}/smaps" || break
done

在我的机器上,有一个方便的小容器,使用 | sort -n -k 2 对输出进行排序,结果如下:
        56: 106496
         1: 147456
        55: 155648

2

使用mincore(2)系统调用。引用手册页:

DESCRIPTION
     The mincore() system call determines whether each of the pages in the
     region beginning at addr and continuing for len bytes is resident.  The
     status is returned in the vec array, one character per page.  Each
     character is either 0 if the page is not resident, or a combination of
     the following flags (defined in <sys/mman.h>):

1
针对提到Freebsd的问题,令人惊讶的是还没有人写出这个方法:
如果您想要类似Linux的/proc/PROCESSID/status输出,请按照以下步骤操作:
mount -t linprocfs none /proc
cat /proc/PROCESSID/status

至少在FreeBSD 7.0中,默认情况下不进行挂载(7.0是一个更旧的版本,但对于这样基本的问题,答案藏在邮件列表中!)


1

请查看,这是gnome-system-monitor的源代码,它认为一个进程"真正使用"的内存是X Server Memory(info->memxserver)和Writable Memory(info->memwritable)之和(info->mem),"可写内存"是在/proc/PID/smaps文件中标记为"Private_Dirty"的内存块。

除了Linux系统外,根据gnome-system-monitor的代码可能会有不同的方式。

static void
get_process_memory_writable (ProcInfo *info)
{
    glibtop_proc_map buf;
    glibtop_map_entry *maps;

    maps = glibtop_get_proc_map(&buf, info->pid);

    gulong memwritable = 0;
    const unsigned number = buf.number;

    for (unsigned i = 0; i < number; ++i) {
#ifdef __linux__
        memwritable += maps[i].private_dirty;
#else
        if (maps[i].perm & GLIBTOP_MAP_PERM_WRITE)
            memwritable += maps[i].size;
#endif
    }

    info->memwritable = memwritable;

    g_free(maps);
}

static void
get_process_memory_info (ProcInfo *info)
{
    glibtop_proc_mem procmem;
    WnckResourceUsage xresources;

    wnck_pid_read_resource_usage (gdk_screen_get_display (gdk_screen_get_default ()),
                                  info->pid,
                                  &xresources);

    glibtop_get_proc_mem(&procmem, info->pid);

    info->vmsize    = procmem.vsize;
    info->memres    = procmem.resident;
    info->memshared = procmem.share;

    info->memxserver = xresources.total_bytes_estimate;

    get_process_memory_writable(info);

    // fake the smart memory column if writable is not available
    info->mem = info->memxserver + (info->memwritable ? info->memwritable : info->memres);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接