對應用程序來講,動態內存的分配和回收,是既核心又複雜的一的一個邏輯功能模塊。管理內存的過程當中,也很容易發生各類各樣的「事故」,linux
好比,沒正確回收分配後的內存,致使了泄漏。訪問的是已分配內存邊界外的地址,致使程序異常退出,等等。docker
你在程序中定義了一個局部變量,好比一個整數數組 int data[64] ,就定義了一個能夠存儲 64 個整數的內存段。因爲這是一個局部變量,它會從內它會從內存空間的棧中分配內存編程
一、棧內存由系統自動分配和管理。一旦程序運行超出了這個局部變量的做用域,棧內存就會被系統自動回收,因此不會產生內存泄漏的問題。ubuntu
二、 堆內存由應用程序本身來分配和管理。 除非程序退出,這些堆內存並不會被系統自動釋放,而是須要應用程序明確調用庫函數 free() 來釋放它們。若是應用程序沒有正確釋放堆內存,數組
就會形成內存泄漏。緩存
這是兩個棧和堆的例子,那麼,其餘內存段是否也會致使內存泄漏呢?通過咱們前面的學習,這個問題並不難回答bash
只讀段:包括程序的代碼和常量,因爲是隻讀的,不會再去分配新的的內存,因此也不會產生內存泄漏。多線程
數據段:包括全局變量和靜態變量,這些變量在定義時就已經肯定了大小,因此也不會產生內存泄漏。app
最後一個內存映射段:包括動態連接庫和共享內存,其中共享內存由程序動態分配和管理。因此,若是程序在分配後忘了回收,就會致使跟堆內存相似的泄漏問題。ionic
內存泄漏的危害很是大,這些忘記釋放的內存,不只應用程序本身不能訪問,系統也不能把他們再次分配給其餘應用,內存泄露不斷累積,甚至耗盡系統內存
雖然,系統最終能夠經過 OOM (Out of Memory)機制殺死進程,但進程在 OOM 前,可能已經引起了一連串的反應,致使嚴重的性能問題
好比,其餘須要內存的進程,可能沒法分配新的內存;內存不足,又會觸發系統的緩存回收以及SWAP 機制,從而進一步致使 I/O 的性能問題等等。
內存泄漏的危害這麼大,那咱們應該怎麼檢測這種問題呢?特別是,若是你已經發現了內存泄漏,該如何定位和處理呢。
接下來,咱們就用一個計算斐波那契數列的案例,
斐波那契數列是一個這樣的數列:0、一、一、二、三、五、8…,也就是除了前兩個數是 0 和1,其餘數都由前面兩數相加獲得,用數學公式來表示就是 F(n)=F(n-1)+F(n-2),(n>=2),F(0)=0, F(1)=1
今天的案例基於 Ubuntu 18.04,固然,一樣適用其餘的 Linux 系統。
機器配置:2 CPU,8GB 內存
預先安裝 sysstat、Docker 以及 bcc 軟件包,好比:
# install sysstat docker sudo apt-get install -y sysstat docker.io # Install bcc sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD echo "deb https://repo.iovisor.org/apt/bionic bionic main" | sudo tee /etc/apt/sources.list.d/iovisor.list sudo apt-get update sudo apt-get install -y bcc-tools libbcc-examples linux-headers-$(uname -r)
其中,sysstat 和 Docker 咱們已經很熟悉了。sysstat 軟件包中的 vmstat ,能夠觀察內存的變化狀況;而 Docker 能夠運行案例程序。
bcc 軟件包前面也介紹過,它提供了一系列的 Linux 性能分析工具,經常使用來動態追蹤進程和內核的行爲。更多工做原理你先不用深究,後面學習咱們會逐步接觸。這裏你只須要記
住,按照上面步驟安裝完後,它提供的全部工具都位於 /usr/share/bcc/tools 這個目錄中。
注意:bcc-tools 須要內核版本爲 4.1 或者更高,若是你使用的是CentOS7,或者其餘內核版本比較舊的系統,那麼你須要手動升級內核版本後再安裝。
root@luoahong ~]# docker run --name=app -itd feisky/app:mem-leak Unable to find image 'feisky/app:mem-leak' locally mem-leak: Pulling from feisky/app 473ede7ed136: Pull complete c46b5fa4d940: Pull complete 93ae3df89c92: Pull complete 6b1eed27cade: Pull complete 22dd80cda054: Pull complete f7c1129fca8d: Pull complete Digest: sha256:a6806d6b0f33aedc31a6e6c9bd77fe80a086b5c97869a25859e4894dec7b8d4b Status: Downloaded newer image for feisky/app:mem-leak b316f0fb07945bd283ffa2d4768440515ffbb01f607a8051a31fce3f9c0fb297
案例成功運行後,你須要輸入下面的命令,確認案例應用已經正常啓動。若是一切正常,你應該能夠看到下面這個界面:
[root@luoahong ~]# docker logs app 2th => 1 3th => 2 4th => 3 5th => 5 ... ... 38th => 39088169 39th => 63245986 40th => 102334155
從輸出中,咱們能夠發現,這個案例會輸出斐波那契數列的一系列數值。實際上,這些數值每隔 1 秒輸出一次。
知道了這些,咱們應該怎麼檢查內存狀況,判斷有沒有泄漏發生呢?你首先想到的多是top 工具,不過,top 雖然能觀察系統和進程的內存佔用狀況,但今天的案例並不適合。
內存泄漏問題,咱們更應該關注內存使用的變化趨勢。
因此,開頭我也提到了,今天推薦的是另外一個老熟人, vmstat 工具。運行下面的 vmstat ,等待一段時間,觀察內存的變化狀況。若是忘了 vmstat 裏各指標的含義,記得複習前面內容,或者執行 man vmstat 查詢。
[root@luoahong ~]# vmstat 3 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 2 0 0 7177068 2072 576796 0 0 466 187 257 365 1 3 90 5 0 0 0 0 7177068 2072 576796 0 0 0 0 132 231 0 0 100 0 0 0 0 0 7177068 2072 576796 0 0 0 0 138 234 0 0 100 0 0 0 0 0 7176908 2072 576796 0 0 0 0 128 228 0 0 100 0 0 0 0 0 7176908 2072 576800 0 0 0 0 129 225 0 0 100 0 0 0 0 0 7176908 2072 576800 0 0 0 0 136 241 0 0 100 0 0 1 0 0 7176968 2072 576800 0 0 0 6 166 257 0 1 99 0 0 0 0 0 7176968 2072 576800 0 0 0 0 137 246 0 0 100 0 0 0 0 0 7176968 2072 576800 0 0 0 0 132 237 0 0 100 0 0 procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu----- r b swpd free buff cache si so bi bo in cs us sy id wa st 0 0 0 7176744 2072 576800 0 0 0 0 138 236 0 1 99 0 0 0 0 0 7176744 2072 576800 0 0 0 0 129 233 0 0 100 0 0 0 0 0 7176744 2072 576800 0 0 0 0 127 240 0 0 100 0 0 0 0 0 7176744 2072 576800 0 0 0 0 122 229 0 0 100 0 0 0 0 0 7176680 2072 576800 0 0 0 0 131 241 0 0 100 0 0 0 0 0 7176680 2072 576800 0 0 0 0 131 233 0 0 100 0 0 0 0 0 7176680 2072 576800 0 0 0 0 145 244 1 0 99 0 0
從輸出中你能夠看到,內存的 free 列在不停的變化,而且是降低趨勢;而 buffer 和cache 基本保持不變。
未使用內存在逐漸減少,而 buffer 和 cache 基本不變,這說明,系統中使用的內存一直在升高。但這並不能說明有內存泄漏,由於應用程序運行中須要的內存也可能會增大。好比說,程序中若是用了一個動態增加的數組來緩存計算結果,佔用內存天然會增加。
那怎麼肯定是否是內存泄漏呢?或者換句話說,有沒有簡單方法找出讓內存增加的進程,並定位增加內存用在哪兒呢?
根據前面內容,你應該想到了用 top 或 ps 來觀察進程的內存使用狀況,而後找出內存使用一直增加的進程,最後再經過 pmap 查看進程的內存分佈。
但這種方法並不太好用,由於要判斷內存的變化狀況,還須要你寫一個腳本,來處理 top或者 ps 的輸出。
這裏,我介紹一個專門用來檢測內存泄漏的工具,memleak。memleak 能夠跟蹤系統或指定進程的內存分配、釋放請求,而後按期輸出一個未釋放內存和相應調用棧的彙總狀況(默認 5 秒)。
固然,memleak 是 bcc 軟件包中的一個工具,咱們一開始就裝好了,執行/usr/share/bcc/tools/memleak 就能夠運行它。好比,咱們運行下面的命令:
# -a 表示顯示每一個內存分配請求的大小以及地址 # -p 指定案例應用的 PID 號 [root@luoahong ~]# /usr/share/bcc/tools/memleak -a -p $(pidof app) WARNING: Couldn't find .text section in /app WARNING: BCC can't handle sym look ups for /app addr = 7f8f704732b0 size = 8192 addr = 7f8f704772d0 size = 8192 addr = 7f8f704712a0 size = 8192 addr = 7f8f704752c0 size = 8192 32768 bytes in 4 allocations from stack [unknown] [app] [unknown] [app] start_thread+0xdb [libpthread-2.27.so]
從 memleak 的輸出能夠看到,案例應用在不停地分配內存,而且這些分配的地址沒有被回收。
Couldn’t find .text section in /app,因此調用棧不能正常輸出,最後的調用棧部分只能看到 [unknown] 的標誌。
爲何會有這個錯誤呢?實際上,這是因爲案例應用運行在容器中致使的。memleak 工具運行在容器以外,並不能直接訪問進程路徑 /app。
比方說,在終端中直接運行 ls 命令,你會發現,這個路徑的確不存在:
ls /app ls: cannot access '/app': No such file or directory
相似的問題,我在 CPU 模塊中的 perf 使用方法中已經提到好幾個解決思路。最簡單的方法,就是在容器外部構建相同路徑的文件以及依賴庫。這個案例只有一個二進制文件,
因此只要把案例應用的二進制文件放到 /app 路徑中,就能夠修復這個問題。
好比,你能夠運行下面的命令,把 app 二進制文件從容器中複製出來,而後從新運行memleak 工具:
docker cp app:/app /app [root@luoahong ~]# /usr/share/bcc/tools/memleak -p $(pidof app) -a Attaching to pid 12512, Ctrl+C to quit. [03:00:41] Top 10 stacks with outstanding allocations: addr = 7f8f70863220 size = 8192 addr = 7f8f70861210 size = 8192 addr = 7f8f7085b1e0 size = 8192 addr = 7f8f7085f200 size = 8192 addr = 7f8f7085d1f0 size = 8192 40960 bytes in 5 allocations from stack fibonacci+0x1f [app] child+0x4f [app] start_thread+0xdb [libpthread-2.27.so]
這一次,咱們終於看到了內存分配的調用棧,原來是 fibonacci() 函數分配的內存沒釋放。
定位了內存泄漏的來源,下一步天然就應該查看源碼,想辦法修復它。咱們一塊兒來看案例應用的源代碼 app.c:
[root@luoahong ~]# docker exec app cat /app.c #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> long long *fibonacci(long long *n0, long long *n1) { long long *v = (long long *) calloc(1024, sizeof(long long)); *v = *n0 + *n1; return v; } void *child(void *arg) { long long n0 = 0; long long n1 = 1; long long *v = NULL; for (int n = 2; n > 0; n++) { v = fibonacci(&n0, &n1); n0 = n1; n1 = *v; printf("%dth => %lld\n", n, *v); sleep(1); } } int main(void) { pthread_t tid; pthread_create(&tid, NULL, child, NULL); pthread_join(tid, NULL); printf("main thread exit\n"); return 0;
你會發現, child() 調用了 fibonacci() 函數,但並無釋放 fibonacci() 返回的內存。因此,想要修復泄漏問題,在 child() 中加一個釋放函數就能夠了,好比:
void *child(void *arg) { ... for (int n = 2; n > 0; n++) { v = fibonacci(&n0, &n1); n0 = n1; n1 = *v; printf("%dth => %lld\n", n, *v); free(v); // 釋放內存 sleep(1); } }
我把修復後的代碼放到了 app-fix.c,也打包成了一個 Docker 鏡像。你能夠運行下面的命令,驗證一下內存泄漏是否修復:
# 清理原來的案例應用 [root@luoahong ~]# docker rm -f app app # 運行修復後的應用 [root@luoahong ~]# docker run --name=app -itd feisky/app:mem-leak-fix Unable to find image 'feisky/app:mem-leak-fix' locally mem-leak-fix: Pulling from feisky/app 473ede7ed136: Already exists c46b5fa4d940: Already exists 93ae3df89c92: Already exists 6b1eed27cade: Already exists 4d87f2538251: Pull complete f7c1129fca8d: Pull complete Digest: sha256:61d1ce0944188fcddb0ee78a2db60365133009b23612f8048b79c0bbc85f7012 Status: Downloaded newer image for feisky/app:mem-leak-fix c2071e234b83d078716d5d5aa664047a8afd2abf67d10ecc89f77db3d173fb16 # 從新執行 memleak 工具檢查內存泄漏狀況 [root@luoahong ~]#/usr/share/bcc/tools/memleak -a -p $(pidof app) Attaching to pid 18808, Ctrl+C to quit. [10:23:18] Top 10 stacks with outstanding allocations: [10:23:23] Top 10 stacks with outstanding allocations:
如今,咱們看到,案例應用已經沒有遺留內存,證實咱們的修復工做成功完成。
應用程序能夠訪問的用戶內存空間,由只讀段、數據段、堆、棧以及文件映射段等組成。其中,堆內存和內存映射,須要應用程序來動態管理內存段,因此咱們必須當心處理。不
僅要會用標準庫函數 malloc() 來動態分配內存,還要記得在用完內存後,調用庫函數_free() 來 _ 釋放它們。
今天的案例比較簡單,只用加一個 free() 調用就能修復內存泄漏。不過,實際應用程序就複雜多了。好比說,
malloc() 和 free() 一般並非成對出現,而是須要你,在每一個異常處理路徑和成功路徑上都釋放內存 。
在多線程程序中,一個線程中分配的內存,可能會在另外一個線程中訪問和釋放。更復雜的是,在第三方的庫函數中,隱式分配的內存可能須要應用程序顯式釋放。
因此,爲了不內存泄漏,最重要的一點就是養成良好的編程習慣,好比分配內存後,必定要先寫好內存釋放的代碼,再去開發其餘邏輯。仍是那句話,有借有還,才能高效運
轉,再借不難。
固然,若是已經完成了開發任務,你還能夠用 memleak 工具,檢查應用程序的運行中,內存是否泄漏。若是發現了內存泄漏狀況,再根據 memleak 輸出的應用程序調用棧,定位內存的分配位置,從而釋放再也不訪問的內存。