一、问题现象
调试Android系统中的某个自定义服务(由C/C++自定义开发),系统每次软重启(未断电重启,此服务进程没被杀掉),重启后会调用到该服务中的一些方法,之后就会发现内存有较大的增长。软重启达到一定次数后,会抛出异常 out of memory异常。
二、排查方法
问题初期,使用adb shell命令,进入终端命令模式进行排查,使用top命令去查看内存占用情况,使用ulimit 命令查看句柄总数和当前句柄数,使用ps来查看进程信息,使用cat /proc/PID/status来查看进程的详细信息。PID即为要查看的进程号。下面详细解释下这几个命令。
1、top命令:
top命令可以用来监控linux的系统状况。
top [-d number] | top [-bnp]
-d:number代表秒数,表示top命令显示的页面更新一次的间隔,默认是5秒。
-b:以批次的方式执行top。
-n:与-b配合使用,表示需要进行几次top命令的输出结果。
-p:指定特定的pid进程号进行观察。
下图以android10模拟器generic_x86_64为例使用top命令的显示结果
top打印信息 :
行标 | 属性 | 含义 |
Tasks | total | 进程总数 |
running | 正在运行的进程数 | |
sleeping | 睡眠的进程数 | |
stopped | 停止的进程数 | |
zombie | 僵尸进程数 | |
Mem | total | 物理内存总量 |
used | 使用的物理内存总量 | |
free | 空闲内存总量 | |
buffers | 用作内核缓存的内存量 | |
Swap | total | 交换区总量 |
used | 使用的交换区总量 | |
free | 空闲交换区总量 | |
cached | 缓冲的交换区总量 | |
%cpu | user | 用户空间占用CPU百分比 |
nice | 用户进程空间内改变过优先级的进程占用CPU百分比 | |
sys | 内核空间占用CPU百分比 | |
idle | 空闲CPU百分比 | |
iow | 即iowait 表示CPU不能工作的时间 | |
irq | 硬中断占用CPU的百分比 | |
sirq | 软中断占用CPU的百分比 | |
host | 用于有虚拟cpu的情况,指示主机的cpu百分比 |
进程信息:
属性 | 含义 |
PID | 进程id |
PPID | 父进程id |
RUSER | 真实姓名 |
UID | 进程所有者的用户id |
USER | 进程所有者的用户名 |
GROUP | 进程所有者的组名 |
TTY | 启动进程的终端名 |
PR | 优先级 |
NI | 负值表示高优先级,正值表示低优先级 |
P | 最后使用的CPU,仅在多CPU环境下有意义 |
%CPU | 上次更新到现在的CPU时间占用百分比 |
TIME | 进程使用的CPU时间总计(秒) |
TIME+ | 进程使用的CPU时间总计(1/100秒) |
%MEM | 进程使用的物理内存百分比 |
VIRT | 进程使用的虚拟内存总量(Kb),VIRT=SWAP+RES |
SWAP | 进程使用的虚拟内存中被换出的大小(Kb) |
RES | 进程使用的未被换出的物理内存大小(Kb),RES=CODE+DATA |
CODE | 可执行代码占用的物理内存大小(Kb) |
DATA | 可执行代码以外的部分(数据段+栈)占用的物理内存大小(Kb) |
SHR | 共享内存大小(Kb) |
nFLT | 页面错误次数 |
nDRT | 最后一次写入到现在,被修改过的页面数 |
S | 进程状态:D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 |
COMMAND | 命令名/命令行 |
WCHAN | 若该进程在睡眠,则显示睡眠中的系统函数名 |
Flags | 任务标志 |
2、ulimit 命令:
属性 | 含义 |
-a | 显示目前资源限制的设定 |
-t | <CPU时间> 指定CPU使用时间的上限,单位为秒 |
-f | <文件大小> shell所能建立的最大文件,单位为区块 |
-c | <core文件上限> 设定core文件的最大值,单位为区块 |
-d | <数据节区大小> 程序数据节区的最大值,单位为KB |
-s | <堆叠大小> 指定堆叠的上限,单位为KB |
-l | <内存中锁定进程的最大值> 设置在内存中锁定进程的最大值,单位为KB |
-n | <文件数目> 指定同一时间最多可开启的文件数(句柄数) |
-p | <缓冲区大小> 指定管道缓冲区的大小,单位512字节 |
-i | 限制最大可加锁内存大小,单位为Kb |
-q | 限制最大消息队列内存大小,单位为Kb |
-e | 限制最大优先级级数 |
-r | 限制最大实时优先级级数 |
-m | <内存大小> 指定可使用内存的上限,单位为KB |
-v | <虚拟内存大小> 指定可使用的虚拟内存上限,单位为KB |
lsof -l|awk '{print $2}'|sort|uniq -c|sort -nr|more
执行上述命令后会出现两列数字,其中第一列是打开的句柄数,第二列是进程ID,根据打开文件句柄的数量降序排列。可以根据ID号找到具体的进程。
cat /proc/PID/limits
上述命令可以打印出某个进程(PID为进程号)所能申请的最大资源,其中Max open files即为最大句柄数。
lsof -p PID | wc -l
上述命令查看某进程(PID为进程号)的打开文件数量(句柄数)。严格说这个命令查询的数据不准确,里面含有重复的句柄文件数
3、ps命令:
属性 | 含义 |
USER | 进程所有者的用户名 |
PID | 进程id |
PPID | 父进程id |
VSZ | 虚拟内存大小 |
RSS | 驻留集大小,这是进程当前加载其所有页面的内存大小 |
WCHAN | 若该进程在睡眠,则显示睡眠中的系统函数名 |
ADDR | 进程的内存地址 |
S | 进程状态:D=不可中断的睡眠状态 R=运行 S=睡眠 T=跟踪/停止 Z=僵尸进程 |
NAME | 进程名称 |
4、cat /proc/PID/status:
cat /proc/PID/status
执行上述命令,PID为进程号,可以看到进程详细信息。
主要属性 | 含义 |
Name | 进程名称 |
State | 进程状态 运行/睡眠/僵死 |
Tgid | 线程组号 |
Pid | 任务ID |
Ppid | 父进程ID |
TracerPid | 接收跟踪该进程信息的进程的ID号 |
Uid | 用户ID |
Gid | 群组ID |
FDSize | 文件描述符的最大个数 |
Groups | 进程所属组 |
VmPeak | 当前进程运行过程中占用内存的峰值 |
VmSize | 进程现在正在占用的内存 |
VmLck | 进程已经锁住的物理内存的大小.锁住的物理内存不能交换到硬盘 |
VmHWM | 程序得到分配到物理内存的峰值 |
VmRSS | 程序现在使用的物理内存 |
VmData | 进程数据段的大小 |
VmStk | 进程堆栈段的大小 |
VmExe | 进程代码的大小 |
VmLib | 进程所使用LIB库的大小 |
VmPTE | 进程占用的页表的大小 |
VmSwap | 进程占用Swap的大小 |
Threads | 当前进程组的线程数量 |
SigPnd | 屏蔽位,存储了该线程的待处理信号,等同于线程的PENDING信号 |
ShnPnd | 屏蔽位,存储了该线程组的待处理信号.等同于进程组的PENDING信号 |
SigBlk | 存放被阻塞的信号,等同于BLOCKED信号 |
SigIgn | 存放被忽略的信号,等同于IGNORED信号 |
SigCgt | 存放捕获的信号,等同于CAUGHT信号 |
CapEff | 当一个进程要进行某个特权操作时,操作系统会检查cap_effective的对应位是否有效,而不再是检查进程的有效UID是否为0 |
CapPrm | 表示进程能够使用的能力,在cap_permitted中可以包含cap_effective中没有的能力,这些能力是被进程自己临时放弃的,也可以说cap_effective是cap_permitted的一个子集 |
CapInh | 表示能够被当前进程执行的程序继承的能力 |
CapBnd | 系统的边界能力 |
Cpus_allowed | 指出该进程可以使用CPU的亲和性掩码,因为我们指定为两块CPU,所以这里就是3,如果该进程指定为4个CPU(如果有话),这里就是F(1111) |
Cpus_allowed_list | 0-1指出该进程可以使用CPU的列表,这里是0-1 |
voluntary_ctxt_switches | 进程主动切换的次数 |
nonvoluntary_ctxt_switches | 进程被动切换的次数 |
于是使用以下代码段加入调试信息,在关键处打印出进程所有信息。
time_t dispatch_t;
int get_phy_mem(const pid_t p)
{
char file[64] = {0}; //文件名
FILE *fd; //定义文件指针fd
char line_buff[SigQ_LINE][256] = {0}; //读取行的缓冲区
sprintf(file, "/proc/%d/status", p);
fprintf(stderr, "current pid:%d\n", p);
fd = fopen(file, "r"); //以R读的方式打开文件再赋给指针fd
int i;
time(&dispatch_t);
ALOGI("DEBUG %s() at %s, pid_t = %d\n", __func__,ctime(&dispatch_t),p);
for (i = 0; i < 35; i++) //打印出status的前35行
{
char *ret = fgets(line_buff[i], sizeof(line_buff[i]), fd);
ALOGI("DEBUG %s() value: %s", __func__,line_buff[i]);
}
fclose(fd); //关闭文件fd
return 0;
}
之后发现,每次执行含有pthread_create的方法后内存显著升高。代码中的方法以pthread_create(&XXX, NULL, XXX, NULL);方式执行。第二个和第四个参数都传了NULL。
三、解决方案
1、pthread_create函数
pthread_create是Linux环境创建线程的函数
#include <pthread.h>
int pthread_create(pthread_t* restrict tidp,const pthread_attr_t* restrict_attr,void* (*start_rtn)(void*),void *restrict arg);
tidp:指向线程标识符的指针
attr:设置线程属性
start_rtn:线程运行函数的起始地址(通常传入函数名指针)
arg:运行函数的参数
2、问题解决
第二个关于线程属性的参数直接置为NULL,这样线程结束时会变成僵死线程,线程资源得不到释放。线程结束后调用pthread_exit并不能释放,使用这种方式频繁的创建线程,就会导致内存占用的持续增长。
a.使用pthread_join
在默认情况下,线程的终止状态会保存直到对该线程调用pthread_join。类似于malloc/free。在使用pthread_create创建线程后,可以使用pthread_join等待线程的终止。pthread_join是阻塞的,直到线程结束才会返回。
b.使用pthread_detach
线程结束时,在线程内部调用pthread_detach(pthread_self()),对线程进行分离,已经分离的线程,其底层存储资源可以在线程终止时立即被收回。(注意:在线程被分离后,不能使用pthread_join等待它的终止状态,这是未定义的行为)
c.设置线程属性
在创建线程时,设置线程的分离状态属性,如下:
pthread_attr_t attr;
pthread_t tid;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);//设置detach属性
pthread_create(&tid, &attr, thr_fun, NULL);
pthread_attr_destroy(&attr);
这种方法和上一种类似,只不过一个是在线程创建时进行分离属性的设置,一个是在线程结束后对线程进行分离,两种方法都不能再使用pthread_join(detachstate线程属性,要么被设置为PTHREAD_CREATE_DETACHED,要么被设置成PTHREAD_CREATE_JOINABLE)。
于是定义改造函数
int pthread_create2(pthread_t* restrict tidp,const pthread_attr_t* restrict_attr,void* (*start_rtn)(void*),void *restrict arg){
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//设置detach属性
int ret = pthread_create(tidp,&attr,start_rtn, arg);
pthread_attr_destroy(&attr);
return ret;
}
用函数pthread_create2替换了原来代码中对函数pthread_create的调用,问题解决。注意:如果程序中存在并发调用pthread_create2的情况,则需要对此函数加锁,否则可能出现未知错误。