33、内存管理:交换、映射与泄漏检测

内存管理:交换、映射与泄漏检测

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 
  1. 在 /etc/fstab 中添加以下内容以在启动时挂载 zram:
/dev/zram0 none swap defaults zramsize=<size in bytes>,swapprio=<swap partition priority>
  1. 使用以下命令开启或关闭交换:
# 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 的使用、不同内存指标的含义以及多种内存监控和检测工具的操作步骤。通过合理运用这些方法和工具,我们可以更好地掌握系统的内存使用情况,及时发现和解决内存泄漏等问题,确保系统的高效稳定运行。

在实际应用中,我们需要根据具体的场景和需求选择合适的内存管理策略和工具。例如,对于嵌入式设备,要考虑其硬件特性和资源限制;对于大型服务器,要关注多进程的内存共享和优化。同时,不断学习和掌握新的内存管理技术,也是提高系统性能的关键。希望本文能够为大家在内存管理方面提供有益的参考和帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值