Linux虛擬內存實現原理

我們都知道,MongoDB 使用內存映射的方式來進行數據文件的存取操作。本文的目的就在於描述操作系統虛擬內存的使用及內存映射的內部實現。

以下是譯文

當你運行一個程序,程序中有許多東西需要存儲,堆、棧以及各種功能庫。而這一切在你寫程序時可能都不需要自己控制,Linux內核會幫你完成這些存儲的調度,你只需要告訴它你需要做什麼,內核就會在合適的地方給你分配內存空間。本文主要通過幾個實例程序的內存使用研究,來爲大家展示Linux的內存使用狀況。

第一個例子:下面一段程序會打印出程序的pid(進程號)後掛起。

#include <stdio.h>




#include <unistd.h> #include <sys/types.h> int main() { printf("run `pmap %d`\n", getpid()); pause(); }

將上面代碼保存成文件 mem_munch.c 然後運行下面程序編譯並執行:

$ gcc mem_munch.c -o mem_munch
$ ./mem_munch
run `pmap 25681`

上面進程號是25681,可能你試驗的結果會不太一樣。

下面我們通過pmap命令來查看一下這個小程序的內存使用情況

$ pmap 25681
25681:   ./mem_munch
0000000000400000      4K r-x--  /home/user/mem_munch
0000000000600000      4K r----  /home/user/mem_munch
0000000000601000      4K rw---  /home/user/mem_munch
00007fcf5af88000   1576K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b112000   2044K -----  /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b311000     16K r----  /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b315000      4K rw---  /lib/x86_64-linux-gnu/libc-2.13.so
00007fcf5b316000     24K rw---    [ anon ]
00007fcf5b31c000    132K r-x--  /lib/x86_64-linux-gnu/ld-2.13.so
00007fcf5b512000     12K rw---    [ anon ]
00007fcf5b539000     12K rw---    [ anon ]
00007fcf5b53c000      4K r----  /lib/x86_64-linux-gnu/ld-2.13.so
00007fcf5b53d000      8K rw---  /lib/x86_64-linux-gnu/ld-2.13.so
00007fff7efd8000    132K rw---    [ stack ]
00007fff7efff000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total             3984K

上面的結果是這個程序的內存使用情況,其實更確切的說是這個程序認爲它使用內存的情況。從上面的結果我們能看到,當你訪問libc庫時,實際上是對內存地址00007fcf5af88000的訪問,當你訪問ld庫時,實際上是對內存地址00007fcf5b31c000的訪問。

上面的輸出可能還比較抽象,下面我們修改一下上面的程序,我們在程序的堆和棧上各放一塊數據。

#include <stdio.h>




#include <unistd.h> #include <sys/types.h> #include <stdlib.h> int main() { int on_stack, *on_heap; //局部變量是放在棧上的,所以 on_stack 的地址就是棧的初始地址 on_stack = 42; printf("stack address: %p\n", &on_stack); //malloc 的內存是在堆上分配的 on_heap = (int*)malloc(sizeof(int)); printf("heap address: %p\n", on_heap); printf("run `pmap %d`\n", getpid()); pause(); }

編譯運行:

$ ./mem_munch
stack address: 0x7fff497670bc
heap address: 0x1b84010
run `pmap 11972`

然後再用pmap命令查看一下內存使用:

$ pmap 11972
11972:   ./mem_munch
0000000000400000      4K r-x--  /home/user/mem_munch
0000000000600000      4K r----  /home/user/mem_munch
0000000000601000      4K rw---  /home/user/mem_munch
0000000001b84000    132K rw---    [ anon ]
00007f3ec4d98000   1576K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec4f22000   2044K -----  /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec5121000     16K r----  /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec5125000      4K rw---  /lib/x86_64-linux-gnu/libc-2.13.so
00007f3ec5126000     24K rw---    [ anon ]
00007f3ec512c000    132K r-x--  /lib/x86_64-linux-gnu/ld-2.13.so
00007f3ec5322000     12K rw---    [ anon ]
00007f3ec5349000     12K rw---    [ anon ]
00007f3ec534c000      4K r----  /lib/x86_64-linux-gnu/ld-2.13.so
00007f3ec534d000      8K rw---  /lib/x86_64-linux-gnu/ld-2.13.so
00007fff49747000    132K rw---    [ stack ]
00007fff497bb000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total             4116K

這次多出了上面紅色的一行內容,紅色內容就是堆的起始位置:

0000000001b84000    132K rw---    [ anon ]

在我們程序運行的輸出裏也有一行紅色的輸出,這是這個地址在程序中的內存地址:

heap address: 0x1b84010

這兩個地址基本上是一樣的,其中的anon是Anonymous的縮寫,表明這段內存是沒有文件映射的。

我們再看上面綠色的兩行,與上面相對應,這兩行分別是用pmap 和應用程序看到的棧起始地址:

00007fff49747000    132K rw---    [ stack ]
stack address: 0x7fff497670bc

上面說到的內存使用,都只是程序認爲自己對內存的使用,實際上程序在分配內存是不知道系統內存的狀態的。所以上面的輸出都只是從程序自己的角度看到的內存使用狀況。比如在上面的例子中,我們看到程序的內存地址空間是從0×0000000000400000到0xffffffffff600000的所有地址(而0xffffffffff600000到0×00007fffffffffffffff之間的地址是有特殊用處的,這裏不多講)。這樣算下來,我們總共可以使用的內存空間有1千萬TB。

但是實際上目前沒有硬件能有1千萬TB的物理內存。爲什麼操作系統會如此設計呢?原因有很多,可以看這裏,但也正因此,我們可以使用遠遠超出物理內存大小的內存空間。

內存映射

內存映射的原理就是讓操作系統將一個文件映射到一段內存中,然後在操作這個文件內存就可以像操作內存一樣。比如我們創建一個完全內容隨機的文件,然後將它用內存映射的方式映射到一段內存空間中。那麼我們在這段內存中隨便取一位就相當於取到了一個隨機數。下面就讓我們來做這個實驗,先用下面命令生成一個內容隨機的文件。

$ dd if=/dev/urandom bs=1024 count=1000000 of=/home/user/random
1000000+0 records in
1000000+0 records out
1024000000 bytes (1.0 GB) copied, 123.293 s, 8.3 MB/s
$ ls -lh random
-rw-r--r-- 1 user user 977M 2011-08-29 16:46 random

然後我們用下面程序來將這個文件內容映射到內存,再從中取出隨機數

#include <stdio.h>




#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <sys/mman.h> int main() { char *random_bytes; FILE *f; int offset = 0; // open "random" for reading f = fopen("/home/user/random", "r"); if (!f) { perror("couldn't open file"); return -1; } // we want to inspect memory before mapping the file printf("run `pmap %d`, then press ", getpid()); getchar(); random_bytes = mmap(0, 1000000000, PROT_READ, MAP_SHARED, fileno(f), 0); if (random_bytes == MAP_FAILED) { perror("error mapping the file"); return -1; } while (1) { printf("random number: %d (press for next number)", *(int*)(random_bytes+offset)); getchar(); offset += 4; } }

然後運行這個程序:

 $ ./mem_munch
run `pmap 12727`, then press

下面我們通過一次次的按下回車鍵來從這個文件中讀取隨機數,按下幾次後我們可以再通過pmap來查看其內存空間的情況:

$ pmap 12727
12727:   ./mem_munch
0000000000400000      4K r-x--  /home/user/mem_munch
0000000000600000      4K r----  /home/user/mem_munch
0000000000601000      4K rw---  /home/user/mem_munch
000000000147d000    132K rw---    [ anon ]
00007fe261c6f000 976564K r--s-  /home/user/random
00007fe29d61c000   1576K r-x--  /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d7a6000   2044K -----  /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d9a5000     16K r----  /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d9a9000      4K rw---  /lib/x86_64-linux-gnu/libc-2.13.so
00007fe29d9aa000     24K rw---    [ anon ]
00007fe29d9b0000    132K r-x--  /lib/x86_64-linux-gnu/ld-2.13.so
00007fe29dba6000     12K rw---    [ anon ]
00007fe29dbcc000     16K rw---    [ anon ]
00007fe29dbd0000      4K r----  /lib/x86_64-linux-gnu/ld-2.13.so
00007fe29dbd1000      8K rw---  /lib/x86_64-linux-gnu/ld-2.13.so
00007ffff29b2000    132K rw---    [ stack ]
00007ffff29de000      4K r-x--    [ anon ]
ffffffffff600000      4K r-x--    [ anon ]
 total           980684K

上面的輸出和之前的大同小異,但是多出了上面紅色的一行。這是我們上面的隨機文件映射到內存中的內存。我們再使用pmap -x 選項來查看一下程序的內存使用,會得到下面的內容,其中RSS(resident set size)列表示真實佔用的內存。

pmap -x 12727
12727:   ./mem_munch
Address           Kbytes     RSS   Dirty Mode   Mapping
0000000000400000       0       4       0 r-x--  mem_munch
0000000000600000       0       4       4 r----  mem_munch
0000000000601000       0       4       4 rw---  mem_munch
000000000147d000       0       4       4 rw---    [ anon ]
00007fe261c6f000       0       4       0 r--s-  random
00007fe29d61c000       0     288       0 r-x--  libc-2.13.so
00007fe29d7a6000       0       0       0 -----  libc-2.13.so
00007fe29d9a5000       0      16      16 r----  libc-2.13.so
00007fe29d9a9000       0       4       4 rw---  libc-2.13.so
00007fe29d9aa000       0      16      16 rw---    [ anon ]
00007fe29d9b0000       0     108       0 r-x--  ld-2.13.so
00007fe29dba6000       0      12      12 rw---    [ anon ]
00007fe29dbcc000       0      16      16 rw---    [ anon ]
00007fe29dbd0000       0       4       4 r----  ld-2.13.so
00007fe29dbd1000       0       8       8 rw---  ld-2.13.so
00007ffff29b2000       0      12      12 rw---    [ stack ]
00007ffff29de000       0       4       0 r-x--    [ anon ]
ffffffffff600000       0       0       0 r-x--    [ anon ]
----------------  ------  ------  ------
total kB          980684     508     100

如果你的虛擬內存佔用(上面的Kbytes列)都是0,不用擔心,這是一個在Debian/Ubuntu系統上pmap -x命令的bug。最後一行輸出的總佔用量是正確的。

現在你可以看一下RSS那一列,這就是實際內存佔用。在random文件上,你的程序實際上可以訪問在00007fe261c6f000之前的數十億字節的內存地址,但是隻要你訪問的地址超過4KB,那麼操作系統就會去磁盤上查找內容。也就是說實際上只有4KB的物理內存被使用了。只有訪問這4KB的東西時,纔是真正的內存操作。其它部分雖然你使用的也是內存操作函數來訪問它,但是由於它沒有被加載到內存中,所以在這些內容被訪問的時候,操作系統會先去磁盤讀random中讀取內容到內存中。

如果我們把程序再修改一下,修改成下面這樣,讓程序把整個random文件都訪問一遍。

#include <stdio.h>




#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <sys/mman.h> int main() { char *random_bytes; FILE *f; int offset = 0; // open "random" for reading f = fopen("/home/user/random", "r"); if (!f) { perror("couldn't open file"); return -1; } random_bytes = mmap(0, 1000000000, PROT_READ, MAP_SHARED, fileno(f), 0); if (random_bytes == MAP_FAILED) { printf("error mapping the file\n"); return -1; } for (offset = 0; offset < 1000000000; offset += 4) { int i = *(int*)(random_bytes+offset); // to show we're making progress if (offset % 1000000 == 0) { printf("."); } } // at the end, wait for signal so we can check mem printf("\ndone, run `pmap -x %d`\n", getpid()); pause(); }

現在我們的pmap -x命令就會得到如下輸出:

$ pmap -x 5378
5378:   ./mem_munch
Address           Kbytes     RSS   Dirty Mode   Mapping
0000000000400000       0       4       4 r-x--  mem_munch
0000000000600000       0       4       4 r----  mem_munch
0000000000601000       0       4       4 rw---  mem_munch
0000000002271000       0       4       4 rw---    [ anon ]
00007fc2aa333000       0  976564       0 r--s-  random
00007fc2e5ce0000       0     292       0 r-x--  libc-2.13.so
00007fc2e5e6a000       0       0       0 -----  libc-2.13.so
00007fc2e6069000       0      16      16 r----  libc-2.13.so
00007fc2e606d000       0       4       4 rw---  libc-2.13.so
00007fc2e606e000       0      16      16 rw---    [ anon ]
00007fc2e6074000       0     108       0 r-x--  ld-2.13.so
00007fc2e626a000       0      12      12 rw---    [ anon ]
00007fc2e6290000       0      16      16 rw---    [ anon ]
00007fc2e6294000       0       4       4 r----  ld-2.13.so
00007fc2e6295000       0       8       8 rw---  ld-2.13.so
00007fff037e6000       0      12      12 rw---    [ stack ]
00007fff039c9000       0       4       0 r-x--    [ anon ]
ffffffffff600000       0       0       0 r-x--    [ anon ]
----------------  ------  ------  ------
total kB          980684  977072     104

我們可以看到,random文件映射實際佔用內存量已經和random文件大小一致了,也就是也random文件通過循環訪問,其內容已經完全加載到內存中了。現在我們再訪問random文件的任何部分,實際上都是內存操作。而不會穿透到磁盤。

話說回來,這就是爲什麼MongoDB的內存使用,可以遠遠超出操作系統物理內存大小。

            <div class="divmark" style="display: none;"></div>
        </div>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章