什么是dumpsys meminfo
Android中通過命令dumpsys meminfo package_name|pid, 查看指定進程的內存使用情況.通過輸出的信息,可以看出來應用在內存哪里分配出現了問題,比如native heap 分配高,就要查一下自己的native部分的代碼哪里分配后沒有及時釋放。
這條命令是如何工作的?
Android中的Service都會注冊到ServiceManager,每個Service要實現binder定義的接口,其中的dump方法dump(int fd, const Vector<String16>& args),通過給定的參數,把信息送到文件描述符fd中。
dumpsys位於/system/bin/dumpsys.運行時它首先后獲得ServiceManager,然后根據參數查找服務,找到后,然后binder調用服務的dump方法.而meminfo是系統的一個服務(內部則是一個MemBinder的類實現的),當dumpsys找到這個服務后,調用dump方法,通過傳遞的參數,顯示出進程內存使用情況。
下面是一個進程的meminfo。
上面圖中的數據分A和B兩個部分, A 部分是進程內存的詳細信息, B 部分是對詳細信息進行的總結。
而A部分的數據是從哪里讀到的?
通過以下三個方式,圖中A.1部分的信息是通過解析/proc/pid/smaps獲得的, A.2 通過IPC (binder 和pipe)方式獲得。而A.3通過memtrack.vendor_xxx.so中的接口獲得的。
下面將從A, B 兩個部分,詳細介紹這些數值是如何生成的。
A部分
A.1 smaps
下面是meminfo讀取進程的smaps從上到下的調用關系圖。
讀取進程smaps的節點
根據屬性的不同,進程的虛擬地址空間被划分成若干個vma,每一個vma通過vm_next和vm_prev組成雙向鏈表,鏈表頭位於進程的task->mm->mmap.當通過proc接口讀取進程的smaps文件時,內核會首先找到該進程的vma鏈表頭,遍歷鏈表中的每一個vma, 通過walk_page_vma統計這塊vma的使用情況,最后顯示出來。
下圖是一塊vma的統計信息
解析smaps用到了以下幾個字段.
-
Pss
PSS (Proportional Set Size) = 進程獨占的內存 + 進程程共享的內存 / 映射次數。
內存的管理是以 page 為單位的, 如果 page 的 _refcount或者 _mapcount為 1, 那么就是進程獨占的內存. 也叫 private. 如果 page 的 _mapcount 為 n ( n >= 2), 這塊內存以 page / n 來統計。 -
Private_Dirty
Private 在上面已經說過了。 而 Dirty 分為 PageDirty和 pte_dirty. PageDirty就是所說的臟頁( 文件讀到內存中被修改過, 就會標記為臟頁)。 pte_dirty則當 vma 用於 anonymous 的時候, 讀寫這段 vma 時候, 觸發 page fault, 調用 do_anonymous_page , 如果vma_flags中包含 VM_WRITE, 則會通過 pte_mkdirty(entry)標記。 -
Private_Clean
與 Private_Dirty 相反。 -
Swap
一般情況下, 在 Android 中就是 zram, 通過壓縮內存頁面並將其放入動態分配的內存交換區來增加系統中的可用內存量, 壓縮的都是匿名頁。
但下面這段vma是文件映射的,但還有swap字段的,這是因為這個文件是通過mmap到進程地址空間的。當標記中有MAP_PRIVATE時,這表示是一個copy-on-write的映射,雖然是file-backed , 但當向這個數據寫入數據的時候,會把數據拷貝到匿名頁里,所以看到上面的Anonymous:也不為0。
A.1信息的分類則通過vma的名字進行字符串匹配,然后將內存信息划分為不同的部分
1. vma用於file時
當vma->vm_file不為空的時候,名字為file->f_path文件路徑,文件包含存儲介質的文件,也包含設備文件,虛擬文件等(everything is a file)。
下面是meminfo的字段對照表
類型 |
匹配字符串 |
Ashmem |
/dev/ashmem |
Gfx dev |
/dev/kgsl-3d0 |
Other dev |
/dev/* |
.so mmap |
*.so 例 /system/lib64/libstdc++.so |
.jar mmap |
*.jar 例 /system/framework/framework.jar |
.apk mmap |
*.apk 例 /system/framework/framework-res.apk |
.ttf mmap |
*.ttf 例 /system/fonts/Roboto-BlackItalic.ttf |
.dex mmap |
*.vdex 例 /system/framework/boot.vdex |
.oat mmap |
*.oat 例 /system/framework/arm64/boot-ext.oat |
.art mmap |
*.art 例 /system/framework/arm64/boot.art |
Other mmap |
不屬於上面的類型 |
例如在解析vma過程中,如果vma的名字包含/dev/kgsl-3d0的時,就把它歸在Gfx Dev 這類。
2. vma 用於anonymous時
這種用途的vma的名字[anon:開頭,雖然叫匿名,但顯示哪里分配的,比如說上圖的[anon:libc_malloc],就說明這段vma是從libc_malloc中分配出去的。
老的內核版本是沒有這個域的,后來因為userspace的進程有很多種不同的分配器(allocators) , 如果出現異常不知道哪里出了問題,就比如說如何區分Dalvik Heap 和Native Heap, 后來android提了一筆可以修改內存區域的名字的補丁,通過prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, start, len, (unsigned long)name)系統調用,就可以把名字保存在vma_area_struct ->shared.anon_name有趣的這是個userspace指針,來降低mm子系統的復雜性.同時復用了vma_area_struct結構體的shared(利用union),shared用在file-backed mappings,這樣就不和用於匿名的vma沖突.連內存都省了(這可能就是程序的魅力所在)。
下面是meminfo的字段對照表
類型 |
匹配字符串 |
Native Heap |
[anon:libc_malloc] 或 [heap] |
Dalvik Heap |
[anon:dalvik-* |
Dalvik Other |
Dalvik Heap 的部分 比如 [anon:dalvik-indirect ref 開頭的) |
A.2進程中讀取
下面是從meminfo讀取進程運行狀態的信息調用圖.這部分信息是通過IPC方式獲得。
Meminfo打開一個pipe,通過異步binder調用並傳遞了pipe fd, 然后開始監聽fd,應用進程獲得了這個pipe fd后 讀取需要的內存數據,通過pipe傳遞到AMS中。
-
mallinfo()
返回內存分配的統計信息, 函數聲明在 android/bionic/libc/include/malloc.h,函數定義在 android/bionic/libc/bionic/malloc_common.h.
#if defined(USE_SCUDO) #include "scudo.h" #else #include "jemalloc.h" #endif |
函數調用Malloc(mallinfo),這是對mallinfo進行封裝,來加載不同的分配器對應函數的實現.因為Android使用的是jemalloc,也就會調用je_mallinfo()。
malloc結構體定義,返回struct mallinfo。
下面是meminfo和mallinfo字段對照表
Native Heap Size |
mallinfo.usmblks |
Maximum total allocated space |
Native Heap Alloc |
mallinfo.uordblks |
Total allocated space |
Native Heap Free |
mallinfo.fordblks |
Total free space |
-
runtime
Runtime runtime = Runtime.getRuntime(); // Do GC since countInstancesOfClass counts unreachable objects. long dalvikMax = runtime.totalMemory() / 1024; |
通過上面獲得Java虛擬機的內存使用情況
下面是meminfo和runtime的字段對照表
Dalvik Heap Free |
runtime.freeMemory() |
Dalvik Heap Size |
runtime.totalMemory() |
Dalvik Heap Alloc |
Dalvik Heap Size - Dalvik Heap Free |
A.3 memtrack.vendor_xxx.so
下面是meminfo讀取進程在GPU上分配內存的從上到下調用關系圖。
讀取進程在GPU上分配的內存
每個廠商統計的策略可能不一樣,這也是出現HAL層的原因. 下面以高通SDM710為例。從代碼中可以看到也是讀內核中GPU文件節點,然后解析數據的。
B部分
-
Java Heap
// dalvik private_dirty |
dalvik private dirty包含任何寫過zygote分配的頁面(應用是從zygote fork 出來的),和應用本身分配的。
art mmap是應用的bootimage,任何private頁面也算在應用上。
-
Native Heap
nativePrivateDirty; // libc_malloc |
通過libc_malloc庫分配的大小
-
Code
// so mmap private_dirty + private_clean |
所有私有靜態資源求和。
-
Stack
// stack private_dirty |
進程本身棧占用的大小。
-
Graphic
//Gfx Dev private_dirty + private_clean |
進程在GPU上分配的內存。
-
System
Pss Total 的和- Private Dirty 和Private Clean 的和
系統占用的內存,例如一些共享的字體,圖像資源等。
應用
當我們查看應用的內存消耗情況時,可以通過meminfo提供的信息,判斷應用中哪個模塊使用的內存出現問題。
下面是同一個應用A和B版本的meminfo:
A版本
B版本
對比A B 版本,先看App Summary, 可以明顯發現Graphic占用高,而Graphic是由Gfx, EGL 和GL組成,從兩圖對比看主要是EGL部分, EGL 是進程在GPU上分配的內存,查看平台對應的GPU內存調試的接口
通過對比發現B版本EGL部分主要是發生在使用egl_image上,查看代碼發現egl_image是從hardware bitmap 分配的,這是Android O 上的一個bitmap的新特性。B 版本使用后,沒有及時釋放所致。
參考文獻:
[1]https://developer.android.com/studio/command-line/dumpsys
[2]http://man7.org/linux/man-pages/man5/proc.5.html
[3]https://bumptech.github.io/glide/doc/hardwarebitmaps.html

“內核工匠”微信公眾號
Linux 內核黑科技 | 技術文章 | 精選教程