概述
项目制作过程中经常出现内存问题,在该处对排查思路进行汇总,也对常见问题进行总结,以期待下一次遇到相似问题时可以快速排查,然后解决问题
排查流程总结
首先检查内存的整体情况
使用工具htop和seme快速得知系统内存使用的全貌,识别高内存占用进程
然后分析内存使用情况
- 检查系统内存和slab缓存(使用/proc文件系统或者slabtop工具)
- 使用strace跟踪系统调用,了解内存分配行为
- 借助AddressSanitizer、Valgrind等工具检测潜在的内存问题
排查流程
检查内存情况
free -h
free -h //查看内存的总量、已用、空闲和缓存的内存情况
- shared:多个进程共享的内存
- buff/cache:用于文件缓存的内存
- available:可用于启动新应用程序的内存(包括缓存内存)
top / htop
查看进程的实时内存消耗,可以判断
vmstat 1命令
- swpd:已使用的交换空间
- free:空闲内存
- buff/cache:缓存和缓冲区内存
- si/so:从交换区交换到内存或从内存交换到交换区的数据量
- bi/bo:块输入和输出的数量
- us/sy/id:用户空间、系统空间、空闲时间的CPU使用率
总结
- 通过上述方法,可以从宏观角度了解到内存以及交换区的占用情况
- 此时可以通过扩大内存、杀死不重要的进程、增加swap缓冲区来减少内存压力减少问题
分析进程的内存使用情况
htop(总览)
- RES(常驻内存集):进程实际使用的物理内存,包含共享的内存
- VIRT(虚拟内存):进程的虚拟内存总量,包含所有分配给该进程的内存,包括共享库、交换区等
- SHR(共享内存):进程使用的共享内存量
- %MEM:进程的内存使用占系统总内存的百分比
smem命令(总览)
它能够显示更加详细和准确的内存信息,特别是共享内存的处理上要比 ps
或 top
更加清晰
- USS:独占的私有内存(Unique Set Size)
- PSS:共享内存分摊后的内存(Proportional Set Size)
- RSS:常驻内存集大小
查看所有进程
ps aux --sort=-%mem
具体实现的功能与htop类似,作为备用,优先使用htop
查看某个特定进程的内存占用
ps -p <pid> -o pid,%mem,vsz,rss,cmd
/proc文件系统(进程细化)
查看某个特定进程的内存使用情况
cat /proc/<pid>/status
- VmSize:进程的虚拟内存大小
- VmRSS:进程的常驻内存大小
- VmData:进程的堆内存大小
- VmStk:进程的栈内存大小
- VmExe:进程的代码段大小
- VmLib:共享库占用的内存
- VmSwap:进程交换空间的使用情况
smaps更详细的内存映射,可以进一步跟踪内存细节
cat /proc/<pid>/smaps
内存泄漏检测(gdb + valgrind)
首先使用valgrind检查内存泄漏
valgrind --leak-check=full ./(可执行文件)
多线程环境下查找内存问题
查看线程状态,然后进入指定线程
(gdb) info threads
(gdb) thread <n>
查看内存问题(以16进制格式查看my_variable变量)32字的内存内容
(gdb) x/32xw &my_variable
查看堆栈情况(显示当前栈指针$sp地址处的20个字的内存内容)
(gdb) x/20xw $sp
条件断点(仅在特定条件下停止程序)
设置一个断点,当变量 my_var
的值等于 10 时触发
(gdb) break your_function if my_var == 10
查看共享资源
(gdb) watch shared_variable
slabtop 工具
slabtop
是一个实时监控工具,用于查看内核中 slab 缓存的分配和使用情况。Slab 是一种内存分配机制,内核通过它高效管理小块内存。Slab 分配器的高效性可能因内存碎片化而降低,因此我们可以通过 slabtop
分析内存碎片
两种启动方式
slabtop // 实时刷新
slabtop -o // 只查看一次
参数查询
- OBJS: 总对象数量(总计的分配内存单元)
- ACTIVE: 活跃的对象数量(当前正在使用的对象数量)
- USE: 使用率,表示
ACTIVE / OBJS
的百分比 - OBJ SIZE: 每个对象的大小(字节)
- SLABS: 分配的 slab 数量(每个 slab 是多个对象的集合)
- OBJ/SLAB: 每个 slab 中包含的对象数量
- CACHE SIZE: 缓存使用的总大小(KB)
- NAME: 缓存名称(如
dentry
表示目录项缓存,buffer_head
表示文件缓冲头)
关键缓存名称查询
dentry
: 目录项缓存,与文件系统有关。如果此缓存过大且无法释放,可能是某些程序频繁访问大量目录造成的inode_cache
: 文件 inode 的缓存,与文件系统的文件和目录节点有关kmalloc
: 通用小块内存分配缓存,可能存在碎片化buffer_head
: 文件系统的数据缓冲头缓存
内存碎片的观察指标
- 查看
USE
列(使用率),如果使用率(ACTIVE / OBJS
)较低(远小于 100%),则表示分配的 slab 中存在大量未使用的对象,可能是内存碎片化的表现 - 便捷方法:按照USE进行排序,slabtop -s u(下列图2)
- 检查
CACHE SIZE
列,找出占用内存最多的缓存,尤其是某些缓存大小持续增长而未释放的情况 - 便捷方法:按照缓存总大小进行排序(图2)slabtop -s c
持续监控
-n 1
表示每 1 秒刷新一次。-o
表示只输出一次结果(避免实时刷新)
watch -n 1 'slabtop -o -s c'
strace 工具
用于跟踪程序执行时与内核交互的系统调用,可以快速定位问题例如程序崩溃、权限错误、内存泄漏等
运行方式
直接运行程序:跟踪 your_program
的所有系统调
strace ./your_program
附加到运行中的进程:跟踪正在运行的进程 <PID>
的系统调用、
strace -p <PID>
输出到文件: 使用 -o
选项将输出保存到文件
strace -o trace.log ./your_program
指定系统调用: 只跟踪特定类型的系统调用(如文件操作)
strace -e trace=open,read,write ./your_program
常见选项
-e trace=<name>
:只跟踪指定的系统调用,例如open
,read
,write
等-p <PID>
:附加到运行中的进程,跟踪该进程的系统调用-o <file>
:将输出写入指定文件,而不是直接打印到标准输出-t
:为每个系统调用添加时间戳-c
:统计各类系统调用的调用次数和执行时间-f
:跟踪子进程的系统调用-v
:打印完整的参数,避免参数被截断-s <len>
:设置最大字符串长度,默认是 32 字节,更长的字符串可能被截断。可以通过此选项增加长度
具体使用
跟踪一个可执行程序,并判断其问题
strace 可执行文件
跟踪一个网络请求
strace curl 网址
检测动态库加载问题(进一步判断动态库加载错误的原因)
strace -e trace=open ./your_program
vmstat 工具
主要关注指标
- 如果
free
显示有空闲内存,但程序分配大块内存失败,可能是内存碎片化问题;因为内存虽然总量充足,但被分割成很多小块,无法满足大内存请求 - 如果
cache
和buff
占用大量内存,但应用程序仍然频繁使用交换空间(si/so
非零或很高),说明页缓存可能占用了大部分可用内存,导致内存碎片化 - 如果
free
有空闲内存,但系统仍然频繁交换(si/so
值较高),可能是内存碎片造成的内存分配困难
具体使用
vmstat 1 (1表示每秒刷新一次输出)
AddressSanitizer 工具
AddressSanitizer 与 Gperftools的区别使用
- 如果需要快速、高效地检测广泛的内存问题(包括越界访问),推荐 AddressSanitizer
- 如果项目中使用了大量堆内存并需要深入分析分配行为,同时优化性能,推荐 Gperftools
主要功能
一种快速内存错误检测工具,它通过编译时插桩和运行时监测机制来检测以下内存问题
- 越界访问:访问数组/内存块的边界之外
- Use-After-Free:释放内存后继续使用
- 堆栈溢出:访问堆栈帧外的内存
- 全局溢出:访问超出全局变量范围的内存
检测方法了解
ASan 将应用程序的内存划分为多个 "块",并为每个块创建一个 影子内存区域。每 8 字节的应用程序内存会对应 1 字节的影子内存,用来标记内存块的状态(有效、无效或越界区域)。
分配内存时,ASan 会在实际的内存块前后添加 红色区域(Red Zones),用于检测越界访问。任何访问红色区域的操作都会被 ASan 标记为越界访问。
插入检测代码,每次内存访问都会检查对应的影子内存。如果发现访问超出了分配的边界或进入了红色区域,ASan 会立即报告错误。
使用方法
gcc -fsanitize=address -g -o main main.cc
Gperftools 工具
基本了解
Gperftools 是一个性能分析和内存调试工具包,其中的 HeapChecker 和 HeapProfiler 可以检测内存问题,例如可以解决内存泄漏、越界访问等问题
具体使用
编译时链接
gcc -o program program.c -ltcmalloc
Thread Sanitizer
Thread Sanitizer是一种竞态检测工具,可以直接检测内存竞态问题。程序运行的时候插桩,检测线程
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
int counter = 0;
void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
counter++; // 存在竞态
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final counter value: %d\n", counter);
return 0;
}
gcc -fsanitize=thread -g -o tsan_test tsan_test.c -pthread
./tsan_test
常见问题总结
内存/交换区已满
free
命令中的空闲内存接近0,系统可能会因为内存不足而出现 OOM (Out of Memory)
错误,这时候操作系统会尝试通过交换空间来补充内存需求。如果交换空间也不足,可能导致系统崩溃或进程被杀掉
- 关闭不必要的进程或服务
- 增加物理内存或调整内存分配
- 使用
swap
或增加交换空间来缓解压力
增加交换区内存(通过交换空间增加swap空间)
# 1. 创建交换文件
# 使用 fallocate 创建一个 2GB 的交换文件
echo "创建 2GB 的交换文件..."
sudo fallocate -l 2G /swapfile
# 如果 fallocate 不可用,可以用 dd 创建:
# sudo dd if=/dev/zero of=/swapfile bs=1M count=2048
# 2. 设置交换文件权限
echo "设置交换文件权限为 600(仅限 root 访问)..."
sudo chmod 600 /swapfile
# 3. 格式化交换文件为 Swap
echo "格式化交换文件为 Swap 类型..."
sudo mkswap /swapfile
# 4. 激活交换文件
echo "激活交换文件..."
sudo swapon /swapfile
# 5. 验证 Swap 是否增加
echo "验证新的 Swap 空间..."
free -h
swapon --show
# 6. 添加到 /etc/fstab 以永久生效
echo "添加到 /etc/fstab 以使配置永久生效..."
sudo bash -c 'echo "/swapfile none swap sw 0 0" >> /etc/fstab'
增加交换区(增加交换区swap空间)
# 提示用户手动操作分区
echo "以下步骤需要手动完成分区操作:"
echo "1. 使用 fdisk 或 parted 创建一个新的分区,并将分区类型设置为 Swap (类型代码 82)"
echo "2. 格式化分区为 Swap 类型,例如:"
echo " sudo mkswap /dev/sdX"
echo "3. 激活 Swap 分区,例如:"
echo " sudo swapon /dev/sdX"
echo "4. 验证 Swap 是否增加:"
echo " free -h"
echo "5. 添加到 /etc/fstab 以使配置永久生效,例如:"
echo " sudo bash -c 'echo \"/dev/sdX none swap sw 0 0\" >> /etc/fstab'"
echo " 将 /dev/sdX 替换为实际分区路径。"
# 调整 swappiness 参数
echo "=== 可选:调整 Swap 使用策略(swappiness) ==="
# 当前 swappiness 设置(默认为 60)
echo "当前 swappiness 值:"
cat /proc/sys/vm/swappiness
# 设置 swappiness 为 10(减少使用 Swap 的倾向)
echo "设置 swappiness 为 10..."
sudo sysctl vm.swappiness=10
# 永久生效,修改 /etc/sysctl.conf 文件
echo "将 swappiness 设置永久生效..."
sudo bash -c 'echo "vm.swappiness=10" >> /etc/sysctl.conf'
内存泄漏
总结
内存泄漏指的是程序在运行过程中,动态分配了内存但未正确释放,导致程序占用的内存不断增长。长时间运行后,程序可能耗尽所有可用内存,导致系统性能下降或崩溃
- 原因:程序没有正确的调用释放内存函数,或者内存引用丢失,从而导致垃圾回收无法回收这些内存
- 诊断工具:valgrind 、 gdb 、memcheck 、 leaks
- 解决办法:通过堆栈信息,通过内存泄漏工具查找内存泄漏的源头,然后将这些内存释放即可
单线程环境下检测
编译可调式的代码,使用valgrind进行内存检测
使用gdb进行调试
打断点然后开始调试
查看内存映射
- Offset(偏移量)
- Perms(权限)
- Objfile(对象文件):该内存区域所映射的文件或对象的路径。如果为空,表示这是匿名映射
总结:多次运行程序,检测堆区的起始地址是否在一直变大,如果在一直变大的话,那么就证明在分配新的内存,而没有及时释放内存,存在内存泄漏
多线程环境检测
编译程序并进入调试
总结:切换到指定线程去检查在该线程的执行过程中是否出现了内存泄漏问题
内存碎片
总结
内存碎片是指系统的内存空间由于频繁的分配和释放操作,导致可用内存分布不均,虽然总的空闲内存足够,但可能无法满足大的内存请求
- 原因:动态内存分配器在长期运行后可能导致内存的碎片化,尤其是在进行频繁的分配和释放操作时
- 诊断:top free vmstat slabtop
- 解决思路:重新启动应用程序,使用合适的内存分配策略(例如内存池),减少内存碎片的影响
解决思路总结
- 快速确认系统内存现状(
free
和vmstat
) - 分析内核内存分配情况(
slabtop
) - 验证是否能分配大块内存(
stress
或手动分配) - 释放缓存进行验证(
echo 3 > /proc/sys/vm/drop_caches
) - 深入追踪程序行为(
strace
和 GDB)(一般是从服务器上无法解决问题的时候)
具体实现
1. 使用free工具,观察空闲区以及缓存区,如果空闲区很多,但是仍然分配失败,那么就有可能是内存碎片
2. 使用vmstat检查内存动态变化,着重关注cache是否占用过多无法释放的问题,这样会导致碎片化
3. 使用 slabtop 检查内核内存碎片
4. 如果出现了上述问题,那么进一步验证大内存的分配能力。通过stress分配大内存,如果分配失败,但是free还是显示有可用的内存,内存碎片可以确定
stress --vm 1 --vm-bytes 1G
5. 清理缓存(下面命令用于强制释放缓存)
echo 3 > /proc/sys/vm/drop_caches
跟踪程序的内存分配行为
使用strace跟踪内存分配或者也可以使用GDB进行跟踪验证内存的使用情况
strace -e trace=memory ./your_program
内存访问越界
内存访问越界发生在程序试图读取或写入超出分配内存区域的地址。这种错误可能导致程序崩溃或数据损坏,并且很难发现
- 原因:程序存在数组越界、指针错误、未初始化的指针等问题
- 诊断工具:gdb \ valgrind \ AddressSanitizer \ gperftools
- 解决:通过工具定位越界位置然后针对性的解决
项目中快速解决内存访问越界
首先使用AddressSanitizer工具,快速检测和定位内存越界问题,如果文件较大可以选择使用gperftools工具然后逐步进行排查
然后运行程序,并检查错误报告
最后对错误进行处理即可
虚拟内存过度使用
虚拟内存是操作系统为进程提供的一块抽象内存区域,它可以包含物理内存和交换空间。当程序使用大量虚拟内存时,系统可能会进行频繁的内存交换(swap),影响性能
- 原因:程序使用了大量虚拟内存(例如大数组、大数据结构),但这些内存区域并没有完全映射到物理内存
- 诊断工具:top / vmstat / seme / ps
- 解决:避免大规模内存映射,调整交换空间配置,减少内存交换的使用
具体解决思路
首先通过free命令查看交换空间的使用情况,重点关注swap的总大小以及目前可用内存的大小
然后使用vmstat检查swap交换区的动态行为,关注swap与si so的数值,如果该数值持续为0那么就表明系统频繁的在使用交换区
最后使用htop工具找到占用内存高的进程,可以将其杀死也可以选择拓展交换区空间,具体根据自己需求而定
内存竞态
内存竞态发生在多线程程序中,多个线程试图并发地读写共享内存区域而没有适当的同步机制。这可能导致数据不一致或崩溃
- 原因:缺少适当的锁(如互斥锁、读写锁等)来保护共享内存,导致竞态条件
- 诊断工具:lockstat / perf / thread sanitizer / valgrind / gdb
- 解决:使用正确的同步机制(如互斥锁、条件变量等)保护共享资源,避免并发访问冲突
具体解决
使用ThreadSanitizer工具快速定位问题,然后解决。如果是对程序成体性能或者内核开发的时候,使用选择使用其他工具
内存映射文件问题
内存映射文件(mmap
)是将文件或设备映射到进程的虚拟内存空间的技术。如果映射的文件过大或未正确管理,可能导致内存访问异常或性能问题
- 原因:错误的内存映射(例如超出文件范围)、内存映射文件的读取/写入不当、文件锁定等
- 诊断:strace / lsof / mmap / gdb
- 解决:检查
mmap
的使用,确保映射范围正确,避免使用过大的内存映射文件,优化文件I/O操作
Kernel内存泄漏
内核中的内存泄漏可能比用户空间中的泄漏更加隐蔽和复杂,通常出现在设备驱动、内核模块或系统调用中
- 原因:内核模块中存在未释放的内存,或者内核数据结构未正确释放
- 诊断工具:dmesg \ slabtop \ vmstat \ kmemleak
- 解决:使用内核工具检查
系统缓存和页缓存问题
Linux 会使用未使用的内存来缓存磁盘数据(即页缓存),这种缓存可以提高文件系统的性能,但有时它会占用大量内存,影响系统其他应用的内存需求
- 原因:大规模的文件 I/O 操作,导致大量内存用于缓存,可能会影响其他进程的内存需求
- 诊断工具:free \ vmstat \ slabtop \ dmesg
- 解决办法:检查系统缓存配置,调整
vm.swappiness
和vm.drop_caches
,或者清理缓存以释放内存