1. 程式人生 > >Linux內存使用方法詳細解析

Linux內存使用方法詳細解析

滿足 trac 不可 drs color 很遺憾 地址空間 多少 root

我是一名程序員,那麽我在這裏以一個程序員的角度來講解Linux內存的使用。

一提到內存管理,我們頭腦中閃出的兩個概念,就是虛擬內存,與物理內存。這兩個概念主要來自於linux內核的支持。

Linux在內存管理上份為兩級,一級是線性區,類似於00c73000-00c88000,對應於虛擬內存,它實際上不占用實際物理內存;一級是具體的物理頁面,它對應我們機器上的物理內存。

這裏要提到一個很重要的概念,內存的延遲分配。Linux內核在用戶申請內存的時候,只是給它分配了一個線性區(也就是虛存),並沒有分配實際物理內存;只有當用戶使用這塊內存的時候,內核才會分配具體的物理頁面給用戶,這時候才占用寶貴的物理內存內核釋放物理頁面是通過釋放線性區,找到其所對應的物理頁面,將其全部釋放的過程

char *p=malloc(2048) //這裏只是分配了虛擬內存2048,並不占用實際內存。 strcpy(p,”123”) //分配了物理頁面,雖然只是使用了3個字節,但內存還是為它分配了2048字節的物理內存。 free(p) //通過虛擬地址,找到其所對應的物理頁面,釋放物理頁面,釋放線性區。

我們知道用戶的進程和內核是運行在不同的級別,進程與內核之間的通訊是通過系統調用來完成的。進程在申請和釋放內存,主要通過brk,sbrk,mmap,unmmap這幾個系統調用(都是虛擬內存),傳遞的參數主要是對應的虛擬內存。

註意一點,在進程只能訪問虛擬內存,它實際上是看不到內核物理內存的使用,這對於進程是完全透明的

glibc內存管理器

那麽我們每次調用malloc來分配一塊內存,都進行相應的系統調用呢?

答案是否定的,這裏我要引入一個新的概念,glibc的內存管理器。

我們知道malloc和free等函數都是包含在glibc庫裏面的庫函數,我們試想一下,每做一次內存操作,都要調用系統調用的話,那麽程序將多麽的低效。

實際上glibc采用了一種批發和零售的方式來管理內存。glibc每次通過系統調用的方式申請一大塊內存(虛擬內存),當進程申請內存時,glibc就從自己獲得的內存中取出一塊給進程。

glibc對於heap內存申請大於128k的內存申請,glibc采用mmap的方式向內核申請內存,也就是此時的malloc是由mmap來實現的,這不能保證內存地址向上增長;小於128k的則采用brk,malloc調用系統調用brk來實現向內核批發虛擬內存,對於它來講是正確的。128k的閥值,可以通過glibc的庫函數進行設置。

內存管理器面臨的困難

我們在寫程序的時候,每次申請的內存塊大小不規律,而且存在頻繁的申請和釋放,這樣不可避免的就會產生內存碎塊。而內存碎塊,直接會導致大塊內存申請無法滿足,從而更多的占用系統資源;如果進行碎塊整理的話,又會增加cpu的負荷,很多都是互相矛盾的指標,這裏我就不細說了。

我們在寫程序時,涉及內存時,有兩個概念heap和stack。傳統的說法stack的內存地址是向下增長的,heap的內存地址是向上增長的。

函數malloc和free,主要是針對heap進行操作,由程序員自主控制內存的訪問。

在這裏heap的內存地址向上增長,這句話不完全正確。

glibc對於heap內存申請大於128k的內存申請,glibc采用mmap的方式向內核申請內存,這不能保證內存地址向上增長;小於128k的則采用brk,對於它來講是正確的。128k的閥值,可以通過glibc的庫函數進行設置。

這裏我先講大塊內存的申請,也即對應於mmap系統調用。

對於大塊內存申請,glibc直接使用mmap系統調用為其劃分出另一塊虛擬地址,供進程單獨使用;在該塊內存釋放時,使用unmmap系統調用將這塊內存釋放,這個過程中間不會產生內存碎塊等問題。

針對小塊內存的申請,在程序啟動之後,進程會獲得一個heap底端的地址,進程每次進行內存申請時,glibc會將堆頂向上增長來擴展內存空間,也就是我們所說的堆地址向上增長。在對這些小塊內存進行操作時,便會產生內存碎塊的問題。實際上brk和sbrk系統調用,就是調整heap頂地址指針。

那麽heap堆的內存是什麽時候釋放呢?

當glibc發現堆頂有連續的128k的空間是空閑的時候,它就會通過brk或sbrk系統調用,來調整heap頂的位置,將占用的內存返回給系統。這時,內核會通過刪除相應的線性區,來釋放占用的物理內存。

下面我要講一個內存空洞的問題:

一個場景,堆頂有一塊正在使用的內存,而下面有很大的連續內存已經被釋放掉了,那麽這塊內存是否能夠被釋放?其對應的物理內存是否能夠被釋放?

很遺憾,不能。

這也就是說,只要堆頂的部分申請內存還在占用,我在下面釋放的內存再多,都不會被返回到系統中,仍然占用著物理內存。為什麽會這樣呢?

這主要是與內核在處理堆的時候,過於簡單,它只能通過調整堆頂指針的方式來調整調整程序占用的線性區;而又只能通過調整線性區的方式,來釋放內存。所以只要堆頂不減小,占用的內存就不會釋放。

提一個問題:

char *p=malloc(2); free(p)

為什麽申請內存的時候,需要兩個參數,一個是內存大小,一個是返回的指針;而釋放內存的時候,卻只要內存的指針呢?

這主要是和glibc的內存管理機制有關。glibc中,為每一塊內存維護了一個chunk的結構。glibc在分配內存時,glibc先填寫chunk結構中內存塊的大小,然後是分配給進程的內存。

chunk ------size p------------ content

在進程釋放內存時,只要 指針-4 便可以找到該塊內存的大小,從而釋放掉。

註:glibc在做內存申請時,最少分配16個字節,以便能夠維護chunk結構。

glibc提供的調試工具:

為了方便調試,glibc 為用戶提供了 malloc 等等函數的鉤子(hook),如 __malloc_hook

對應的是一個函數指針,

void *function (size_t size, const void *caller);

其中 caller 是調用 malloc 返回值的接受者(一個指針的地址)。另外有 __malloc_initialize_hook函數指針,僅僅會調用一次(第一次分配動態內存時)。(malloc.h)

一些使用 malloc 的統計量(SVID 擴展)可以用 struct mallinfo 儲存,可調用獲得。

struct mallinfo mallinfo (void);

如何檢測 memory leakage?glibc 提供了一個函數

void mtrace (void)及其反作用void muntrace (void)

這時會依賴於一個環境變量 MALLOC_TRACE 所指的文件,把一些信息記錄在該文件中

用於偵測 memory leakage,其本質是安裝了前面提到的 hook。一般將這些函數用

#ifdef DEBUGGING 包裹以便在非調試態下減少開銷。產生的文件據說不建議自己去讀,

而使用 mtrace 程序(perl 腳本來進行分析)。下面用一個簡單的例子說明這個過程,這是

源程序:

#include #include #include intmain( int argc, char *argv[] ) { int *p, *q ; #ifdef DEBUGGING mtrace( ) ; #endif p = malloc( sizeof( int ) ) ; q = malloc( sizeof( int ) ) ; printf( "p = %p\nq = %p\n", p, q ) ; *p = 1 ; *q = 2 ; free( p ) ; return 0 ; }

很簡單的程序,其中 q 沒有被釋放。我們設置了環境變量後並且 touch 出該文件

執行結果如下:

p = 0x98c0378q = 0x98c0388

該文件內容如下

= Start @./test30:[0x8048446] + 0x98c0378 0x4 @./test30:[0x8048455] + 0x98c0388 0x4 @./test30:[0x804848f] - 0x98c0378

到這裏我基本上講完了,我們寫程序時,數據部分內存使用的問題。

代碼占用的內存

數據部分占用內存,那麽我們寫的程序是不是也占用內存呢?

在linux中,程序的加載,涉及到兩個工具,linker 和loader。Linker主要涉及動態鏈接庫的使用,loader主要涉及軟件的加載。

  1. exec執行一個程序
  2. elf為現在非常流行的可執行文件的格式,它為程序運行劃分了兩個段,一個段是可以執行的代碼段,它是只讀,可執行;另一個段是數據段,它是可讀寫,不能執行。
  3. loader會啟動,通過mmap系統調用,將代碼端和數據段映射到內存中,其實也就是為其分配了虛擬內存,註意這時候,還不占用物理內存;只有程序執行到了相應的地方,內核才會為其分配物理內存。
  4. loader會去查找該程序依賴的鏈接庫,首先看該鏈接庫是否被映射進內存中,如果沒有使用mmap,將代碼段與數據段映射到內存中,否則只是將其加入進程的地址空間。這樣比如glibc等庫的內存地址空間是完全一樣。

因此一個2M的程序,執行時,並不意味著為其分配了2M的物理內存,這與其運行了的代碼量,與其所依賴的動態鏈接庫有關。

運行過程中鏈接動態鏈接庫與編譯過程中鏈接動態庫的區別

我們調用動態鏈接庫有兩種方法:一種是編譯的時候,指明所依賴的動態鏈接庫,這樣loader可以在程序啟動的時候,來所有的動態鏈接映射到內存中;一種是在運行過程中,通過dlopen和dlfree的方式加載動態鏈接庫,動態將動態鏈接庫加載到內存中。

這兩種方式,從編程角度來講,第一種是最方便的,效率上影響也不大,在內存使用上有些差別。

第一種方式,一個庫的代碼,只要運行過一次,便會占用物理內存,之後即使再也不使用,也會占用物理內存,直到進程的終止。

第二中方式,庫代碼占用的內存,可以通過dlfree的方式,釋放掉,返回給物理內存。

這個差別主要對於那些壽命很長,但又會偶爾調用各種庫的進程有關。如果是這類進程,建議采用第二種方式調用動態鏈接庫。

占用內存的測量

測量一個進程占用了多少內存,linux為我們提供了一個很方便的方法,/proc目錄為我們提供了所有的信息,實際上top等工具也通過這裏來獲取相應的信息。

/proc/meminfo 機器的內存使用信息 /proc/pid/maps pid為進程號,顯示當前進程所占用的虛擬地址。 /proc/pid/statm 進程所占用的內存 [root@localhost ~]# cat /proc/self/statm 654 57 44 0 0 334 0

輸出解釋

CPU 以及CPU0。。。的每行的每個參數意思(以第一行為例)為:

參數 解釋 /proc//status

Size (pages) 任務虛擬地址空間的大小 VmSize/4 Resident(pages) 應用程序正在使用的物理內存的大小 VmRSS/4 Shared(pages) 共享頁數 0 Trs(pages) 程序所擁有的可執行虛擬內存的大小 VmExe/4 Lrs(pages) 被映像到任務的虛擬內存空間的庫的大小 VmLib/4 Drs(pages) 程序數據段和用戶態的棧的大小 (VmData+ VmStk )4 dt(pages) 04

查看機器可用內存

/proc/28248/>free total used free shared buffers cached Mem: 1023788 926400 97388 0 134668 503688 -/+ buffers/cache: 288044 735744 Swap: 1959920 89608 1870312

我們通過free命令查看機器空閑內存時,會發現free的值很小。這主要是因為,在linux中有這麽一種思想,內存不用白不用,因此它盡可能的cache和buffer一些數據,以方便下次使用。但實際上這些內存也是可以立刻拿來使用的。

所以 空閑內存=free+buffers+cached=total-used

查看進程使用的內存

查看一個進程使用的內存,是一個很令人困惑的事情。因為我們寫的程序,必然要用到動態鏈接庫,將其加入到自己的地址空間中,但是/proc/pid/statm統計出來的數據,會將這些動態鏈接庫所占用的內存也簡單的算進來。

這樣帶來的問題,動態鏈接庫占用的內存有些是其他程序使用時占用的,卻算在了你這裏。你的程序中包含了子進程,那麽有些動態鏈接庫重用的內存會被重復計算。

因此要想準確的評估一個程序所占用的內存是十分困難的,通過寫一個module的方式,來準確計算某一段虛擬地址所占用的內存,可能對我們有用。(T002)

Linux內存使用方法詳細解析