Skip to content

Latest commit

 

History

History
707 lines (527 loc) · 39.2 KB

File metadata and controls

707 lines (527 loc) · 39.2 KB

十八、管理内存

本章讨论与内存管理相关的问题,这对于任何 Linux 系统都是一个重要的主题,尤其是对于嵌入式 Linux,因为在嵌入式 Linux 中,系统内存通常是有限的。 在简要回顾了一下虚拟内存之后,我将向您展示如何度量内存使用情况、如何检测内存分配问题(包括内存泄漏)以及内存用完时会发生什么情况。 您必须了解可用的工具,从简单的工具(如freetop)到复杂的工具(如mtrace和 Valgrind)。

我们将了解内核和用户空间内存之间的区别,以及内核如何将内存的物理页面映射到进程的地址空间。 然后,我们将定位并读取proc文件系统下各个进程的内存映射。 我们将了解如何使用mmap系统调用将程序的内存映射到文件,以便它可以批量分配内存或与另一个进程共享内存。 在本章的后半部分,我们将使用ps测量每个进程的内存使用情况,然后再使用更精确的工具,如smemps_mem

在本章中,我们将介绍以下主题:

  • 虚拟内存基础知识
  • 内核空间内存布局
  • 用户空间内存布局
  • 进程内存映射
  • 交换 / 适合交换的东西 / 互惠信贷 / 交换之物
  • mmap映射内存
  • 我的应用使用多少内存?
  • 每进程内存使用量
  • 识别内存泄漏
  • 内存不足

技术要求

要按照示例操作,请确保您具备以下条件:

  • 安装了gccmaketopprocpsvalgrindsmem的基于 Linux 的主机系统

所有这些工具都可以在大多数流行的 Linux 发行版(如 Ubuntu、Arch 等)上使用。

本章的所有代码都可以在本书 GitHub 存储库的Chapter18文件夹中找到:https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition

虚拟内存基础知识

简单地说,Linux 配置了 CPU 的内存管理单元(MMU),以向 32 位处理器上运行的 progRAM 提供一个虚拟地址空间,该内存从零开始,以最高地址0xffffffff结束。 默认情况下,此地址空间分为 4 KiB 的页面。 如果 4 KiB 页对于您的应用来说太小,则可以将内核配置为使用HugePages,从而减少访问页表项所需的系统资源量,并增加转换后备缓冲器(TLB)的命中率。

Linux 将这个虚拟地址空间划分为应用的区域(称为用户空间)和内核的区域(称为内核空间)。 两者之间的分割由名为PAGE_OFFSET的内核配置参数设置。 在典型的 32 位嵌入式系统中,PAGE_OFFSET0xc0000000,将较低的 3 GB 分配给用户空间,将最高的 1 GB 分配给内核空间。 用户地址空间是为每个进程分配的,因此每个进程都运行在一个沙箱中,与其他进程分开。 所有进程的内核地址空间都是相同的,因为只有一个内核。

此虚拟地址空间中的页面由 MMU 映射到物理地址,MMU 使用页表执行映射。

虚拟内存的每一页可以按如下方式取消映射或映射:

  • 未映射,因此尝试访问这些地址将导致SIGSEGV
  • 映射到进程私有的物理内存页。
  • 映射到与其他进程共享的物理内存页。
  • 映射和与写入时复制(COW)标志集共享:写入被困在内核中,它生成页面的副本并将其映射到进程,以代替原始页面,然后允许写入发生。
  • 映射到内核使用的物理内存页。

内核还可以将页面映射到保留的存储器区域,例如以访问设备驱动程序中的寄存器和存储器缓冲区。

一个明显的问题是这:为什么我们要这样做,而不是像典型的 RTOS 那样直接引用物理内存?

虚拟内存有许多优点,下面将介绍其中一些优点:

  • 捕获无效的存储器访问,并由SIGSEGV向应用发出警报。
  • 进程在自己的内存空间中运行,与其他进程隔离。
  • 通过共享公共代码和数据(例如,在库中)有效地使用内存。
  • 虽然在嵌入式目标上进行交换的可能性很小,但通过添加交换文件来增加物理内存量的可能性很小。

这些都是有力的论据,但我必须承认也有一些缺点。 很难确定应用的实际内存预算,这是本章主要关注的问题之一。 默认的分配策略是过度提交,这会导致棘手的内存不足情况,我将在后面的内存不足小节中讨论这一点。 最后,内存管理代码在处理异常(页面错误)时引入的延迟降低了系统的确定性,这对实时程序很重要。 我将在第 21 章实时编程中介绍这一点。

内核空间和用户空间的内存管理是不同的。 接下来的几节描述了本质区别和您需要知道的事情。

内核空间内存布局

内核内存以一种相当简单的方式进行管理。 它不是按需分页的,这意味着对于使用kmalloc()或类似函数的每个分配,都有实际的物理内存。 内核内存永远不会被丢弃或调出。

某些体系结构在内核日志消息中显示引导时内存映射的摘要。 此跟踪来自 32 位 ARM 设备(Beaglebone Black):

Memory: 511MB = 511MB total
Memory: 505980k/505980k available, 18308k reserved, 0K highmem
Virtual kernel memory layout:
    vector  : 0xffff0000 - 0xffff1000  (   4 kB)
    fixmap  : 0xfff00000 - 0xfffe0000  ( 896 kB)
    vmalloc : 0xe0800000 - 0xff000000  ( 488 MB)
    lowmem  : 0xc0000000 - 0xe0000000  ( 512 MB)
    pkmap   : 0xbfe00000 - 0xc0000000  (   2 MB)
    modules : 0xbf800000 - 0xbfe00000  (   6 MB)
      .text : 0xc0008000 - 0xc0763c90  (7536 kB)
      .init : 0xc0764000 - 0xc079f700  ( 238 kB)
      .data : 0xc07a0000 - 0xc0827240  ( 541 kB)
       .bss : 0xc0827240 - 0xc089e940  ( 478 kB)

可用 505980 KiB 的数字是内核在开始执行但在开始进行动态分配之前看到的空闲内存量。

内核空间内存的使用者包括:

  • 内核本身,换句话说,即引导时从内核映像文件加载的代码和数据。 这在前面的内核日志.text.init.data.bss中显示。 段一旦内核完成初始化,.init段就会被释放。
  • 通过板片分配器分配的内存,用于各种内核数据结构。 这包括使用kmalloc()进行的分配。 他们来自标有LOW MEM的区域。
  • 通过vmalloc()分配的内存,通常用于比通过kmalloc()可用的内存块更大的内存块。 它们位于vmalloc区域。
  • 设备驱动程序访问属于各种硬件位的寄存器和内存的映射,您可以通过读取/proc/iomem来查看。 这些内存也来自vmalloc区域,但是因为它们被映射到主系统内存之外的物理内存,所以它们不会占用任何实际内存。
  • 内核模块,加载到标记为模块的区域。
  • 在其他任何地方都未跟踪的其他低级别分配。

既然我们已经了解了内核空间中的内存布局,让我们来了解一下内核实际使用了多少内存。

内核使用多少内存?

不幸的是,对于内核使用了多少内存的问题,没有的确切答案,但下面是我们所能得到的最接近的答案。

首先,您可以在前面所示的内核日志中查看内核代码和数据占用的内存,也可以使用size命令,如下所示:

$ arm-poky-linux-gnueabi-size vmlinux
text data bss dec hex filename
9013448 796868 8428144 18238460 1164bfc vmlinux

通常,与内存总量相比,内核为此处显示的静态代码和数据段占用的内存量很小。 如果不是这样,您需要检查内核配置并删除不需要的组件。 一项允许构建被称为Linux Kernel Tinalization的小内核的努力一直在取得良好进展,直到该项目陷入停滞,Josh Triplett 的补丁最终在 2016 年从linux-next树中删除。 现在,要减少内核的内存大小,最好的办法是就地执行(xip),用内存换取闪存(https://lwn.net/Articles/748198/)。

您可以通过阅读/proc/meminfo获取有关内存使用的更多信息:

# cat /proc/meminfo
MemTotal: 509016 kB
MemFree: 410680 kB
Buffers: 1720 kB
Cached: 25132 kB
SwapCached: 0 kB
Active: 74880 kB
Inactive: 3224 kB
Active(anon): 51344 kB
Inactive(anon): 1372 kB
Active(file): 23536 kB
Inactive(file): 1852 kB
Unevictable: 0 kB
Mlocked: 0 kB
HighTotal: 0 kB
HighFree: 0 kB
LowTotal: 509016 kB
LowFree: 410680 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 16 kB
Writeback: 0 kB
AnonPages: 51248 kB
Mapped: 24376 kB
Shmem: 1452 kB
Slab: 11292 kB
SReclaimable: 5164 kB
SUnreclaim: 6128 kB
KernelStack: 1832 kB
PageTables: 1540 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 254508 kB
Committed_AS: 734936 kB
VmallocTotal: 499712 kB
VmallocUsed: 29576 kB
VmallocChunk: 389116 kB

手册页proc(5)上有对每个字段的说明。 内核内存使用量是以下各项的总和:

  • Slab:分片分配器分配的总内存
  • KernelStack:执行内核代码时使用的堆栈空间
  • PageTables:用于存储页表的内存
  • VmallocUsedvmalloc()分配的内存

在片分配的情况下,您可以通过读取/proc/slabinfo来获取更多信息。 同样,vmalloc区域在/proc/vmallocinfo中的分配也有细分。 在这两种情况下,您都需要了解内核及其子系统的详细知识,才能确切了解哪个子系统正在进行分配以及为什么进行分配,这超出了本文的讨论范围。

对于模块,您可以使用lsmod来查找代码和数据占用的内存空间:

# lsmod
Module Size Used by
g_multi 47670 2
libcomposite 14299 1 g_multi
mt7601Usta 601404 0

这就留下了没有记录的低级别分配,这阻止了我们生成内核空间内存使用的准确帐户。 当我们将我们所知道的所有内核和用户空间分配加在一起时,这将显示为缺少内存。

测量内核空间内存使用情况很复杂。 /proc/meminfo中的信息有些有限,/proc/slabinfo/proc/vmallocinfo提供的附加信息很难解释。 用户空间通过进程内存映射提供了更好的内存使用情况可见性。

用户空间内存布局

Linux 对用户空间采用了一种惰性的分配策略,仅在程序访问时映射物理内存页面。 例如,使用malloc(3)分配 1 MiB 的缓冲区会返回指向内存地址块的指针,但不会返回实际物理内存。 在页表条目中设置标志,使得内核捕获任何读或写访问。 这称为,即页错误。 只有在这一点上,内核才会尝试查找一页物理内存,并将其添加到进程的页表映射中。 用一个简单的程序MELP/Chapter18/pagefault-demo来演示这一点是值得的:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#define BUFFER_SIZE (1024 * 1024)
void print_pgfaults(void)
{
     int ret;
     struct rusage usage;
     ret = getrusage(RUSAGE_SELF, &usage);
     if (ret == -1) {
           perror("getrusage");
      } else {
           printf("Major page faults %ld\n", usage.ru_majflt);
           printf("Minor page faults %ld\n", usage.ru_minflt);
     }
}
int main(int argc, char *argv[])
{
     unsigned char *p;
     printf("Initial state\n");
     print_pgfaults();
     p = malloc(BUFFER_SIZE);
     printf("After malloc\n");
     print_pgfaults();
     memset(p, 0x42, BUFFER_SIZE);
     printf("After memset\n");
     print_pgfaults();
     memset(p, 0x42, BUFFER_SIZE);
     printf("After 2nd memset\n");
     print_pgfaults();
     return 0;
}

当运行它时,您将看到如下输出:

Initial state 
Major page faults 0 
Minor page faults 172 
After malloc 
Major page faults 0 
Minor page faults 186 
After memset 
Major page faults 0 
Minor page faults 442 
After 2nd memset 
Major page faults 0 
Minor page faults 442

在初始化程序环境后遇到 172 个小页面错误,在调用getrusage(2)时又遇到了 14 个小页面错误(这些数字将根据您使用的 C 库的体系结构和版本的不同而有所不同)。 重要的部分是在内存中填满数据时的增加:442-186=256。 缓冲区为 1MiB,即 256 页。 第二次调用memset(3)没有什么不同,因为现在所有页面都已映射。

如您所见,当内核捕获对尚未映射的页面的访问时,会生成页面错误。 实际上,有两种页面错误:minormajor。 如果出现一个小故障,内核只需找到一页物理内存并将其映射到进程地址空间,如前面的代码所示。 当虚拟内存被映射到文件时,例如,使用mmap(2)(我将稍后描述),就会发生重大的页面错误。 从这个内存中读取意味着内核不仅要找到一页内存并将其映射进去,还必须用文件中的数据填充它。 因此,主要故障在时间和系统资源方面的成本要高得多。

虽然getrusage(2)提供了关于 进程内的次要和主要页面错误的有用度量,但有时我们真正希望看到的是进程的总体内存 map。

进程内存映射

在用户空间中运行的每个进程都有一个我们可以检查的进程映射。 这些内存映射告诉我们程序的内存是如何分配的,以及它链接到哪些共享库。

您可以通过proc文件系统查看进程的内存映射。 作为示例,下面是init进程 PID1的映射:

# cat /proc/1/maps
00008000-0000e000 r-xp 00000000 00:0b 23281745 /sbin/init
00016000-00017000 rwxp 00006000 00:0b 23281745 /sbin/init
00017000-00038000 rwxp 00000000 00:00 0        [heap]
b6ded000-b6f1d000 r-xp 00000000 00:0b 23281695 /lib/libc-2.19.so
b6f1d000-b6f24000 ---p 00130000 00:0b 23281695 /lib/libc-2.19.so
b6f24000-b6f26000 r-xp 0012f000 00:0b 23281695 /lib/libc-2.19.so
b6f26000-b6f27000 rwxp 00131000 00:0b 23281695 /lib/libc-2.19.so
b6f27000-b6f2a000 rwxp 00000000 00:00 0
b6f2a000-b6f49000 r-xp 00000000 00:0b 23281359 /lib/ld-2.19.so
b6f4c000-b6f4e000 rwxp 00000000 00:00 0
b6f4f000-b6f50000 r-xp 00000000 00:00 0        [sigpage]
b6f50000-b6f51000 r-xp 0001e000 00:0b 23281359 /lib/ld-2.19.so
b6f51000-b6f52000 rwxp 0001f000 00:0b 23281359 /lib/ld-2.19.so
beea1000-beec2000 rw-p 00000000 00:00 0        [stack]
ffff0000-ffff1000 r-xp 00000000 00:00 0        [vectors]

前两列显示开始和结束虚拟地址以及每个映射的权限。 权限如下所示:

  • r:读取
  • w:写入
  • ==同步,由 Elderman 更正==@ELDER_MAN
  • s:共享
  • p:私有(写入时拷贝)

如果映射与文件相关联,则文件名将显示在最后一列中,而第三、四和五列包含距文件开头的偏移量、数据块设备号和文件的信息节点。 大多数映射都指向程序本身及其链接的库。 程序可以在两个区域分配内存,标记为[heap][stack]。 使用 malloc 分配的内存来自前者(除了非常大的分配,我们将在后面讨论);堆栈上的分配来自后者。 这两个区域的最大大小由进程的ulimit控制:

  • ulimit -d,默认为无限制
  • 堆栈ulimit -s,默认 8 MiB

超出限制的分配将被SIGSEGV拒绝。

当内存耗尽时,内核可能决定丢弃映射到文件且为只读的页面。 如果再次访问该页,将导致严重的页错误,并从文件中读回该页。

交换

交换的想法是保留一些存储,内核可以在其中放置未映射到文件的内存页,从而释放内存用于其他用途。 它通过交换文件的大小增加物理内存的有效大小。 这不是灵丹妙药:向交换文件复制页面和从交换文件复制页面是有成本的,这一点在实际内存太少而无法承载工作负载的系统上变得明显,因此交换成为主要活动。 这有时称为,也称为磁盘抖动

交换很少在嵌入式设备上使用,因为它在闪存上不能很好地工作,因为在闪存中持续写入会很快耗尽它。 但是,您可能需要考虑交换到压缩 RAM(zram)

交换到压缩内存(Zram)

zram驱动程序创建名为/dev/zram0/dev/zram1等的基于 RAM 的块设备。 写入这些设备的页面在存储之前会被压缩。 当压缩比在 30%到 50%的范围内时,您可以预期空闲内存的总体增长约为 10%,但代价是更多的处理和相应的电力使用增加。

要启用 zram,请使用以下选项配置内核:

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

将内存换出到 zram 比换出到闪存要好,但这两种技术都不能替代足够的物理内存。

用户空间进程依赖内核来管理它们的虚拟内存。 有时,程序希望对其内存映射进行比内核所能提供的更大的控制。 有一个系统调用,它允许我们将内存映射到一个文件,以便从用户空间进行更直接的访问。

使用 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描述符映射文件中的length字节内存(从文件中的offset开始),并返回指向映射的指针(假设映射成功)。 由于底层硬件是以页为单位工作的,因此将length四舍五入为最接近的整数页数。 保护参数prot是读取、写入和执行权限的组合,flags参数至少包含MAP_SHAREDMAP_PRIVATE。 还有许多其他标志,在mmap手册页中有描述。

使用mmap可以做很多事情。 我将在接下来的 部分中展示其中的一些内容。

使用 mmap 分配私有内存

通过在flags参数中设置MAP_ANONYMOUS并将文件描述符fd设置为-1,可以使用mmap来分配私有内存区。 这与使用malloc从堆中分配内存类似,不同之处在于内存是按页对齐的,并且是以页的倍数为单位的。 内存分配在与存储库相同的区域中。 事实上,由于这个原因,这个区域被一些人称为mmap区域。

匿名映射更适合大型分配,因为它们不会用内存块限制堆,这会增加碎片的可能性。 有趣的是,您会发现malloc(至少在glibc中)停止从堆中为超过 128 KiB 的请求分配内存,并以这种方式使用mmap,因此在大多数情况下,只使用malloc是正确的做法。 系统将选择满足请求的最佳方式。

使用 mmap 共享内存

正如我们在第 17 章了解进程和线程中看到的那样,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);

另一个进程使用相同的调用、文件名、长度和标志来映射到该内存区域进行共享。 当对内存的更新传递到底层文件时,对msync(2)的后续调用进行控制。

通过mmap共享内存也提供了一种直接的方式来读取和写入设备内存。

使用 mmap 访问设备内存

正如我在第 11 章与设备驱动程序接口中提到的,驱动程序可以允许其设备节点进行内存映射,并与应用共享一些设备内存。 具体的实现取决于驱动程序。

Linux 帧缓冲区/dev/fb0就是一个例子。 Xilinx Zynq 系列等 FPGA 也可以通过 Linux 的mmap作为内存进行访问。 帧缓冲区接口在/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 */

第二个例子是流视频接口,Video 4 Linux,Version 2V4L2,它在/usr/include/linux/videodev2.h中定义。 每个视频设备都有一个名为/dev/videoN的节点,以/dev/video0开头。 有一个ioctl函数可以要求驱动程序分配一些您可以mmap到用户空间的视频缓冲区。 然后,这只是一个循环缓冲区并用视频数据填充或清空它们的问题,具体取决于您是在回放还是捕获视频流。

既然我们已经介绍了内存布局和映射,让我们从如何度量开始看内存使用情况。

我的应用使用多少内存?

与内核空间一样,分配、映射和共享用户空间内存的不同方式使得回答这个看似简单的问题非常困难。

首先,您可以询问内核它认为有多少内存可用,这可以使用free命令来完成。 以下是输出的典型示例:

   total used free shared buffers cached
Mem: 509016 504312 4704 0 26456 363860
-/+ buffers/cache: 113996 395020
Swap: 0 0 0

乍一看,这似乎是一个几乎内存不足的系统,509,016 KiB 中只有 4,704 KiB 可用:不到 1%。 但是,请注意,26,456 KiB 在缓冲区中, 在缓存中有高达 363,860 KiB。 Linux 认为空闲内存是浪费的内存;内核使用空闲内存作为缓冲区和缓存,并知道它们可以在需要时缩小。 从测量中删除缓冲区和缓存可提供真正的空闲内存,为 395,020 KiB:占总内存的 77%。 使用free时,标记为-/+ buffers/cache的第二行上的数字是重要的。

您可以通过向 /proc/sys/vm/drop_caches写入一个介于 1 和 3 之间的数字来强制内核释放缓存:

# echo 3 > /proc/sys/vm/drop_caches

这个数字实际上是一个位掩码,它决定要释放两种主要类型的缓存中的哪一种:1用于页面缓存,2用于 Dentry 和 inode 缓存的组合。 由于12是不同的位,因此写入3将释放这两种类型的缓存。 这些缓存的确切角色在这里并不是特别重要,只是内核正在使用的内存可以在短时间内回收。

free命令告诉我们正在使用的内存和剩余的内存。 它既不会告诉我们哪些进程正在使用不可用的内存,也不会告诉我们使用的比例是多少。 要衡量这一点,我们需要其他工具。

每个进程的内存使用量

有几个度量标准可以衡量进程正在使用的内存量。 我将从最容易获得的两个开始:虚拟集大小(VSS)和常驻内存大小(RSS),这两个参数在pstop命令的大多数实现中都可用:

  • VSS:在ps命令中称为 VSZ,在top中称为 VIRT,这是进程映射的内存总量 。 它是 /proc/<PID>/map中显示的所有区域的总和。 由于在任何时候只有部分虚拟内存被提交给物理内存,因此这个数字的意义是有限的。
  • rss:在ps中称为 rss,在top中称为 res,这是映射到内存的物理页的内存总和。 这更接近于进程的实际内存预算,但存在一个问题:如果添加所有进程的 RSS,则会高估正在使用的内存,因为某些页面将被共享。

让我们更多地了解topps命令。

使用 TOP 和 PS

BusyBox 的版本topps提供的信息非常有限。 下面的示例使用procps包中的完整版本。

ps命令使用选项-Aly显示VSS(VSZ) 和 RSS(RSS),也可以使用包含vszrss的自定义格式,如下所示:

# 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 保持 不断增加时第一个指示您有内存泄漏。 但是,它们在内存使用的绝对度量方面并不十分准确。

使用 SMEM

在 2009 年,MattMackall 开始研究进程内存测量中共享页面的记帐问题,并添加了两个新度量,称为唯一集大小,或USS比例集大小,或PSS

  • USS:这是提交给物理内存的内存量,对于一个进程是唯一的;它是,不与任何其他进程共享。 它是进程终止时将释放的内存量。
  • pss:这将在映射了共享页面的所有进程之间划分提交到物理内存的共享页面的记账。 例如,如果库代码区有 12 页长,并由六个进程共享,则每个进程将在 PSS 中累积两页。 因此,如果将所有进程的 PSS 编号相加,就会得到这些进程使用的实际内存量。 换句话说,PSS 就是我们一直在寻找的号码。

/proc/<PID>/smaps中提供了有关 PSS 的信息,其中包含/proc/<PID>/maps中显示的每个映射的附加信息。 下面是这样一个文件中的一节,它提供了有关libc代码段映射的信息:

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文件中整理信息, 以各种方式显示信息,包括饼图或条形图。 SMEM 的项目页面是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捕获状态,也可以使用root

# smemcap > smem-bbb-cap.tar

然后,将 tar 文件复制到主机并使用smem -S读取它,尽管这次不需要以root身份运行:

$ smem -t -S smem-bbb-cap.tar

输出与我们在本地运行smem时获得的输出相同。

需要考虑的其他工具

另一种显示 PSs 的方法是通过ps_mem(https://github.com/pixelb/ps_mem)显示,它打印大致相同的信息,但格式更简单。 它也是用 Python 编写的 。

Android 还有一个工具,可以显示每个进程的 USS 和 PSS 摘要,名为procran,只需做一些小改动,就可以为嵌入式 Linux 交叉编译。 您可以从https://github.com/csimmonds/procrank_linux获取代码。

我们现在知道如何测量每个进程的内存使用情况。 假设我们使用刚刚显示的工具 来查找系统中占用大量内存的进程。 那么,我们如何 深入该流程,以找出哪里出了问题? 这是下一节的主题。

识别内存泄漏

当分配了内存但不再需要时未释放时,就会发生内存泄漏。 内存泄漏绝不是嵌入式系统独有的,但它会成为一个问题,部分原因是目标一开始就没有太多内存,部分原因是它们经常在没有重启的情况下长时间运行,从而使泄漏成为 个大水坑。

您会发现在运行freetop时会出现泄漏,并且会发现即使您删除缓存,空闲内存也会持续减少,如上一节所示。 您将能够通过查看每个进程的 USS 和 RSS 来识别罪魁祸首。

有几种工具可以识别程序中的内存泄漏。 我来看两个:mtracevalgrind

mtrace

mtraceglibc的一个组件,它跟踪对mallocfree和相关的函数的调用,并标识程序退出时未释放的内存区域。 您需要从程序内部调用mtrace()函数来开始跟踪,然后在运行时将路径名写入写入跟踪信息的MALLOC_TRACE环境变量。 如果MALLOC_TRACE不存在或文件无法打开,则不会安装mtrace挂钩。 虽然跟踪信息是用 ASCII 编写的,但通常使用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不会告诉您内存泄漏。 它必须先终止。

==同步,由 Elderman 更正==@ELDER_MAN

Valgrind是一个非常强大的工具,用于发现内存问题,包括泄漏和其他问题。 一个优点是,您不必重新编译要检查的程序和库,尽管如果使用-g选项编译它们,使它们包含调试符号表,效果会更好。 它的工作原理是在模拟环境中运行程序,并捕获各个点的执行。 这导致了 Valgrind 的一个很大的缺点,那就是程序的运行速度只有正常速度的一小部分,这使得它在测试任何有实时约束的东西时用处不大。

重要音符

顺便说一句,这个名字经常发音错误:在 Valgrind 常见问题解答中,它说 grind 部分的发音是短的i,就像 grinned(与 tinned 押韵)而不是 grind(与 find 押韵)。 常见问题解答、文档和下载位于https://valgrind.org

Valgrind 包含几个诊断工具:

  • memcheck:这是默认工具,它检测内存泄漏和内存的一般误用。
  • cachegrind:这将计算处理器缓存命中率。
  • callgrind:这将计算每个函数调用的成本。
  • helgrind:这突出了对 PthreadAPI 的滥用,包括潜在的死锁和争用条件。
  • DRD:这是另一个 Pthread 分析工具。
  • massif:这将分析堆和堆栈的使用情况。

您可以使用-tool选项选择您想要的工具。 Valgrind 可以在主要的嵌入式平台上运行:32 位和 64 位版本的 ARM(Cortex-A)、PowerPC、MIPS 和 x86。 它在 Yocto 项目和 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)

Valgrind 的输出显示,在mtrace-example.c中发现了两个内存泄漏:第 12 行的 amalloc和第 14 行的 acalloc。程序中缺少对free的后续调用,即应该伴随这两个内存分配的。 如果不进行检查,长期运行进程中的内存泄漏最终可能会导致系统内存不足。

内存不足

标准的内存分配策略是过量使用,这意味着内核将允许应用分配比物理内存更多的内存。 大多数情况下,这都很好用,因为应用请求的内存比它们实际需要的要多,这是很常见的。 这也有助于实现fork(2):复制大程序是安全的,因为设置了写入时复制标志时共享内存页。 在大多数情况下,fork之后是一个exec函数调用,它取消共享内存,然后加载一个新程序。

然而,总有一种可能性是,某个特定的工作负载会导致一组进程试图利用它们同时获得的分配来变现,因此需求会比实际情况更多。 这是内存不足情况,或OOM。 在这一点上,没有其他选择,只能终止进程,直到问题消失。 这是内存不足杀手的工作。

在此之前,在/proc/sys/vm/overcommit_memory中有一个针对内核分配的调优参数,您可以将其设置为以下值:

  • 0:启发式过量使用
  • 1:总是过量使用;从不检查
  • 2:始终检查;永不过量

选项0是默认选项,在大多数情况下是最佳选择。

选项1只有在运行使用大型稀疏数组并分配较大内存区域但写入其中一小部分内存的程序时才真正有用。 这样的程序在嵌入式系统环境中很少见。

如果您担心会耗尽内存(可能是在任务或安全关键型应用中),选项2(永不过度提交)似乎是个不错的选择。 超过提交限制(交换空间大小加上总内存乘以过量提交比率)的分配将失败。 过量使用率由 /proc/sys/vm/overcommit_ratio控制,默认值为 50%。

例如,假设您有一个具有 512MB 系统 RAM 的设备,并且您将 设置为 25%的非常保守的比率:

# echo 25 > /proc/sys/vm/overcommit_ratio
# grep -e MemTotal -e CommitLimit /proc/meminfo
MemTotal: 509016 kB
CommitLimit: 127252 kB

没有交换,因此提交限制为MemTotal的 25%,这与预期不谋而合。

/proc/meminfo中还有另一个重要的变量,称为Committed_AS。 这是完成到目前为止进行的所有分配所需的内存总量。 我在一个系统上发现了以下内容:

# grep -e MemTotal -e Committed_AS /proc/meminfo
MemTotal: 509016 kB
Committed_AS: 741364 kB

换句话说,内核已经承诺了比可用内存更多的内存。 因此,将overcommit_memory设置为2意味着无论overcommit_ratio如何,所有分配都将失败。 要进入正常运行的系统, 我要么必须安装双倍的 RAM,要么必须严重减少正在运行的进程的数量,其中大约有 40 个。

在所有情况下,最后的防御是oom-killer。 它使用启发式方法为每个进程计算 个 0 到 1,000 之间的坏分数,然后终止得分最高的进程,直到有足够的空闲内存。 您应该在内核日志中看到类似以下内容:

[44510.490320] eatmem invoked oom-killer: gfp_mask=0x200da,
order=0, oom_score_adj=0
...

您可以使用echo f > /proc/sysrq-trigger强制执行 OOM 事件。

您可以通过将调整值写入 /proc/<PID>/oom_score_adj来影响进程的不良分数。 值-1000意味着坏分数永远不能大于零,因此它永远不会被杀死;值+1000意味着它总是大于 1,000,因此它总是会被杀死。

摘要

计算虚拟内存系统中使用的每个内存字节是不可能的。 但是,您可以使用free命令找到一个相当准确的空闲内存总量(不包括缓冲区和缓存占用的内存)的数字。 通过在一段时间内使用不同的工作负载监视它,您应该确信它将保持在给定的限制内。

当您想要调优内存使用率或确定意外分配的来源时,有一些资源可以提供更详细的信息。 对于内核空间,最有用的信息在/procmeminfoslabinfovmallocinfo中。

当涉及到对用户空间的精确测量时,最好的度量标准是 PSS, ,如smem和其他工具所示。 对于内存调试,您可以从 简单跟踪程序(如mtrace)获得帮助,也可以选择 Valgrindmemcheck工具的重量级选项。

如果您担心 OOM 情况的后果,可以通过/proc/sys/vm/overcommit_memory微调分配机制,并且可以通过oom_score_adj参数控制特定进程被终止的可能性。

下一章是关于使用 GNU 调试器调试用户空间和内核代码,以及在代码运行时观察代码可以获得的洞察力,包括我在这里描述的内存管理功能。

进一步阅读

以下资源提供了有关本章中介绍的主题的更多信息:

  • Linux 内核开发第三版,Robert Love 著
  • Linux 系统编程第二版,Robert Love 著
  • 了解 Linux VM 管理器,梅尔·戈尔曼:https://www.kernel.org/doc/gorman/pdf/understand.pdf
  • Valgrind 3.3-Gnu/Linux 应用的高级调试和配置,作者:J Seward、N.Nethercote 和 J.Weidendorfer