經常使用top命令瞭解程式資訊,其中包括記憶體方面的資訊。命令top幫助文件是這麼解釋各個欄位的。
VIRT , Virtual Image (kb)
RES, Resident size (kb)
SHR, Shared Mem size (kb)
%MEM, Memory usage(kb)
SWAP, Swapped size (kb)
CODE, Code size (kb)
DATA, Data+Stack size (kb)
nFLT, Page Fault count
nDRT, Dirty Pages count
儘管有註釋,但依然感覺有些晦澀,不知所指何意?
正在執行的程式,叫程式。每個程式都有完全屬於自己的,獨立的,不被干擾的記憶體空間。此空間,被分成幾個段(Segment),分別是Text, Data, BSS, Heap, Stack。使用者程式記憶體空間,也是系統核心分配給該程式的VM(虛擬記憶體),但並不表示這個程式佔用了這麼多的RAM(實體記憶體)。這個空間有多大?命令top輸出的VIRT值告訴了我們各個程式記憶體空間的大小(程式記憶體空間隨著程式的執行會增大或者縮小)。你還可以通過/proc//maps,或者pmap –d 瞭解某個程式記憶體空間都分佈,比如:
1 2 3 4 5 6 7 8 9 10 11 |
#cat /proc/1449/maps … 0012e000-002a4000 r-xp 00000000 08:07 3539877 /lib/i386-linux-gnu/libc-2.13.so 002a4000-002a6000 r--p 00176000 08:07 3539877 /lib/i386-linux-gnu/libc-2.13.so 002a6000-002a7000 rw-p 00178000 08:07 3539877 /lib/i386-linux-gnu/libc-2.13.so 002a7000-002aa000 rw-p 00000000 00:00 0 … 08048000-0875b000 r-xp 00000000 08:07 4072287 /usr/local/mysql/libexec/mysqld 0875b000-0875d000 r--p 00712000 08:07 4072287 /usr/local/mysql/libexec/mysqld 0875d000-087aa000 rw-p 00714000 08:07 4072287 /usr/local/mysql/libexec/mysqld … |
PS:線性地址,訪問許可權, offset, 裝置號,inode,對映檔案
VM分配與釋放
“記憶體總是被程式佔用”,這句話換過來可以這麼理解:程式總是需要記憶體。當fork()或者exec()一個程式的時候,系統核心就會分配一定量的VM給程式,作為程式的記憶體空間,大小由BSS段,Data段的已定義的全域性變數、靜態變數、Text段中的字元直接量、程式本身的記憶體映像等,還有Stack段的區域性變數決定。當然,還可以通過malloc()等函式動態分配記憶體,向上擴大heap。
動態分配與靜態分配,二者最大的區別在於:1. 直到Run-Time的時候,執行動態分配,而在compile-time的時候,就已經決定好了分配多少Text+Data+BSS+Stack。2.通過malloc()動態分配的記憶體,需要程式設計師手工呼叫free()釋放記憶體,否則容易導致記憶體洩露,而靜態分配的記憶體則在程式執行結束後系統釋放(Text, Data), 但Stack段中的資料很短暫,函式退出立即被銷燬。
我們使用幾個示例小程式,加深理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
/* @filename: example-2.c */ #include <stdio.h> int main(int argc, char *argv[]) { char arr[] = "hello world"; /* Stack段,rw--- */ char *p = "hello world"; /* Text段,字串直接量, r-x-- */ arr[1] = 'l'; *(++p) = 'l'; /* 出錯了,Text段不能write */ return 0; } PS:變數p,它在Stack段,但它所指的”hello world”是一個字串直接量,放在Text段。 /* @filename:example_2_2.c */ #include <stdio.h> #include <stdlib.h> #include <string.h> char *get_str_1() { char str[] = "hello world"; return str; } char *get_str_2() { char *str = "hello world"; return str; } char *get_str_3() { char tmp[] = "hello world"; char *str; str = (char *)malloc(12 * sizeof(char)); memcpy(str, tmp, 12); return str; } int main(int argc, char *argv[]) { char *str_1 = get_str_1(); //出錯了,Stack段中的資料在函式退出時就銷燬了 char *str_2 = get_str_2(); //正確,指向Text段中的字元直接量,退出程式後才會回收 char *str_3 = get_str_3(); //正確,指向Heap段中的資料,還沒free() printf("%s\n", str_1); printf("%s\n", str_2); printf("%s\n", str_3); if (str_3 != NULL) { free(str_3); str_3 = NULL; } return 0; } PS:函式get_str_1()返回Stack段資料,編譯時會報錯。Heap中的資料,如果不用了,應該儘早釋放free()。 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> char data_var = '1'; char *mem_killer() { char *p; p = (char *)malloc(1024*1024*4); memset(p, '\0', 1024*1024*4); p = &data_var; //危險,記憶體洩露 return p; } int main(int argc, char *argv[]) { char *p; for (;;) { p = mem_killer(); // 函式中malloc()分配的記憶體沒辦法free() printf("%c\n", *p); sleep(20); } return 0; } |
PS:使用malloc(),特別要留意heap段中的記憶體不用時,儘早手工free()。通過top輸出的VIRT和RES兩值來觀察程式佔用VM和RAM大小。
本節結束之前,介紹工具size。因為Text, BSS, Data段在編譯時已經決定了程式將佔用多少VM。可以通過size,知道這些資訊。
# size example_2_3
text data bss dec hex filename
1403 272 8 1683 693 example_2_3
malloc()
編碼人員在編寫程式之際,時常要處理變化資料,無法預料要處理的資料集變化是否大(phper可能難以理解),所以除了變數之外,還需要動態分配記憶體。GNU libc庫提供了二個記憶體分配函式,分別是malloc()和calloc()。呼叫malloc(size_t size)函式分配記憶體成功,總會分配size位元組VM(再次強調不是RAM),並返回一個指向剛才所分配記憶體區域的開端地址。分配的記憶體會為程式一直保留著,直到你顯示地呼叫free()釋放它(當然,整個程式結束,靜態和動態分配的記憶體都會被系統回收)。開發人員有責任儘早將動態分配的記憶體釋放回系統。記住一句話:儘早free()!
我們來看看,malloc()小示例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/* @filename:example_2_4.c */ #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char *p_4kb, *p_128kb, *p_300kb; if ((p_4kb = malloc(4*1024)) != NULL) { free(p_4kb); } if ((p_128kb = malloc(128*1024)) != NULL) { free(p_128kb); } if ((p_300kb = malloc(300*1024)) != NULL) { free(p_300kb); } return 0; } #gcc example_2_4.c –o example_2_4 #strace –t ./example_2_4 … 00:02:53 brk(0) = 0x8f58000 00:02:53 brk(0x8f7a000) = 0x8f7a000 00:02:53 brk(0x8f79000) = 0x8f79000 00:02:53 mmap2(NULL, 311296, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb772d000 00:02:53 munmap(0xb772d000, 311296) = 0 … |
PS:系統呼叫brk(0)取得當前堆的地址,也稱為斷點。
通過跟蹤系統核心呼叫,可見glibc函式malloc()總是通過brk()或mmap()系統呼叫來滿足記憶體分配需求。函式malloc(),根據不同大小記憶體要求來選擇brk(),還是mmap(), 128Kbytes是臨界值。小塊記憶體(<=128kbytes),會呼叫brk(),它將資料段的最高地址往更高處推(堆從底部向上增長)。大塊記憶體,則使用mmap()進行匿名對映(設定標誌MAP_ANONYMOUS)來分配記憶體,與堆無關,在堆之外。這樣做是有道理的,試想:如果大塊記憶體,也呼叫brk(),則容易被小塊記憶體釘住,必竟用大塊記憶體不是很頻繁;反過來,小塊記憶體分配更為頻繁得多,如果也使用mmap(),頻繁的建立記憶體對映會導致更多的開銷,還有一點就是,記憶體對映的大小要求必須是“頁”(單位,記憶體頁面大小,預設4Kbytes或8Kbytes)的倍數,如果只是為了”hello world”這樣小資料就對映一“頁”記憶體,那實在是太浪費了。
跟malloc()一樣,釋放記憶體函式free(),也會根據記憶體大小,選擇使用brk()將斷點往低處回推,或者選擇呼叫munmap()解除對映。有一點需要注意:並不是每次呼叫free()小塊記憶體,都會馬上呼叫brk(),即堆並不會在每次記憶體被釋放後就被縮減,而是會被glibc保留給下次malloc()使用(必竟小塊記憶體分配較為頻繁),直到glibc發現堆空閒大小顯著大於記憶體分配所需數量時,則會呼叫brk()。但每次free()大塊記憶體,都會呼叫munmap()解除對映。下面是二張malloc()小塊記憶體和大塊記憶體的示例圖。
示意圖:函式malloc(100000),小於128kbytes,往高處推(heap->)。留意紫圈標註
示意圖:函式malloc(1024*1024),大於128kbytes,在heap與stack之間。留意紫圈。PS:圖中的Data Segment泛指BSS, Data, Heap。有些文件有說明:資料段有三個子區域,分別是BSS, Data, Heap。
缺頁異常(Fault Page)
每次呼叫malloc(),系統都只是給程式分配線性地址(VM),並沒有隨即分配頁框(RAM)。系統儘量將分配頁框的工作推遲到最後一刻—用到時缺頁異常處理。這種頁框按需延遲分配策略最大好處之一:充分有效地善用系統稀缺資源RAM。
當指標引用的記憶體頁沒有駐留在RAM中,即在RAM找不到與之對應的頁框,則會發生缺頁異常(對程式來說是透明的),核心便陷入缺頁異常處理。發生缺頁異常有幾種情況:1.只分配了線性地址,並沒有分配頁框,常發生在第一次訪問某記憶體頁。2.已經分配了頁框,但頁框被回收,換出至磁碟(交換區)。3.引用的記憶體頁,在程式空間之外,不屬於該程式,可能已被free()。我們使用一段虛擬碼來大致瞭解缺頁異常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
/* @filename: example_2_5.c */ … demo() { char *p; //分配了100Kbytes線性地址 if ((p = malloc(1024*100)) != NULL) // L0 { *p = ‘t’; // L1 … //過去了很長一段時間,不管系統忙否,長久不用的頁框都有可能被回收 *p = ‘m’; // L2 p[4096] = ‘p’; // L3 … free(p); //L4 if (p == NULL) { *p = ‘l’; // L5 } } } … |
- L0,函式malloc()通過brk()給程式分配了100Kbytes的線性地址區域(VM).然而,系統並沒有隨即分配頁框(RAM)。即此時,程式沒有佔用100Kbytes的實體記憶體。這也表明了,你時常在使用top的時候VIRT值增大,而RES值卻不變的原因。
- L1,通過*p引用了100Kbytes的第一頁(4Kbytes)。因為是第一次引用此頁,在RAM中找不到與之相對應的頁框。發生缺頁異常(對於程式而言缺頁異常是透明的),系統靈敏地捕獲這一異常,進入缺頁異常處理階段:接下來,系統會分配一個頁框(RAM)對映給它。我們把這種情況(被訪問的頁還沒有被放在任何一個頁框中,核心分配一新的頁框並適當初始化來滿足呼叫請求),也稱為Demand Paging。
- L2,過了很長一段時間,通過*p再次引用100Kbytes的第一頁。若系統在RAM找不到它對映的頁框(可能交換至磁碟了)。發生缺頁異常,並被系統捕獲進入缺頁異常處理。接下來,系統則會分配一頁頁框(RAM),找到備份在磁碟的那“頁”,並將它換入記憶體(其實因為換入操作比較昂貴,所以不總是隻換入一頁,而是預換入多頁。這也表明某些文件說:”vmstat某時出現不少si並不能意味著實體記憶體不足”)。凡是類似這種會迫使程式去睡眠(很可能是由於當前磁碟資料填充至頁框(RAM)所花的時間),阻塞當前程式的缺頁異常處理稱為主缺頁(major falut),也稱為大缺頁(參見下圖)。相反,不會阻塞程式的缺頁,稱為次缺頁(minor fault),也稱為小缺面。
- L3,引用了100Kbytes的第二頁。參見第一次訪問100Kbytes第一頁, Demand Paging。
- L4,釋放了記憶體:線性地址區域被刪除,頁框也被釋放。
- L5,再次通過*p引用記憶體頁,已被free()了(使用者程式本身並不知道)。發生缺頁異常,缺面異常處理程式會檢查出這個缺頁不在程式記憶體空間之內。對待這種程式設計錯誤引起的缺頁異常,系統會殺掉這個程式,並且報告著名的段錯誤(Segmentation fault)。
主缺頁異常處理過程示意圖,參見Page Fault Handling
頁框回收PFRA
隨著網路併發使用者數量增多,程式數量越來越多(比如一般守護程式會fork()子程式來處理使用者請求),缺頁異常也就更頻繁,需要快取更多的磁碟資料(參考下篇OS Page Cache),RAM也就越來越緊少。為了保證有夠用的頁框供給缺頁異常處理,Linux有一套自己的做法,稱為PFRA。PFRA總會從使用者態進記憶體程空間和頁面快取中,“竊取”頁框滿足供給。所謂”竊取”,指的是:將使用者程式記憶體空間對應占用的頁框中的資料swap out至磁碟(稱為交換區),或者將OS頁面快取中的記憶體頁(還有使用者程式mmap()的記憶體頁)flush(同步fsync())至磁碟裝置。PS:如果你觀察到因為RAM不足導致系統病態式般慢,通常都是因為缺頁異常處理,以及PFRA在”盜頁”。我們從以下幾個方面瞭解PFRA。
候選頁框:找出哪些頁框是可以被回收?
- 程式記憶體空間佔用的頁框,比如資料段中的頁(Heap, Data),還有在Heap與Stack之間的匿名對映頁(比如由malloc()分配的大記憶體)。但不包括Stack段中的頁。
- 程式空間mmap()的記憶體頁,有對映檔案,非匿名對映。
- 快取在頁面快取中Buffer/Cache佔用的頁框。也稱OS Page Cache。
頁框回收策略:確定了要回收的頁框,就要進一步確定先回收哪些候選頁框
- 儘量先回收頁面快取中的Buffer/Cache。其次再回收記憶體空間佔用的頁框。
- 程式空間佔用的頁框,要是沒有被鎖定,都可以回收。所以,當某程式睡眠久了,佔用的頁框會逐漸地交換出去至交換區。
- 使收LRU置換演算法,將那些久而未用的頁框優先被回收。這種被放在LRU的unused連結串列的頁,常被認為接下來也不太可能會被引用。
- 相對回收Buffer/Cache而言,回收程式記憶體頁,昂貴很多。所以,Linux預設只有swap_tendency(交換傾向值)值不小於100時,才會選擇換出程式佔用的RES。其實交換傾向值描述的是:系統越忙,且RES都被程式佔用了,Buffer/Cache只佔了一點點的時候,才開始回收程式佔用頁框。PS:這正表明了,某些DBA提議將MySQL InnoDB伺服器vm.swappiness值設定為0,以此讓InnoDB Buffer Pool資料在RES呆得更久。
- 如果實在是沒有頁框可回收,PFRA使出最狠一招,殺掉一個使用者態程式,並釋放這些被佔的頁框。當然,這個被殺的程式不是胡亂選的,至少應該是佔用較多頁框,執行優選級低,且不是root使用者的程式。
啟用回收頁框:什麼時候會回收頁框?
- 緊急回收。系統核心發現沒有夠用的頁框分配,供給讀檔案和記憶體缺頁處理的時候,系統核心開始”緊急回收頁框”。喚醒pdflush核心執行緒,先將1024頁髒頁從頁面快取寫回磁碟。然後開始回收32頁框,若反覆回收13次,還收不齊32頁框,則發狠殺一個程式。
- 週期性回收。在緊急回收之前,PFRA還會喚醒核心執行緒kswapd。為了避免更多的“緊急回收”,當發現空閒頁框數量低於設定的警告值時,核心執行緒kswapd就會被喚醒,回收頁框。直到空閒的頁框的數量達到設定的安全值。PS:當RES資源緊張的時候,你可以通過ps命令看到更多的kswapd執行緒被喚醒。
- OOM。在高峰時期,RES高度緊張的時候,kswapd持續回收的頁框供不應求,直到進入”緊急回收”,直到 OOM。
Paging 和Swapping
這二個關鍵字在很多地方出現,譯過來應該是Paging(調頁),Swapping(交換)。PS:英語裡面用得多的動詞加上ing,就成了名詞,比如building。咬文嚼字,實在是太難。看二圖
Swapping的大部分時間花在資料傳輸上,交換的資料也越多,意味時間開銷也隨之增加。對於程式而言,這個過程是透明的。由於RAM資源不足,PFRA會將部分匿名頁框的資料寫入到交換區(swap area),備份之,這個動作稱為so(swap out)。等到發生記憶體缺頁異常的時候,缺頁異常處理程式會將交換區(磁碟)的頁面又讀回實體記憶體,這個動作稱為si(swap in)。每次Swapping,都有可能不只是一頁資料,不管是si,還是so。Swapping意味著磁碟操作,更新頁表等操作,這些操作開銷都不小,會阻塞使用者態程式。所以,持續飈高的si/so意味著實體記憶體資源是效能瓶頸。
Paging,前文我們有說過Demand Paging。通過線性地址找到實體地址,找到頁框。這個過程,可以認為是Paging,對於程式來講,也是透明的。Paging意味著產生缺頁異常,也有可能是大缺頁,也就意味著浪費更多的CPU時間片資源。
總結
1.使用者程式記憶體空間分為5段,Text, DATA, BSS, Heap, Stack。其中Text只讀可執行,DATA全域性變數和靜態變數,Heap用完就儘早free(),Stack裡面的資料是臨時的,退出函式就沒了。
2.glibc malloc()動態分配記憶體。使用brk()或者mmap(),128Kbytes是一個臨界值。避免記憶體洩露,避免野指標。
3.核心會盡量延後Demand Paging。主缺頁是昂貴的。
4.先回收Buffer/Cache佔用的頁框,然後程式佔用的頁框,使用LRU置換演算法。調小vm.swappiness值可以減少Swapping,減少大缺頁。
5.更少的Paging和Swapping
6.fork()繼承父程式的地址空間,不過是隻讀,使用cow技術,fork()函式特殊在於它返回二次。