十問 Linux 虛擬內存管理 (glibc) (一)

作者:陳福榮

最近在做 MySQL 版本升級時( 5.1->5.5 ) , 發現了 mysqld 疑似「內存泄露」現象,但通過 valgrind 等工具檢測後,並沒發現類似的問題。因此,需要深入學習 Linux 的虛擬內存管理方面的內容來解釋這個現象。

Linux 的虛擬內存管理有幾個關鍵概念:

  1. 每個進程有獨立的虛擬地址空間,進程訪問的虛擬地址並不是真正的物理地址

  2. 虛擬地址可通過每個進程上頁表與物理地址進行映射,獲得真正物理地址

  3. 如果虛擬地址對應物理地址不在物理內存中,則產生缺頁中斷,真正分配物理地址,同時更新進程的頁表;如果此時物理內存已耗盡,則根據內存替換演算法淘汰部分頁面至物理磁碟中。

基於以上認識,這篇文章通過本人以前對虛擬內存管理的疑惑由淺入深整理了以下十個問題,並通過例子和系統命令嘗試進行解答。

  1. Linux 虛擬地址空間如何分布? 32 位和 64 位有何不同?

  2. malloc 是如何分配內存的?

  3. malloc 分配多大的內存,就佔用多大的物理內存空間嗎?

  4. 如何查看進程虛擬地址空間的使用情況?

  5. free 的內存真的釋放了嗎(還給 OS ) ?

  6. 程序代碼中 malloc 的內存都有相應的 free ,就不會出現內存泄露了嗎?

  7. 既然堆內內存不能直接釋放,為什麼不全部使用 mmap 來分配?

  8. 如何查看進程的缺頁中斷信息?

  9. 如何查看堆內內存的碎片情況?

  10. 除了 glibc 的 malloc/free ,還有其他第三方實現嗎?

一.Linux 虛擬地址空間如何分布? 32 位和 64 位有何不同?

Linux 使用虛擬地址空間,大大增加了進程的定址空間,由低地址到高地址分別為:

  1. 只讀段:該部分空間只能讀,不可寫,包括代碼段、 rodata 段( C 常量字元串和 #define 定義的常量)
  2. 數據段:保存全局變數、靜態變數的空間
  3. 堆 :就是平時所說的動態內存, malloc/new 大部分都來源於此。其中堆頂的位置可通過函數 brk 和 sbrk 進行動態調整。
  4. 文件映射區域 :如動態庫、共享內存等映射物理空間的內存,一般是 mmap 函數所分配的虛擬地址空間。
  5. 棧:用於維護函數調用的上下文空間,一般為 8M ,可通過 ulimit –s 查看。
  6. 內核虛擬空間:用戶代碼不可見的內存區域,由內核管理。

下圖是 32 位系統典型的虛擬地址空間分布(來自《深入理解計算機系統》)。

32 位系統有 4G 的地址空間,其中0x08048000~0xbfffffff 是用戶空間, 0xc0000000~0xffffffff 是內核空間,包括內核代碼和數據、與進程相關的數據結構(如頁表、內核棧)等。另外, %esp 執行棧頂,往低地址方向變化; brk/sbrk 函數控制堆頂往高地址方向變化。

可通過以下代碼驗證進程的地址空間分布,其中 sbrk(0) 函數用於返回棧頂指針。

#include <stdlib.h>n#include <stdio.h>n#include <string.h>n#include <unistd.h>nint global_num = 0;nchar global_str_arr [65536] = {a};nint main(int argc, char** argv)n{n char* heap_var = NULL;n int local_var = 0;n printf("Address of function main 0x%lxn", main);n printf("Address of global_num 0x%lxn", &global_num);n printf("Address of global_str_arr 0x%lx ~ 0x%lxn", &global_str_arr[0], &global_str_arr[65535]);n printf("Top of stack is 0x%lxn", &local_var);n printf("Top of heap is 0x%lxn", sbrk(0));n heap_var = malloc(sizeof(char) * 127 * 1024);n printf("Address of heap_var is 0x%lxn", heap_var);n printf("Top of heap after malloc is 0x%lxn", sbrk(0));n free(heap_var);n heap_var = NULL;n printf("Top of heap after free is 0x%lxn", sbrk(0));n return 1;n}n

32 位系統的結果如下,與上圖的劃分保持一致,並且棧頂指針在 mallloc 和 free 一個 127K 的存儲空間時都發生了變化(增大和縮小)。

Address of function main 0x8048474nAddress of global_num 0x8059904nAddress of global_str_arr 0x8049900 ~ 0x80598ffnTop of stack is 0xbfd0886cnTop of heap is 0x805a000nAddress of heap_var is 0x805a008nTop of heap after malloc is 0x809a000nTop of heap after free is 0x807b000n

但是, 64 位系統結果怎樣呢? 64 位系統是否擁有 2^64 的地址空間嗎?

64 位系統運行結果如下:

Address of function main 0x400594nAddress of global_num 0x610b90nAddress of global_str_arr 0x600b80 ~ 0x610b7fnTop of stack is 0x7fff2e9e4994nTop of heap is 0x8f5000nAddress of heap_var is 0x8f5010nTop of heap after malloc is 0x935000nTop of heap after free is 0x916000n

從結果知,與上圖的分布並不一致。而事實上, 64 位系統的虛擬地址空間劃分發生了改變:

  1. 地址空間大小不是 2^32 ,也不是 2^64 ,而一般是 2^48 。因為並不需要 2^64 這麼大的定址空間,過大空間只會導致資源的浪費。 64 位 Linux 一般使用 48 位來表示虛擬地址空間, 40 位表示物理地址,這可通過 /proc/cpuinfo 來查看

    address sizes : 40 bits physical, 48 bits virtual

  2. 其中, 0x0000000000000000~0x00007fffffffffff表示用戶空間,0xFFFF800000000000~ 0xFFFFFFFFFFFFFFFF表示內核空間,共提供 256TB(2^48) 的定址空間。這兩個區間的特點是,第 47 位與 48~63 位相同,若這些位為 0 表示用戶空間,否則表示內核空間。

  3. 用戶空間由低地址到高地址仍然是只讀段、數據段、堆、文件映射區域和棧

二.malloc 是如何分配內存的?

malloc 是 glibc 中內存分配函數,也是最常用的動態內存分配函數,其內存必須通過 free 進行釋放,否則導致內存泄露。

關於 malloc 獲得虛存空間的實現,與 glibc 的版本有關,但大體邏輯是:

  1. 若分配內存小於 128k ,調用 sbrk() ,將堆頂指針向高地址移動,獲得新的虛存空間。

  2. 若分配內存大於 128k ,調用 mmap() ,在文件映射區域中分配匿名虛存空間。

  3. 這裡討論的是簡單情況,如果涉及並發可能會複雜一些,不過先不討論。

其中 sbrk 就是修改棧頂指針位置,而 mmap 可用於生成文件的映射以及匿名頁面的內存,這裡指的是匿名頁面。

而這個 128k ,是 glibc 的默認配置,可通過函數 mallopt 來設置,可通過以下例子說明。

#include <stdlib.h>n#include <stdio.h>n#include <string.h>n#include <unistd.h>n#include <sys/mman.h>n#include <malloc.h>nvoid print_info(n char* var_name,n char* var_ptr,n size_t size_in_kbn)nn{n printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lxn",n var_name, size_in_kb, var_ptr, sbrk(0));nn}nint main(int argc, char** argv)n{n char *heap_var1, *heap_var2, *heap_var3 ;n char *mmap_var1, *mmap_var2, *mmap_var3 ;n char *maybe_mmap_var;n printf("Orginal heap top is 0x%lxn", sbrk(0));n heap_var1 = malloc(32*1024);n print_info("heap_var1", heap_var1, 32);n heap_var2 = malloc(64*1024);n print_info("heap_var2", heap_var2, 64);n heap_var3 = malloc(127*1024);n print_info("heap_var3", heap_var3, 127);n printf("n");n maybe_mmap_var = malloc(128*1024);n print_info("maybe_mmap_var", maybe_mmap_var, 128);n //mmapn mmap_var1 = malloc(128*1024);n print_info("mmap_var1", mmap_var1, 128);n // set M_MMAP_THRESHOLD to 64kn mallopt(M_MMAP_THRESHOLD, 64*1024);n printf("set M_MMAP_THRESHOLD to 64kn");n mmap_var2 = malloc(64*1024);n print_info("mmap_var2", mmap_var2, 64);n mmap_var3 = malloc(127*1024);n print_info("mmap_var3", mmap_var3, 127);n return 1;n}n

這個例子很簡單,通過 malloc 申請多個不同大小的動態內存,同時通過介面 print_info 列印變數大小和地址等相關信息,其中 sbrk(0) 可返回堆頂指針位置。另外,粗體部分是將 MMAP 分配的臨界點由 128k 轉為 64k ,再列印變數地址的不同。

下面是 Linux 64 位機器的執行結果(後文所有例子都是通過 64 位機器上的測試結果)。

Orginal heap top is 0x17da000nAddress of heap_var1(32k) 0x17da010, now heap top is 0x1803000nAddress of heap_var2(64k) 0x17e2020, now heap top is 0x1803000nAddress of heap_var3(127k) 0x17f2030, now heap top is 0x1832000nAddress of maybe_mmap_var(128k) 0x1811c40, now heap top is 0x1832000nAddress of mmap_var1(128k) 0x7f4a0b1f2010, now heap top is 0x1832000nset M_MMAP_THRESHOLD to 64knAddress of mmap_var2(64k) 0x7f4a0b1e1010, now heap top is 0x1832000nAddress of mmap_var3(127k) 0x7f4a0b1c1010, now heap top is 0x1832000n

三.malloc 分配多大的內存,就佔用多大的物理內存空間嗎?

我們知道, malloc 分配的的內存是虛擬地址空間,而虛擬地址空間和物理地址空間使用進程頁表進行映射,那麼分配了空間就是佔用物理內存空間了嗎?

首先,進程使用多少內存可通過 ps aux 命令 查看,其中關鍵的兩信息(第五、六列)為:

  1. VSZ , virtual memory size ,表示進程總共使用的虛擬地址空間大小,包括進程地址空間的代碼段、數據段、堆、文件映射區域、棧、內核空間等所有虛擬地址使用的總和,單位是 K

  2. RSS , resident set size ,表示進程實際使用的物理內存空間, RSS 總小於 VSZ 。

可通過一個例子說明這個問題:

#include <stdlib.h>n#include <stdio.h>n#include <string.h>n#include <unistd.h>n#include <sys/mman.h>n#include <malloc.h>nchar ps_cmd[1024];nvoid print_info(n char* var_name,n char* var_ptr,n size_t size_in_kbn)nn{n printf("Address of %s(%luk) 0x%lx, now heap top is 0x%lxn",n var_name, size_in_kb, var_ptr, sbrk(0));n system(ps_cmd);n}nnint main(int argc, char** argv)n{n char *non_set_var, *set_1k_var, *set_5k_var, *set_7k_var;n pid_t pid;n pid = getpid();n sprintf(ps_cmd, "ps aux | grep %lu | grep -v grep", pid);n non_set_var = malloc(32*1024);n print_info("non_set_var", non_set_var, 32);n set_1k_var = malloc(64*1024);n memset(set_1k_var, 0, 1024);n print_info("set_1k_var", set_1k_var, 64);n set_5k_var = malloc(127*1024);n memset(set_5k_var, 0, 5*1024);n print_info("set_5k_var", set_5k_var, 127);n set_7k_var = malloc(64*1024);n memset(set_1k_var, 0, 7*1024);n print_info("set_7k_var", set_7k_var, 64);n return 1;n}n

該代碼擴展了上一個例子print_info能力,處理列印變數信息,同時通過 ps aux 命令獲得當前進程的 VSZ 和 RSS 值。並且程序 malloc 一塊內存後,會 memset 內存的若干 k 內容。

執行結果為

Address of non_set_var(32k) 0x502010, now heap top is 0x52b000nnmysql 12183 0.0 0.0 2692 452 pts/3 S+ 20:29 0:00 ./test_vsznnAddress of set_1k_var(64k) 0x50a020, now heap top is 0x52b000nnmysql 12183 0.0 0.0 2692 456 pts/3 S+ 20:29 0:00 ./test_vsznnAddress of set_5k_var(127k) 0x51a030, now heap top is 0x55a000nnmysql 12183 0.0 0.0 2880 464 pts/3 S+ 20:29 0:00 ./test_vsznnAddress of set_7k_var(64k) 0x539c40, now heap top is 0x55a000nnmysql 12183 0.0 0.0 2880 472 pts/3 S+ 20:29 0:00 ./test_vszn

由以上結果知:

  1. VSZ 並不是每次 malloc 後都增長,是與上一節說的堆頂沒發生變化有關,因為可重用堆頂內剩餘的空間,這樣的 malloc 是很輕量快速的。

  2. 但如果 VSZ 發生變化,基本與分配內存量相當,因為 VSZ 是計算虛擬地址空間總大小。

  3. RSS 的增量很少,是因為 malloc 分配的內存並不就馬上分配實際存儲空間,只有第一次使用,如第一次 memset 後才會分配。

  4. 由於每個物理內存頁面大小是 4k ,不管 memset 其中的 1k 還是 5k 、 7k ,實際佔用物理內存總是 4k 的倍數。所以 RSS 的增量總是 4k 的倍數。

  5. 因此,不是 malloc 後就馬上佔用實際內存,而是第一次使用時發現虛存對應的物理頁面未分配,產生缺頁中斷,才真正分配物理頁面,同時更新進程頁面的映射關係。這也是 Linux 虛擬內存管理的核心概念之一。

四. 如何查看進程虛擬地址空間的使用情況?

進程地址空間被分為了代碼段、數據段、堆、文件映射區域、棧等區域,那怎麼查詢這些虛擬地址空間的使用情況呢?

Linux 提供了 pmap 命令來查看這些信息,通常使用 pmap -d $pid (高版本可提供 pmap -x $pid)查詢,如下所示:

mysql@ TLOG_590_591:~/vin/test_memory> pmap -d 17867nn17867: test_mmapnnSTART SIZE RSS DIRTY PERM OFFSET DEVICE MAPPINGnn00400000 8K 4K 0K r-xp 00000000 08:01 /home/mysql/vin/test_memory/test_mmapnn00501000 68K 8K 8K rw-p 00001000 08:01 /home/mysql/vin/test_memory/test_mmapnn00512000 76K 0K 0K rw-p 00512000 00:00 [heap]nn0053e000 256K 0K 0K rw-p 0053e000 00:00 [anon]nn2b3428f97000 108K 92K 0K r-xp 00000000 08:01 /lib64/ld-2.4.sonn2b3428fb2000 8K 8K 8K rw-p 2b3428fb2000 00:00 [anon]nn2b3428fc1000 4K 4K 4K rw-p 2b3428fc1000 00:00 [anon]nn2b34290b1000 8K 8K 8K rw-p 0001a000 08:01 /lib64/ld-2.4.sonn2b34290b3000 1240K 248K 0K r-xp 00000000 08:01 /lib64/libc-2.4.sonn2b34291e9000 1024K 0K 0K ---p 00136000 08:01 /lib64/libc-2.4.sonn2b34292e9000 12K 12K 12K r--p 00136000 08:01 /lib64/libc-2.4.sonn2b34292ec000 8K 8K 8K rw-p 00139000 08:01 /lib64/libc-2.4.sonn2b34292ee000 1048K 36K 36K rw-p 2b34292ee000 00:00 [anon]nn7fff81afe000 84K 12K 12K rw-p 7fff81afe000 00:00 [stack]nnffffffffff600000 8192K 0K 0K ---p 00000000 00:00 [vdso]nnTotal: 12144K 440K 96Kn

從這個結果可以看到進程虛擬地址空間的使用情況,包括起始地址、大小、實際使用內存、臟頁大小、許可權、偏移、設備和映射文件等。 pmap 命令就是基於下面兩文件內容進行解析的:

/proc/$pid/mapsnn /proc/$pid/smapsn

並且對於上述每個內存塊區間,內核會使用一個 vm_area_struct 結構來維護,同時通過頁面建立與物理內存的映射關係,如下圖所示。

相關推薦:

【有獎討論】程序員,怎麼應對你的三十歲? - 騰雲閣 - 騰訊雲

十問 Linux 虛擬內存管理 (glibc) (二)

Linux性能監控--CPU,Memory,IO,Network - 騰雲閣 - 騰訊雲
推薦閱讀:

你最常用的一個 linux 命令是什麼?為什麼?
像蘋果Time Machine 一樣備份Linux伺服器(基於rsync)
微軟為什麼用帶 BOM 的 UTF-8,造成和多數系統的不兼容?
vmware, visual studio, steam dota2這種需要極大內存的程序是如何分配內存的?
程序計數器(Program Counter)是一個實際存在的寄存器嗎?

TAG:Linux | MySQL | 操作系统内核 |