深入理解內存映射mmap

內存映射mmap是Linux內核的一個重要機制,它和虛擬內存管理以及文件IO都有直接的關係,這篇細說一下mmap的一些要點。

修改(2015-11-12):Linux的虛擬內存管理是基於mmap來實現的。vm_area_struct是在mmap的時候創建的,vm_area_strcut代表了一段連續的虛擬地址,這些虛擬地址相應地映射到一個後備文件或者一個匿名文件的虛擬頁。一個vm_area_struct映射到一組連續的頁表項。頁表項又指向物理內存page,這樣就把一個文件和物理內存頁相映射。

來理解一下虛擬地址映射的過程:拿到一個虛擬地址,根據已有的vm_area_struct看這個虛擬地址是否屬於某個vm_area_struct

  • 如果沒有匹配到,就報段錯誤,訪問了一個沒有分配的虛擬地址。
  • 如果匹配到了vm_area_struct,根據虛擬地址和頁表的映射關係,找到對應的頁表項PTE,如果PTE沒有分配,就報一個缺頁異常,去加載相應的文件數據到物理內存,如果PTE分配,就去相應的物理頁的偏移位置讀取數據

所以虛擬頁的三種狀態的實際含義如下:

  • 未分配虛擬頁,指的是沒有使用mmap建立vm_area_struct,所以也就沒有對應到具體的頁表項
  • 已分配虛擬頁,未映射到物理頁,指的是已經使用了mmap建立的vm_area_struct,可以映射到對應的頁表項,但是頁表項沒有指向具體的物理頁
  • 已分配虛擬頁,已映射到物理頁,指的是已經使用了mmap建立的vm_area_struct,可以映射到對應的頁表項,並且頁表項指向具體的物理頁

mmap要麼映射到一個後備文件,要麼映射到一個匿名文件。操作系統分配物理內存時實際用到了匿名文件的mmap。

mmap和虛擬內存管理

先來看看Linux內核的用戶進程虛擬內存管理。內核定義了mm_struct結構來表示一個用戶進程的虛擬內存地址空間。

  1. start_code, end_code指定了進程的代碼段的邊界,start_data, end_data指定了進程數據段的邊界。在ELF二進制文件映射到虛擬內存地址空間後,這幾個長度就不會再改變
  2. start_brk, brk指定了堆的邊界。start_brk表示堆的起始地址,在進程整個生命週期不會改變,brk表示堆的結束位置,會隨著堆的長度改變而改變
  3. stack_top指定了棧的起始位置,一般是
  4. task_size指定了用戶進程地址空間的長度,也就是用戶進程地址空間的頂部邊界
  5. mmap_base指定了用戶進程虛擬地址空間中用作內存映射部分的地址的基地址,這個位置不是隨機的,通常是TASK_SIZE / 3位置處
深入理解內存映射mmap

這裡各個區域的地址都是用戶進程的虛擬地址,用戶進程使用虛擬地址和頁表結構來訪問內存

  1. 首先根據所在區域的虛擬地址轉換成對應的頁表數組的數組項索引,找到頁表索引最後定為到PTE中保存的物理內存頁的頁號,加上虛擬地址低12位的offset來確定一個唯一的物理內存地址
  2. 如果物理內存地址所在的頁存在,就返回該物理地址存放的內容。如果不存在就觸發缺頁異常。虛擬內存管理採用按需分配 + 缺頁異常機制來管理頁表項和分配對應的物理內存頁。當一個虛擬地址對應的頁表項不存在時,先創建頁表結構,再分配物理內存頁,再修改頁表
深入理解內存映射mmap

進程的mm_struct除了包含虛擬內存地址空間佈局的信息,還包含了虛擬內存區域vm_area_struct的信息。虛擬內存區域vm_area_struct是內核管理用戶進程虛擬地址空間的方式,實際上數據段,文本段,共享庫這些都是通過vm_area_struct來管理的

深入理解內存映射mmap

vm_area_struct由兩種組織形式,一種是單鏈表,包含了所有創建的vm_area_struct實例,一種是紅黑樹結構,加速區域的查找。這兩個數據結構都是面向同一份vm_area_struct實例,只是組織形式不同。

再考慮一下vm_area_struct和頁表的關係,vm_area_struct本質上是一段用戶進程的虛擬地址,而我們知道虛擬地址和頁表數組的索引是對應的,頁表數組的最後一級PTE數組的數組項存放著物理內存頁的頁號,這樣就建立了虛擬內存地址到物理內存地址的對應關係。

  1. 有一種情況是先有虛擬地址,再由訪問虛擬地址引起缺頁異常去加載物理內存,再更新頁表建立虛擬地址,頁表,物理內存三者的聯繫
  2. 有一種情況(mmap)是先從設備加載文件,建立address_space,頁緩存(物理內存),再創建vm_area_struct結構,更新頁表,返回虛擬地址。
深入理解內存映射mmap

vm_area_struct的結構體如下

  1. vm_start, vm_end表示區域的開始位置和結束位置,確定了區域的邊界。兩個vm_area_struct不會出現交叉的情況
  2. vm_page_prot 表示這個區域的頁的訪問權限
  3. shared結構處理有後備文件的內存映射,和後備文件的address_space地址空間關聯起來
  4. anon_vma_node, anon_vma處理匿名文件共享內存映射的情況,映射到同一物理內存頁的映射都保存在一個鏈表中
  5. vm_pgoff, vm_file都是處理有後備文件內存映射的情況,獲得該映射在文件的頁偏移量,以及打開文件file實例的信息
深入理解內存映射mmap

對於有後備文件的映射,內核還提供了一個優先查找樹結構,來加速確定一個文件和所有映射到這個文件的虛擬內存區域vm_area_struct實例的關係,從而可以得到所有映射到這個文件的進程信息。這張圖表示了一個後備文件被mmap映射後內核建立的一些數據結構,涉及到了內存管理的數據和文件系統的數據

深入理解內存映射mmap

內核提供了一系列的函數對虛擬內存區域vm_area_struct進行操作,比如創建,刪除,合併,查找等等。而mmap是C標準庫提供給用戶程序的一個函數來使用內存映射,建立起文件地址空間和虛擬內存區域的映射關係。

mmap的4種類型

mmap分為有後備文件的映射和匿名文件的映射,這兩種映射又有私有映射和共享映射之分,所以mmap可以創建4種類型的映射

  1. 後備文件的共享映射,多個進程的vm_area_struct指向同一個物理內存區域,一個進程對文件內容的修改,會被其他進程可見。對文件內容的修改會被寫回到後備文件。
  2. 後備文件的私有映射,多個進程的vm_area_struct指向同一個物理內存區域,採用寫時拷貝的方式,當一個進程對文件內容做修改,不會被其他進程看到。另外對文件內的修改也不會被寫回到後備文件。當內存不夠需要進行頁回收時,私有映射的頁被交換到交換區。一般用在加載共享代碼庫
  3. 匿名文件的共享映射,內核創建一個初始都是0的物理內存區域,然後多個進程的vm_area_struct指向這個共享的物理內存區域,對該區域內容的修改對所有進程可見。匿名文件在頁回收時被交換到交換區
  4. 匿名文件的私有映射,內核創建一個初始都是0的物理內存區域,對該區域內容的修改只對創建者進程可見。匿名文件在頁回收時被交換到交換區。malloc()底層是用了匿名文件的私有映射來分配大塊內存。

比如下面的例子,mmap會涉及到物理內存的變化(加載後備文件到頁緩存,或者分配都是0的物理內存塊),創建vm_area_struct虛擬內存區域實例,更新頁表


// 後備文件的共享映射
fd = open("/home/xxx/a.txt", O_RDWR)
addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARD, fd, 0)

// 匿名文件的私有映射
fd = open("/dev/zero", O_RDWR)
addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_PRIVATE, fd, 0)

內存映射的用途很多,比如

  1. 後備文件的共享映射可以用作內存映射IO來對大文件進行操作,比普通IO減少一次複製。需要注意的是內存映射IO涉及到內核的很多操作,比如vm_area_struct的創建,頁表的修改等等,比普通IO的操作更復雜。小文件的讀寫使用普通IO更合適
  2. 後備文件的私有映射可以用作共享庫二進制文件代碼段,數據段的加載
  3. 匿名文件的共享映射可以用作fork時讓父子進程共享匿名映射分配的內存
  4. 匿名文件的私有映射可以用作進程的私有內存分配

內核對堆空間的管理

實際上從內核的管理用戶進程虛擬地址空間的角度來說,內存映射是管理用戶進程虛擬地址空間的主要手段,通過建立vm_area_struct來分配虛擬內存區域。內核對堆空間的分配主要是brk系統調用,brk系統調用本質上也是利用了匿名文件私有映射,分配初始化0的物理內存頁,建立vm_area_struct,然後更新頁表結構。brk系統調用分配的內存最小單位是頁,需要按頁對齊,從start_brk位置向上擴展,也就是說從內核的角度來說,每次對堆空間的分配最小就是一頁,更細粒度的字節內存空間分配由C語言標準庫實現的。


分享到:


相關文章: