如何在 Linux 上使用 pmap 命令

找出一个 Linux 进程使用了多少 RAM 并不是一件简单的事情——尤其是在需要考虑共享内存的时候。值得庆幸的是,pmap
命令可以帮助您理解这一切。
内存映射
在现代操作系统中,每个进程都位于自己分配的内存区域或分配空间中。分配区域的边界不直接映射到物理硬件地址。操作系统为每个进程创建一个虚拟内存空间,并充当将虚拟内存映射到物理内存的抽象层。
内核为每个进程维护一个转换表,并由 CPU 访问。当内核更改在特定 CPU 核心上运行的进程时,它会更新将进程和 CPU 核心联系在一起的转换表。
抽象的好处
这个方案有好处。对于用户空间中的每个进程,内存的使用在某种程度上被封装和沙盒化。进程仅根据虚拟内存地址“看到”内存。这意味着它只能使用操作系统提供的内存。除非它可以访问某些共享内存,否则它既不知道也无法访问分配给其他进程的内存。
将基于硬件的物理内存抽象为虚拟内存地址,使内核可以更改某些虚拟内存映射到的物理地址。它可以通过更改虚拟内存区域指向的实际地址来将内存交换到磁盘。它还可以推迟提供物理内存,直到实际需要为止。
只要读取或写入内存的请求按要求得到服务,内核就可以随意调整映射表,因为它认为合适。
按需分配内存
映射表和“RAM on demand”的概念开启了共享内存的可能性。内核会尽量避免将相同的东西多次加载到内存中。例如,它会将一个共享库加载到内存中一次,并将其映射到需要使用它的不同进程。每个进程都有自己唯一的共享库地址,但它们都指向相同的实际位置。
如果共享内存区域是可写的,则内核使用一种称为写时复制的方案。如果一个进程写入共享内存,而共享该内存的其他进程不应该看到更改,则在写入请求时创建共享内存的副本。
2009 年 12 月发布的 Linux 内核 2.6.32 为 Linux 提供了一项名为“Kernel SamePage Merging”的功能。这意味着 Linux 可以检测不同地址空间中的相同数据区域。想象一下在一台计算机上运行一系列虚拟机,并且虚拟机都运行相同的操作系统。使用共享内存模型和写时复制,可以大大减少主机上的开销。
所有这些都使得 Linux 中的内存处理变得复杂并且尽可能地优化。但是这种复杂性使得很难查看进程并了解其内存使用情况。
pmap 实用程序
内核通过“/proc”系统信息伪文件系统中的两个伪文件公开了它对 RAM 所做的很多事情。每个进程有两个文件,以每个进程的进程 ID 或 PID 命名:“/proc/maps”和“/proc//smaps”。
pmap
工具从这些文件中读取信息并在终端窗口中显示结果。很明显,每当我们使用 pmap
时,我们都需要提供我们感兴趣的进程的 PID。
查找进程 ID
有几种方法可以找到进程的 PID。这是我们将在示例中使用的一个简单程序的源代码。它是用 C 语言编写的。它所做的只是向终端窗口打印一条消息并等待用户按下“Enter”键。
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("How-To Geek test program.");
getc(stdin);
} // end of main
该程序使用 gcc
编译器编译为名为 pm
的可执行文件:
gcc -o pm pm.c

因为程序会等待用户点击“Enter”,所以它会一直运行到我们想要的时间。
./pm

程序启动、打印消息并等待击键。我们现在可以搜索它的 PID。 ps
命令列出正在运行的进程。 -e
(显示所有进程)选项使 ps
列出每个进程。我们将通过 grep
传输输出并过滤掉名称中包含“pm”的条目。
ps -e | grep pm

这会列出名称中任意位置带有“pm”的所有条目。
我们可以使用 pidof
命令更加具体。我们在命令行中为 pidof
提供我们感兴趣的进程的名称,它会尝试找到匹配项。如果找到匹配项,pidof
会打印匹配进程的 PID。
pidof pm

当您知道进程名称时,pidof
方法更简洁,但即使只知道进程名称的一部分,ps
方法也能正常工作。
使用 pmap
随着我们的测试程序运行,一旦我们确定了它的 PID,我们就可以像这样使用 pmap:
pmap 40919

为我们列出了进程的内存映射。

这是命令的完整输出:
40919: ./pm
000056059f06c000 4K r---- pm
000056059f06d000 4K r-x-- pm
000056059f06e000 4K r---- pm
000056059f06f000 4K r---- pm
000056059f070000 4K rw--- pm
000056059fc39000 132K rw--- [ anon ]
00007f97a3edb000 8K rw--- [ anon ]
00007f97a3edd000 160K r---- libc.so.6
00007f97a3f05000 1616K r-x-- libc.so.6
00007f97a4099000 352K r---- libc.so.6
00007f97a40f1000 4K ----- libc.so.6
00007f97a40f2000 16K r---- libc.so.6
00007f97a40f6000 8K rw--- libc.so.6
00007f97a40f8000 60K rw--- [ anon ]
00007f97a4116000 4K r---- ld-linux-x86-64.so.2
00007f97a4117000 160K r-x-- ld-linux-x86-64.so.2
00007f97a413f000 40K r---- ld-linux-x86-64.so.2
00007f97a4149000 8K r---- ld-linux-x86-64.so.2
00007f97a414b000 8K rw--- ld-linux-x86-64.so.2
00007ffca0e7e000 132K rw--- [ stack ]
00007ffca0fe1000 16K r---- [ anon ]
00007ffca0fe5000 8K r-x-- [ anon ]
ffffffffff600000 4K --x-- [ anon ]
total 2756K
第一行是进程名称及其 PID。其他每一行都显示一个映射的内存地址,以及该地址的内存量,以千字节为单位。每行接下来的五个字符称为虚拟内存权限。有效权限是:
- r:映射的内存可以被进程读取。
- w:映射内存可以被进程写入。
- x:进程可以执行映射内存中包含的任何指令。
- s:映射内存是共享的,对共享内存所做的更改对所有共享内存的进程都是可见的。
- R:没有为这个映射内存预留交换空间。
每行的最后信息是映射源的名称。这可以是进程名称、库名称或系统名称,例如堆栈或堆。
扩展显示
-x
(扩展)选项提供了两个额外的列。
pmap -x 40919

这些列被赋予标题。我们已经看到了“地址”、“千字节”、“模式”和“映射”列。新列称为“RSS”和“Dirty”。

这是完整的输出:
40919: ./pm
Address Kbytes RSS Dirty Mode Mapping
000056059f06c000 4 4 0 r---- pm
000056059f06d000 4 4 0 r-x-- pm
000056059f06e000 4 4 0 r---- pm
000056059f06f000 4 4 4 r---- pm
000056059f070000 4 4 4 rw--- pm
000056059fc39000 132 4 4 rw--- [ anon ]
00007f97a3edb000 8 4 4 rw--- [ anon ]
00007f97a3edd000 160 160 0 r---- libc.so.6
00007f97a3f05000 1616 788 0 r-x-- libc.so.6
00007f97a4099000 352 64 0 r---- libc.so.6
00007f97a40f1000 4 0 0 ----- libc.so.6
00007f97a40f2000 16 16 16 r---- libc.so.6
00007f97a40f6000 8 8 8 rw--- libc.so.6
00007f97a40f8000 60 28 28 rw--- [ anon ]
00007f97a4116000 4 4 0 r---- ld-linux-x86-64.so.2
00007f97a4117000 160 160 0 r-x-- ld-linux-x86-64.so.2
00007f97a413f000 40 40 0 r---- ld-linux-x86-64.so.2
00007f97a4149000 8 8 8 r---- ld-linux-x86-64.so.2
00007f97a414b000 8 8 8 rw--- ld-linux-x86-64.so.2
00007ffca0e7e000 132 12 12 rw--- [ stack ]
00007ffca0fe1000 16 0 0 r---- [ anon ]
00007ffca0fe5000 8 4 0 r-x-- [ anon ]
ffffffffff600000 4 0 0 --x-- [ anon ]
---------------- ------- ------- -------
total kB 2756 1328 96
- RSS:这是驻留集大小。也就是说,当前位于 RAM 中且未换出的内存量。
- 脏:“脏”内存自进程和映射启动以来已更改。
给我看一切
-X
(甚至超过扩展)向输出添加额外的列。注意大写的“X”。另一个名为 -XX
的选项(甚至超过 -X
)向您展示了 pmap
可以从内核获得的一切。由于 -X
是 -XX
的子集,我们将描述 -XX
的输出。
pmap -XX 40919

输出在终端窗口中可怕地缠绕在一起,几乎无法辨认。这是完整的输出:
40919: ./pm
Address Perm Offset Device Inode Size KernelPageSize MMUPageSize Rss Pss Shared_Clean Shared_Dirty Private_Clean Private_Dirty Referenced Anonymous LazyFree AnonHugePages ShmemPmdMapped FilePmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked THPeligible VmFlags Mapping
56059f06c000 r--p 00000000 08:03 393304 4 4 4 4 4 0 0 4 0 4 0 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw sd pm
56059f06d000 r-xp 00001000 08:03 393304 4 4 4 4 4 0 0 4 0 4 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me dw sd pm
56059f06e000 r--p 00002000 08:03 393304 4 4 4 4 4 0 0 4 0 4 0 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw sd pm
56059f06f000 r--p 00002000 08:03 393304 4 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw ac sd pm
56059f070000 rw-p 00003000 08:03 393304 4 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me dw ac sd pm
56059fc39000 rw-p 00000000 00:00 0 132 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd [heap]
7f97a3edb000 rw-p 00000000 00:00 0 8 4 4 4 4 0 0 0 4 4 4 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd
7f97a3edd000 r--p 00000000 08:03 264328 160 4 4 160 4 160 0 0 0 160 0 0 0 0 0 0 0 0 0 0 0 rd mr mw me sd libc.so.6
7f97a3f05000 r-xp 00028000 08:03 264328 1616 4 4 788 32 788 0 0 0 788 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me sd libc.so.6
7f97a4099000 r--p 001bc000 08:03 264328 352 4 4 64 1 64 0 0 0 64 0 0 0 0 0 0 0 0 0 0 0 rd mr mw me sd libc.so.6
7f97a40f1000 ---p 00214000 08:03 264328 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 mr mw me sd libc.so.6
7f97a40f2000 r--p 00214000 08:03 264328 16 4 4 16 16 0 0 0 16 16 16 0 0 0 0 0 0 0 0 0 0 rd mr mw me ac sd libc.so.6
7f97a40f6000 rw-p 00218000 08:03 264328 8 4 4 8 8 0 0 0 8 8 8 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd libc.so.6
7f97a40f8000 rw-p 00000000 00:00 0 60 4 4 28 28 0 0 0 28 28 28 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me ac sd
7f97a4116000 r--p 00000000 08:03 264305 4 4 4 4 0 4 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw sd ld-linux-x86-64.so.2
7f97a4117000 r-xp 00001000 08:03 264305 160 4 4 160 11 160 0 0 0 160 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me dw sd ld-linux-x86-64.so.2
7f97a413f000 r--p 00029000 08:03 264305 40 4 4 40 1 40 0 0 0 40 0 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw sd ld-linux-x86-64.so.2
7f97a4149000 r--p 00032000 08:03 264305 8 4 4 8 8 0 0 0 8 8 8 0 0 0 0 0 0 0 0 0 0 rd mr mw me dw ac sd ld-linux-x86-64.so.2
7f97a414b000 rw-p 00034000 08:03 264305 8 4 4 8 8 0 0 0 8 8 8 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me dw ac sd ld-linux-x86-64.so.2
7ffca0e7e000 rw-p 00000000 00:00 0 132 4 4 12 12 0 0 0 12 12 12 0 0 0 0 0 0 0 0 0 0 rd wr mr mw me gd ac [stack]
7ffca0fe1000 r--p 00000000 00:00 0 16 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 rd mr pf io de dd sd [vvar]
7ffca0fe5000 r-xp 00000000 00:00 0 8 4 4 4 0 4 0 0 0 4 0 0 0 0 0 0 0 0 0 0 0 rd ex mr mw me de sd [vdso]
ffffffffff600000 --xp 00000000 00:00 0 4 4 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ex [vsyscall]
==== ============== =========== ==== === ============ ============ ============= ============= ========== ========= ======== ============= ============== ============= ============== =============== ==== ======= ====== ===========
2756 92 92 1328 157 1220 0 12 96 1328 96 0 0 0 0 0 0 0 0 0 0 KB
这里有很多信息。这是列的内容:
- 地址:该映射的起始地址。这使用虚拟内存寻址。
- Perm:内存的权限。
- Offset:如果内存是基于文件的,则此映射在文件中的偏移量。
- Device:Linux 设备号,以主要和次要编号给出。您可以通过运行
lsblk
命令查看计算机上的设备编号。 - Inode:与映射关联的文件的 inode。例如,在我们的示例中,这可能是保存有关 pm 程序的信息的 inode。
- 大小:内存映射区域的大小。
- KernelPageSize:内核使用的页面大小。
- MMUPageSize:内存管理单元使用的页面大小。
- Rss:这是驻留集大小。也就是说,当前位于 RAM 中且未换出的内存量。
- Pss:这是比例份额。这是添加到(共享大小除以共享数量)的私有共享大小。
- Shared_Clean:自映射创建以来未更改的与其他进程共享的内存量。请注意,即使内存是可共享的,如果它实际上没有被共享,它仍然被认为是私有内存。
- Shared_Dirty:自映射创建以来已更改的与其他进程共享的内存量。
- Private_Clean:自映射创建以来未更改的私有内存量(未与其他进程共享)。
- Private_Dirty:自映射创建以来已更改的私有内存量。
- 已引用:当前标记为已引用或已访问的内存量。
- Anonymous:没有可换出设备的内存。也就是说,它没有文件支持。
- LazyFree:已标记为
MADV_FREE
的页面。这些页面已被标记为可供释放和回收,即使它们可能有未写入的更改。但是,如果在内存映射上设置了MADV_FREE
之后发生后续更改,则会删除MADV_FREE
标志,并且在写入更改之前不会回收页面。< /李> - AnonHugePages:这些是非文件支持的“巨大”内存页面(大于 4 KB)。
- ShmemPmdMapped:与大页面关联的共享内存。它们也可以被完全驻留在内存中的文件系统使用。
- FilePmdMapped:页面中间目录是内核可用的分页方案之一。这是 PMD 条目指向的文件支持页面的数量。
- Shared_Hugetlb:翻译后备表或 TLB 是内存缓存,用于优化访问用户空间内存位置所花费的时间。这个数字是在与共享大内存页面相关联的 TLB 中使用的 RAM 量。
- Private_Hugetlb:这个数字是在与私有大内存页关联的 TLB 中使用的 RAM 量。
- Swap:正在使用的交换量。
- SwapPss:交换比例份额大小。这是由添加到(共享大小除以共享数)的交换专用内存页组成的交换量。
- 锁定:可以锁定内存映射以防止操作系统调出堆或堆外内存。
- THPeligible:这是一个标志,指示映射是否符合分配透明大页面的条件。 1 表示真,0 表示假。透明大页面是一种内存管理系统,可减少在具有大量 RAM 的计算机上查找 TLB 页面的开销。
- VmFlags:请参阅下面的标志列表。
- Mapping:映射源的名称。这可以是进程名称、库名称或系统名称,例如堆栈或堆。
VmFlags——虚拟内存标志——将是以下列表的一个子集。
- rd:可读。
- wr:可写。
- ex:可执行。
- sh:共享。
- 先生:可以阅读。
- mw:可以写。
- 我:可以执行。
- ms:可以分享。
- gd:堆栈段向下增长。
- pf:纯页框编号范围。页框编号是物理内存页的列表。
- dw:禁止写入映射文件。
- lo:页面被锁定在内存中。
- io:内存映射I/O区。
- sr:提供顺序阅读建议(由
madvise()
函数提供。) - rr:提供随机阅读建议。
- dc:如果进程被分叉,则不要复制此内存区域。
- de:不要在重新映射时扩展此内存区域。
- ac:区域负责。
- nr:Swap空间未预留给该区域。
- ht:区域使用巨大的 TLB 页面。
- sf:同步页面错误。
- ar:特定于体系结构的标志。
- wf:如果进程被分叉,则擦除此内存区域。
- dd:不要在核心转储中包含此内存区域。
- sd:软脏标志。
- mm:混合地图区域。
- hg:大页面建议标志。
- nh:没有大页面建议标志。
- mg:可合并建议标志。
- bt:ARM64 偏置温度不稳定受保护页面。
- mt:启用 ARM64 内存标记扩展标记。
- 嗯:Userfaultfd 缺少跟踪。
- uw:Userfaultfd wr-protect 跟踪。
内存管理很复杂
从数据表逆向分析以了解实际情况是很困难的。但至少 pmap
为您提供了全貌,因此您有最好的机会弄清楚您需要了解的内容。
有趣的是,我们的示例程序编译为 16 KB 的二进制可执行文件,但它使用(或共享)大约 2756 KB 的内存,几乎完全是由于运行时库。
最后一个巧妙的技巧是,您可以同时使用 pmap
和 pidof
命令,结合查找进程的 PID 并将其传递给 pmap的操作代码> 变成一个命令:
pmap $(pidof pm)