在嵌入式Linux开发中,如果存在编程不当,申请的内存未按预期释放,就会存在内存泄漏的情况,严重的时候会导致整个程序因oom而崩溃,本篇先简单介绍一些Linux系统中查看指定进程的内存使用情况的方法,并通过一个实例来对比查看出现内存泄漏后的内存占用情况。
这里暂且先介绍两种方式,查看status文件和查看smaps文件
在 Linux 系统中,/proc/[pid]/status 文件中的 VmRSS(Virtual Memory Resident Set Size)表示进程当前实际占用的物理内存大小。
示例:

在 Linux 系统的 /proc/[pid]/smaps文件中,Private_Clean 和 Private_Dirty 之和代表了该进程独占的、实际驻留在物理内存中的页面大小。
Private_Clean 与 Private_Dirty 之和即为该进程的 USS(Unique Set Size,唯一集大小)
示例:

一共18个内存分段:
USS的计算:
RSS的计算:
#!/bin/bashPID=`ps -aux | grep test5 | grep -v grep | grep -v test5.c | awk '{print $2}'`FILE=/proc/$PID/smapsif [ ! -f "$FILE" ]; then echo "进程 $PID 不存在" exit 1fi# 计算 USS = Private_Clean + Private_DirtyUSS_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 列数值全部累加存入pdEND {print pc + pd}' "$FILE")END { ... }:awk 内置结束块,等文件所有行全部遍历处理完之后,只执行一次print pc + pd:打印两个累加值相加的结果"$FILE":awk 脚本结束,指定要处理的文件是 Shell 变量 $FILE该脚本运行一次检查一次,若想持续运行,也可继续修改脚本,使之按指定时间间隔执行。
这里以cJSON解析示例,cJSON_Parse会申请内存,生成json树,在使用完会,需要通过cJSON_Delete来释放内存。
这里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:%dn", __func__, __LINE__, json->valueint); } char *data = cJSON_PrintUnformatted(jRoot); printf("[%s:%d] ok:%sn", __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_Deleten", __func__, __LINE__, i);}else{ printf("[%s:%d] [%d] err, null jRoot!n", __func__, __LINE__, i);}如果在cJSON_Delete时,没有对jRoot是NULL的错误打印,问题将被掩盖,因为json解析代码运行结果是正常的。
正确的做法是使用二级指针。
简单分析:
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:%dn", __func__, __LINE__, json->valueint); } char *data = cJSON_PrintUnformatted(*jRoot); printf("[%s:%d] ok:%sn", __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_Deleten", __func__, __LINE__, i);}else{ printf("[%s:%d] [%d] err, null jRoot!n", __func__, __LINE__, i);}// 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 failedn", 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:%dn", __func__, __LINE__, json->valueint); } char *data = cJSON_PrintUnformatted(jRoot); printf("[%s:%d] ok:%sn", __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:%dn", __func__, __LINE__, json->valueint); } char *data = cJSON_PrintUnformatted(*jRoot); printf("[%s:%d] ok:%sn", __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_Deleten", __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;}运行没有内存泄漏的分支,可以看到在循环5000次后,无论是USS还是RSS,都只有几百K

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

本篇初步介绍了Linux进程内存监测的简单方案,包括查看status和smaps文件中相关内存数据记录,然后通过cJSON的实例,对比有无内存泄漏的情况下,通过查询该进程的USS和RSS的大小,确认内存泄漏的存在。
以上就是Linux进程内存监测与内存泄漏的检测方法的详细内容,更多关于Linux内存监测与内存泄漏检测的资料请关注本站其它相关文章!
您可能感兴趣的文章: