Linux进程内存监测与内存泄漏的检测方法

 更新时间:2026年06月16日 08:45:50   作者:码农爱学习  
在嵌入式Linux开发中,了解内存泄漏至关重要,本文介绍了如何通过查看status文件和smaps文件中的VmRSS和USS来监测Linux进程内存使用情况,并通过cJSON解析示例展示了内存泄漏的成因及正确处理方法,需要的朋友可以参考下

在嵌入式Linux开发中,如果存在编程不当,申请的内存未按预期释放,就会存在内存泄漏的情况,严重的时候会导致整个程序因oom而崩溃,本篇先简单介绍一些Linux系统中查看指定进程的内存使用情况的方法,并通过一个实例来对比查看出现内存泄漏后的内存占用情况。

1 Linux进程内存使用情况的查看方法

这里暂且先介绍两种方式,查看status文件和查看smaps文件

1.1 proc/pid/status中的VmRSS

在 Linux 系统中,/proc/[pid]/status 文件中的 ‌VmRSS‌(Virtual Memory Resident Set Size)表示进程当前实际占用的‌物理内存大小‌。

示例:

1.2 proc/pid/smaps中的USS(Private_Clean + Private_Dirty)

在 Linux 系统的 /proc/[pid]/smaps文件中,‌Private_Clean‌ 和 ‌Private_Dirty‌ 之和代表了该进程‌独占的、实际驻留在物理内存中的页面大小‌。

  • ‌**Private (私有)**‌: 指该内存页仅被当前进程引用(引用计数为 1),其他进程无法访问。这通常包括进程的堆(heap)、栈(stack)以及私有数据段。
  • Clean (干净)‌: 指该内存页的内容与 backing store(如磁盘上的文件映射或初始状态)‌一致‌,未被修改过。如果系统需要回收内存,可以直接丢弃这些页,无需写回磁盘。
  • Dirty (脏)‌: 指该内存页的内容‌已被修改‌,与原始来源不一致。如果系统需要回收这些页,必须先将数据写回交换分区(Swap)或关联的文件中。

Private_CleanPrivate_Dirty 之和即为该进程的 ‌USS‌(Unique Set Size,唯一集大小)

示例:

一共18个内存分段:

  • 程序自身 ELF 文件映射段(3 段)
    • 55ee5c381000-55ee5c389000 r-xp … test5:可执行代码段 (.text)
    • 55ee5c588000-55ee5c589000 r–p … test5:只读常量段 (.rodata)
    • 55ee5c589000-55ee5c58a000 rw-p … test5:全局数据段 (.data/.bss)
  • 进程堆段(1 段)
    • 55ee5d904000-55ee5d925000 rw-p … [heap]:堆内存
  • libc-2.27.so C 标准库映射(5 段)
    • 7f8fb5397000-7f8fb557e000 r-xp … libc-2.27.so:代码段.text
    • 7f8fb557e000-7f8fb577e000 —p … libc-2.27.so:保护隔离页 (Gap 防护页)
    • 7f8fb577e000-7f8fb5782000 r–p … libc-2.27.so:只读常量段.rodata
    • 7f8fb5782000-7f8fb5784000 rw-p … libc-2.27.so:全局数据段
    • 7f8fb5784000-7f8fb5788000 rw-p …(匿名):匿名私有段
  • 动态链接器 ld-2.27.so 映射(5 段)
    • 7f8fb5788000-7f8fb57b1000 r-xp … ld-2.27.so:链接器代码段
    • 7f8fb598b000-7f8fb598d000 rw-p …(匿名):链接器运行时匿名内存
    • 7f8fb59b1000-7f8fb59b2000 r–p … ld-2.27.so:链接器只读常量段
    • 7f8fb59b2000-7f8fb59b3000 rw-p … ld-2.27.so:链接器全局数据段
    • 7f8fb59b3000-7f8fb59b4000 rw-p …(匿名):链接器额外匿名私有内存
  • 内核预留特殊匿名段(4 段)
    • 7ffe646ef000-7ffe64710000 rw-p … [stack]:主线程栈
    • 7ffe64798000-7ffe6479b000 r–p … [vvar]:内核只读变量页
    • 7ffe6479b000-7ffe6479c000 r-xp … [vdso]:虚拟动态共享库
    • ffffffff600000-ffffffffff601000 --xp … [vsyscall]:老式系统调用兼容段

1.3 获取RSS与USS的简易脚本

USS的计算:

  • 首先通过ps和grep、awk指令过滤出指定进程(本例中是名为test5的程序)的进程id
  • 然后读取该进程id的smaps文件,再通过awk指令计算出Private_Clean + Private_Dirty得到USS

RSS的计算:

  • 读取该进程id的status文件,grep指令找到VmRSS对应的大小
#!/bin/bash

PID=`ps -aux | grep test5 | grep -v grep | grep -v test5.c | awk '{print $2}'`
FILE=/proc/$PID/smaps

if [ ! -f "$FILE" ]; then
    echo "进程 $PID 不存在"
    exit 1
fi

# 计算 USS = Private_Clean + Private_Dirty
USS_KB=$(awk '/Private_Clean/ {pc += $2}
               /Private_Dirty/ {pd += $2}
               END {print pc + pd}' "$FILE")

RSS_KB=$(grep VmRSS /proc/$PID/status | awk '{print $2}')

echo "PID: $PID"
echo "USS: $USS_KB KB ($((USS_KB/1024)) MB)"
echo "RSS: $RSS_KB KB ($((RSS_KB/1024)) MB)"

这里解释下USS_KB的计算:

USS_KB=$(awk '/Private_Clean/ {pc += $2}
  • USS_KB=:定义 Shell 变量,用来存储最终计算结果
  • $(...):命令替换,把括号内命令的输出赋值给变量
  • awk:调用 awk 文本处理工具逐行扫描 $FILE 文件
  • /Private_Clean/:awk 匹配规则:只要当前行包含字符串 Private_Clean
  • {pc += $2}:匹配成功后执行的动作
    • $2:当前行的第二个字段(默认以空格 / 制表符分割列)
    • pc:自定义变量,初始值默认为 0,pc += $2,即所有Private_Clean行的第 2 列数值全部累加存入pc
/Private_Dirty/ {pd += $2}
  • 类似的,所有Private_Dirty行的第 2 列数值全部累加存入pd
END {print pc + pd}' "$FILE")
  • END { ... }:awk 内置结束块,等文件所有行全部遍历处理完之后,只执行一次
  • print pc + pd:打印两个累加值相加的结果
  • "$FILE":awk 脚本结束,指定要处理的文件是 Shell 变量 $FILE

该脚本运行一次检查一次,若想持续运行,也可继续修改脚本,使之按指定时间间隔执行。

2 内存泄漏示例举例

这里以cJSON解析示例,cJSON_Parse会申请内存,生成json树,在使用完会,需要通过cJSON_Delete来释放内存。

2.1 存在内存泄漏的写法

这里test_parse_json是对json数据(从json文件读取,先保存在buf缓存中)进行解析和处理,期望解析得到的json树在外部释放。

这里的问题是,cJSON *jRoot不应该使用一次指针,而应该使用二级指针。

简单分析:

  • 参数cJSON *jRoot虽然是指针,但按照参数按值传递的原则,是传入的外部cJSON *jRoot的NULL值(外部cJSON *jRoot是实参,其值NULL传给函数参数中的形参cJSON *jRoot
  • 内部的jRoot,实际是另一个副本,初始值就是传入的NULL,然后接受cJSON_Parse的赋值
  • test_parse_json退出后,外部的cJSON *jRoot实际未被修改,仍为NULL,所以未能释放内存,造成内存泄漏
// 错误示例
void test_parse_json(char *buf, cJSON *jRoot)
{
    jRoot = cJSON_Parse(buf);
    if (jRoot)
    {
        cJSON *json = NULL;
        
        if ((json = cJSON_GetObjectItem(jRoot, "version")))
        {
            printf("[%s:%d] version:%d\n", __func__, __LINE__, json->valueint);
        }
        
        char *data = cJSON_PrintUnformatted(jRoot);
        printf("[%s:%d] ok:%s\n", __func__, __LINE__, data);
        free(data);
    }
}

//================== 使用示例
cJSON *jRoot = NULL;
test_parse_json(buf, jRoot);

if (jRoot)
{
    cJSON_Delete(jRoot); // 释放cJSON_Parse申请的内存
    printf("[%s:%d] [%d] cJSON_Delete\n", __func__, __LINE__, i);
}
else
{
    printf("[%s:%d] [%d] err, null jRoot!\n", __func__, __LINE__, i);
}

如果在cJSON_Delete时,没有对jRoot是NULL的错误打印,问题将被掩盖,因为json解析代码运行结果是正常的。

2.2 正确的写法

正确的做法是使用二级指针。

简单分析:

  • 参数cJSON **jRoot是二级指针,但按照参数按值传递的原则,是传入的外部cJSON *jRoot的地址(外部cJSON *jRoot是实参,其地址值传给函数参数中的形参cJSON **jRoot
  • 内部的jRoot,实际是另一个副本,初始值就是传入的外部cJSON *jRoot的地址,而*jRoot就是外部cJSON *jRoot所存储的值,然后接受cJSON_Parse的赋值,这里,外部的cJSON *jRoot就记录到了内部cJSON_Parse申请的内存地址
  • test_parse_json退出后,外部的cJSON *jRoot就可以释放内存

关于二级指针的介绍,具体可看之前的文章

// 正确示例
void test_parse_json_2(char *buf, cJSON **jRoot)
{
    *jRoot = cJSON_Parse(buf);
    if (*jRoot)
    {
        cJSON *json = NULL;

        if ((json = cJSON_GetObjectItem(*jRoot, "version")))
        {
            printf("[%s:%d] version:%d\n", __func__, __LINE__, json->valueint);
        }
        
        char *data = cJSON_PrintUnformatted(*jRoot);
        printf("[%s:%d] ok:%s\n", __func__, __LINE__, data);
        free(data);
    }
}

//================== 使用示例
cJSON *jRoot = NULL;
test_parse_json_2(buf, &jRoot);

if (jRoot)
{
    cJSON_Delete(jRoot); // 释放cJSON_Parse申请的内存
    printf("[%s:%d] [%d] cJSON_Delete\n", __func__, __LINE__, i);
}
else
{
    printf("[%s:%d] [%d] err, null jRoot!\n", __func__, __LINE__, i);
}

2.3 完整示例代码

// gcc test5.c cjson/cJSON.c -o test5
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "cjson/cJSON.h"
#define CONFIG_FILE1 "config1.json"
int read_json_file(const char *filePath, char **buf, int *buf_len)
{   
    FILE *fp = fopen(filePath, "r");
    if (!fp) 
    {
        printf("fopen:%s failed\n", filePath);
        return -1;
    }
    // 计算文件的大小
    fseek(fp, 0, SEEK_END);
    *buf_len = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    // 分配内存
    *buf = malloc(*buf_len + 1);
    if (NULL == *buf) 
    {
        fclose(fp);
        return -1;
    }
    memset(*buf, 0, *buf_len + 1);  // 清空,防止脏数据
    // 读取内存
    fread(*buf, 1, *buf_len, fp);
    fclose(fp);
    cJSON_Minify(*buf); // 删除注释+压缩
    printf("[%s:%d] buf_len:%d, buf:%s]\n", __func__, __LINE__, *buf_len, *buf);
    return 0;
}
// 错误示例
void test_parse_json(char *buf, cJSON *jRoot)
{
    jRoot = cJSON_Parse(buf);
    if (jRoot)
    {
        cJSON *json = NULL;
        if ((json = cJSON_GetObjectItem(jRoot, "version")))
        {
            printf("[%s:%d] version:%d\n", __func__, __LINE__, json->valueint);
        }
        char *data = cJSON_PrintUnformatted(jRoot);
        printf("[%s:%d] ok:%s\n", __func__, __LINE__, data);
        free(data);
    }
}
// 正确示例
void test_parse_json_2(char *buf, cJSON **jRoot)
{
    *jRoot = cJSON_Parse(buf);
    if (*jRoot)
    {
        cJSON *json = NULL;
        if ((json = cJSON_GetObjectItem(*jRoot, "version")))
        {
            printf("[%s:%d] version:%d\n", __func__, __LINE__, json->valueint);
        }
        char *data = cJSON_PrintUnformatted(*jRoot);
        printf("[%s:%d] ok:%s\n", __func__, __LINE__, data);
        free(data);
    }
}
int main(void)
{   
    char *buf = NULL;
    int buf_len = 0;
    read_json_file(CONFIG_FILE1, &buf, &buf_len);
    printf("[%s:%d] buf_len:%d, buf:%s]\n", __func__, __LINE__, buf_len, buf);
    for (int i = 0; i < 5000; i++) // 多循环几次,效果更明显
    {
        cJSON *jRoot = NULL;
#if 0
        // 错误示例
        test_parse_json(buf, jRoot);
#else   
        // 正确示例
        test_parse_json_2(buf, &jRoot);
#endif
        if (jRoot)
        {
            cJSON_Delete(jRoot); // 释放cJSON_Parse申请的内存
            printf("[%s:%d] [%d] cJSON_Delete\n", __func__, __LINE__, i);
        }
        else
        {
            printf("[%s:%d] [%d] err, null jRoot!\n", __func__, __LINE__, i);
        }
    }
    // 释放内存
    free(buf);
    buf = NULL;
    while(1)
    {
        sleep(1); // 程序先暂停在这里,便于观察内存使用情况
    }
    return 0;
}

3 运行结果

运行没有内存泄漏的分支,可以看到在循环5000次后,无论是USS还是RSS,都只有几百K

运行有内存泄漏的分支,可以看到在循环5000次后,因为每次都没有释放json,最终USS是约5MB,RSS是约6MB,对比可见内存未释放

4 总结

本篇初步介绍了Linux进程内存监测的简单方案,包括查看status和smaps文件中相关内存数据记录,然后通过cJSON的实例,对比有无内存泄漏的情况下,通过查询该进程的USS和RSS的大小,确认内存泄漏的存在。

以上就是Linux进程内存监测与内存泄漏的检测方法的详细内容,更多关于Linux内存监测与内存泄漏检测的资料请关注脚本之家其它相关文章!

相关文章

  • Linux 每天自动备份mysql数据库的方法

    Linux 每天自动备份mysql数据库的方法

    linux下为了安全有时候需要自动备份mysql数据库,下面是具体的实现步骤。感兴趣的朋友跟随小编一起看看吧
    2009-09-09
  • 101个脚本之建立linux回收站的脚本

    101个脚本之建立linux回收站的脚本

    众所周知,linux是没有回收站的,一些人很害怕删错东西(有经验的linux管理员极少范这错误),个人不建议回收站,而应该是培养个人的安全意识。有点小跑题
    2016-08-08
  • Linux 下目录文件权限(命令)的查看和修改

    Linux 下目录文件权限(命令)的查看和修改

    这篇文章主要介绍了Linux 下目录文件权限(命令)的查看和修改的相关资料,需要的朋友可以参考下
    2016-11-11
  • Linux服务器安装GRUB步骤

    Linux服务器安装GRUB步骤

    在本篇文章中我们给大家整理了Linux服务器安装GRUB的详细步骤以及相关注意事项,有需要的朋友们参考下。
    2018-09-09
  • Linux镜像拉取失败的解决方案

    Linux镜像拉取失败的解决方案

    文章总结:本文介绍了两个Docker镜像拉取错误的原因及解决方法,第一个问题是由于镜像源速度太慢导致超时,解决方法是更换更好的镜像源,第二个问题是连接被拒绝,解决方法同样是更换镜像源,希望本文能为大家提供帮助,也欢迎在脚本之家分享更多经验
    2026-03-03
  • LINUX磁盘分区、格式化、挂载、卸载详细过程

    LINUX磁盘分区、格式化、挂载、卸载详细过程

    这篇文章主要介绍了LINUX磁盘分区、格式化、挂载、卸载详细过程,具有一定的参考价值,有需要的可以了解一下。
    2016-11-11
  • Linux文件系统之inode与软硬链接详解

    Linux文件系统之inode与软硬链接详解

    这篇文章讨论的话题是没有被打开的文件,文件等于文件内容加文件属性,没打开的文件一定是存储在磁盘上的,文件内容以数据块的形式进行存储,文件属性以 inode 的形式进行存储,文中通过图文给大家介绍的非常详细,需要的朋友可以参考下
    2024-03-03
  • 详解Linux下挂载新硬盘方法

    详解Linux下挂载新硬盘方法

    这篇文章主要介绍了详解Linux下挂载新硬盘方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-05-05
  • Linux下实现修改时区为东八区

    Linux下实现修改时区为东八区

    这篇文章主要介绍了Linux下实现修改时区为东八区,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2026-06-06
  • Linux进程间通信之管道如何实现进程池

    Linux进程间通信之管道如何实现进程池

    这篇文章主要介绍了Linux进程间通信之管道如何实现进程池问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03

最新评论