什么是Linux内存管理中的RSS和VSZ

Linux内存管理中的RSS和VSZ是什么?在多线程环境中,如何管理和跟踪这两者呢?

410538 次浏览

它们不是被管理的,而是被度量的,并且可能是有限的(参见getrlimit系统调用,也在getrlimit (2)上)。

RSS意味着常驻机组尺寸(位于RAM中的虚拟地址空间的一部分)。

你可以使用proc (5)cat /proc/1234/maps查询1234进程的虚拟地址空间,并通过cat /proc/1234/status查询进程的状态(包括内存消耗)

RSS是常驻设置大小(物理常驻内存——这是当前占用机器物理内存中的空间),VSZ是虚拟内存大小(分配的地址空间——它在进程的内存映射中分配了地址,但现在它背后不一定有任何实际内存)。

请注意,在虚拟机非常普遍的今天,从机器的角度来看,物理内存可能并不是真正的物理内存。

RSS是常驻设置大小,用于显示分配给该进程的内存大小和内存大小。它不包括被换出的内存。它确实包括来自共享库的内存,只要来自这些库的页面实际上在内存中。它确实包括所有堆栈和堆内存。

VSZ是虚拟内存大小。它包括进程可以访问的所有内存,包括交换出的内存、已分配但未使用的内存以及来自共享库的内存。

因此,如果进程A有500K的二进制文件,并链接到2500K的共享库,有200K的堆栈/堆分配,其中100K实际在内存中(其余的被交换或未使用),并且它实际上只加载了1000K的共享库和400K的自己的二进制文件,那么:

RSS: 400K + 1000K + 100K = 1500K
VSZ: 500K + 2500K + 200K = 3200K

由于部分内存是共享的,许多进程可能会使用它,因此如果您将所有RSS值加起来,您很容易得到比系统拥有的更大的空间。

在程序实际使用之前,分配的内存也可能不在RSS中。因此,如果您的程序预先分配了一堆内存,然后随着时间的推移使用它,您可以看到RSS增加,而VSZ保持不变。

还有PSS(比例设定大小)。这是一种较新的测量方法,用于跟踪当前进程使用的共享内存的比例。因此,如果有两个进程使用之前的相同共享库:

PSS: 400K + (1000K/2) + 100K = 400K + 500K + 100K = 1000K

所有线程共享相同的地址空间,因此每个线程的RSS、VSZ和PSS与进程中所有其他线程相同。使用ps或top在linux/unix中查看此信息。

还有比这更多的方法,要了解更多,请查看以下参考资料:

还看到:

我想关于RSS和VSZ已经说过很多了。从管理员/程序员/用户的角度来看,当我设计/编写应用程序时,我更关心RSZ(常驻内存),当你不断拉出越来越多的变量(堆积)时,你会看到这个值急剧上升。尝试一个简单的程序在循环中构建基于malloc的空间分配,并确保在malloc的空间中填充数据。RSS一直在上升。 就VSZ而言,它更多的是linux所做的虚拟内存映射,它的核心特性之一源自传统的操作系统概念。VSZ管理是由内核的虚拟内存管理来完成的,关于VSZ的更多信息,请参见Robert Love关于mm_struct和vm_struct的描述,它们是内核中基本task_struct数据结构的一部分

最小可运行示例

为了让这有意义,你必须理解分页:x86分页是如何工作的?的基础知识,特别是操作系统可以通过页表/其内部内存簿记(VSZ虚拟内存)分配虚拟内存,然后才在RAM或磁盘上实际拥有备份存储(RSS常驻内存)。

现在,为了观察这个过程,让我们创建一个程序:

  • 使用mmap分配比物理内存更多的RAM
  • 在每个页面上写入一个字节,以确保每个页面都从虚拟内存(VSZ)转换为实际使用的内存(RSS)
  • 使用C语言中当前进程的内存使用情况中提到的方法之一检查进程的内存使用情况

c

#define _GNU_SOURCE
#include <assert.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>


typedef struct {
unsigned long size,resident,share,text,lib,data,dt;
} ProcStatm;


/* https://stackoverflow.com/questions/1558402/memory-usage-of-current-process-in-c/7212248#7212248 */
void ProcStat_init(ProcStatm *result) {
const char* statm_path = "/proc/self/statm";
FILE *f = fopen(statm_path, "r");
if(!f) {
perror(statm_path);
abort();
}
if(7 != fscanf(
f,
"%lu %lu %lu %lu %lu %lu %lu",
&(result->size),
&(result->resident),
&(result->share),
&(result->text),
&(result->lib),
&(result->data),
&(result->dt)
)) {
perror(statm_path);
abort();
}
fclose(f);
}


int main(int argc, char **argv) {
ProcStatm proc_statm;
char *base, *p;
char system_cmd[1024];
long page_size;
size_t i, nbytes, print_interval, bytes_since_last_print;
int snprintf_return;


/* Decide how many ints to allocate. */
if (argc < 2) {
nbytes = 0x10000;
} else {
nbytes = strtoull(argv[1], NULL, 0);
}
if (argc < 3) {
print_interval = 0x1000;
} else {
print_interval = strtoull(argv[2], NULL, 0);
}
page_size = sysconf(_SC_PAGESIZE);


/* Allocate the memory. */
base = mmap(
NULL,
nbytes,
PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_ANONYMOUS,
-1,
0
);
if (base == MAP_FAILED) {
perror("mmap");
exit(EXIT_FAILURE);
}


/* Write to all the allocated pages. */
i = 0;
p = base;
bytes_since_last_print = 0;
/* Produce the ps command that lists only our VSZ and RSS. */
snprintf_return = snprintf(
system_cmd,
sizeof(system_cmd),
"ps -o pid,vsz,rss | awk '{if (NR == 1 || $1 == \"%ju\") print}'",
(uintmax_t)getpid()
);
assert(snprintf_return >= 0);
assert((size_t)snprintf_return < sizeof(system_cmd));
bytes_since_last_print = print_interval;
do {
/* Modify a byte in the page. */
*p = i;
p += page_size;
bytes_since_last_print += page_size;
/* Print process memory usage every print_interval bytes.
* We count memory using a few techniques from:
* https://stackoverflow.com/questions/1558402/memory-usage-of-current-process-in-c */
if (bytes_since_last_print > print_interval) {
bytes_since_last_print -= print_interval;
printf("extra_memory_committed %lu KiB\n", (i * page_size) / 1024);
ProcStat_init(&proc_statm);
/* Check /proc/self/statm */
printf(
"/proc/self/statm size resident %lu %lu KiB\n",
(proc_statm.size * page_size) / 1024,
(proc_statm.resident * page_size) / 1024
);
/* Check ps. */
puts(system_cmd);
system(system_cmd);
puts("");
}
i++;
} while (p < base + nbytes);


/* Cleanup. */
munmap(base, nbytes);
return EXIT_SUCCESS;
}

GitHub上游

编译并运行:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o main.out main.c
echo 1 | sudo tee /proc/sys/vm/overcommit_memory
sudo dmesg -c
./main.out 0x1000000000 0x200000000
echo $?
sudo dmesg

地点:

  • 0x1000000000 == 64GiB: 2x我的计算机的物理RAM为32GiB
  • 0x200000000 == 8GiB:每8GiB打印一次内存,所以我们应该在崩溃前得到4次打印,大约32GiB
  • echo 1 | sudo tee /proc/sys/vm/overcommit_memory: Linux中允许我们进行大于物理RAM的mmap调用所必需的

项目输出:

extra_memory_committed 0 KiB
/proc/self/statm size resident 67111332 768 KiB
ps -o pid,vsz,rss | awk '{if (NR == 1 || $1 == "29827") print}'
PID    VSZ   RSS
29827 67111332 1648


extra_memory_committed 8388608 KiB
/proc/self/statm size resident 67111332 8390244 KiB
ps -o pid,vsz,rss | awk '{if (NR == 1 || $1 == "29827") print}'
PID    VSZ   RSS
29827 67111332 8390256


extra_memory_committed 16777216 KiB
/proc/self/statm size resident 67111332 16778852 KiB
ps -o pid,vsz,rss | awk '{if (NR == 1 || $1 == "29827") print}'
PID    VSZ   RSS
29827 67111332 16778864


extra_memory_committed 25165824 KiB
/proc/self/statm size resident 67111332 25167460 KiB
ps -o pid,vsz,rss | awk '{if (NR == 1 || $1 == "29827") print}'
PID    VSZ   RSS
29827 67111332 25167472


Killed

退出状态:

137

通过128 +信号数规则意味着我们得到了信号号9man 7 signal表示是SIGKILL,它是由Linux 内存不足的杀手发送的。

输出的解释:

  • VSZ虚拟内存在mmap之后保持在printf '0x%X\n' 0x40009A4 KiB ~= 64GiB (ps值在KiB中)不变。
  • RSS "实际内存使用"只有当我们触摸书页时,才会慵懒地增加。例如:
    • 在第一次打印时,我们有extra_memory_committed 0,这意味着我们还没有触及任何页面。RSS是一个小的1648 KiB,它被分配给正常的程序启动,如文本区域,全局变量等。
    • 在第二次打印时,我们已经写了8388608 KiB == 8GiB值的页面。结果,RSS恰好增加了8GIB到8390256 KiB == 8388608 KiB + 1648 KiB
    • RSS继续以8GiB的增量增加。最后一次打印显示了大约24 GiB的内存,在打印32 GiB之前,OOM杀手终止了进程

参见:https://unix.stackexchange.com/questions/35129/need-explanation-on-resident-set-size-virtual-size

OOM杀手日志

我们的dmesg命令已经显示了OOM杀手日志。

对这些问题的确切解释是:

  • 了解Linux oom-killer日志但是让我们在这里快速看一下。
  • https://serverfault.com/questions/548736/how-to-read-oom-killer-syslog-messages < a href = " https://serverfault.com/questions/548736/how-to-read-oom-killer-syslog-messages " > < / >

日志的第一行是:

[ 7283.479087] mongod invoked oom-killer: gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0

因此,我们看到有趣的是,总是在我的笔记本电脑后台运行的MongoDB守护进程首先触发了OOM杀手,大概是在这个可怜的东西试图分配一些内存时。

然而,OOM杀手并不一定会杀死唤醒它的人。

调用之后,内核打印一个表或进程,包括oom_score:

[ 7283.479292] [  pid  ]   uid  tgid total_vm      rss pgtables_bytes swapents oom_score_adj name
[ 7283.479303] [    496]     0   496    16126        6   172032      484             0 systemd-journal
[ 7283.479306] [    505]     0   505     1309        0    45056       52             0 blkmapd
[ 7283.479309] [    513]     0   513    19757        0    57344       55             0 lvmetad
[ 7283.479312] [    516]     0   516     4681        1    61440      444         -1000 systemd-udevd

再往前,我们会看到我们自己的小main.out实际上在前面的调用中被杀死了:

[ 7283.479871] Out of memory: Kill process 15665 (main.out) score 865 or sacrifice child
[ 7283.479879] Killed process 15665 (main.out) total-vm:67111332kB, anon-rss:92kB, file-rss:4kB, shmem-rss:30080832kB
[ 7283.479951] oom_reaper: reaped process 15665 (main.out), now anon-rss:0kB, file-rss:0kB, shmem-rss:30080832kB

这个日志提到了进程拥有的score 865,大概是在https://unix.stackexchange.com/questions/153585/how-does-the-oom-killer-decide-which-process-to-kill-first中提到的最高(最差)OOM杀手得分

同样有趣的是,一切都发生得非常快,以至于在被释放的内存被计算之前,oomDeadlineMonitor进程再次唤醒:

[ 7283.481043] DeadlineMonitor invoked oom-killer: gfp_mask=0x6200ca(GFP_HIGHUSER_MOVABLE), order=0, oom_score_adj=0

这一次,它杀死了一些铬进程,这通常是我的电脑正常的内存占用:

[ 7283.481773] Out of memory: Kill process 11786 (chromium-browse) score 306 or sacrifice child
[ 7283.481833] Killed process 11786 (chromium-browse) total-vm:1813576kB, anon-rss:208804kB, file-rss:0kB, shmem-rss:8380kB
[ 7283.497847] oom_reaper: reaped process 11786 (chromium-browse), now anon-rss:0kB, file-rss:0kB, shmem-rss:8044kB

在Ubuntu 19.04, Linux内核5.0.0中测试。

Linux内核文档

https://github.com/torvalds/linux/blob/v5.17/Documentation/filesystems/proc.rst有一些点。“vz "不是用在这里,而是"RSS"是,没有什么太有启发性的(惊喜?!)

而不是VSZ,内核似乎使用术语VmSize,它出现在例如/proc/$PID/status

一些有趣的引用:

第一行显示的信息与在/proc/ pid /maps中显示的映射信息相同下面几行显示映射的大小(size);支持VMA时分配的每个页面的大小(KernelPageSize),通常与页表项中的大小相同;MMU在支持VMA时使用的页面大小(在大多数情况下,与KernelPageSize相同);当前驻留在RAM (RSS)中的映射量;该过程在映射中的比例份额(PSS);以及映射中干净和脏的共享和私有页面的数量。

“比例设定尺寸”;进程的PSS是指它在内存中的页数,其中每一页除以共享它的进程数。因此,如果一个进程有1000页属于自己,其中1000页与另一个进程共享,那么它的PSS将是1500。

请注意,即使一个页面是MAP_SHARED映射的一部分,但只有一个pte映射,即当前只被一个进程使用,也被视为私有而不是共享。

所以我们可以猜测更多的事情:

  • 单个进程使用的共享库出现在RSS中,如果不止一个进程拥有共享库,则没有
  • PSS是JMH提到,并且在“我是唯一拥有共享库的进程”和“我是唯一拥有共享库的进程”之间有一个更相称的方法。并且“有N个进程占用共享库,所以每个进程平均占用N个内存”;

虚拟集大小

  • 虚拟集大小是在初始执行期间分配给进程(程序)的内存大小。虚拟集大小内存只是一个进程可用于执行的内存数量。

RSS -常驻设置大小(有点RAM)

  • 相对于VSZ(虚拟集大小),RSS是进程当前使用的内存。这是以千字节为单位的当前进程使用RAM的实际数字。

Source .

总结@jmh 优秀的答案:

在#linux中,进程的内存包括:

  • 它自己的二进制
  • 它的共享库
  • 它的堆栈和堆

由于分页,并不是所有这些都总是完全在内存中,只有有用的、最近使用的部分(页面)是。其他部分被换出(或换出),以便为其他进程腾出空间。

下面的表格摘自@jmh的回答,它展示了一个用于特定进程的常驻内存和虚拟内存的示例。

+-------------+-------------------------+------------------------+
| portion     | actually in memory      | total (allocated) size |
|-------------+-------------------------+------------------------|
| binary      | 400K                    | 500K                   |
| shared libs | 1000K                   | 2500K                  |
| stack+heap  | 100K                    | 200K                   |
|-------------+-------------------------+------------------------|
|             | RSS (Resident Set Size) | VSZ (Virtual Set Size) |
|-------------+-------------------------+------------------------|
|             | 1500K                   | 3200K                  |
+-------------+-------------------------+------------------------+

总而言之:常驻内存是当前物理内存中实际存在的内存,而虚拟大小是加载所有组件所需的全部物理内存。

当然,这些数字不能相加,因为库是在多个进程之间共享的,并且它们的内存是为每个进程单独计算的,即使它们的单个实例存在于内存中。