使用者態程式的虛擬地址如何轉換成實體地址

山羊哥-老宋發表於2020-09-29

使用者態程式的虛擬地址如何轉換成實體地址?

區分一個程式,我們都知道最簡單就是程式的pid。我們就從(pid,virtualaddress)來看看如何將一個程式的虛擬地址轉換為實體地址phyaddress。

  • 首先根據pid我們可以得到這個程式的task_struct,進而通過task_struct得到mm,通過mm得到pgd。
    好了,現在我們有pgd和virtualaddress.
    通過pgd和virtualaddress我們可以得到頁表pte.
  • 有了pte和virtualaddress,我們就可以計算實體地址了
    phyaddress=(pte_val(pte)&PAGE_MASK)|(virtualladdress&~PAGE_MASK)
  • 實體地址既然出來了,訪問這個地址的值就比較簡單了,只需要將實體地址轉換為核心線性地址就行。
    phyaddress=((char *)phyaddress+PAGE_OFFSET)

mmap

mmap基礎概念

mmap是一種記憶體對映檔案的方法,即將一個檔案或者其它物件對映到程式的地址空間,實現檔案磁碟地址和程式虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的對映關係後,程式就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫read,write等系統呼叫函式。相反,核心空間對這段區域的修改也直接反映使用者空間,從而可以實現不同程式間的檔案共享。如下圖所示:
記憶體對映
由上圖可以看出,程式的虛擬地址空間,由多個虛擬記憶體區域構成。虛擬記憶體區域是程式的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址範圍。上圖中所示的text資料段(程式碼段)、初始資料段、BSS資料段、堆、棧和記憶體對映,都是一個獨立的虛擬記憶體區域。而為記憶體對映服務的地址空間處在堆疊之間的空餘部分。

linux核心使用vm_area_struct結構來表示一個獨立的虛擬記憶體區域,由於每個不同質的虛擬記憶體區域功能和內部機制都不同,因此一個程式使用多個vm_area_struct結構來分別表示不同型別的虛擬記憶體區域。各個vm_area_struct結構使用連結串列或者樹形結構連結,方便程式快速訪問,如下圖所示:
執行緒對映地址

vm_area_struct結構中包含區域起始和終止地址以及其他相關資訊,同時也包含一個vm_ops指標,其內部可引出所有針對這個區域可以使用的系統呼叫函式。這樣,程式對某一虛擬記憶體區域的任何操作需要用要的資訊,都可以從vm_area_struct中獲得。mmap函式就是要建立一個新的vm_area_struct結構,並將其與檔案的物理磁碟地址相連。

mmap記憶體對映原理

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

  1. 程式啟動對映過程,並在虛擬地址空間中為對映建立虛擬對映區域
  • 程式在使用者空間呼叫庫函式mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
  • 在當前程式的虛擬地址空間中,尋找一段空閒的滿足要求的連續的虛擬地址
  • 為此虛擬區分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化
  • 將新建的虛擬區結構(vm_area_struct)插入程式的虛擬地址區域連結串列或樹中
  1. 呼叫核心空間的系統呼叫函式mmap(不同於使用者空間函式),實現檔案實體地址和程式虛擬地址的一一對映關係
  • 為對映分配了新的虛擬地址區域後,通過待對映的檔案指標,在檔案描述符表中找到對應的檔案描述符,通過檔案描述符,連結到核心“已開啟檔案集”中該檔案的檔案結構體(struct file),每個檔案結構體維護著和這個已開啟檔案相關各項資訊。
  • 通過該檔案的檔案結構體,連結到file_operations模組,呼叫核心函式mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同於使用者空間庫函式。
  • 核心mmap函式通過虛擬檔案系統inode模組定位到檔案磁碟實體地址。
  • 通過remap_pfn_range函式建立頁表,即實現了檔案地址和虛擬地址區域的對映關係。此時,這片虛擬地址並沒有任何資料關聯到主存中。
  1. 程式發起對這片對映空間的訪問,引發缺頁異常,實現檔案內容到實體記憶體(主存)的拷貝

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

  • 程式的讀或寫操作訪問虛擬地址空間這一段對映地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因為目前只建立了地址對映,真正的硬碟資料還沒有拷貝到記憶體中,因此引發缺頁異常。
  • 缺頁異常進行一系列判斷,確定無非法操作後,核心發起請求調頁過程。
  • 調頁過程先在交換快取空間(swap cache)中尋找需要訪問的記憶體頁,如果沒有則呼叫nopage函式把所缺的頁從磁碟裝入到主存中。
  • 之後程式即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間後系統會自動回寫髒頁面到對應磁碟地址,也即完成了寫入到檔案的過程。

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

mmap詳解

UMA和NUMA:

UMA(Uniform Memory Access),即一致性記憶體訪問。這種情況下,CPU訪問記憶體的任何位置,代價都是一樣的。

NUMA(Non Uniform Memory Access),即非一致性記憶體訪問。這種情況下,CPU訪問不同位置的記憶體,代價是不一樣的。在多CPU情況下,對每個CPU來說有本地記憶體和遠端記憶體,訪問本地記憶體的代價比訪問遠端記憶體的代價小。確保CPU訪問記憶體代價最小,是非常重要的一點。

Linux支援多種硬體體系結構,因此Linux必須採用通用的方法來描述記憶體,以方便對記憶體進行管理。為此,Linux有了記憶體節點、記憶體區、頁框的概念,這些概念也是一目瞭然的。

  • 記憶體節點:主要依據CPU訪問代價的不同而劃分。多CPU下環境下,本地記憶體和遠端記憶體就是不同的節點。即使在單CPU環境下,訪問所有記憶體的代價都是一樣的,Linux核心依然存在記憶體節點的概念,只不過只有一個記憶體節點而已。核心以struct pg_data_t來描述記憶體分割槽。
  • 記憶體分割槽:Linux對記憶體節點再進行劃分,分為不同的分割槽。核心以struct zone來描述記憶體分割槽。通常一個節點分為DMA、Normal和High Memory記憶體區,具體下面再介紹。
  • 頁框:Linux採用頁式記憶體管理,頁是實體記憶體管理的基本單位,每個記憶體分割槽又由大量的頁框組成。核心以struct page來描述頁框。頁框有很多屬性,這些屬性描述了這個頁框的狀態、用途等,例如是否被分配。

在這裡插入圖片描述

上圖中的zone_mem_map是一個頁框的陣列,它記錄了一個記憶體分割槽的所有頁框的使用情況。

mmap優點總結

由上文討論可知,mmap優點共有一下幾點:

  1. 對檔案的讀取操作跨過了頁快取,減少了資料的拷貝次數,用記憶體讀寫取代I/O讀寫,提高了檔案讀取效率。

  2. 實現了使用者空間和核心空間的高效互動方式。兩空間的各自修改操作可以直接反映在對映的區域內,從而被對方空間及時捕捉。

  3. 提供程式間共享記憶體及相互通訊的方式。不管是父子程式還是無親緣關係的程式,都可以將自身使用者空間對映到同一個檔案或匿名對映到同一片區域。從而通過各自對對映區域的改動,達到程式間通訊和程式間共享的目的。

    同時,如果程式A和程式B都對映了區域C,當A第一次讀取C時通過缺頁從磁碟複製檔案頁到記憶體中;但當B再讀C的相同頁面時,雖然也會產生缺頁異常,但是不再需要從磁碟中複製檔案過來,而可直接使用已經儲存在記憶體中的檔案資料。

  4. 可用於實現高效的大規模資料傳輸。記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是藉助硬碟空間協助操作,補充記憶體的不足。但是進一步會造成大量的檔案I/O操作,極大影響效率。這個問題可以通過mmap對映很好的解決。換句話說,但凡是需要用磁碟空間代替記憶體的時候,mmap都可以發揮其功效。

mmap相關函式

函式原型

void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset);

返回說明
成功執行時,mmap()返回被對映區的指標。失敗時,mmap()返回MAP_FAILED[其值為(void *)-1],error被設為以下的某個值:

引數
start:對映區的開始地址
length:對映區的長度
prot:期望的記憶體保護標誌,不能與檔案的開啟模式衝突。是以下的某個值,可以通過or運算合理地組合在一起
flags:指定對映物件的型別,對映選項和對映頁是否可以共享。它的值可以是一個或者多個以下位的組合體
fd:有效的檔案描述詞。如果MAP_ANONYMOUS被設定,為了相容問題,其值應為-1
offset:被對映物件內容的起點

int munmap( void * addr, size_t len ) 
成功執行時,munmap()返回0。失敗時,munmap返回-1,error返回標誌和mmap一致;

該呼叫在程式地址空間中解除一個對映關係,addr是呼叫mmap()時返回的地址,len是對映區的大小;
當對映關係解除後,對原來對映地址的訪問將導致段錯誤發生。

int msync( void *addr, size_t len, int flags )

一般說來,程式在對映空間的對共享內容的改變並不直接寫回到磁碟檔案中,往往在呼叫munmap()後才執行該操作。
可以通過呼叫msync()實現磁碟上檔案內容與共享記憶體區的內容一致。

相關文章