内存管理:交换、映射与泄漏检测
1. 交换(Swapping)
交换的核心思想是预留一块存储区域,内核可以将未映射到文件的内存页面放置其中,从而释放内存以供其他用途。交换文件的大小增加了物理内存的有效容量。不过,交换并非万能之策,在交换文件与内存之间复制页面存在成本。当系统的实际内存不足以应对工作负载时,交换会成为主要活动,这就是所谓的磁盘抖动(disk thrashing)。
嵌入式设备很少使用交换,因为它与闪存存储不兼容,频繁写入会迅速损耗闪存。不过,可以考虑使用压缩内存交换(zram)。
1.1 压缩内存交换(zram)
zram 驱动会创建基于 RAM 的块设备,如 /dev/zram0、/dev/zram1 等。写入这些设备的页面在存储前会被压缩,压缩比在 30% 到 50% 之间,预计可使可用内存总体增加约 10%,但会增加处理负担和功耗。
启用 zram 的步骤如下:
1. 配置内核选项:
CONFIG_SWAP
CONFIG_CGROUP_MEM_RES_CTLR
CONFIG_CGROUP_MEM_RES_CTLR_SWAP
CONFIG_ZRAM
- 在 /etc/fstab 中添加以下内容以在启动时挂载 zram:
/dev/zram0 none swap defaults zramsize=<size in bytes>,swapprio=<swap partition priority>
- 使用以下命令开启或关闭交换:
# swapon /dev/zram0
# swapoff /dev/zram0
2. 使用 mmap 映射内存
进程启动时,会将一定量的内存映射到程序文件的文本(代码)和数据段,以及与之链接的共享库。进程可以在运行时使用
malloc(3)
在堆上分配内存,通过局部作用域变量和
alloca(3)
在栈上分配内存,还可以使用
dlopen(3)
动态加载库,这些映射由内核处理。此外,进程还可以使用
mmap(2)
显式操作其内存映射:
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
该函数从文件描述符为
fd
的文件中,从偏移量
offset
开始映射
length
字节的内存,并返回映射的指针(假设成功)。由于底层硬件按页工作,
length
会向上取整到最接近的整页数。保护参数
prot
是读、写和执行权限的组合,
flags
参数至少包含
MAP_SHARED
或
MAP_PRIVATE
。
2.1 使用 mmap 分配私有内存
通过在
flags
参数中设置
MAP_ANONYMOUS
并将文件描述符
fd
设置为 -1,可以使用
mmap
分配私有内存区域。这类似于使用
malloc
从堆中分配内存,但内存是页对齐的,且以页为倍数。匿名映射更适合大内存分配,因为它们不会使堆碎片化。实际上,当请求超过 128 KiB 时,
malloc
(至少在 glibc 中)会停止从堆中分配内存,而使用
mmap
。
2.2 使用 mmap 共享内存
POSIX 共享内存需要使用
mmap
访问内存段。在这种情况下,设置
MAP_SHARED
标志并使用
shm_open()
返回的文件描述符:
int shm_fd;
char *shm_p;
shm_fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, 65536);
shm_p = mmap(NULL, 65536, PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
2.3 使用 mmap 访问设备内存
驱动程序可以允许其设备节点进行 mmap 操作,与应用程序共享部分设备内存,具体实现取决于驱动程序。
2.3.1 Linux 帧缓冲器(/dev/fb0)
接口定义在
/usr/include/linux/fb.h
中,包括一个
ioctl
函数用于获取显示大小和每像素位数。可以使用
mmap
让视频驱动程序与应用程序共享帧缓冲器:
int f;
int fb_size;
unsigned char *fb_mem;
f = open("/dev/fb0", O_RDWR);
/* Use ioctl FBIOGET_VSCREENINFO to find the display dimensions
and calculate fb_size */
fb_mem = mmap(0, fb_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* read and write pixels through pointer fb_mem */
2.3.2 视频流接口(V4L2)
定义在
/usr/include/linux/videodev2.h
中,每个视频设备有一个名为
/dev/videoN
的节点。可以使用
ioctl
函数让驱动程序分配视频缓冲区,然后使用
mmap
将其映射到用户空间。
3. 应用程序内存使用情况
确定应用程序使用了多少内存并非易事,因为用户空间内存的分配、映射和共享方式多种多样。可以使用
free
命令询问内核可用内存:
total used free shared buffers cached
Mem: 509016 504312 4704 0 26456 363860
-/+ buffers/cache: 113996 395020
Swap: 0 0 0
乍一看,系统似乎几乎耗尽了内存,但实际上,Linux 会将空闲内存用于缓冲区和缓存,需要时可以回收。真正的可用内存是去除缓冲区和缓存后的数值,即 395,020 KiB,占总量的 77%。使用
free
命令时,第二行标记为
-/+ buffers/cache
的数字很重要。
可以通过向
/proc/sys/vm/drop_caches
写入 1 到 3 之间的数字来强制内核释放缓存:
# echo 3 > /proc/sys/vm/drop_caches
这个数字是一个位掩码,用于确定要释放的缓存类型:1 表示页缓存,2 表示目录项和索引节点缓存。
4. 进程内存使用情况
有几个指标可以衡量进程使用的内存量,这里介绍两个最容易获取的指标:虚拟集大小(Vss)和常驻内存大小(Rss),它们在大多数
ps
和
top
命令的实现中都可用。
| 指标 | 含义 |
|---|---|
Vss(在
ps
中为 VSZ,在
top
中为 VIRT)
|
进程映射的总内存量,是
/proc/<PID>/map
中所有区域的总和。由于虚拟内存只有部分会在任何时候提交到物理内存,这个数字的参考价值有限。
|
Rss(在
ps
中为 RSS,在
top
中为 RES)
| 映射到物理内存页的内存总和,更接近进程的实际内存预算,但如果将所有进程的 Rss 相加,会高估内存使用量,因为有些页面是共享的。 |
4.1 使用 top 和 ps
BusyBox 中的
top
和
ps
版本提供的信息有限,这里使用 procps 包中的完整版本。
ps
命令可以使用
-Aly
选项显示 Vss(VSZ)和 Rss(RSS),也可以使用自定义格式:
# ps -eo pid,tid,class,rtprio,stat,vsz,rss,comm
PID TID CLS RTPRIO STAT VSZ RSS COMMAND
1 1 TS - Ss 4496 2652 systemd
[...]
205 205 TS - Ss 4076 1296 systemd-journal
228 228 TS - Ss 2524 1396 udevd
581 581 TS - Ss 2880 1508 avahi-daemon
584 584 TS - Ss 2848 1512 dbus-daemon
590 590 TS - Ss 1332 680 acpid
594 594 TS - Ss 4600 1564 wpa_supplicant
top
命令会显示空闲内存摘要和每个进程的内存使用情况:
top - 21:17:52 up 10:04, 1 user, load average: 0.00, 0.01, 0.05
Tasks: 96 total, 1 running, 95 sleeping, 0 stopped, 0 zombie
%Cpu(s): 1.7 us, 2.2 sy, 0.0 ni, 95.9 id, 0.0 wa, 0.0 hi
KiB Mem: 509016 total, 278524 used, 230492 free, 25572 buffers
KiB Swap: 0 total, 0 used, 0 free, 170920 cached
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
595 root 20 0 64920 9.8m 4048 S 0.0 2.0 0:01.09 node
866 root 20 0 28892 9152 3660 S 0.2 1.8 0:36.38 Xorg
[...]
这些简单的命令可以让你大致了解内存使用情况,当看到某个进程的 Rss 不断增加时,可能存在内存泄漏。但它们在内存使用的绝对测量方面并不十分准确。
4.2 使用 smem
2009 年,Matt Mackall 引入了两个新指标:唯一集大小(Uss)和比例集大小(Pss)。
- Uss:提交到物理内存且特定于某个进程的内存量,即进程终止时会释放的内存量。
- Pss:将提交到物理内存的共享页面的会计信息分配给所有映射它们的进程。例如,如果一个 12 页长的库代码区域由六个进程共享,每个进程在 Pss 中会累积两页。因此,将所有进程的 Pss 相加,可得到这些进程实际使用的内存量。
Pss 的信息可以在
/proc/<PID>/smaps
中找到,以下是一个示例:
b6e6d000-b6f45000 r-xp 00000000 b3:02 2444 /lib/libc-2.13.so
Size: 864 kB
Rss: 264 kB
Pss: 6 kB
Shared_Clean: 264 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 0 kB
Referenced: 264 kB
Anonymous: 0 kB
AnonHugePages: 0 kB
Swap: 0 kB
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Locked: 0 kB
VmFlags: rd ex mr mw me
可以看到,虽然 Rss 为 264 KiB,但由于该区域被多个进程共享,Pss 仅为 6 KiB。
有一个名为
smem
的工具可以从
smaps
文件中收集信息,并以各种方式呈现,包括饼图或条形图。其项目页面为 https://www.selenic.com/smem,大多数桌面发行版都有该工具的包。但由于它是用 Python 编写的,在嵌入式目标上安装需要 Python 环境,可能比较麻烦。为此,有一个名为
smemcap
的小程序,它可以捕获目标上
/proc
的状态并保存到 TAR 文件中,以便稍后在主机计算机上分析。它是 BusyBox 的一部分,也可以从
smem
源代码编译。
以 root 身份本地运行
smem
的结果如下:
# smem -t
PID User Command Swap USS PSS RSS
610 0 /sbin/agetty -s ttyO0 11 0 128 149 720
1236 0 /sbin/agetty -s ttyGS0 1 0 128 149 720
609 0 /sbin/agetty tty1 38400 0 144 163 724
578 0 /usr/sbin/acpid 0 140 173 680
819 0 /usr/sbin/cron 0 188 201 704
634 103 avahi-daemon: chroot hel 0 112 205 500
980 0 /usr/sbin/udhcpd -S /etc 0 196 205 568
...
836 0 /usr/bin/X :0 -auth /var 0 7172 7746 9212
583 0 /usr/bin/node autorun.js 0 8772 9043 10076
1089 1000 /usr/bin/python -O /usr/ 0 9600 11264 16388
------------------------------------------------------------------
53 6 0 65820 78251 146544
从输出的最后一行可以看出,在这种情况下,总 Pss 约为 Rss 的一半。
如果目标上没有或不想安装 Python,可以使用
smemcap
捕获状态:
# smemcap > smem-bbb-cap.tar
然后将 TAR 文件复制到主机,并使用
smem -S
读取:
$ smem -t -S smem-bbb-cap.tar
输出与本地运行
smem
相同。
4.3 其他工具
-
ps_mem(https://github.com/pixelb/ps_mem):以更简单的格式显示 Pss 信息,也是用 Python 编写的。 -
procrank:Android 中的一个工具,显示每个进程的 Uss 和 Pss 摘要,可以对其进行一些小修改后为嵌入式 Linux 进行交叉编译,代码可从 https://github.com/csimmonds/procrank_linux 获取。
5. 内存泄漏检测
内存泄漏是指内存分配后在不再需要时未被释放的情况。虽然内存泄漏并非嵌入式系统所独有,但由于嵌入式设备内存有限且通常长时间运行而不重启,泄漏问题可能会变得更加严重。
当运行
free
或
top
命令,发现即使释放缓存后空闲内存仍持续减少时,可能存在内存泄漏。可以通过查看每个进程的 Uss 和 Rss 来确定泄漏的进程。
5.1 mtrace
mtrace
是 glibc 的一个组件,用于跟踪
malloc
、
free
及相关函数的调用,并在程序退出时识别未释放的内存区域。使用步骤如下:
1. 在程序中调用
mtrace()
函数开始跟踪。
2. 在运行时,将跟踪信息的写入路径写入
MALLOC_TRACE
环境变量。如果
MALLOC_TRACE
不存在或文件无法打开,
mtrace
钩子将不会安装。
3. 通常使用
mtrace
命令查看跟踪信息。
以下是一个示例:
#include <mcheck.h>
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
int j;
mtrace();
for (j = 0; j < 2; j++)
malloc(100); /* Never freed:a memory leak */
calloc(16, 16); /* Never freed:a memory leak */
exit(EXIT_SUCCESS);
}
运行程序并查看跟踪信息的结果如下:
$ export MALLOC_TRACE=mtrace.log
$ ./mtrace-example
$ mtrace mtrace-example mtrace.log
Memory not freed:
-----------------
Address Size Caller
0x0000000001479460 0x64 at /home/chris/mtrace-example.c:11
0x00000000014794d0 0x64 at /home/chris/mtrace-example.c:11
0x0000000001479540 0x100 at /home/chris/mtrace-example.c:15
遗憾的是,
mtrace
只能在程序退出后才能告诉你泄漏的内存情况。
5.2 Valgrind
Valgrind 是一个强大的工具,用于发现内存问题,包括泄漏和其他问题。其优点是无需重新编译要检查的程序和库,但如果程序和库使用
-g
选项编译并包含调试符号表,效果会更好。
Valgrind 通过在模拟环境中运行程序并在各个点捕获执行来工作,但这也导致了一个很大缺点,即程序运行速度会大幅降低,因此在测试有实时约束的程序时不太有用。
Valgrind 包含多个诊断工具:
-
memcheck
:默认工具,用于检测内存泄漏和内存的一般误用。
-
cachegrind
:计算处理器缓存命中率。
-
callgrind
:计算每个函数调用的成本。
-
helgrind
:突出显示 Pthread API 的误用,包括潜在的死锁和竞态条件。
-
DRD
:另一个 Pthread 分析工具。
-
massif
:分析堆和栈的使用情况。
可以使用
-tool
选项选择所需的工具。Valgrind 支持主要的嵌入式平台,如 ARM(cortex A)、PPC、MIPS 和 x86(32 位和 64 位变体),在 Yocto Project 和 Buildroot 中都有可用的包。
使用默认的
memcheck
工具和
--leak-check=full
选项来查找内存泄漏:
$ valgrind --leak-check=full ./mtrace-example
==17235== Memcheck, a memory error detector
==17235== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward
et al.==17235== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for
copyright info
==17235== Command: ./mtrace-example
==17235==
==17235==
==17235== HEAP SUMMARY:
==17235== in use at exit: 456 bytes in 3 blocks
==17235== total heap usage: 3 allocs, 0 frees, 456 bytes
allocated
==17235==
==17235== 200 bytes in 2 blocks are definitely lost in loss record
1 of 2==17235== at 0x4C2AB80: malloc (in
/usr/lib/valgrind/vgpreload_memcheck-linux.so)
==17235== by 0x4005FA: main (mtrace-example.c:12)
==17235==
==17235== 256 bytes in 1 blocks are definitely lost in loss record
2 of 2==17235== at 0x4C2CC70: calloc (in
/usr/lib/valgrind/vgpreload_memcheck-linux.so)
==17235== by 0x400613: main (mtrace-example.c:14)
==17235==
==17235== LEAK SUMMARY:
==17235== definitely lost: 456 bytes in 3 blocks
==17235== indirectly lost: 0 bytes in 0 blocks
==17235== possibly lost: 0 bytes in 0 blocks
==17235== still reachable: 0 bytes in 0 blocks
==17235== suppressed: 0 bytes in 0 blocks
==17235==
==17235== For counts of detected and suppressed errors, rerun
with: -v==17235== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0
from 0)
通过以上工具和方法,可以有效地管理和监控内存使用情况,及时发现和解决内存泄漏问题,确保系统的稳定运行。
6. 内存管理流程总结
为了更清晰地展示内存管理的整体流程,下面通过 mermaid 格式的流程图来呈现:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B{是否需要交换内存?}:::decision
B -- 是 --> C(启用交换:配置内核选项):::process
C --> D(在 /etc/fstab 中挂载 zram):::process
D --> E(使用 swapon 或 swapoff 控制交换):::process
B -- 否 --> F{是否需要映射内存?}:::decision
F -- 是 --> G(使用 mmap 进行内存映射):::process
G --> H{映射类型?}:::decision
H -- 私有内存 --> I(设置 MAP_ANONYMOUS 和 fd=-1):::process
H -- 共享内存 --> J(设置 MAP_SHARED 并使用 shm_open 描述符):::process
H -- 设备内存 --> K(根据设备驱动使用 mmap):::process
F -- 否 --> L{是否需要监控内存使用?}:::decision
L -- 是 --> M(使用 free 查看可用内存):::process
M --> N(使用 top/ps/smem 查看进程内存):::process
N --> O{是否存在内存泄漏?}:::decision
O -- 是 --> P(使用 mtrace/Valgrind 检测泄漏):::process
O -- 否 --> Q([结束]):::startend
E --> F
I --> L
J --> L
K --> L
P --> Q
这个流程图涵盖了内存交换、映射、监控以及泄漏检测的主要步骤,帮助我们从宏观上理解内存管理的过程。
7. 不同内存指标对比
为了更直观地对比不同的内存指标,我们将之前介绍的 Vss、Rss、Uss 和 Pss 进行整理,形成如下表格:
| 指标 | 含义 | 特点 | 用途 |
| ---- | ---- | ---- | ---- |
| Vss(VSZ/VIRT) | 进程映射的总内存量 | 包含虚拟内存,参考价值有限 | 初步了解进程内存映射规模 |
| Rss(RSS/RES) | 映射到物理内存页的内存总和 | 接近实际内存预算,但会高估共享内存 | 快速判断进程物理内存使用情况 |
| Uss | 特定于某个进程的物理内存量 | 进程终止时会释放的内存 | 精确衡量单个进程独占内存 |
| Pss | 共享页面按比例分配的物理内存量 | 所有进程 Pss 相加为实际使用内存 | 准确统计所有进程实际使用内存 |
通过这个表格,我们可以清晰地看到每个指标的特点和适用场景,在实际监控内存时可以根据需求选择合适的指标。
8. 内存管理工具使用总结
8.1 工具选择建议
不同的内存管理工具适用于不同的场景,以下是一个简单的选择建议列表:
- 快速查看系统整体内存情况:使用
free
命令,重点关注
-/+ buffers/cache
行的数字。
- 初步了解进程内存使用:使用
top
或
ps
命令,查看 Vss 和 Rss 指标。
- 精确统计进程实际内存使用:使用
smem
工具,查看 Uss 和 Pss 指标。
- 简单查看 Pss 信息:使用
ps_mem
工具。
- 嵌入式系统获取内存状态:使用
smemcap
捕获状态,后续在主机分析。
- 检测 Android 进程内存:使用
procrank
工具。
- 检测内存泄漏:使用
mtrace
或
Valgrind
工具。
8.2 工具使用流程对比
为了更清晰地对比不同工具的使用流程,下面通过表格展示:
| 工具 | 使用步骤 | 优点 | 缺点 |
| ---- | ---- | ---- | ---- |
| free | 直接在终端输入
free
命令 | 简单快捷,查看系统整体内存 | 无法精确到进程 |
| top/ps | 输入相应命令,如
ps -eo pid,tid,class,rtprio,stat,vsz,rss,comm
| 查看进程基本内存信息 | 绝对测量不准确 |
| smem | 以 root 身份运行
smem -t
,或使用
smemcap
后在主机分析 | 提供 Uss 和 Pss 指标,统计准确 | 嵌入式安装需要 Python 环境 |
| ps_mem | 运行命令查看 Pss 信息 | 格式简单 | 功能相对单一 |
| procrank | 交叉编译后在嵌入式系统运行 | 适用于 Android 进程内存查看 | 需要交叉编译 |
| mtrace | 在程序中调用
mtrace()
,设置
MALLOC_TRACE
,使用
mtrace
查看 | 可定位未释放内存 | 只能在程序退出后检测 |
| Valgrind | 使用
valgrind --leak-check=full
检测 | 功能强大,可检测多种内存问题 | 运行速度慢 |
9. 内存管理最佳实践
9.1 内存交换方面
- 对于嵌入式设备,由于闪存存储的特性,优先考虑使用压缩内存交换(zram),但要注意其会增加处理负担和功耗。
- 合理配置交换分区的优先级和大小,避免频繁的磁盘抖动。
9.2 内存映射方面
-
对于大内存分配,优先使用匿名映射(
mmap设置MAP_ANONYMOUS),减少堆碎片化。 -
在共享内存时,确保正确设置
MAP_SHARED标志和使用shm_open返回的文件描述符。
9.3 内存监控方面
-
定期使用
free命令查看系统可用内存,结合top、ps和smem工具监控进程内存使用。 -
当发现某个进程的 Rss 持续增加时,及时使用
mtrace或Valgrind检测是否存在内存泄漏。
9.4 内存泄漏方面
-
在编写代码时,养成良好的内存管理习惯,确保每次
malloc或calloc后都有对应的free操作。 - 对于复杂的程序,定期使用内存检测工具进行检查,尤其是在长时间运行的系统中。
通过遵循这些最佳实践,可以有效地管理内存,提高系统的性能和稳定性。
10. 总结
内存管理是系统运行中至关重要的一部分,涉及到内存交换、映射、监控和泄漏检测等多个方面。本文详细介绍了各种内存管理的方法和工具,包括交换内存的启用、
mmap
的使用、不同内存指标的含义以及多种内存监控和检测工具的操作步骤。通过合理运用这些方法和工具,我们可以更好地掌握系统的内存使用情况,及时发现和解决内存泄漏等问题,确保系统的高效稳定运行。
在实际应用中,我们需要根据具体的场景和需求选择合适的内存管理策略和工具。例如,对于嵌入式设备,要考虑其硬件特性和资源限制;对于大型服务器,要关注多进程的内存共享和优化。同时,不断学习和掌握新的内存管理技术,也是提高系统性能的关键。希望本文能够为大家在内存管理方面提供有益的参考和帮助。
超级会员免费看

被折叠的 条评论
为什么被折叠?



