1. 程式人生 > >malloc動態記憶體分配機制原理_及_linux/proc/介紹

malloc動態記憶體分配機制原理_及_linux/proc/介紹

程序系統資源的使用原理

         大部分程序通過glibc申請使用記憶體,但是glibc也是一個應用程式庫,它最終也是要呼叫作業系統的記憶體管理介面來使用記憶體。大部分情況下,glibc對使用者和作業系統是透明的,所以直接觀察作業系統記錄的程序對記憶體的使用情況有很大的幫助。但是glibc自己的實現也是有問題的,所以太特殊情況下追究程序的記憶體使用也要考慮glibc的因素。其他作業系統資源使用情況則可以直接通過proc檔案系統檢視。

程序所需要的系統資源種類

記憶體

         程序需要記憶體,但並不一定需要實體記憶體。一個程序可以申請1個g的記憶體,核心也確實會批准給他,但是核心真正給他安排實際對應的實體記憶體的時候是在程序需要實際使用這篇記憶體的時候,這個時候會引發缺頁異常,核心就在這個異常處理程式碼中把實際的記憶體安排給程序。就像你在銀行存錢的時候,大部分時候你的資產都是個數目,只有當你要取出現金的時候銀行才有必要籌措現金支付給你(因為他之前承諾過),但是通常銀行的現金數永遠是小與儲戶的總資產數的(資產泡沫也就是這麼來的),當大家都要擠兌的時候,銀行就得破產。作業系統的記憶體也一樣,當程序都要求兌現的時候,核心不一定能夠全部兌現,核心也得崩潰。

         程序的記憶體的種類:程序使用來存放資料的(堆),用來執行程序的(棧),與其他程序的實體記憶體(例如共享庫,共享記憶體),程序的虛擬地址空間大小(取決於你是32位還是64位,2的該數次方),應用程序實際正在使用的實體地址的大小(rss),程序用來放要執行的程式碼的部分(trs)。總體來說,應用程式實際要使用的實體地址由資料、棧、可執行程式碼存放三部分組成。大部分程序需要最多的一般是資料記憶體。

分析程序的入口點

         每個程序都有父程序、程序組、會話組(一般程序組屬於會話組,而程序屬於程序組)。

         每個程序都有屬於自己的執行緒組(有的甚至有協程)

發生缺頁的次數可以用來診斷是否是記憶體敏感的或記憶體使用過度。

在核心態和使用者態分別的執行時間、任務的累計等待時間可以看出該程序對系統呼叫的依賴程度(或許你需要把程序放到核心實現或者是使用非阻塞的)

排程策略、執行在哪個CPU上和程序的優先順序可以用來在系統資源不足時不公平的分配資源。

被swap換出的頁數和使用的各種記憶體的統計表明瞭程序的記憶體使用力量。

程序的cap能力集可以看出程序是否有超過的許可權

一、/proc/pid/statm

pid/statm包含了在此程序中所有CPU活躍的資訊,該檔案中的所有值都是從系統啟動開始累計到當前時刻。

/proc/1 # catstatm

550 70 62 451 0 97 0

輸出解釋

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

引數解釋 /proc/1/status

Size (pages)= 550  任務虛擬地址空間的大小 VmSize/4

Resident(pages)= 70  應用程式正在使用的實體記憶體的大小VmRSS/4

Shared(pages)= 62  共享頁數

Trs(pages)= 451  程式所擁有的可執行虛擬記憶體的大小VmExe/4

Lrs(pages)= 0  被映像到任務的虛擬記憶體空間的庫的大小VmLib/4

Drs(pages)= 97  程式資料段和使用者態的棧的大小 (VmData+ VmStk )4

dt(pages) 0

二、/proc/pid/stat

pid/stat包含了程序所有CPU活躍的資訊,該檔案中的所有值都是從系統啟動開始累計到當前時刻。

/proc/1 # cat stat

 

1 (linuxrc) S 0 0 0 0 -1 8388864 50 633 204 2 357 72 342 16 0 1 0 22 2252800 70 4294967295 32768 1879936 31992707043199269552 1113432 0 0 0 674311 3221479524 0 0 0 0 0 0

每個引數意思為:

引數解釋

pid=1 程序(包括輕量級程序,即執行緒)號

comm= linuxrc 應用程式或命令的名字

task_state=S 任務的狀態,R:runnign,S:sleeping (TASK_INTERRUPTIBLE), D:disk sleep (TASK_UNINTERRUPTIBLE), T:stopped, T:tracing stop,Z:zombie, X:dead

ppid=0 父程序ID

pgid=0 執行緒組號

sid=0 c該任務所在的會話組ID

tty_nr=0(pts/3) 該任務的tty終端的裝置號,INT(0/256)=主裝置號,(0-主裝置號)=次裝置號

tty_pgrp=-1 終端的程序組號,當前執行在該任務所在終端的前臺任務(包括shell 應用程式)的PID。

task->flags=8388864程序標誌位,檢視該任務的特性

min_flt=50該任務不需要從硬碟拷資料而發生的缺頁(次缺頁)的次數

cmin_flt=633 累計的該任務的所有的waited-for程序曾經發生的次缺頁的次數目

maj_flt=20該任務需要從硬碟拷資料而發生的缺頁(主缺頁)的次數

cmaj_flt=4 累計的該任務的所有的waited-for程序曾經發生的主缺頁的次數目

當一個程序發生缺頁中斷的時候,程序會陷入核心態,執行以下操作:

1、檢查要訪問的虛擬地址是否合法

2、查詢/分配一個物理頁

3、填充物理頁內容(讀取磁碟,或者直接置0,或者啥也不幹)

4、建立對映關係(虛擬地址到實體地址)

重新執行發生缺頁中斷的那條指令

如果第3步,需要讀取磁碟,那麼這次缺頁中斷就是majflt,否則就是minflt。

 

utime=2 該任務在使用者態執行的時間,單位為jiffies

stime=357 該任務在核心態執行的時間,單位為jiffies

cutime=72 累計的該任務的所有的waited-for程序曾經在使用者態執行的時間,單位為jiffies

cstime=342 累計的該任務的所有的waited-for程序曾經在核心態執行的時間,單位為jiffies

priority=16 任務的動態優先順序

nice=0 任務的靜態優先順序

num_threads=1 該任務所在的執行緒組裡執行緒的個數

it_real_value=0 由於計時間隔導致的下一個SIGALRM 傳送程序的時延,以 jiffy 為單位.

start_time=22 該任務啟動的時間,單位為jiffies

vsize=2252800(bytes) 該任務的虛擬地址空間大小

rss=70(page) 該任務當前駐留實體地址空間的大小

 

這些頁可能用於程式碼,資料和棧。

rlim=4294967295=0xFFFFFFFF(bytes) 該任務能駐留實體地址空間的最大值

start_code=32768=0x8000  該任務在虛擬地址空間的程式碼段的起始地址(由聯結器決定)

end_code=1879936該任務在虛擬地址空間的程式碼段的結束地址

start_stack=3199270704=0Xbeb0ff30該任務在虛擬地址空間的棧的開始地址

kstkesp=3199269552  sp(32 位堆疊指標) 的當前值, 與在程序的核心堆疊頁得到的一致.

kstkeip=1113432 =0X10FD58 指向將要執行的指令的指標,PC(32 位指令指標)的當前值.

pendingsig=0 待處理訊號的點陣圖,記錄傳送給程序的普通訊號

block_sig=0 阻塞訊號的點陣圖

sigign=0 忽略的訊號的點陣圖

sigcatch=674311被俘獲的訊號的點陣圖

wchan=3221479524  如果該程序是睡眠狀態,該值給出排程的呼叫點

nswap=0 被swapped的頁數

cnswap=0 所有子程序被swapped的頁數的和

exit_signal=0  該程序結束時,向父程序所傳送的訊號

task_cpu(task)=0 執行在哪個CPU上

task_rt_priority=0 實時程序的相對優先級別

task_policy=0 程序的排程策略,0=非實時程序,1=FIFO實時程序;2=RR實時程序

 

三、/proc/pid/status

包含了所有CPU活躍的資訊,該檔案中的所有值都是從系統啟動開始累計到當前時刻。

/proc/286 # cat status

Name:  mmtest

State: R (running)

SleepAVG:       0%

Tgid:  286

Pid:   286

PPid:  243

TracerPid:      0

Uid:   0       0       0      0

Gid:   0       0       0      0

FDSize: 32

Groups:

VmPeak:    1464 kB

VmSize:    1464 kB

VmLck:         0 kB

VmHWM:      344 kB

VmRSS:      344 kB

VmData:       20 kB

VmStk:        84 kB

VmExe:         4 kB

VmLib:     1300 kB

VmPTE:         6 kB

Threads:        1

SigQ:  0/256

SigPnd: 0000000000000000

ShdPnd: 0000000000000000

SigBlk: 0000000000000000

SigIgn: 0000000000000000

SigCgt: 0000000000000000

CapInh: 0000000000000000

CapPrm: 00000000fffffeff

CapEff: 00000000fffffeff

 

輸出解釋

引數解釋

Name 應用程式或命令的名字

State 任務的狀態,執行/睡眠/僵死/

SleepAVG 任務的平均等待時間(以nanosecond為單位),互動式任務因為休眠次數多、時間長,它們的 sleep_avg 也會相應地更大一些,所以計算出來的優先順序也會相應高一些。

Tgid=286 執行緒組號

Pid=286 任務ID

Ppid=243 父程序ID

TracerPid=0 接收跟蹤該程序資訊的程序的ID號

Uid Uid euid suid fsuid

Gid Gid egid sgid fsgid

FDSize=32 檔案描述符的最大個數,最多能開啟的檔案控制代碼的個數file->fds

Groups:

VmPeak: 60184 kB /*程序地址空間的大小*/

VmHWM: 18020 kB /*檔案記憶體對映和匿名記憶體對映的大小*/

VmSize(KB)=1499136 任務虛擬地址空間的大小(total_vm-reserved_vm),其中total_vm為程序的地址空間的大小,reserved_vm:程序在預留或特殊的記憶體間的物理頁

VmLck(KB)=0 任務已經鎖住的實體記憶體的大小。鎖住的實體記憶體不能交換到硬碟 (locked_vm)

VmRSS(KB)= 344 kB 應用程式正在使用的實體記憶體的大小,就是用ps命令的引數rss的值 (rss)

VmData(KB)=20KB 程式資料段的大小(所佔虛擬記憶體的大小),存放初始化了的資料; (total_vm-shared_vm-stack_vm)

VmStk(KB)=84KB 任務在使用者態的棧的大小(stack_vm)

VmExe(KB)=4KB 程式所擁有的可執行虛擬記憶體的大小,程式碼段,不包括任務使用的庫 (end_code-start_code)

VmLib(KB)=1300KB 被映像到任務的虛擬記憶體空間的庫的大小 (exec_lib)

VmPTE=6KB 該程序的所有頁表的大小,單位:kb

Threads=1 共享使用該訊號描述符的任務的個數,在POSIX多執行緒序應用程式中,執行緒組中的所有執行緒使用同一個訊號描述符。

SigQ 待處理訊號的個數

SigPnd 遮蔽位,儲存了該執行緒的待處理訊號

ShdPnd 遮蔽位,儲存了該執行緒組的待處理訊號

SigBlk 存放被阻塞的訊號

SigIgn 存放被忽略的訊號

SigCgt 存放被俘獲到的訊號

CapInh Inheritable,能被當前程序執行的程式的繼承的能力

CapPrm Permitted,程序能夠使用的能力,可以包含CapEff中沒有的能力,這些能力是被程序自己臨時放棄的,CapEff是CapPrm的一個子集,程序放棄沒有必要的能力有利於提高安全性

CapEff Effective,程序的有效能力

 

四、/proc/loadavg

該檔案中的所有值都是從系統啟動開始累計到當前時刻。該檔案只給出了所有CPU的集合資訊,不能該出每個CPU的資訊。

/proc # cat loadavg

1.0  1.00 0.93 2/19 301

每個值的含義為:

引數 解釋

lavg_1 (1.0) 1-分鐘平均負載

lavg_5 (1.00) 5-分鐘平均負載

lavg_15(0.93) 15-分鐘平均負載

nr_running (2) 在取樣時刻,執行佇列的任務的數目,與/proc/stat的procs_running表示相同意思

nr_threads (19) 在取樣時刻,系統中活躍的任務的個數(不包括執行已經結束的任務)

last_pid(301) 最大的pid值,包括輕量級程序,即執行緒。

假設當前有兩個CPU,則每個CPU的當前任務數為4.61/2=2.31

五、/proc/286/smaps

該檔案反映了該程序的相應線性區域的大小

/proc/286 # cat smaps

00008000-00009000 r-xp 00000000 00:0c1695459    /memtest/mmtest

Size:                 4 kB

Rss:                  4 kB

Shared_Clean:         0 kB

Shared_Dirty:         0 kB

Private_Clean:        4 kB

Private_Dirty:        0 kB

00010000-00011000 rw-p 00000000 00:0c1695459    /memtest/mmtest

Size:                 4 kB

Rss:                  4 kB

Shared_Clean:         0 kB

Shared_Dirty:         0 kB

Private_Clean:        0 kB

Private_Dirty:        4 kB

00011000-00012000 rwxp 00011000 00:000          [heap]

Size:                 4 kB

Rss:                  0 kB

Shared_Clean:         0 kB

Shared_Dirty:         0 kB

Private_Clean:        0 kB

Private_Dirty:        0 kB

40000000-40019000 r-xp 00000000 00:0c2413396    /lib/ld-2.3.2.so

Size:               100 kB

Rss:                 96 kB

使用者態程序使用核心記憶體管理的介面方法

從作業系統角度來看,程序分配記憶體有兩種方式,分別由兩個系統呼叫完成:brk和mmap(不考慮共享記憶體)。

1、brk是將資料段(.data)的最高地址指標_edata往高地址推(詳見glibc部分)

2、mmap是在程序的虛擬地址空間中(堆和棧中間,稱為檔案對映區域的地方)找一塊空閒的虛擬記憶體。

     這兩種方式分配的都是虛擬記憶體,沒有分配實體記憶體。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,作業系統負責分配實體記憶體,然後建立虛擬記憶體和實體記憶體之間的對映關係。

在標準C庫中,提供了malloc/free函式分配釋放記憶體,這兩個函式底層是由brk,mmap,munmap這些系統呼叫實現的。

Glibc的記憶體管理方法

程序記憶體區域的預定義

http://techpubs.sgi.com/library/tpl/cgi-bin/getdoc.cgi?coll=0650&db=man&raw=1&fname=/usr/share/catman/p_man/cat3/standard/_rt_symbol_table_size.z

glibc記憶體分配辦法

下面以一個例子來說明記憶體分配的原理,預設是用的ptmalloc,後續有jemalloc對pymalloc的改進實現。

原理

當malloc小於128k的記憶體,使用brk分配記憶體,將_edata往高地址推(只分配虛擬空間,不對應實體記憶體(因此沒有初始化),第一次讀/寫資料時,引起核心缺頁中斷,核心才分配對應的實體記憶體,然後虛擬地址空間建立對映關係),如下圖:

 

1、程序啟動的時候,其(虛擬)記憶體空間的初始佈局如圖1所示。

其中,mmap記憶體對映檔案是在堆和棧的中間(例如libc-2.2.93.so,其它資料檔案等),為了簡單起見,省略了記憶體對映檔案。_edata指標(glibc裡面定義)指向資料段的最高地址。

2、程序呼叫A=malloc(30K)以後,記憶體空間如圖2:

malloc函式會呼叫brk系統呼叫,將_edata指標往高地址推30K,就完成虛擬記憶體分配。

你可能會問:只要把_edata+30K就完成記憶體分配了?

事實是這樣的,_edata+30K只是完成虛擬地址的分配,A這塊記憶體現在還是沒有物理頁與之對應的,等到程序第一次讀寫A這塊記憶體的時候,發生缺頁中斷,這個時候,核心才分配A這塊記憶體對應的物理頁。也就是說,如果用malloc分配了A這塊內容,然後從來不訪問它,那麼,A對應的物理頁是不會被分配的。

3、程序呼叫B=malloc(40K)以後,記憶體空間如圖3。

情況二、malloc大於128k的記憶體,使用mmap分配記憶體,在堆和棧之間找一塊空閒記憶體分配(對應獨立記憶體,而且初始化為0),如下圖:

 

4、程序呼叫C=malloc(200K)以後,記憶體空間如圖4:

預設情況下,malloc函式分配記憶體,如果請求記憶體大於128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指標了,而是利用mmap系統呼叫,從堆和棧的中間分配一塊虛擬記憶體。

這樣子做主要是因為:

brk分配的記憶體需要等到高地址記憶體釋放以後才能釋放(例如,在B釋放之前,A是不可能釋放的,這就是記憶體碎片產生的原因,什麼時候緊縮看下面),而mmap分配的記憶體可以單獨釋放。當然,還有其它的好處,也有壞處,再具體下去,有興趣的同學可以去看glibc裡面malloc的程式碼了。

5、程序呼叫D=malloc(100K)以後,記憶體空間如圖5;

6、程序呼叫free(C)以後,C對應的虛擬記憶體和實體記憶體一起釋放。

 

7、程序呼叫free(B)以後,如圖7所示: B對應的虛擬記憶體和實體記憶體都沒有釋放,因為只有一個_edata指標,如果往回推,那麼D這塊記憶體怎麼辦呢?當然,B這塊記憶體,是可以重用的,如果這個時候再來一個40K的請求,那麼malloc很可能就把B這塊記憶體返回回去了。

8、程序呼叫free(D)以後,如圖8所示:B和D連線起來,變成一塊140K的空閒記憶體。

9、預設情況下:當最高地址空間的空閒記憶體超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行記憶體緊縮操作(trim)。在上一個步驟free的時候,發現最高地址空閒記憶體超過128K,於是記憶體緊縮,變成圖9所示。

實驗

在瞭解了記憶體分配原理以後來看一個現象:

1 壓力測試過程中,發現被測物件效能不夠理想,具體表現為:

程序的系統態CPU消耗20,使用者態CPU消耗10,系統idle大約70

2 用ps -o majflt,minflt-C program命令檢視,發現majflt每秒增量為0,而minflt每秒增量大於10000。

初步分析

majflt代表major fault,中文名叫大錯誤,minflt代表minor fault,中文名叫小錯誤。這兩個數值表示一個程序自啟動以來所發生的缺頁中斷的次數。當一個程序發生缺頁中斷的時候,程序會陷入核心態,執行以下操作:

l  檢查要訪問的虛擬地址是否合法

l  查詢/分配一個物理頁

l  填充物理頁內容(讀取磁碟,或者直接置0,或者啥也不幹)

l  建立對映關係(虛擬地址到實體地址)

l  重新執行發生缺頁中斷的那條指令

l  如果第3步,需要讀取磁碟,那麼這次缺頁中斷就是majflt,否則就是minflt。

l  此程序minflt如此之高,一秒10000多次,不得不懷疑它跟程序核心態cpu消耗大有很大關係。

分析程式碼

檢視程式碼,發現是這麼寫的:一個請求來,用malloc分配2M記憶體,請求結束後free這塊記憶體。看日誌,發現分配記憶體語句耗時10us,平均一條請求處理耗時1000us 。原因已找到!

雖然分配記憶體語句的耗時在一條處理請求中耗時比重不大,但是這條語句嚴重影響了效能。要解釋清楚原因,需要先了解一下記憶體分配的原理。

真相大白

說完記憶體分配的原理,那麼被測模組在核心態cpu消耗高的原因就很清楚了:每次請求來都malloc一塊2M的記憶體,預設情況下,malloc呼叫mmap分配記憶體,請求結束的時候,呼叫munmap釋放記憶體。假設每個請求需要6個物理頁,那麼每個請求就會產生6個缺頁中斷,在2000的壓力下,每秒就產生了10000多次缺頁中斷,這些缺頁中斷不需要讀取磁碟解決,所以叫做minflt;缺頁中斷在核心態執行,因此程序的核心態cpu消耗很大。缺頁中斷分散在整個請求的處理過程中,所以表現為分配語句耗時(10us)相對於整條請求的處理時間(1000us)比重很小。

解決辦法

將動態記憶體改為靜態分配,或者啟動的時候,用malloc為每個執行緒分配,然後儲存在threaddata裡面。但是,由於這個模組的特殊性,靜態分配,或者啟動時候分配都不可行。另外,Linux下預設棧的大小限制是10M,如果在棧上分配幾M的記憶體,有風險。

禁止malloc呼叫mmap分配記憶體,禁止記憶體緊縮。

在程序啟動時候,加入以下兩行程式碼:

mallopt(M_MMAP_MAX,0); // 禁止malloc呼叫mmap分配記憶體

mallopt(M_TRIM_THRESHOLD,-1); // 禁止記憶體緊縮

效果:加入這兩行程式碼以後,用ps命令觀察,壓力穩定以後,majlt和minflt都為0。程序的系統態cpu從20降到10。

小結

可以用命令ps -o majfltminflt -C program來檢視程序的majflt, minflt的值,這兩個值都是累加值,從程序啟動開始累加。在對高效能要求的程式做壓力測試的時候,我們可以多關注一下這兩個值。

如果一個程序使用了mmap將很大的資料檔案對映到程序的虛擬地址空間,我們需要重點關注majflt的值,因為相比minflt,majflt對於效能的損害是致命的,隨機讀一次磁碟的耗時數量級在幾個毫秒,而minflt只有在大量的時候才會對效能產生影響。

其他記憶體申請管理演算法實現

         Glibc中使用的malloc並不是唯一可用的記憶體管理方法。Bionic的dlmalloc, google 的Tcmalloc還有被普遍認為最強的jemalloc。jemalloc的核心思想是將記憶體池分為3級,每個執行緒都有自己的記憶體池,向上有一些large的記憶體池,最上是huge記憶體池。而tcmalloc管理的是一系列記憶體池,每個執行緒會發展出與某個記憶體池的親和度。所以jemalloc適合執行緒數比較固定的場合,而tcmalloc適合執行緒數變動比較大的場合。