定位內存泄漏基本上是從宏觀到微觀,進而定位到代碼位置。php
從/proc/meminfo能夠看到整個系統內存消耗狀況,使用top能夠看到每一個進程的VIRT(虛擬內存)和RES(實際佔用內存),基本上就能夠將泄漏內存定位到進程範圍。html
以前也大概瞭解過/proc/self/maps,基於裏面信息能大概判斷泄露的內存的屬性,是哪一個區域在泄漏、對應哪一個文件。輔助工具procmem輸出更可讀的maps信息。node
下面分別從進程地址空間各段劃分、maps和段如何對應、各段異常如何定位三方面展開。linux
首先經過下圖簡單看一下,進程地址空間從低地址開始依次是代碼段(Text)、數據段(Data)、BSS段、堆、內存映射段(mmap)、棧。程序員
代碼段也稱正文段或文本段,一般用於存放程序執行代碼(即CPU執行的機器指令)。通常C語言執行語句都編譯成機器代碼保存在代碼段。一般代碼段是可共享的,所以頻繁執行的程序只須要在內存中擁有一份拷貝便可。數組
代碼段一般屬於只讀,以防止其餘程序意外地修改其指令(對該段的寫操做將致使段錯誤)。某些架構也容許代碼段爲可寫,即容許修改程序。緩存
代碼段指令根據程序設計流程依次執行,對於順序指令,只會執行一次(每一個進程);如有反覆,則需使用跳轉指令;若進行遞歸,則須要藉助棧來實現。數據結構
代碼段指令中包括操做碼和操做對象(或對象地址引用)。若操做對象是當即數(具體數值),將直接包含在代碼中;如果局部數據,將在棧區分配空間,而後引用該數據地址;若位於BSS段和數據段,一樣引用該數據地址。架構
代碼段最容易受優化措施影響。app
數據段一般用於存放程序中已初始化且初值不爲0的全局變量和靜態局部變量。數據段屬於靜態內存分配(靜態存儲區),可讀可寫。
數據段保存在目標文件中(在嵌入式系統裏通常固化在鏡像文件中),其內容由程序初始化。例如,對於全局變量int gVar = 10,必須在目標文件數據段中保存10這個數據,而後在程序加載時複製到相應的內存。
數據段與BSS段的區別以下:
1) BSS段不佔用物理文件尺寸,但佔用內存空間;數據段佔用物理文件,也佔用內存空間。
對於大型數組如int ar0[10000] = {1, 2, 3, ...}和int ar1[10000],ar1放在BSS段,只記錄共有10000*4個字節須要初始化爲0,而不是像ar0那樣記錄每一個數據一、二、3...,此時BSS爲目標文件所節省的磁盤空間至關可觀。
2) 當程序讀取數據段的數據時,系統會出發缺頁故障,從而分配相應的物理內存;當程序讀取BSS段的數據時,內核會將其轉到一個全零頁面,不會發生缺頁故障,也不會爲其分配相應的物理內存。
運行時數據段和BSS段的整個區段一般稱爲數據區。某些資料中「數據段」指代數據段 + BSS段 + 堆。
BSS(Block Started by Symbol)段中一般存放程序中如下符號:
C語言中,未顯式初始化的靜態分配變量被初始化爲0(算術類型)或空指針(指針類型)。因爲程序加載時,BSS會被操做系統清零,因此未賦初值或初值爲0的全局變量都在BSS中。BSS段僅爲未初始化的靜態分配變量預留位置,在目標文件中並不佔據空間,這樣可減小目標文件體積。但程序運行時需爲變量分配內存空間,故目標文件必須記錄全部未初始化的靜態分配變量大小總和(經過start_bss和end_bss地址寫入機器代碼)。當加載器(loader)加載程序時,將爲BSS段分配的內存初始化爲0。在嵌入式軟件中,進入main()函數以前BSS段被C運行時系統映射到初始化爲全零的內存(效率較高)。
注意,儘管均放置於BSS段,但初值爲0的全局變量是強符號,而未初始化的全局變量是弱符號。若其餘地方已定義同名的強符號(初值可能非0),則弱符號與之連接時不會引發重定義錯誤,但運行時的初值可能並不是指望值(會被強符號覆蓋)。所以,定義全局變量時,若只有本文件使用,則儘可能使用static關鍵字修飾;不然須要爲全局變量定義賦初值(哪怕0值),保證該變量爲強符號,以便連接時發現變量名衝突,而不是被未知值覆蓋。
某些編譯器將未初始化的全局變量保存在common段,連接時再將其放入BSS段。在編譯階段可經過-fno-common選項來禁止將未初始化的全局變量放入common段。
堆用於存放進程運行時動態分配的內存段,可動態擴張或縮減。堆中內容是匿名的,不能按名字直接訪問,只能經過指針間接訪問。當進程調用malloc(C)/new(C++)等函數分配內存時,新分配的內存動態添加到堆上(擴張);當調用free(C)/delete(C++)等函數釋放內存時,被釋放的內存從堆中剔除(縮減) 。
分配的堆內存是通過字節對齊的空間,以適合原子操做。堆管理器經過鏈表管理每一個申請的內存,因爲堆申請和釋放是無序的,最終會產生內存碎片。堆內存通常由應用程序分配釋放,回收的內存可供從新使用。若程序員不釋放,程序結束時操做系統可能會自動回收。
堆的末端由break指針標識,當堆管理器須要更多內存時,可經過系統調用brk()和sbrk()來移動break指針以擴張堆,通常由系統自動調用。
使用堆時常常出現兩種問題:1) 釋放或改寫仍在使用的內存(「內存破壞」);2)未釋放再也不使用的內存(「內存泄漏」)。當釋放次數少於申請次數時,可能已形成內存泄漏。泄漏的內存每每比忘記釋放的數據結構更大,由於所分配的內存一般會圓整爲下個大於申請數量的2的冪次(如申請212B,會圓整爲256B)。
此處,內核將硬盤文件的內容直接映射到內存, 任何應用程序均可經過Linux的mmap()系統調用請求這種映射。內存映射是一種方便高效的文件I/O方式, 於是被用於裝載動態共享庫。用戶也可建立匿名內存映射,該映射沒有對應的文件, 可用於存放程序數據。在 Linux中,若經過malloc()請求一大塊內存,C運行庫將建立一個匿名內存映射,而不使用堆內存。」大塊」 意味着比閾值 MMAP_THRESHOLD還大,缺省爲128KB,可經過mallopt()調整。
該區域用於映射可執行文件用到的動態連接庫。在Linux 2.4版本中,若可執行文件依賴共享庫,則系統會爲這些動態庫在從0x40000000開始的地址分配相應空間,並在程序裝載時將其載入到該空間。在Linux 2.6內核中,共享庫的起始地址被往上移動至更靠近棧區的位置。
從進程地址空間的佈局能夠看到,在有共享庫的狀況下,留給堆的可用空間還有兩處:一處是從.bss段到0x40000000,約不到1GB的空間;另外一處是從共享庫到棧之間的空間,約不到2GB。這兩塊空間大小取決於棧、共享庫的大小和數量。這樣來看,是否應用程序可申請的最大堆空間只有2GB?事實上,這與Linux內核版本有關。在上面給出的進程地址空間經典佈局圖中,共享庫的裝載地址爲0x40000000,這其實是Linux kernel 2.6版本以前的狀況了,在2.6版本里,共享庫的裝載地址已經被挪到靠近棧的位置,即位於0xBFxxxxxx附近,所以,此時的堆範圍就不會被共享庫分割成2個「碎片」,故kernel 2.6的32位Linux系統中,malloc申請的最大內存理論值在2.9GB左右。
棧又稱堆棧,由編譯器自動分配釋放,行爲相似數據結構中的棧(先進後出)。堆棧主要有三個用途:
持續地重用棧空間有助於使活躍的棧內存保持在CPU緩存中,從而加速訪問。進程中的每一個線程都有屬於本身的棧。向棧中不斷壓入數據時,若超出其容量就會耗盡棧對應的內存區域,從而觸發一個頁錯誤。此時若棧的大小低於堆棧最大值RLIMIT_STACK(一般是8M),則棧會動態增加,程序繼續運行。映射的棧區擴展到所需大小後,再也不收縮。
Linux中ulimit -s命令可查看和設置堆棧最大值,當程序使用的堆棧超過該值時, 發生棧溢出(Stack Overflow),程序收到一個段錯誤(Segmentation Fault)。注意,調高堆棧容量可能會增長內存開銷和啓動時間。
堆棧既可向下增加(向內存低地址)也可向上增加, 這依賴於具體的實現。本文所述堆棧向下增加。
棧的大小在運行時由內核動態調整。
①管理方式:棧由編譯器自動管理;堆由程序員控制,使用方便,但易產生內存泄露。
②生長方向:棧向低地址擴展(即」向下生長」),是連續的內存區域;堆向高地址擴展(即」向上生長」),是不連續的內存區域。這是因爲系統用鏈表來存儲空閒內存地址,天然不連續,而鏈表從低地址向高地址遍歷。
③空間大小:棧頂地址和棧的最大容量由系統預先規定(一般默認2M或10M);堆的大小則受限於計算機系統中有效的虛擬內存,32位Linux系統中堆內存可達2.9G空間。
④存儲內容:棧在函數調用時,首先壓入主調函數中下條指令(函數調用語句的下條可執行語句)的地址,而後是函數實參,而後是被調函數的局部變量。本次調用結束後,局部變量先出棧,而後是參數,最後棧頂指針指向最開始存的指令地址,程序由該點繼續運行下條可執行語句。堆一般在頭部用一個字節存放其大小,堆用於存儲生存期與函數調用無關的數據,具體內容由程序員安排。
⑤分配方式:棧可靜態分配或動態分配。靜態分配由編譯器完成,如局部變量的分配。動態分配由alloca函數在棧上申請空間,用完後自動釋放。堆只能動態分配且手工釋放。
⑥分配效率:棧由計算機底層提供支持:分配專門的寄存器存放棧地址,壓棧出棧由專門的指令執行,所以效率較高。堆由函數庫提供,機制複雜,效率比棧低得多。Windows系統中VirtualAlloc可直接在進程地址空間中分配一塊內存,快速且靈活。
⑦分配後系統響應:只要棧剩餘空間大於所申請空間,系統將爲程序提供內存,不然報告異常提示棧溢出。
操做系統爲堆維護一個記錄空閒內存地址的鏈表。當系統收到程序的內存分配申請時,會遍歷該鏈表尋找第一個空間大於所申請空間的堆結點,而後將該結點從空閒結點鏈表中刪除,並將該結點空間分配給程序。若無足夠大小的空間(可能因爲內存碎片太多),有可能調用系統功能去增長程序數據段的內存空間,以便有機會分到足夠大小的內存,而後進行返回。,大多數系統會在該內存空間首地址處記錄本次分配的內存大小,供後續的釋放函數(如free/delete)正確釋放本內存空間。
此外,因爲找到的堆結點大小不必定正好等於申請的大小,系統會自動將多餘的部分從新放入空閒鏈表中。
⑧碎片問題:棧不會存在碎片問題,由於棧是先進後出的隊列,內存塊彈出棧以前,在其上面的後進的棧內容已彈出。而頻繁申請釋放操做會形成堆內存空間的不連續,從而形成大量碎片,使程序效率下降。
可見,堆容易形成內存碎片;因爲沒有專門的系統支持,效率很低;因爲可能引起用戶態和內核態切換,內存申請的代價更爲昂貴。因此棧在程序中應用最普遍,函數調用也利用棧來完成,調用過程當中的參數、返回地址、棧基指針和局部變量等都採用棧的方式存放。因此,建議儘可能使用棧,僅在分配大量或大塊內存空間時使用堆。
使用棧和堆時應避免越界發生,不然可能程序崩潰或破壞程序堆、棧結構,產生意想不到的後果。
struct mm_struct是進程內存結構體,裏面的參數和各段地址對應關係以下圖。
struct mm_struct { struct vm_area_struct *mmap; /* list of VMAs */ ... unsigned long mmap_base; /* base of mmap area */ unsigned long mmap_legacy_base; /* base of mmap area in bottom-up allocations */... unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; ... struct mm_rss_stat rss_stat; ... };
mm_strutc數據結構和段對應關係以下:
在瞭解了段及其做用以後,再來看看maps中各個vma對應哪一個段?
static void show_map_vma(struct seq_file *m, struct vm_area_struct *vma, int is_pid) { struct mm_struct *mm = vma->vm_mm; struct file *file = vma->vm_file; struct proc_maps_private *priv = m->private; vm_flags_t flags = vma->vm_flags; unsigned long ino = 0; unsigned long long pgoff = 0; unsigned long start, end; dev_t dev = 0; const char *name = NULL; if (file) { struct inode *inode = file_inode(vma->vm_file); dev = inode->i_sb->s_dev; ino = inode->i_ino; pgoff = ((loff_t)vma->vm_pgoff) << PAGE_SHIFT;------------------------------此vma第一頁在地址空間中是第幾頁。 } /* We don't show the stack guard page in /proc/maps */ start = vma->vm_start; end = vma->vm_end; seq_setwidth(m, 25 + sizeof(void *) * 6 - 1); seq_printf(m, "%08lx-%08lx %c%c%c%c %08llx %02x:%02x %lu ", start, end, flags & VM_READ ? 'r' : '-', flags & VM_WRITE ? 'w' : '-', flags & VM_EXEC ? 'x' : '-', flags & VM_MAYSHARE ? 's' : 'p', pgoff, MAJOR(dev), MINOR(dev), ino);-------------------------------------------首先打印maps裏面前5項數據,起訖地址、屬性、偏移地址、主從設備號、inode編號。 /* * Print the dentry name for named mappings, and a * special [heap] marker for the heap: */ if (file) {---------------------------------------------------------------------若是是個文件,那麼打印文件完整路徑。 seq_pad(m, ' '); seq_file_path(m, file, "\n"); goto done; } if (vma->vm_ops && vma->vm_ops->name) { name = vma->vm_ops->name(vma); if (name) goto done; } name = arch_vma_name(vma); if (!name) { if (!mm) { name = "[vdso]";---------------------------------------------------------vDSO是系統調用相關,詳細信息見vDSO。 goto done; } if (vma->vm_start <= mm->brk && vma->vm_end >= mm->start_brk) {------------------------------------------知足start_brk <= vma <= brk,則其vma是[heap]。 name = "[heap]"; goto done; } if (is_stack(priv, vma))-----------------------------------------------------知足vma包含所在地址空間的start_stack地址,則vma是[stack]。 name = "[stack]"; } done: if (name) { seq_pad(m, ' '); seq_puts(m, name); } seq_putc(m, '\n'); } static int is_stack(struct proc_maps_private *priv, struct vm_area_struct *vma) { return vma->vm_start <= vma->vm_mm->start_stack &&------------------------------判斷一個vma是否屬於stack,只須要判斷start_stack是否在其區域內。 vma->vm_end >= vma->vm_mm->start_stack; }
本實例中的用戶空間地址從0x00000000到0x80000000,從地址空間劃分可知,從低到高依次是:
經過top或者procrank之類工具發現某個進程存在內存泄漏的風險,而後查看進程的maps信息,進而能夠縮小泄漏點範圍。
通常狀況下泄漏點常在堆和文件/匿名映射區域。
對於堆,須要瞭解哪些函數申請的內存在堆中,而後加以監控相關係統調用。
對於文件映射,定位較簡單,能夠經過文件名找到對應代碼。
對於匿名映射,則須要根據大小或者地址範圍猜想用途。固然也能夠經過strace 跟蹤和maps對應找到對應的泄漏點。
00008000-00590000 r-xp 00000000 b3:01 1441836 /root/xxx----------------------------可執行文件的代碼段,下面分別是隻讀和可讀寫的段。00590000-005b2000 r--p 00587000 b3:01 1441836 /root/xxx 005b2000-005c4000 rw-p 005a9000 b3:01 1441836 /root/xxx 005c4000-0280c000 rwxp 00000000 00:00 0 [heap]-------------------------------若是堆在業務穩定後,還繼續單向增長,則可能存在泄漏。 2aaa8000-2aac5000 r-xp 00000000 b3:01 786621 /lib/ld-2.28.9000.so-----------------下面是最複雜的部分,存在各類各類樣的內存使用狀況,大致上有庫映射、匿名內存映射、文件內存映射等。 2aac5000-2aac6000 r--p 0001c000 b3:01 786621 /lib/ld-2.28.9000.so 2aac6000-2aac7000 rw-p 0001d000 b3:01 786621 /lib/ld-2.28.9000.so 2aac7000-2aac8000 r-xp 00000000 00:00 0 [vdso] 2aac8000-2aaca000 rw-p 00000000 00:00 0... 2d9aa000-2d9c8000 r-xp 00000000 b3:01 656126 /usr/lib/libv4lconvert.so.0.0.0 2d9c8000-2d9c9000 ---p 0001e000 b3:01 656126 /usr/lib/libv4lconvert.so.0.0.0 2d9c9000-2d9ca000 r--p 0001e000 b3:01 656126 /usr/lib/libv4lconvert.so.0.0.0 2d9ca000-2d9cb000 rw-p 0001f000 b3:01 656126 /usr/lib/libv4lconvert.so.0.0.0 2d9cb000-2da23000 rw-p 00000000 00:00 0... 3e8aa000-3e90c000 rw-s 00000000 00:06 5243 /dev/mem_cma 3ea00000-3ea42000 rw-p 00000000 00:00 0 3ea42000-3eb00000 ---p 00000000 00:00 07fa4a000-7fa6b000 rwxp 00000000 00:00 0 [stack]--------------------------------棧的大小是可變的,可是不能超過RLIMIT_STACK規定的大小。
堆內存主要由malloc()/calloc()/realloc()/fre()申請釋放,因此若是發生了堆泄漏就須要重點看着幾個函數調用狀況。
malloc()對應的系統調用是brk(),可是當申請超過128KB內存時就會調用mmap()。
關於堆內存管理參考:《Linux堆內存管理深刻分析(上)》、《Linux堆內存管理深刻分析(下)》、《對堆棧中分析的比較好的文章進行的總結》、《Linux內存分配小結--malloc、brk、mmap》、《Linux C 堆內存管理函數malloc()、calloc()、realloc()、free()詳解》。
棧的地址方向是從高到低,範圍由RLIMIT_STACK規定。
能夠經過ulimit -s查看,通常是8MB。
棧相關問題可能是溢出問題。
重點關注mmap相關調用《Linux內存管理 (9)mmap》、《Linux內存管理 (9)mmap(補充)》。
經過/proc/<pid>/maps獲取的數據可讀性不強,經過腳本可使其更容易理解。更容易找出內存消耗在哪裏。
在jupyter-notebook中輸入以下腳本:
import re import pandas as pd import matplotlib.pyplot as plt import sys reload(sys) sys.setdefaultencoding('utf8') maps_filename="maps_aie_thd_165.txt" #maps_filename="maps_init.txt" maps_list=[] maps_file = open(maps_filename, 'rb') maps_columns = ["start", "end", "size(KB)", "filename", 'permission'] maps_process_end='80000000' pre_end=0 for line in maps_file: #00008000-0000b000 r-xp 00000000 b3:01 1023 /root/pidmax #0000b000-0000c000 r--p 00002000 b3:01 1023 /root/pidmax #0000c000-0000d000 rw-p 00003000 b3:01 1023 /root/pidmax maps_line_fmt = '(?P<start>.{8})-(?P<end>.{8}) (?P<permission>.{4}) (?P<size>.{8}) (?P<major>.{2}):(?P<minor>.{2}) (?P<handle>[0-9]*) *(?P<filename>.*)' m = re.match(maps_line_fmt, line) if(not m): continue start = m.group('start') end = m.group('end') permission = m.group('permission') #size = m.group('size') #major = m.group('major') #minor = m.group('minor') #handle = m.group('handle') filename = m.group('filename') start_int = int(start, 16) end_int = int(end, 16) if(pre_end != start_int): maps_list.append([ "{:0>8x}".format(pre_end), start, (start_int - pre_end)/1024, 'NOT USED', 'unknown']) #print start+','+end+','+permission+','+filename #---p r--p rw-p r-xp rwxp rw-s if permission == '---p': permission = 'guard/thread stack' elif (permission == 'r--p'): permission = 'readonly var' elif (permission == 'rw-p'): permission = 'read/write var' elif (permission == 'r-xp'): permission = 'code' elif (permission == 'rwxp'): permission = 'heap/stack' elif (permission == 'rw-s'): permission = 'sharememory' else: permission = 'unkown' maps_list.append([start, end, (end_int - start_int)/1024, filename, permission]) pre_end = end_int maps_file.close() maps_list.append([end, maps_process_end, (int(maps_process_end, 16) - end_int)/1024, 'NOT USED', 'unknown']) maps_pd = pd.DataFrame(columns=maps_columns, data=maps_list) maps_pd.to_csv("maps.csv", encoding='utf-8') print 'Total memory =', maps_pd['size(KB)'].sum()/1024,'(MB)' rectangle_width = 800 maps_height_base = 40 maps_height_diff = 160 maps_size_min = maps_pd['size(KB)'].min() maps_size_max = 16384 rectangle_x = 50 rectangle_y = 50 fig = plt.figure() ax = fig.add_subplot(111) for index, maps in maps_pd.iterrows(): rectangle_height = (float)(min(maps_size_max, maps['size(KB)']) - maps_size_min)*maps_height_diff/maps_size_max + maps_height_base if maps['filename'] == 'NOT USED': color = 'red' text_color = 'white' else: color_value = (int)((float)(min(maps_size_max, maps['size(KB)']) - maps_size_min)/maps_size_max*0xffffff) color = '#%06x'%(color_value) text_color = '#%06x'%(0xffffff - color_value) if maps['size(KB)'] >= 1024: maps_label = "(%.2fMB)%s(%s)"%((float)(maps["size(KB)"])/1024, maps["filename"], maps['permission']) else: maps_label = "(%dKB)%s(%s)"%(maps["size(KB)"], maps["filename"], maps['permission']) #print rectangle_x, rectangle_y, rectangle_width, rectangle_height, maps['size(KB)'], color plt.bar(rectangle_x, rectangle_height, width=rectangle_width, bottom=rectangle_y, align='edge',facecolor=color, linewidth=1, edgecolor='red') plt.text(rectangle_x+10, rectangle_y+rectangle_height/2, maps_label, horizontalalignment='left', verticalalignment='center', color=text_color, fontsize=12) rectangle_y += rectangle_height plt.xlim(0, rectangle_width+100) plt.ylim(0, rectangle_y+100) plt.axis('off') #plt.show() fig.set_size_inches(rectangle_width/100, rectangle_y/100) fig.savefig('maps.svg', dpi=120, bbox_inches='tight', format='svg')
輸出兩份文檔:maps.csv和maps.svg,一個是對每一個vma進行文本統計,一個是將maps圖形化。
1. 查看未使用區域,從大到小排列。
能夠看出進程虛擬地址空間的空閒大小,以及分佈在哪裏。
2. 查看已使用區域,從大到小排列。
能夠看出進程虛擬內存都被誰使用了,是否有異常。
3. maps圖形化
未使用部分用紅色高亮顯示,高度根據大小響應變化。
其餘模塊顏色和大小都根據模塊變化。
參考資料:《Linux虛擬地址空間佈局以及進程棧和線程棧總結》