背景
随着對用戶端穩定性品質的不斷深入,部分的重點、難點問題逐漸治理,記憶體品質逐漸成為了影響用戶端品質的最突出的問題之一。是以淘寶對此進行了系統性的記憶體治理,成立了記憶體專項。
“工欲善其事、必先利其器”。本文主要講述記憶體專項的工具之一,記憶體洩漏分析memunreachable。
記憶體洩漏
記憶體洩漏(Memory Leak)是指程式中已動态配置設定的堆記憶體由于某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導緻程式運作速度減慢甚至系統崩潰等嚴重後果。
對于 c/c++記憶體洩漏,由于存在指針要精确找到那些對象沒有被引用是非常困難的,一直是困擾 c/c++重點、難點問題之一。目前也有一些基于類似 GC Swap-Mark 的算法去找到記憶體洩露,常見工具如 libmemunreachable,kmemleak,llvm leaksanitizer 這類工具也需要記錄配置設定資訊。
Android 的 libmemunreachable 是一個零開銷的本地記憶體洩漏檢測器。 它會使用不精确的“标記-清除”垃圾回收器周遊所有本機記憶體,同時将任何不可通路的塊報告為洩漏。 有關使用說明,請參閱 libmemunacachable 文檔[1]。雖然 Android 提供了 libmemunreachable 如此優秀的開源 c/c++記憶體洩漏工具,并内嵌到 Android 的系統環境,幫忙我們去定位記憶體洩漏問題,但是目前 libmemunreachable 使用依賴線下的 Debug 配置環境,無法支援淘寶 Release 包。
本文結合 libmemunreachable 源碼,我們一起來欣賞 libmemunreachable 的實作原理以及淘寶對 libmemunreachable 改造用來實作對 Release 包的支援,幫助淘寶定位和排查線上的記憶體洩漏問題。
libmemunreachable 分析
基本原理
我們知道 JAVA GC 算法中,如果記憶體中的對象中,如果不在被 GcRoot 節點直接或間接持有,那麼 GC 在适當的時間會觸發垃圾回收機制,去釋放記憶體。那麼哪些節點可以被作為 GC 的 Root 節點:
- 虛拟機棧(棧幀中的本地變量表)中引用的對象;
- 方法區中的類靜态屬性引用的對象;
- 方法區中常量引用的對象;
- 本地方法棧中 JNI(即一般說的 Native 方法)中引用的對象。(JVM 中判斷對象是否清理的一種方法是可達性算法.可達性算法就是采用 GC Roots 為根節點, 采用樹狀結構,向下搜尋.如果對象直接到達 GC Roots ,中間沒有任何中間節點.則判斷對象可回收. 而堆區是 GC 的重點區域,是以堆區不能作為 GC roots。)
而 C/C++記憶體模型,堆 heap 、棧 stack、全局/靜态存儲區 (.bss 段和.data 段)、常量存儲區 (.rodata 段) 、代碼區 (.text 段)。libmemunreachable 通過 C/C++記憶體模型結合可達性算法,将棧 stack、全局/靜态存儲區 (.bss 段和.data 段)作為 GC Root 節點,判斷堆 heap 中的記憶體是否被 GC Root 所持有,如果不被直接或間接持有,則被判定為洩漏(别較真,不一定要 100%的判斷 C/C++的記憶體洩漏,而是可以分析可能存在的潛在洩漏)。
圖 1 C/C++記憶體模型可達性算法示意圖
libmemunreachable 會使用不精确的“标記-清除”垃圾回收器周遊所有本機記憶體,同時将任何不可通路的塊報告為洩漏。
libmemunreachable 流程圖
圖 2 memunreachable 時序圖
memunreachable 時序:
- 建立 LeakPipe:用來與子程序通信,子程序發送資料,父程序接受資料;
- Fork 子程序:通過 fork 子程序的方式來保護目前程序的狀态;
- CaptureThreads:通過 Ptrace 的方式使得目标程序可以被子程序 Dump,進而使得子程序擷取父程序的資訊;
- CaptureThreadInfo:通過 PTRACE_GETREGSET 擷取寄存器的資訊,部分 Heap 的記憶體可能被寄存器持有,這些被寄存器持有的 Heap 不應該被判定為洩漏;
- ProcessMappings:解析/proc/self/maps 檔案資訊,maps 檔案記錄了堆 heap 、棧 stack、全局/靜态存儲區 (.bss 段和.data 段)、常量存儲區 (.rodata 段) 、代碼區 (.text 段)等記憶體相關的資訊;
- ReleaseThreads:通過 Ptrace 的方式恢複目标程序的 Ptrace 狀态,并且主程序結束等待,開始接受資料;
- 第二次 Fork 子程序:這裡又 Fork 一次子程序,我的了解可能是為了性能,第一次 Fork 的是收集了需要分析記憶體洩漏的相關資訊,第二次 Fork 則在收集的相關資訊基礎上去分析;
- CollectAllocations:從/proc/pid/maps 的資訊中分類,将棧 stack、全局/靜态存儲區 (.bss 段和.data 段)放入 GC Root 節點,堆 heap 放入被檢查的對象;
- GetUnreachableMemory:擷取不可達的洩漏記憶體,C/C++記憶體模型結合可達性算法開始工作,去分析可能洩漏的 Heaps;
- PipeSend:通過 Pipe 将洩漏資訊發送給主程序;
- PipeReceiver:主程序接受洩漏資料。
核心代碼如下:
//MemUnreachable.cpp
bool GetUnreachableMemory(UnreachableMemoryInfo &info, size_t limit) {
int parent_pid = getpid();
int parent_tid = gettid();
Heap heap;
Semaphore continue_parent_sem;
LeakPipe pipe;
PtracerThread thread{[&]() -> int {
/////////////////////////////////////////////
// Collection thread
/////////////////////////////////////////////
ALOGE("collecting thread info for process %d...", parent_pid);
ThreadCapture thread_capture(parent_pid, heap);
allocator::vector<ThreadInfo> thread_info(heap);
allocator::vector<Mapping> mappings(heap);
allocator::vector<uintptr_t> refs(heap);
// ptrace all the threads
if (!thread_capture.CaptureThreads()) {
LOGE("CaptureThreads failed");
}
// collect register contents and stacks
if (!thread_capture.CapturedThreadInfo(thread_info)) {
LOGE("CapturedThreadInfo failed");
}
// snapshot /proc/pid/maps
if (!ProcessMappings(parent_pid, mappings)) {
continue_parent_sem.Post();
LOGE("ProcessMappings failed");
return 1;
}
// malloc must be enabled to call fork, at_fork handlers take the same
// locks as ScopedDisableMalloc. All threads are paused in ptrace, so
// memory state is still consistent. Unfreeze the original thread so it
// can drop the malloc locks, it will block until the collection thread
// exits.
thread_capture.ReleaseThread(parent_tid);
continue_parent_sem.Post();
// fork a process to do the heap walking
int ret = fork();
if (ret < 0) {
return 1;
} else if (ret == 0) {
/////////////////////////////////////////////
// Heap walker process
/////////////////////////////////////////////
// Examine memory state in the child using the data collected above and
// the CoW snapshot of the process memory contents.
if (!pipe.OpenSender()) {
_exit(1);
}
MemUnreachable unreachable{parent_pid, heap};
//C/C++記憶體模型結合可達性算法開始工作
if (!unreachable.CollectAllocations(thread_info, mappings)) {
_exit(2);
}
size_t num_allocations = unreachable.Allocations();
size_t allocation_bytes = unreachable.AllocationBytes();
allocator::vector<Leak> leaks{heap};
size_t num_leaks = 0;
size_t leak_bytes = 0;
bool ok = unreachable.GetUnreachableMemory(leaks, limit, &num_leaks, &leak_bytes);
ok = ok && pipe.Sender().Send(num_allocations);
ok = ok && pipe.Sender().Send(allocation_bytes);
ok = ok && pipe.Sender().Send(num_leaks);
ok = ok && pipe.Sender().Send(leak_bytes);
ok = ok && pipe.Sender().SendVector(leaks);
if (!ok) {
_exit(3);
}
_exit(0);
} else {
// Nothing left to do in the collection thread, return immediately,
// releasing all the captured threads.
ALOGI("collection thread done");
return 0;
}
}};
/////////////////////////////////////////////
// Original thread
/////////////////////////////////////////////
{
// Disable malloc to get a consistent view of memory
ScopedDisableMalloc disable_malloc;
// Start the collection thread
thread.Start();
// Wait for the collection thread to signal that it is ready to fork the
// heap walker process.
continue_parent_sem.Wait(300s);
// Re-enable malloc so the collection thread can fork.
}
// Wait for the collection thread to exit
int ret = thread.Join();
if (ret != 0) {
return false;
}
// Get a pipe from the heap walker process. Transferring a new pipe fd
// ensures no other forked processes can have it open, so when the heap
// walker process dies the remote side of the pipe will close.
if (!pipe.OpenReceiver()) {
return false;
}
bool ok = true;
ok = ok && pipe.Receiver().Receive(&info.num_allocations);
ok = ok && pipe.Receiver().Receive(&info.allocation_bytes);
ok = ok && pipe.Receiver().Receive(&info.num_leaks);
ok = ok && pipe.Receiver().Receive(&info.leak_bytes);
ok = ok && pipe.Receiver().ReceiveVector(info.leaks);
if (!ok) {
return false;
}
LOGD("unreachable memory detection done");
LOGD("%zu bytes in %zu allocation%s unreachable out of %zu bytes in %zu allocation%s",
info.leak_bytes, info.num_leaks, plural(info.num_leaks),
info.allocation_bytes, info.num_allocations, plural(info.num_allocations));
return true;
}
CaptureThreads(核心函數)
//ThreadCapture.cpp
bool ThreadCaptureImpl::CaptureThreads() {
TidList tids{allocator_};
bool found_new_thread;
do {
//從/proc/pid/task中擷取全部線程Tid
if (!ListThreads(tids)) {
LOGE("ListThreads failed");
ReleaseThreads();
return false;
}
found_new_thread = false;
for (auto it = tids.begin(); it != tids.end(); it++) {
auto captured = captured_threads_.find(*it);
if (captured == captured_threads_.end()) {
//通過ptrace(PTRACE_SEIZE, tid, NULL, NULL)使得線程tid可以被DUMP
if (CaptureThread(*it) < 0) {
LOGE("CaptureThread(*it) failed");
ReleaseThreads();
return false;
}
found_new_thread = true;
}
}
} while (found_new_thread);
return true;
}
CaptureThreads 存在兩個核心核心函數
- ListThreads:從/proc/pid/task 中擷取全部線程 Tid
- CaptureThread:通過 ptrace(PTRACE_SEIZE, tid, NULL, NULL)使得線程 tid 可以被 DUMP
CaptureThreadInfo(核心函數)
//ThreadCaptureImpl.cpp
bool ThreadCaptureImpl::CapturedThreadInfo(ThreadInfoList &threads) {
threads.clear();
for (auto it = captured_threads_.begin(); it != captured_threads_.end(); it++) {
ThreadInfo t{0, allocator::vector<uintptr_t>(allocator_),
std::pair<uintptr_t, uintptr_t>(0, 0)};
//ptrace(PTRACE_GETREGSET, tid, reinterpret_cast<void *>(NT_PRSTATUS), &iovec)
if (!PtraceThreadInfo(it->first, t)) {
return false;
}
threads.push_back(t);
}
return true;
}
CaptureThreads 的個核心函數
- PtraceThreadInfo:ptrace(PTRACE_GETREGSET, tid...),通過 ptrace 獲寄存器資訊,部分 Heap 的記憶體可能被寄存器持有,這些被寄存器持有的 Heap 不應該被判定為洩漏。
ProcessMappings(核心函數)
//ProcessMappings.cpp
bool ProcessMappings(pid_t pid, allocator::vector<Mapping> &mappings) {
char map_buffer[1024];
snprintf(map_buffer, sizeof(map_buffer), "/proc/%d/maps", pid);
android::base::unique_fd fd(open(map_buffer, O_RDONLY));
if (fd == -1) {
LOGE("ProcessMappings parent pid failed to open %s: %s", map_buffer, strerror(errno));
//get self pid to replace
//Release 包有權限問題隻能通路自身程序
snprintf(map_buffer, sizeof(map_buffer), "/proc/self/maps");
fd.reset(open(map_buffer, O_RDONLY));
if (fd == -1) {
LOGE("ProcessMappings failed to open %s: %s", map_buffer, strerror(errno));
return false;
}
}
LineBuffer line_buf(fd, map_buffer, sizeof(map_buffer));
char *line;
size_t line_len;
while (line_buf.GetLine(&line, &line_len)) {
int name_pos;
char perms[5];
Mapping mapping{};
if (sscanf(line, "%" SCNxPTR "-%" SCNxPTR " %4s %*x %*x:%*x %*d %n",
&mapping.begin, &mapping.end, perms, &name_pos) == 3) {
if (perms[0] == 'r') {
mapping.read = true;
}
if (perms[1] == 'w') {
mapping.write = true;
}
if (perms[2] == 'x') {
mapping.execute = true;
}
if (perms[3] == 'p') {
mapping.priv = true;
}
if ((size_t) name_pos < line_len) {
strlcpy(mapping.name, line + name_pos, sizeof(mapping.name));
}
mappings.emplace_back(mapping);
}
}
return true;
}
- ProcessMappings 解析 maps 檔案資訊。
CollectAllocations(核心函數)
//MemUnreachable.cpp
bool MemUnreachable::ClassifyMappings(const allocator::vector<Mapping> &mappings,
allocator::vector<Mapping> &heap_mappings,
allocator::vector<Mapping> &anon_mappings,
allocator::vector<Mapping> &globals_mappings,
allocator::vector<Mapping> &stack_mappings) {
heap_mappings.clear();
anon_mappings.clear();
globals_mappings.clear();
stack_mappings.clear();
allocator::string current_lib{allocator_};
for (auto it = mappings.begin(); it != mappings.end(); it++) {
if (it->execute) {
current_lib = it->name;
continue;
}
if (!it->read) {
continue;
}
const allocator::string mapping_name{it->name, allocator_};
if (mapping_name == "[anon:.bss]") {
// named .bss section
globals_mappings.emplace_back(*it);
} else if (mapping_name == current_lib) {
// .rodata or .data section
globals_mappings.emplace_back(*it);
} else if (has_prefix(mapping_name, "[anon:scudo:secondary]")) {
// named malloc mapping
heap_mappings.emplace_back(*it);
} else if (has_prefix(mapping_name, "[anon:scudo:primary]")) {
// named malloc mapping
heap_mappings.emplace_back(*it);
} else if (mapping_name == "[anon:libc_malloc]") {
// named malloc mapping
heap_mappings.emplace_back(*it);
} else if (has_prefix(mapping_name, "/dev/ashmem/dalvik")
|| has_prefix(mapping_name, "[anon:dalvik")) {
// named dalvik heap mapping
globals_mappings.emplace_back(*it);
} else if (has_prefix(mapping_name, "[stack")) {
// named stack mapping
stack_mappings.emplace_back(*it);
} else if (mapping_name.size() == 0 || mapping_name == "") {
globals_mappings.emplace_back(*it);
} else if (has_prefix(mapping_name, "[anon:stack_and_tls")) {
stack_mappings.emplace_back(*it);
} else if (has_prefix(mapping_name, "[anon:") &&
mapping_name != "[anon:leak_detector_malloc]") {
// TODO(ccross): it would be nice to treat named anonymous mappings as
// possible leaks, but naming something in a .bss or .data section makes
// it impossible to distinguish them from mmaped and then named mappings.
globals_mappings.emplace_back(*it);
}
}
return true;
}
bool MemUnreachable::CollectAllocations(const allocator::vector<ThreadInfo> &threads,
const allocator::vector<Mapping> &mappings) {
ALOGI("searching process %d for allocations", pid_);
allocator::vector<Mapping> heap_mappings{mappings};
allocator::vector<Mapping> anon_mappings{mappings};
allocator::vector<Mapping> globals_mappings{mappings};
allocator::vector<Mapping> stack_mappings{mappings};
if (!ClassifyMappings(mappings, heap_mappings, anon_mappings,
globals_mappings, stack_mappings)) {
return false;
}
for (auto it = heap_mappings.begin(); it != heap_mappings.end(); it++) {
HeapIterate(*it, [&](uintptr_t base, size_t size) {
if (!heap_walker_.Allocation(base, base + size)) {
LOGD("Allocation Failed base:%p size:%d name:%s", base, size, it->name);
}
});
}
for (auto it = anon_mappings.begin(); it != anon_mappings.end(); it++) {
if (!heap_walker_.Allocation(it->begin, it->end)) {
LOGD("Allocation Failed base:%p end:%d name:%s", it->begin, it->end, it->name);
}
}
for (auto it = globals_mappings.begin(); it != globals_mappings.end(); it++) {
heap_walker_.Root(it->begin, it->end);
}
if (threads.size() > 0) {
for (auto thread_it = threads.begin(); thread_it != threads.end(); thread_it++) {
for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
if (thread_it->stack.first >= it->begin && thread_it->stack.first <= it->end) {
heap_walker_.Root(thread_it->stack.first, it->end);
}
}
//寫入寄存器的資訊,作為根節點
heap_walker_.Root(thread_it->regs);
}
} else {
//由于擷取寄存器資訊失敗,采取降級邏輯
for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
heap_walker_.Root(it->begin, it->end);
}
}
if (threads.size() > 0) {
for (auto thread_it = threads.begin(); thread_it != threads.end(); thread_it++) {
for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
if (thread_it->stack.first >= it->begin && thread_it->stack.first <= it->end) {
heap_walker_.Root(thread_it->stack.first, it->end);
}
}
//寫入寄存器的資訊,作為根節點
heap_walker_.Root(thread_it->regs);
}
} else {
//由于擷取寄存器資訊失敗,采取降級邏輯
for (auto it = stack_mappings.begin(); it != stack_mappings.end(); it++) {
heap_walker_.Root(it->begin, it->end);
}
}
ALOGI("searching done");
return true;
}
CollectAllocations 将 maps 分四個子產品,分别是 1.heap_mappings 存放堆資訊,stack_mappings 存放線程棧資訊(GC Root),globals_mappings 存放.bss .data 資訊(GC Root),anon_mappings 其他可讀的記憶體資訊(GC Root,這些也會作為 GC Root 防止有洩漏誤報):
- ClassifyMappings:将 maps 資訊存放到目标子產品中;
- HeapIterate:周遊有效記憶體分布;Android 記憶體配置設定算法,在申請的過程中會通過 mmap 申請一塊塊的大記憶體,最後通過記憶體配置設定器進行記憶體管理,Android 11 以上使用了scudo 記憶體配置設定[2](Android 11 以下使用的是jemalloc 記憶體配置設定器[3]),無論是那種配置設定器,Android 都提供了周遊有效記憶體的便利函數 malloc_iterate 這使得我們擷取有效記憶體變得容易很多。相關内容可以看malloc_debug[4]。
GetUnreachableMemory(核心函數)
//HeapWalker.cpp
void HeapWalker::RecurseRoot(const Range &root) {
allocator::vector<Range> to_do(1, root, allocator_);
while (!to_do.empty()) {
Range range = to_do.back();
to_do.pop_back();
//将GC Root的節點的一個塊記憶體作為指針,去周遊,直到隊列為空
ForEachPtrInRange(range, [&](Range &ref_range, AllocationInfo *ref_info) {
if (!ref_info->referenced_from_root) {
ref_info->referenced_from_root = true;
to_do.push_back(ref_range);
}
});
}
}
bool HeapWalker::DetectLeaks() {
// Recursively walk pointers from roots to mark referenced allocations
for (auto it = roots_.begin(); it != roots_.end(); it++) {
RecurseRoot(*it);
}
Range vals;
vals.begin = reinterpret_cast<uintptr_t>(root_vals_.data());
vals.end = vals.begin + root_vals_.size() * sizeof(uintptr_t);
RecurseRoot(vals);
return true;
}
bool MemUnreachable::GetUnreachableMemory(allocator::vector<Leak> &leaks,
size_t limit, size_t *num_leaks, size_t *leak_bytes) {
ALOGI("sweeping process %d for unreachable memory", pid_);
leaks.clear();
if (!heap_walker_.DetectLeaks()) {
return false;
}
//資料統計
...
return true;
}
核心函數 DetectLeaks
- ForEachPtrInRange:将 GC Root 的節點的一個塊記憶體作為指針,去周遊,直到隊列為空
- DetectLeaks:周遊 GC Root 節點,将能通路到的 Heap 對象标記;
- 資料統計:沒有周遊到的 Heap 對象設定為洩漏。
淘寶 Release 包改進
Android 10 之後系統收回了程序私有檔案的權限,如 /proc/pid/maps,/proc/pid/task 等,fork 出來的子程序無法擷取父程序目錄下的檔案,否則會抛“Operation not permitted”的異常。是以當我們通過 dlsym 的方式去調用系統 libmemunreachable.so 庫的時候在 Release 包的時候會抛“Failed to get unreachable memory if you are trying to get unreachable memory from a system app (like com.android.systemui), disable selinux first using setenforce 0”(當然我們無法去設定使用者的系統環境)。
針對這問題,淘寶選擇重新編譯了 libmemunreachable 庫,并且修改了相關所需權限的配置,如/proc/pid/maps 的擷取不在擷取父程序(目标程序)的 maps(沒有權限),而擷取/proc/self/maps,因為子程序保留了父程序的記憶體資訊,這與擷取/proc/pid/maps 的效果是一緻。
Ptrace 失敗的修複:google unreachable 在 debug 包可以,在 release 包裡不能運作,原因是 PR_GET_DUMPABLE 在 debug 的時候預設是 1,直接可以 ATTACH,而在 release 的預設是 0,不可以 attach,導緻 release 跑 unreach 不正常工作 (google 太壞了),修複方案:設定下 prctl(PR_SET_DUMPABLE, 1);
其他改造:
- 工程化的改造,打通 TBRest,使得線上的洩漏資料上報到 EMAS;
- 非核心權限繞開,如/proc/pid/task 擷取線程寄存器資訊,如果擷取失敗不終止流程(雖然線程寄存器有可能會指向記憶體,并且這個記憶體不被.bss .data 和 stack 等持有,導緻誤判,但是這樣的場景不多)。
可能的誤報場景
base+offset 的場景特定的記憶體分析會失敗。比如他申請的記憶體是 A,但是堆棧和 Global 是通過 Base+offset=A 這種方法來引用的 ,就會誤判,因為 Base 和 offset 在堆和.bss 裡,但是堆和.bss 沒有 A ,就判斷 A 洩漏了 就誤報了
小結
libmemunreachable 通過子程序的方式,通過“标記-清理”收集和分析記憶體,基本不影響主線程性能,是一個零開銷的本地記憶體洩漏檢測器。
參考資料
[1]
libmemunacachable 文檔: https://android.googlesource.com/platform/system/memory/libmemunreachable/+/master/README.md
[2]
scudo 記憶體配置設定: https://source.android.google.cn/docs/security/test/scudo?hl=zh-cn
[3]
jemalloc 記憶體配置設定器: http://jemalloc.net/
[4]
malloc_debug: https://android.googlesource.com/platform/bionic/+/master/libc/malloc_debug/README.md
作者:秦靜超(非台)
來源:微信公衆号:阿裡巴巴終端技術
出處:https://mp.weixin.qq.com/s/UA44lfu_Twn6smP9jfngGA