當Linux用盡記憶體

maojunxu發表於2012-03-09

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

這個問題下面隱含著別的問題:你需要分配多少記憶體?作業系統給你分配了多少?OOM的基本原因很簡單,你申請的記憶體多於系統可用量。我得說是虛擬記憶體,因為交換分割槽也包括在內。
<–more–>

瞭解OOM

開始瞭解OOM,首先試試這段會分配大量記憶體的程式碼:

#include <stdio.h>
#include <stdlib.h>


#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寫入:
#include <stdio.h>
#include <stdlib.h>


#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;
                memset(myblock,1, MEGABYTE);
                printf(”Currently allocating %d MBn”,++count);
        }
        exit(0);



}

發現差別了麼?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/<pid>/status來確認資訊。

$ cat /proc/<pid of program A>/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被殺之前的記錄:

$ cat /proc/<pid of program B>/status 
VmPeak:  1072512 kB
VmSize:  1072512 kB
VmLck:         0 kB
VmHWM:    234636 kB
VmRSS:    204692 kB
VmData:  1071200 kB
VmStk:        88 kB
VmExe:         4 kB
VmLib:      1204 kB
VmPTE:      1064 kB

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

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

注意:有核心補丁可以實現全部分配4GB給使用者空間,需要一些上下文切換的開銷。

OOM的結論:

  1. VM中沒有可用頁。
  2. 沒有足夠的使用者地址空間。
  3. 以上兩者。

所以避免這些情況的策略是:

  1. 知道使用者空間有多少。
  2. 知道可用頁有多少。

當使用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減去所有對映掉的部分。

How does the map for program A look when it can’t allocate more memory blocks? With a trivial change to pause the program (seeloop.c andloop-calloc.c)
just before it exits, the final map is:

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

0009a000-0039d000 rwxp 0009a000 00:00 0 ---------> (allocated block)
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
005ce000-08048000 rwxp 005ce000 00:00 0 ———> (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 ———> (allocated block)
b7f73000-b7f75000 rwxp b7f73000 00:00 0 ———> (allocated block)
b7f75000-b7f76000 r-xp b7f75000 00:00 0          [vdso]
b7f76000-bf7ee000 rwxp b7f76000 00:00 0 ———> (allocated block)
bf80d000-bf822000 rw-p bf80d000 00:00 0          [stack]
bf822000-bff29000 rwxp bf822000 00:00 0 ———> (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個頁沒有了。如果使用更多核心特性和模組,就會消耗更多。

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

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

  1. 不能保證分配的記憶體物理上連續;他們只是虛擬的連續。
    這個假象來自地址轉換的方式。在保護模式環境,使用者使用虛擬地址,而硬體使用實體地址。頁目錄和頁表起到轉換作用。比如說兩個開始於0和4096的塊實際上可能對映到1024和8192地址。

  2. 這樣分配更容易。因為很難找到連續的塊。核心將尋找滿足需要的塊而不是連續的塊,也會調整頁表使之看起來虛擬連續。
    這也有代價。因為記憶體塊的不連續,有時CPU L1和L2的緩衝會欠滿,虛擬連續的記憶體分散在不同的物理緩衝行,會減慢連續的記憶體訪問。
    記憶體分配包括兩步:第一步擴充套件記憶體區域的長度,然後根據需要分配頁。這就是按需分頁。在VMA擴充套件過程中,核心只檢查請求是否和現有VMA重疊,範圍是否在使用者空間內。預設情況下,會忽略檢查是否能進行實際的分配。
    所以,如果你的應用程式能請求並得到1G記憶體,而你只有16M加64Mswap也沒什麼奇怪。這種樂觀的方式大家都滿意。核心有對應的引數可以調整過度承諾。

  3. 有兩種頁型別:匿名頁和檔案頁。當你在磁碟上mmap()一個檔案就產生了檔案頁,匿名頁來自malloc()。他們和檔案無關。當記憶體緊張時,核心會把匿名頁交換出去並清空檔案頁。換句話說,匿名頁會消耗交換分割槽。例外是,mmap()的檔案有MAP_PRIVATE標籤。這時檔案的修尬只發生在記憶體中。

    這些幫助你理解如何把swap當記憶體擴充套件。當然,訪問一個頁需要它回到記憶體裡。

分配器內幕

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

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

glibc uses an allocator namedptmalloc. Wolfram Gloger created it as a modified version of the originalmalloc
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() ormmap() requires one simple check. If the request is equal or larger thanM_MMAP_THRESHOLD,
the allocator usesmmap(). If it is smaller, the allocator callsbrk(). By default,M_MMAP_THRESHOLD is 128KB,
but you may freely change it by usingmallopt().
使用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()衡量現有程式的得分。得分最高的就是受害者。以下是評分標準:

  1. VM尺寸。這不是所有分配頁的尺寸,而是程式擁有的所有VMA的總量。尺寸越大得分越高。
  2. 和一有關,子程式的VM尺寸也很重要。這個計數是累積的。
  3. 程式優先順序小於0的(nice過的)得分高。
  4. 超級使用者的程式被假設更重要,因而得分低。
  5. 程式執行時。時間越長得分越低。
  6. 程式進行直接硬體訪問的可以免疫。
  7. swapper和init以及其他核心執行緒都免疫。

程式得分最高的贏得選舉,然後被殺。

這個機制不完美,但是基本有效。標準一和二非常明確的表明VMA的尺寸的重要性,而不是實際頁的數量。你可能覺得VMA尺寸也許會導致假警報,但是其實不會。badness()呼叫發生在頁分配函式中,當只有少數自由頁而回收失敗時,所以基本上這個值很接近程式擁有的頁數。

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

核心使用SIGTERM訊號通知目標程式關閉。

如何降低OOM風險

簡單的規則:不要分配超出實際空閒的記憶體。然而,有很多因素會影響結果,所以策略要更精細一點兒:

通過有序的分配減少碎片

不需要高階的分配器。你可以通過有序的分配和釋放減少碎片。使用LIFO策略:最後分配的最先釋放。

比如以下程式碼:

        void *a;
        void *b;
        void *c;
        …………
        a = malloc(1024);
        b = malloc(5678);
        c = malloc(4096);


        ………………….



        free(b);
        b = malloc(12345);

可以換成:

        a = malloc(1024);
        c = malloc(4096);
        b = malloc(5678);
        ………………….


        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()的請求尺寸。

調整核心的overcommit行為

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可以用來追蹤這一現象。

總是查詢記憶體分配統計

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 usingget_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工具。

關於其他記憶體分配器的實驗

不同的分配器使用不同方法管理記憶體chunk。Hoard是一個例子。Emery Berger from the University of Massachusetts用它來進行高效能的記憶體分配。用於多執行緒程式,引入了每CPU heap的概念。

使用64位平臺

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

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

考慮在結構中使用打包的型別

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

struct test
   {
        char a;
        long b;
   } __attribute__ ((packed));

這個招數在於它使各行不對齊,因而消耗了更多的CPU週期。對齊意味著變數的地址是資料型別的原本地址的整數倍。基於資料的訪問頻率,這樣會更慢,但是考慮到排序和緩衝的相關性。

在使用者程式使用ulimit()

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

 

http://hi.baidu.com/shanyefeng/blog/item/5e0c501fb30080fde1fe0b71.html


相關文章