天天看點

當Linux用盡記憶體

<a href="http://blog.chinaunix.net/link.php?url=http://www.linuxdevcenter.com%2Fpub%2Fau%2F2883" target="_blank">Mulyadi Santosa</a>

也許你很少面臨這一情況,但是一旦如此,你一定知道出什麼錯了:可用記憶體不足或者說記憶體用盡(OOM)。結果非常典型:你不能再配置設定記憶體,核心會殺掉一個任務(一般是正在運作那個)。一般半随着大量的交換讀寫,你可以從螢幕和磁盤動向看出來。

這個問題下面隐含着别的問題:你需要配置設定多少記憶體?作業系統給你配置設定了多少?OOM的基本原因很簡單,你申請的記憶體多于系統可用量。我得說是虛拟記憶體,因為交換分區也包括在内。

了解OOM

開始了解OOM,首先試試這段會配置設定大量記憶體的代碼:

#include

#define MEGABYTE 1024*1024

int main(int argc, char *argv[])

{

void *myblock = NULL;

int count = 0;

while (1)

myblock = (void *) malloc(MEGABYTE);

if (!myblock) break;

printf(”Currently allocating %d MBn”, ++count);

}

exit(0);

編譯一下,運作它之後等一會。系統早晚會OOM。然後試試下面這段,配置設定大量記憶體并用1寫入:

while(1)

memset(myblock,1, MEGABYTE);

printf(”Currently allocating %d MBn”,++count);

發現差别了麼?A比B配置設定了更多記憶體。而且B被殺掉的更早一些。兩個程式都因為沒有可用記憶體而退出。更準确的說,A因為失敗的malloc()而優雅的退出了,B是被OOM殺手幹掉了。

首先觀察配置設定的記憶體塊數。假設你使用256M記憶體,888M交換分區(我的情況),B結束時:

Currently allocating 1081 MB

而A結束時:

Currently allocating 3056 MB

A怎麼弄來的另外1975M?我騙人?沒有!如果你仔細看,你會發現B用1填滿得到的記憶體,而A幾乎不拿他們幹什麼。Linux允許推遲的頁配置設定, 換句話說,隻當你真的要用的時候才開始配置設定動作,比如寫入資料時。是以,除非寫入資料,否則你可以一直要更多記憶體。術語稱之為樂觀的記憶體配置設定。

檢視/proc//status來确認資訊。

$ cat /proc//status

VmPeak: 3141876 kB

VmSize: 3141876 kB

VmLck: 0 kB

VmHWM: 12556 kB

VmRSS: 12556 kB

VmData: 3140564 kB

VmStk: 88 kB

VmExe: 4 kB

VmLib: 1204 kB

VmPTE: 3072 kB

這是在B被殺之前的記錄:

VmPeak: 1072512 kB

VmSize: 1072512 kB

VmHWM: 234636 kB

VmRSS: 204692 kB

VmData: 1071200 kB

VmPTE: 1064 kB

VmRSS需要再詳細點解釋。RSS是Resident Set Size,也就是目前程序在記憶體中配置設定的塊。也注意,在B到OOM之前已經用掉了幾乎全部交換分區,而A根本沒用。很明顯malloc()除了保留記憶體之外什麼也沒做。

另外一個問題是:既然沒有寫頁,為什麼有3056M這個上限?這暴露出另外一個限制。在32位系統上,記憶體位址有4GB。其中0-3GB是使用者使用,3-4GB為核心空間。

注意:有核心更新檔可以實作全部配置設定4GB給使用者空間,需要一些上下文切換的開銷。

OOM的結論:

VM中沒有可用頁。

沒有足夠的使用者位址空間。

以上兩者。

是以避免這些情況的政策是:

知道使用者空間有多少。

知道可用頁有多少。

當使用malloc()申請記憶體塊時,你實際是要runtime的C庫檢視是否有預先配置設定的塊可用。這個塊尺寸至少應當和使用者請求一樣大。如果 有,malloc()會指派這個塊給使用者并标記為使用。否則malloc()必須通過擴充堆棧heap得到更多記憶體。所有申請的塊都放在堆棧裡。不要和 stack混淆,stack是用來存儲本地變量和函數傳回位址的。

Heap到底在哪裡?可以看看程序位址映射:

$ cat /proc/self/maps

0039d000-003b2000 r-xp 00000000 16:41 1080084 /lib/ld-2.3.3.so

003b2000-003b3000 r-xp 00014000 16:41 1080084 /lib/ld-2.3.3.so

003b3000-003b4000 rwxp 00015000 16:41 1080084 /lib/ld-2.3.3.so

003b6000-004cb000 r-xp 00000000 16:41 1080085 /lib/tls/libc-2.3.3.so

004cb000-004cd000 r-xp 00115000 16:41 1080085 /lib/tls/libc-2.3.3.so

004cd000-004cf000 rwxp 00117000 16:41 1080085 /lib/tls/libc-2.3.3.so

004cf000-004d1000 rwxp 004cf000 00:00 0

08048000-0804c000 r-xp 00000000 16:41 130592 /bin/cat

0804c000-0804d000 rwxp 00003000 16:41 130592 /bin/cat

0804d000-0806e000 rwxp 0804d000 00:00 0 [heap]

b7d95000-b7f95000 r-xp 00000000 16:41 2239455 /usr/lib/locale/locale-archive

b7f95000-b7f96000 rwxp b7f95000 00:00 0

b7fa9000-b7faa000 r-xp b7fa9000 00:00 0 [vdso]

bfe96000-bfeab000 rw-p bfe96000 00:00 0 [stack]

這是cat實際的映射分布。你的結果可能不一樣,取決于核心和排程的C庫。最近的核心(2.6.x)都有标記,但是不能完全依賴這些标記。

Heap基本上是沒有配置設定給程式映射和stack的自由空間,是以會縮小可用的位址空間,也就是3GB減去所有映射掉的部分。

當A不能配置設定記憶體塊時看起來什麼樣子?對程式小小調整一下,暫停下來看看:

0009a000-0039d000 rwxp 0009a000 00:00 0 ---------&gt; (allocated block)

005ce000-08048000 rwxp 005ce000 00:00 0 ———&gt; (allocated block)

08048000-08049000 r-xp 00000000 16:06 1267 /test-program/loop

08049000-0804a000 rwxp 00000000 16:06 1267 /test-program/loop

0806d000-b7f62000 rwxp 0806d000 00:00 0 ———&gt; (allocated block)

b7f73000-b7f75000 rwxp b7f73000 00:00 0 ———&gt; (allocated block)

b7f75000-b7f76000 r-xp b7f75000 00:00 0 [vdso]

b7f76000-bf7ee000 rwxp b7f76000 00:00 0 ———&gt; (allocated block)

bf80d000-bf822000 rw-p bf80d000 00:00 0 [stack]

bf822000-bff29000 rwxp bf822000 00:00 0 ———&gt; (allocated block)

六個虛拟記憶體區域VMA,反映出了記憶體請求。VMA是一組有相同通路權限的記憶體頁,可以存在于使用者空間的任意位置。

你現在會想,為什麼是六個,而不是一個大區域?有兩個原因。第一,一般很難在記憶體中找到這麼大的“洞”。第二,程式不會一次申請所有的記憶體。是以glibc配置設定器可以在可用的頁根據需要自由規劃。

為什麼我說是在可用的頁?記憶體配置設定是以頁的尺寸為機關的。這不是OS的限制,而是記憶體管理單元MMU的特性。頁的尺寸不一定,一般x86平台是 4K。你可以通過getpagesize() 或者 sysconf() (_SC_PAGESIZE參數)獲得。libc配置設定器管理所有頁:分成較小的塊,指派給程序,釋放,等等。比如說,程式使用4097位元組,你需要兩個 頁,盡管實際上配置設定器給你的在4105-4109位元組之間。

使用256M記憶體,無交換分區的情況下,你有65536個可用頁。對嗎?不完全是。要知道一些記憶體區域被核心代碼和資料占用,還有一些保留給緊急情況或者高優先的需求。dmesg可以顯示這些資訊:

$ dmesg | grep -n kernel

36:Memory: 255716k/262080k available (2083k kernel code, 5772k reserved,

637k data, 172k init, 0k highmem)

171:Freeing unused kernel memory: 172k freed

核心代碼和資料在初始化時使用的init部分172K,之後會被核心釋放。這樣實際占用了2083 + 5772 + 637 = 8492位元組。。實際的說,2123個頁沒有了。如果使用更多核心特性和子產品,就會消耗更多。

另外一個核心的資料結構是頁緩沖。頁緩沖儲存着讀塊裝置的内容。緩沖的越多,可用的記憶體越少。不過如果系統記憶體不夠,核心會回收緩沖占用的記憶體。

從核心和硬體的角度,以下非常重要:

不能保證配置設定的記憶體實體上連續;他們隻是虛拟的連續。

這個假象來自位址轉換的方式。在保護模式環境,使用者使用虛拟位址,而硬體使用實體位址。頁目錄和頁表起到轉換作用。比如說兩個開始于0和4096的塊實際上可能映射到1024和8192位址。

這樣配置設定更容易。因為很難找到連續的塊。核心将尋找滿足需要的塊而不是連續的塊,也會調整頁表使之看起來虛拟連續。

這也有代價。因為記憶體塊的不連續,有時CPU L1和L2的緩沖會欠滿,虛拟連續的記憶體分散在不同的實體緩沖行,會減慢連續的記憶體通路。

記憶體配置設定包括兩步:第一步擴充記憶體區域的長度,然後根據需要配置設定頁。這就是按需分頁。在VMA擴充過程中,核心隻檢查請求是否和現有VMA重疊,範圍是否在使用者空間内。預設情況下,會忽略檢查是否能進行實際的配置設定。

是以,如果你的應用程式能請求并得到1G記憶體,而你隻有16M加64Mswap也沒什麼奇怪。這種樂觀的方式大家都滿意。核心有對應的參數可以調整過度承諾。

有兩種頁類型:匿名頁和檔案頁。當你在磁盤上mmap()一個檔案就産生了檔案頁,匿名頁來自malloc()。他們和檔案無關。當記憶體緊張時,内 核會把匿名頁交換出去并清空檔案頁。換句話說,匿名頁會消耗交換分區。例外是,mmap()的檔案有MAP_PRIVATE标簽。這時檔案的修尬隻發生在 記憶體中。

這些幫助你了解如何把swap當記憶體擴充。當然,通路一個頁需要它回到記憶體裡。

<a href="http://blog.chinaunix.net/link.php?url=http://www.linuxdevcenter.com%2Flpt%2Fa%2F%253C%2521--CS_NEXT_REF--%253E" target="_blank"></a>

<b>配置設定器内幕</b>

實際的工作由glibc記憶體配置設定器完成。配置設定器把塊交給程式,從核心的heap中去掉。

配置設定器就是經理,核心是勞工。這樣就能明白,最大的效率來自好的配置設定器而非核心。

glibc uses an allocator named ptmalloc. Wolfram Gloger created it as a modified version of the original malloc library created by Doug Lea. The allocator manages the allocated blocks in terms of “chunks.” Chunks represent the memory block you actually requested, but not its size. There is an extra header added inside this chunk besides the user data.

glibc使用ptmalloc作為配置設定器。Wolfram Gloger創造了這個修改版以替代Doug Lea的malloc。配置設定器使用chunk管理所有配置設定的塊。chunk代表你實際申請的記憶體塊,但不是那個尺寸。在塊内部還有一個額外的頭資訊。

The allocator uses two functions to get a chunk of memory from the kernel:

配置設定器使用兩個函數得到對應的記憶體chunk:

brk() 設定程序資料段的結尾。

mmap() 建立一個VMA,傳遞給配置設定器。

當然,malloc()隻當目前池中沒有chunk時才使用這些函數。

The decision on whether to use brk() or mmap() requires one simple check. If the request is equal or larger than M_MMAP_THRESHOLD, the allocator uses mmap(). If it is smaller, the allocator calls brk(). By default, M_MMAP_THRESHOLD is 128KB, but you may freely change it by using mallopt().

使用brk()或者mmap()需要一個簡單的檢查。如果請求大于等于M_MMAP_THRESHOLD,配置設定器使用mmap()。如果是小于,就使用brk()。預設情況下M_MMAP_THRESHOLD為128K,可以使用mallopt()調整。

在OOM情況下,ptmalloc如何釋放記憶體是很有趣的。使用mmap()配置設定的塊通過unmap()釋放之後就完全釋放了,使用brk()配置設定的塊是 做釋放标記,但是他們仍在配置設定器控制之下。如果另外一個malloc()請求尺寸小于等于自由chunk。配置設定器可以把多個連續的自由chunk合并,也 可以把它分割來滿足要求。

這也就是說,一個自由chunk可能因為不能用來滿足請求而被丢棄。失敗的自由chunk合并也會加速OOM的産生。這也是糟糕的記憶體碎片的标志。

恢複

一旦發生了OOM,怎麼辦?核心會終止一個程序。為什麼?這是唯一終止進一步請求記憶體的方法。核心不會假設程序有什麼機制能自動終止,唯一的選擇就是殺掉。

核心怎麼知道改殺誰呢?答案在mm/oom_kill.c源碼中。這個所謂的OOM殺手用函數badness()衡量現有程序的得分。得分最高的就是受害者。以下是評分标準:

VM尺寸。這不是所有配置設定頁的尺寸,而是程序擁有的所有VMA的總量。尺寸越大得分越高。

和一有關,子程序的VM尺寸也很重要。這個計數是累積的。

程序優先級小于0的(nice過的)得分高。

超級使用者的程序被假設更重要,因而得分低。

程序運作時。時間越長得分越低。

程序進行直接硬體通路的可以免疫。

swapper和init以及其他核心線程都免疫。

程序得分最高的赢得選舉,然後被殺。

這個機制不完美,但是基本有效。标準一和二非常明确的表明VMA的尺寸的重要性,而不是實際頁的數量。你可能覺得VMA尺寸也許會導緻假警報,但是 其實不會。badness()調用發生在頁配置設定函數中,當隻有少數自由頁而回收失敗時,是以基本上這個值很接近程序擁有的頁數。

為什麼不數實際的頁數呢?因為這樣需要更多時間和更多鎖,也導緻快速判斷的開銷增大。是以OOM并不完美,也可能殺錯。

核心使用SIGTERM信号通知目标程序關閉。

<b>如何降低OOM風險</b>

簡單的規則:不要配置設定超出實際空閑的記憶體。然而,有很多因素會影響結果,是以政策要更精細一點兒:

通過有序的配置設定減少碎片

不需要進階的配置設定器。你可以通過有序的配置設定和釋放減少碎片。使用LIFO政策:最後配置設定的最先釋放。

比如以下代碼:

void *a;

void *b;

void *c;

…………

a = malloc(1024);

b = malloc(5678);

c = malloc(4096);

………………….

free(b);

b = malloc(12345);

可以換成:

這樣,a 和c 兩個chunk之間就不會有漏洞。你也可以考慮使用realloc()來調整已經産生的malloc()塊的尺寸。

兩個示例示範了這個影響。程式結束時會報告系統配置設定的記憶體位元組數(核心和glibc配置設定器)以及實際使用的數量。例如,在2.6.11.1核心和glibc2.3.3.27上,不用參數fragmented1浪費了319858832 位元組(約 305 MB) 而fragmented2 浪費了 2089200 位元組 (越 2MB).152倍!

你可以進一步實驗傳遞各種參數的結果。參數是malloc()的請求尺寸。

<b>調整核心的overcommit行為</b>

You can change the behavior of the Linux kernel through the /proc filesystem, as documented inDocumentation/vm/overcommit-accounting in the Linux kernel’s source code. You have three choices when tuning kernel overcommit, expressed as numbers in /proc/sys/vm/overcommit_memory:

你可以根據Documentation/vm/overcommit-accounting通過/proc目錄的配置改變linux核心的行為。有三個選擇:

0意味着使用預設的模式判斷是否overcommit。

1意味着總是overcommit。 你現在應該知道有多危險了。

2防止過度overcommit。可以調整/proc/sys/vm/overcommit_ratio. 最大的承諾值是swap + overcommit_ratio*MEM.

一般預設就夠用了,但是模式2有更好的保護。相應的,模式2也需要你小心估計程式的需求。你肯定不想程式因為這個不能執行。當然這樣也可以避免出現被殺掉。

配置設定記憶體後檢查NULL指針,審計記憶體洩露

這是個簡單的規則,但是容易被忽略掉。檢查NULL可以知道配置設定器能夠擴充記憶體區域,雖然不保證能配置設定需要的頁。一般你需要擔保或者推後配置設定,取決于情況。和overcommit配合, malloc()會因為認為不能申請自由頁而傳回NULL,進而避免了OOM。

記憶體洩露是不必要的記憶體消耗。應用程式将不再追蹤洩露的記憶體塊但是核心也不會回收,因為核心認為程式還在用。valgrind可以用來追蹤這一現象。

<b>總是查詢記憶體配置設定統計</b>

linux核心提供了/proc/meminfo來找到記憶體狀态資訊。top free vmstat的資訊皆來于此。

你需要檢查的是自由的和可回收的記憶體。自由不用解釋,但什麼是可回收的?這是指buffer和頁cache。當記憶體緊張系統可以寫回磁盤來回收。

$ cat /proc/meminfo

MemTotal: 255944 kB

MemFree: 3668 kB

Buffers: 13640 kB

Cached: 171788 kB

SwapCached: 0 kB

HighTotal: 0 kB

HighFree: 0 kB

LowTotal: 255944 kB

LowFree: 3668 kB

SwapTotal: 909676 kB

SwapFree: 909676 kB

基于以上輸出,自由的虛拟記憶體為MemFree + Buffers + Cached + SwapFree

I failed to find any formalized C (glibc) function to find out free (including reclaimable) memory space. The closest I found is by using get_avphys_pages() or sysconf() (with the_SC_AVPHYS_PAGES parameter). They only report the amount of free memory, not the free + reclaimable amount.我不能找到一個正式的C函數來找出自由(含可回收)記憶體的空間。最接近的是get_avphys_pages() 或者 sysconf() (加 _SC_AVPHYS_PAGES 參數)他們隻報告自由記憶體總量而不是自由加可回收。

這意味着為了精确的資訊,你需要自己解析/proc/meminfo并計算。如果你懶,可以參考procps源代碼。它包含ps top free工具。

<b>關于其他記憶體配置設定器的實驗</b>

<b>使用64位平台</b>

需要使用更大使用者位址空間的人可以考慮64位計算。核心不再使用3:1方式分割VM,因而對大于4G記憶體的機器也很合适

這個和擴充位址無關,比如INTEL的PAE,允許32位的處理器定址64G記憶體。這個是實體位址定址,跟使用者無關。在虛拟位址部分使用者仍然使用3GB。多餘的記憶體可以通路,但是不是都可以映射到位址空間。不能映射的部分就不可用。

<b>考慮在結構中使用打包的類型</b>

Packed attributes can help to squeeze the size of structs, enums, and unions. This is a way to save more bytes, especially for array of structs. Here is a declaration example:打包的屬性可以壓縮struct enum 和 union的尺寸。這樣對struct尤其可以節省

struct test

char a;

long b;

} __attribute__ ((packed));

這個招數在于它使各行不對齊,因而消耗了更多的CPU周期。對齊意味着變量的位址是資料類型的原本位址的整數倍。基于資料的通路頻率,這樣會更慢,但是考慮到排序和緩沖的相關性。

在使用者程序使用ulimit() 

使用ulimit -v可以限制使用者能mmap()的記憶體位址空間。到上限後,mmap(),以及malloc()會傳回0因而OOM不會啟動。對于多使用者系統很有用,因為避免了亂殺無辜。

繼續閱讀