1. 程式人生 > >記憶體對映 mmap的理解(轉載+整理)

記憶體對映 mmap的理解(轉載+整理)

前言

上一篇解釋了RMQ為了提高大檔案的讀寫效率,使用了記憶體對映的方法,將磁碟上的檔案與程序中的程序虛擬空間進行了對映,減少一次核心空間到使用者空間的一次複製。看到這裡我就有了疑惑,既然記憶體對映有這麼好的特性,為什麼還需要傳統的IO呢?看下文的分析。

程序中的虛擬記憶體

mmap是將檔案與程序虛擬空間進行了對映,所以你需要先明白程序虛擬空間是什麼概念。下圖的左邊就是一個程序地址空間可檢視。

在這裡插入圖片描述

你可以看到程序地址空間有分成好多一段段的,比如text資料段、初始資料段等。我們把這個段也稱為個虛擬記憶體區域。可以看到記憶體對映的記憶體區域位於堆疊之間的空餘部分。

Linux通過下圖的方式來組織虛擬記憶體。這裡其他先不看,重點關注以下vm_area_struct
在這裡插入圖片描述

在Linux核心,我們使用vm_area_struct結構來表示一個虛擬記憶體區域,一個具體的vm_area_struct包含以下欄位:

  • vm_start:指向這個區域的起始處。
  • vm_end:指向這個區域的結束處。
  • vm_port:描述這個區域包含的所有頁的讀寫許可權
  • vm_flags:描述這個區域是否是私有的還是共享的
  • vm_next:指向連結串列中下一個區域結構。

為了解釋清楚這裡說一下上圖中與vm_area_struct有關聯的task_strcut

mm_strcut

核心系統為每個程序維護一個單獨的任務結構在核心原始碼中就是task_strcut ,該結構中的元素包含核心執行該程序所需要的所有資訊,(如PID、執行使用者棧的指標、程式計數器)。

任務結構中的一個條目指向mm_struct,它描述了虛擬記憶體的當前狀態。其中有兩個欄位是我們感興趣的,pgd 和 mmap。pgd指向第一級頁表的基址,mmap指向vm_area_struct的連結串列。

mmap記憶體對映原理

mmap記憶體對映的實現過程,總的來說可以分為三個階段:

(一)程序啟動對映過程,並在虛擬地址空間中為對映建立虛擬對映區域

1、程序在使用者空間呼叫庫函式mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

2、在當前程序的虛擬地址空間中,尋找一段空閒的滿足要求的連續的虛擬地址

3、為此虛擬區分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化

4、將新建的虛擬區結構(vm_area_struct)插入程序的虛擬地址區域連結串列或樹中

(二)呼叫核心空間的系統呼叫函式mmap(不同於使用者空間函式),實現檔案實體地址和程序虛擬地址的一一對映關係

5、為對映分配了新的虛擬地址區域後,通過待對映的檔案指標,在檔案描述符表中找到對應的檔案描述符,通過檔案描述符,連結到核心“已開啟檔案集”中該檔案的檔案結構體(struct file),每個檔案結構體維護著和這個已開啟檔案相關各項資訊。

6、通過該檔案的檔案結構體,連結到file_operations模組,呼叫核心函式mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同於使用者空間庫函式。

7、核心mmap函式通過虛擬檔案系統inode模組定位到檔案磁碟實體地址。

8、通過remap_pfn_range函式建立頁表,即實現了檔案地址和虛擬地址區域的對映關係。此時,這片虛擬地址並沒有任何資料關聯到主存中。

(三)程序發起對這片對映空間的訪問,引發缺頁異常,實現檔案內容到實體記憶體(主存)的拷貝

注:前兩個階段僅在於建立虛擬區間並完成地址對映,但是並沒有將任何檔案資料的拷貝至主存。真正的檔案讀取是當程序發起讀或寫操作時。

9、程序的讀或寫操作訪問虛擬地址空間這一段對映地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因為目前只建立了地址對映,真正的硬碟資料還沒有拷貝到記憶體中,因此引發缺頁異常。

10、缺頁異常進行一系列判斷,確定無非法操作後,核心發起請求調頁過程。

11、調頁過程先在交換快取空間(swap cache)中尋找需要訪問的記憶體頁,如果沒有則呼叫nopage函式把所缺的頁從磁碟裝入到主存中。

12、之後程序即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間後系統會自動回寫髒頁面到對應磁碟地址,也即完成了寫入到檔案的過程。

注:修改過的髒頁面並不會立即更新迴文件中,而是有一段時間的延遲,可以呼叫msync()來強制同步, 這樣所寫的內容就能立即儲存到檔案裡了。

mmap使用細節

1、使用mmap需要注意的一個關鍵點是,mmap對映區域大小必須是物理頁大小(page_size)的整倍數(32位系統中通常是4k位元組)。原因是,記憶體的最小粒度是頁,而程序虛擬地址空間和記憶體的對映也是以頁為單位。為了匹配記憶體的操作,mmap從磁碟到虛擬地址空間的對映也必須是頁。

2、核心可以跟蹤被記憶體對映的底層物件(檔案)的大小,程序可以合法的訪問在當前檔案大小以內又在記憶體對映區以內的那些位元組。也就是說,如果檔案的大小一直在擴張,只要在對映區域範圍內的資料,程序都可以合法得到,這和對映建立時檔案的大小無關。具體情形參見“情形三”。

3、對映建立之後,即使檔案關閉,對映依然存在。因為對映的是磁碟的地址,不是檔案本身,和檔案控制代碼無關。同時可用於程序間通訊的有效地址空間不完全受限於被對映檔案的大小,因為是按頁對映。

在上面的知識前提下,我們下面看看如果大小不是頁的整倍數的具體情況:

情形一:一個檔案的大小是5000位元組,mmap函式從一個檔案的起始位置開始,對映5000位元組到虛擬記憶體中。

分析:因為單位物理頁面的大小是4096位元組,雖然被對映的檔案只有5000位元組,但是對應到程序虛擬地址區域的大小需要滿足整頁大小,因此mmap函式執行後,實際對映到虛擬記憶體區域8192個 位元組,5000~8191的位元組部分用零填充。對映後的對應關係如下圖所示:
在這裡插入圖片描述

此時:

(1)讀/寫前5000個位元組(0~4999),會返回操作檔案內容。

(2)讀位元組50008191時,結果全為0。寫50008191時,程序不會報錯,但是所寫的內容不會寫入原檔案中 。

(3)讀/寫8192以外的磁碟部分,會返回一個SIGSECV錯誤。

情形二:一個檔案的大小是5000位元組,mmap函式從一個檔案的起始位置開始,對映15000位元組到虛擬記憶體中,即對映大小超過了原始檔案的大小。

分析:由於檔案的大小是5000位元組,和情形一一樣,其對應的兩個物理頁。那麼這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現在原檔案中。由於程式要求對映15000位元組,而檔案只佔兩個物理頁,因此8192位元組~15000位元組都不能讀寫,操作時會返回異常。如下圖所示:
在這裡插入圖片描述

此時:

(1)程序可以正常讀/寫被對映的前5000位元組(0~4999),寫操作的改動會在一定時間後反映在原檔案中。

(2)對於5000~8191位元組,程序可以進行讀寫過程,不會報錯。但是內容在寫入前均為0,另外,寫入後不會反映在檔案中。

(3)對於8192~14999位元組,程序不能對其進行讀寫,會報SIGBUS錯誤。

(4)對於15000以外的位元組,程序不能對其讀寫,會引發SIGSEGV錯誤。

情形三:一個檔案初始大小為0,使用mmap操作映射了1000*4K的大小,即1000個物理頁大約4M位元組空間,mmap返回指標ptr。

分析:如果在對映建立之初,就對檔案進行讀寫操作,由於檔案大小為0,並沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。

但是如果,每次操作ptr讀寫前,先增加檔案的大小,那麼ptr在檔案大小內部的操作就是合法的。例如,檔案擴充4096位元組,ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要檔案擴充的範圍在1000個物理頁(對映範圍)內,ptr都可以對應操作相同的大小。

這樣,方便隨時擴充檔案空間,隨時寫入檔案,不造成空間浪費。

為什麼不直接用記憶體對映代替IO?

現在回到一開始的問題,既然記憶體對映可以提高檔案的讀取效率,為什麼還要使用IO呢?

首先你要明白一點,直接將檔案對映到虛擬記憶體,意味著沒有資料沒有快取在核心快取空間,而是直接讀到了使用者空間,回想一下系統的IO和核心快取搭配可以是的部分的檔案使用效率更高。而且從mmap的細節我們可以看到,對映的檔案最好是大於4k的(一個記憶體頁的大小),並且最好是4k的倍數。也就是說兩個方式都是有優缺點的,所以不存在代替這個說法,只能通過分析其場景而選擇不同的方式。

而RMQ的commitlog剛好1G,是符合記憶體對映的高效率的特點,所以RMQ可以使用記憶體對映加快檔案的讀寫效率。

參考文章

https://www.cnblogs.com/huxiao-tee/p/4660352.html#4008787