一次因pthread_create导致的内存泄漏排查与解决

一、问题现象

        调试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打印信息 :

行标属性含义
Taskstotal进程总数
running正在运行的进程数
sleeping睡眠的进程数
stopped停止的进程数
zombie 僵尸进程数
Memtotal物理内存总量
used使用的物理内存总量
free空闲内存总量
buffers用作内核缓存的内存量
Swaptotal交换区总量
used使用的交换区总量
free空闲交换区总量
cached缓冲的交换区总量
%cpuuser用户空间占用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_list0-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的情况,则需要对此函数加锁,否则可能出现未知错误。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值