赞
踩
接上文,本系列文章,最重要的部分——————对native堆内存的分析,即将上演
分成六大板块:
本篇文章,实现第一个板块,并作为后续内存分析的基础知识。
先对,用到的理论知识,做说明,然后再进行实现。
我们先简单介绍操作系统的系统调用,然后简单说明android libc对其的封装,最后是我们自己对libc的分配函数的封装,以达到内存统计分析的目的
对于内存而言,它属于系统资源,它的管理者是操作系统。而为了让用户能够使用系统的资源。有三种系统调用,提供给用户。分别是:
其中brk系统调用和sbrk系统调用的区别是:brk用于直接设置break地址。即修改进程数据段的结束地址。
sbrk用于在原有的break地址上,进行增加或减少
mmap和munmap则是,直接进行进程地址空间的映射。
从上面可以知道,操作系统只提供了一些非常简单的内存操作接口,
如果一个进程中,有多个线程,应该怎么管理这部分堆空间呢?
如果一个线程要反反复复频繁的分配空间怎么办呢?是否要频繁的调用系统调用呢?
如果分配的空间,一会大的要死,一会儿小的要死,应该怎么办呢?
此时,Android的libc库,对上面的内存做了进一步的管理。它使用了一种被称为jemalloc的分配器策略。
jemalloc,维护了大小不同的内存池。当应用程序请求分配内存时,jemalloc会选择合适的内存满足应用的请求。
jemalloc,还为每个线程维护本地缓存,当线程请求内存时,则尽可能返回本地缓存中的内存块。
jemalloc的所有这些内存管理,都是基于上面介绍的三种系统调用。
同时Android的libc库,也是一种轻量级的c库,因此对jemalloc的分配器策略,进行了一种标准接口的封装
即我们熟悉的:malloc,remalloc,calloc标准接口
当应用程序,调用malloc,remalloc,或者calloc函数请求分配内存时.libc库则根据jemalloc的分配器策略分配不同的内存快
那么我们还可以对,libc库的内存分配函数,做进一步的封装。在应用每次调用内存分配的时候,记录下调用分配的堆栈,和分配的大小。
在合适的时候,将其打印出来,以观察内存的分配情况,从而达到分析内存泄漏的问题。
malloc,free函数位于stdlib.h文件中,定义如下
void* malloc (size_t size);
void free (void* ptr);
下面是我自定义的malloc函数,我将其放在一个单独的命名为Memory.cpp的文件中
void* malloc(size_t size) { void* ptr = NULL; // 在这里,你可以添加自定义的逻辑,例如记录内存分配的信息,或者改变内存分配的行为。 // 调用系统的malloc函数进行实际的内存分配。 // 注意,这里需要使用函数指针来调用系统的malloc函数,以避免无限递归。 void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc"); //查找下一个叫做malloc的符号 if (libc_malloc) { ptr = libc_malloc(size); //将ptr保存,以便于后续分析 addToAllptr(ptr,size); } return ptr; }
为了保存,分配的地址和,大小,新增下面的函数
//保存所有指针的地方 AllPtr ptrBuffer; static std::recursive_mutex mutex; //为了在ptrBuffer.push的时候,防止循环调用 static bool * isPush = nullptr; void addToAllptr(void *ptr,std::size_t sz){ std::unique_lock<std::recursive_mutex> _l(mutex); //为什么不直接用一个bool变量? //因为发现,在部分版本中,会被优化掉,因此通过使用单独的内存来存储这个bool值 if(!isPush){ void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc"); if (libc_malloc) { isPush =(bool *) libc_malloc(sizeof(bool)); *isPush = false; } } if(isPush){ if(*isPush){ }else{ *isPush = true; ptrBuffer.push(ptr,sz); *isPush = false; } } } void popFromAllptr(void *ptr) { std::unique_lock<std::recursive_mutex> _l(mutex); ptrBuffer.pop(ptr); }
其中。prtBuffer是类型为Allptr的一个自定义类,这个类负责管理,分配的内存。代码如下:
struct AllPtr{ //每次内存的分配,就会有一个PtrItem与之对应 struct PtrItem{ bool isEmpty = true;//表示该PtrItem是否为空 void * ptr = nullptr; //存储分配的指针 std::size_t size = 0;//存储分配的内存大小 struct timespec now;//存储分配内存的时间 char * stack = nullptr;//存储分配内存的调用栈 }; PtrItem * allptr = nullptr;//PtrItem数组头指针 std::size_t size = 0;//PtrItem数组大小 int line = 20;//辅助打印的行数 //判断PtrItem数组是否满 bool isFull() const{ bool full = true; for(auto i =0;i<size;i++){ if(allptr[i].isEmpty){ full = false; break; } } return full; } //创建一个新的PtrItem数组,它的大小是老数组大小的2倍,并将老数组内容,复制到新数组中 void createAndCopy(){ std::size_t newSize = 0; if(size > 0 ){ newSize = size * 2; ALOG(LOG_INFO, __FUNCTION__ , "create and copy %lu",newSize); }else{ newSize = 1; } void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc"); if (!libc_malloc) { ALOG(LOG_ERROR, __FUNCTION__ , "find lib malloc error"); return ; } auto newPtr = (PtrItem *)libc_malloc(sizeof(PtrItem) * newSize); for(int i= 0;i<newSize;i++){ newPtr[i].isEmpty = true; newPtr[i].ptr = nullptr; newPtr[i].size = 0; newPtr[i].now.tv_sec = 0; newPtr[i].now.tv_nsec = 0; newPtr[i].stack = nullptr; } //复制老数组内容 for(int i = 0; i < size;i++){ newPtr[i].isEmpty = allptr[i].isEmpty; newPtr[i].ptr = allptr[i].ptr; newPtr[i].size = allptr[i].size; newPtr[i].now.tv_sec= allptr[i].now.tv_sec; newPtr[i].now.tv_nsec= allptr[i].now.tv_nsec; newPtr[i].stack = allptr[i].stack; } if(allptr){ void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free"); if (lib_free) { lib_free(allptr); } } allptr = newPtr; size = newSize; } //保存ptr,新建一个PtrItem void push(void * ptr,std::size_t size) { //是否为空//是否满 if (!allptr || isFull()) { createAndCopy(); } for (auto i = 0; i < size; i++) { if (allptr[i].isEmpty) { allptr[i].isEmpty = false; allptr[i].ptr = ptr; allptr[i].size = size; if (clock_gettime(CLOCK_REALTIME, &(allptr[i].now)) == -1) { perror("clock_gettime"); } //initialize void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc"); if (libc_malloc) { allptr[i].stack = (char * )libc_malloc( 1024*line); //获取对应的调用栈 Find::Debug().printStackTrace(allptr[i].stack,1024*line); } return; } } } //弹出Ptr void pop(void *ptr) const { for(auto i=0;i<size;i++) { if(allptr[i].ptr == ptr) { allptr[i].isEmpty = true; if (allptr[i].stack){ void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free"); if (lib_free) { lib_free(allptr[i].stack); } } return; } } } ~AllPtr(){ if(allptr){ for(auto i =0;i<size;i++){ if(!allptr[i].isEmpty){ if (allptr[i].stack){ void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free"); if (lib_free) { lib_free(allptr[i].stack); } } break; } } void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free"); if (lib_free) { lib_free(allptr); } } } //在合适的时候,dump出PtrItem中的内容,便于分析 //在这里,只是简单的将其输出到Log系统 //事实上,应该按照时间进行采样,然后输出到文件中,通过其他工具将其图形化 //这里为了简化远离,仅仅将其输出到log系统中 void dump(){ for(auto i=0;i<size;i++){ if(allptr[i].isEmpty == false){ time_t t = allptr[i].now.tv_sec; struct tm *local; local = localtime(&t); if (local == NULL) { perror("localtime"); } ALOG(LOG_INFO, __FUNCTION__ , "%02d:%02d:%02d:%04d,ptr %p,size %lu", local->tm_hour,local->tm_min,local->tm_sec,allptr[i].now.tv_nsec/1000000, allptr[i].ptr,allptr[i].size); for(int i=0;i<line;i++){ ALOG(LOG_INFO, __FUNCTION__ ,"%s",allptr[i].stack+(i*1024)); } } } } };
在上面的例子中,使用Find::Debug().printStackTrace来获取android的native调用栈
它的实现如下:
namespace Find { //将调用栈信息打印到buffer中 void Debug::printStackTrace(char * buffer,int size){ const auto maxStackDeep = 10; intptr_t stackBuf[maxStackDeep]; for(int i = 0; i < maxStackDeep; ++i){ stackBuf[i] = 0; } memset(buffer, 0, size); dumpBacktraceIndex(buffer, stackBuf, captureBacktrace(stackBuf, maxStackDeep)); } //捕获调用栈信息 size_t Debug::captureBacktrace(intptr_t *buffer, size_t maxStackDeep) { BacktraceState state = {buffer, buffer + maxStackDeep}; _Unwind_Backtrace(unwindCallback, &state); return state.current - buffer; } //解析调用栈信息 void Debug::dumpBacktraceIndex(char *out, intptr_t *buffer, size_t count) { for (size_t idx = 0; idx < count; ++idx) { intptr_t addr = buffer[idx]; const char *symbol = " "; const char *dlfile = " "; void * baseA = nullptr; Dl_info info; info.dli_fbase = nullptr; if (dladdr((void *) addr, &info)) { if (info.dli_sname) { symbol = info.dli_sname; } if (info.dli_fname) { dlfile = info.dli_fname; } if(info.dli_fbase){ baseA = info.dli_fbase; } } else { strcat(out, "# \n"); continue; } char temp[50]; memset(temp, 0, sizeof(temp)); sprintf(temp, "%zu", idx); strcat(out, "#"); strcat(out, temp); strcat(out, ": "); memset(temp, 0, sizeof(temp)); sprintf(temp, "%p", (void *) addr); strcat(out, temp); strcat(out, " "); memset(temp, 0, sizeof(temp)); //尤其要注意,这里的两地址相减,这是为了让addr2line工具能够正常解析 sprintf(temp, "%p", (void *) ((long)addr - (long )baseA)); strcat(out, temp); strcat(out, " "); strcat(out, symbol); strcat(out, " "); strcat(out, dlfile); strcat(out, "\n"); } } _Unwind_Reason_Code Debug::unwindCallback(struct _Unwind_Context *context, void *arg) { BacktraceState *state = static_cast<BacktraceState *>(arg); intptr_t ip = (intptr_t) _Unwind_GetIP(context); if (ip) { if (state->current == state->end) { return _URC_END_OF_STACK; } else { state->current[0] = ip; state->current++; } } return _URC_NO_REASON; } }
注意:在Debug中,并没有使用backtrace函数,这是因为,backtrace函数是由GNUC库提供的。而android的c库,是一个被称为bionic的Google单独编写的c库,并没有提供backtrace函数。因此我们选择了libunwind库中的_Unwind_Backtrace。
然后使用dladdr函数进行地址的解析。
需要再次注意的是:addr地址为内存中的地址,并不能直接使用
addr2line进行直接转换。需要将addr和共享库的基址相减,得到相对地址,才能够被add2line进行转化。
有了上面的malloc,还需要有free函数,如下
//释放对应的ptr
void free (void* ptr){
//删除Allptr中的ptr
popFromAllptr(ptr);
void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free");
if (lib_free) {
lib_free(ptr);
}
}
为了能够直观的观测到记录下来的内存,现在增加一个广播,如下:
application.registerReceiver(object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Timber.d(intent.action)
native_dumpCpluplusObjecs()
}
}, IntentFilter("find.dump.objects"))
当收到"find.dump.objects"时,会调用native_dumpCpluplusObjecs。它的实现如下:
extern AllPtr ptrBuffer;
extern "C"
JNIEXPORT void JNICALL
Java_cn_findpiano_romsdk_hardware_midi_MidiManager_native_1dumpCpluplusObjecs(JNIEnv *env,
jobject thiz) {
ptrBuffer.dump();
}
我们调用ptrBuffer的dump函数,将记录下来的内存信息输出到log系统中。
现在进入adb shell ,输入如下的命令
am broadcast -a find.dump.objects
那么将会在log系统看到如下的输出
注意:自定义malloc函数,并不会影响realloc和calloc,因为我目前的项目并没有使用这两函数,因此也没有做演示,但他们的核心思想同malloc一样
对于c++这门语言而言,它的new和delete以及new[] delete[]都是直接调用对应的运算符函数,如void* operator new(std::size_t sz);
因此我们可以重载这些运算符函数,以达到监控内存的目的。事实上android的c++库,不管是来自LLVM项目的libc++库。还是,来自android自定义的libstdc++库,都会间接调用,android的libc库的分配函数和释放函数。
注意注意:Android的libstdc++.so和来自GNU项目的libstdc++.so是不一样的。具体原因可以参看
ndk的问题744:https://github.com/android/ndk/issues/744
即然会间接调用libc的分配函数,那么上面关于malloc和free的实现,也已经覆盖了c++的使用。
但是这里为了演示说明,将重新实现void* operator new(std::size_t sz),然后它使用系统提供的malloc,而不是我们自己实现的malloc,下面的代码来自c++中的例子,并增加了部分,前面介绍的内容,如使用libc中的malloc
void* operator new(std::size_t sz) { if (sz == 0) ++sz; // avoid std::malloc(0) which may return nullptr on success void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc"); if (libc_malloc) { if (void *ptr = libc_malloc(sz)){ ALOGD("new an object %lu,ptr %p",sz,ptr); addToAllptr(ptr,sz); return ptr; } } throw std::bad_alloc{}; } // no inline, required by [replacement.functions]/3 void* operator new[](std::size_t sz) { if (sz == 0) ++sz; // avoid std::malloc(0) which may return nullptr on success void* (*libc_malloc)(size_t) = (void* (*)(size_t))dlsym(RTLD_NEXT, "malloc"); if (libc_malloc) { if (void *ptr = libc_malloc(sz)){ ALOGD("new an array object %lu,ptr %p",sz,ptr); addToAllptr(ptr,sz); return ptr; } } throw std::bad_alloc{}; } void operator delete(void* ptr) noexcept { ALOGD("delete an object %p",ptr); popFromAllptr(ptr); void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free"); if (lib_free) { lib_free(ptr); } } void operator delete[](void* ptr) noexcept { ALOGD("delete an array object %p",ptr); popFromAllptr(ptr); void (*lib_free)(void*) = (void (*)(void*))dlsym(RTLD_NEXT, "free"); if (lib_free) { lib_free(ptr); } }
上面关于c++的new和delete仅仅是演示使用。c++提供了更加丰富的自定义分配函数的操作。
注意:在上面的代码中,我们使用了dlsym(RTLD_NEXT, “malloc”);来查找下一个叫做malloc的函数,如果有多个so库且都含有malloc,这样的行为,将是根据搜索路径中出现的先后顺序决定。
通过在一段时间中,多次dump,然后使用shell脚本,按照时间排序,可以非常轻松的观测到长期占据且没有释放的内存。然后使用对应的调用栈,找到源码进行分析。
至此,关于手动实现内存的分配和释放,并保存调用栈。已完成。
使用此种方法,基本上只会影响本so库中内容,这取决于编译链接的机制,那么怎样能够监控所有的内存分配呢,下一节molloc_hooks将会介绍这部分内容。敬请期待
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。