虛擬地址物理地址linux
虛擬地址物理地址linux
?學習啦小編整理了linux環(huán)境下虛擬地址物理地址的相關資料。供大家參考!
虛擬地址物理地址linux
內核從3G開始的那一段是連續(xù)映射
而且這種固定映射最大到896M的地址范圍,也即從0xc0000000-0xf7ffffff的虛擬地址采用固定映射,稱為內核邏輯地址.剩下的1G-896=128M范圍的虛擬地址可以映射到任意物理地址.稱為內核虛擬地址.當實際內存大于1G時(實際上是> 896M時),用這塊地址空間做映射.
實際的計算機體系結構有硬件的制約,這限制了頁框可以使用的方式。尤其是,Linux內核必須處理80x86體系結構的兩種硬件約束:
ISA總線的直接存儲器(DMA)處理器有一個嚴格的限制:它們只能對RAM的前16MB尋址。
在具有大容量RAM的現(xiàn)代32位計算機中,CPU不能直接訪問所有的物理存儲器,因為線形地址空間太小。
為了應付這兩種限制,Linux把物理存儲器劃分為三個管理區(qū)(zone):
ZONE_DMA:包含低于16MB的存儲器頁
ZONE_NORMAL:包含高于16MB且低于896MB的存儲器頁
ZONE_HIGHMEM:包含高于896MB的存儲器頁
ZONE_DMA區(qū)包含的頁可以由老式基于ISA的設備通過DMA使用。
ZONE_DMA和ZONE_NORMAL和區(qū)包含的存儲器的“常規(guī)”頁,通過把它們線性地映射到線性地址空間的第4個GB,內核就可以直接進行訪問。相反,包含的存儲器頁不能由內核直接訪問,但它們也線性映射到了線性地址空間的第4個GB。在64位體系結構上沒有使用在64位體系結構上沒有使用ZONE_NORMAL。
這里只分析分配連續(xù)物理地址的函數(shù)。對于 vmalloc() 這種分配非連續(xù)物理地址的函數(shù)不在本記錄范圍之內。
1、kmalloc() 分配連續(xù)的物理地址,用于小內存分配。
2、__get_free_page() 分配連續(xù)的物理地址,用于整頁分配。
至于為什么說以上函數(shù)分配的是連續(xù)的物理地址和返回的到底是物理地址還是虛擬地址,下面的記錄會做出解釋。
kmalloc() 函數(shù)本身是基于 slab 實現(xiàn)的。slab是為分配小內存提供的一種高效機制。但 slab 這種分配機制又不是獨立的,它本身也是在頁分配器的基礎上來劃分更細粒度的內存供調用者使用。也就是說系統(tǒng)先用頁分配器分配以頁為最小單位的連續(xù)物理地址,然后 kmalloc() 再在這上面根據(jù)調用者的需要進行切分。關于以上論述,我們可以查看malloc() 的實現(xiàn),kmalloc()函數(shù)的實現(xiàn)是在 __do_kmalloc() 中,可以看到在__do_kmalloc() 代碼里最終調用了_cache_alloc() 來分配一個slab,其實kmem_cache_alloc() 等函數(shù)的實現(xiàn)也是調用了這個函數(shù)來分配新的 slab。我們按_cache_alloc() 函數(shù)的調用路徑一直跟蹤下去會發(fā)現(xiàn)在 cache_grow() 函數(shù)中使用了kmem_getpages() 函數(shù)來分配一個物理,kmem_getpages() 函數(shù)中調用的alloc_pages_node() 最終是使用 __alloc_pages() 來返回一個struct page 結構,而這個結構正是系統(tǒng)用來描述物理頁面的。這樣也就證實了上面所說的,slab 是在物理頁面基礎上實現(xiàn)的。kmalloc() 分配的是物理地址。
__get_free_page() 是頁面分配器提供給調用者的最底層的內存分配函數(shù)。它分配連續(xù)的物理內。__get_free_page() 函數(shù)本身是基于 buddy 實現(xiàn)的。在使用 buddy 實現(xiàn)的物理內存管理中最小分配粒度是以頁為單位的。關于以上論述,我們可以查看__get_free_page() 的實現(xiàn),可以看到 __get_free_page() 函數(shù)只是一個非常簡單的封狀,它的整個函數(shù)實現(xiàn)就是無條件的調用 __alloc_pages() 函數(shù)來分配物理內存,上面記錄 kmalloc()實現(xiàn)時也提到過是在調用_alloc_pages() 函數(shù)來分配物理頁面的前提下進行的 slab 管理。那么這個函數(shù)是如何分配到物理頁面又是在什么區(qū)域中進行分配的?回答這個問題只能看下相關的實現(xiàn)??梢钥吹皆?__alloc_pages() 函數(shù)中,多次嘗試調用get_page_from_freelist() 函數(shù)從 zonelist 中取得相關 zone,并從其中返回一個可用的 struct page 頁面(這里的有些調用分支是因為標志不同)。至此,可以知道一個物理頁面的分配是從 zonelist(一個 zone 的結構數(shù)組)中的 zone 返回的。那么 zonelist/zone 是如何與物理頁面關聯(lián),又是如何初始化的呢?繼續(xù)來看 free_area_init_nodes() 函數(shù),此函數(shù)在系統(tǒng)初始化時由 zone_sizes_init() 函數(shù)間接調用的,zone_sizes_init()
函數(shù)填充了三個區(qū)域:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。并把他們作為參數(shù)調用 free_area_init_nodes(),在這個函數(shù)中會分配一個 pglist_data 結構,此結構中包含了zonelist/zone結構和一個 struct page 的物理頁結構,在函數(shù)最后用此結構作為參數(shù)調用了 free_area_init_node() 函數(shù),在這個函數(shù)中首先使用 calculate_node_totalpages() 函數(shù)標記 pglist_data 相關區(qū)域,然后調用 alloc_node_mem_map() 函數(shù)初始化 pglist_data結構中的 struct page 物理頁。最后使用free_area_init_core()函數(shù)關聯(lián) pglist_data 與 zonelist??梢奯_get_free_page()是從buddy systems分配的頁框。現(xiàn)在通以上分析已經明確了__get_free_page() 函數(shù)分配物理內存的流程。但這里又引出了幾個新問題,那就是此函數(shù)分配的物理頁面是如何映射的?映射到了什么位置?到這里不得不去看下與 VMM 相關的引導代碼。
在看 VMM 相關的引導代碼前,先來看一下virt_to_phys() 與phys_to_virt 這兩個函數(shù)。顧名思義,即是虛擬地址到物理地址和物理地址到虛擬地址的轉換。函數(shù)實現(xiàn)十分簡單,前者調用了__pa( address ) 轉換虛擬地址到物理地址,后者調用 __va( addrress ) 將物理地址轉換為虛擬地址。再看下 __pa __va 這兩個宏到底做了什么。
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
通過上面可以看到僅僅是把地址加上或減去 PAGE_OFFSET,而PAGE_OFFSET 在 x86 下定義為0xC0000000。這里又引出了疑問,在 linux 下寫過 driver 的人都知道,在使用 kmalloc() 與__get_free_page() 分配完物理地址后,如果想得到正確的物理地址需要使用 virt_to_phys() 進行轉換。那么為什么要有這一步呢?我們不分配的不就是物理地址么?怎么分配完成還需要轉換?如果返回的是虛擬地址,那么根據(jù)如上對 virt_to_phys() 的分析,為什么僅僅對 PAGE_OFFSET 操作就能實現(xiàn)地址轉換呢?虛擬地址與物理地址之間的轉換不需要查頁表么?代著以上諸多疑問來看 VMM 相關的引導代碼。
直接從 start_kernel() 內核引導部分來查找VMM 相關內容??梢钥吹降谝粋€應該關注的函數(shù)是 setup_arch(),在這個函數(shù)當中使用paging_init() 函數(shù)來初始化和映射硬件頁表(在初始化前已有 8M內存被映射,在這里不做記錄),而 paging_init() 則是調用的pagetable_init() 來完成內核物理地址的映射以及相關內存的初始化。在pagetable_init() 函數(shù)中,首先是一些PAE/PSE/PGE 相關判斷與設置,然后使用 kernel_physical_mapping_init() 函數(shù)來實現(xiàn)內核物理內存的映射。在這個函數(shù)中可以很清楚的看到,pgd_idx 是以PAGE_OFFSET 為啟始地址進行映射的,也就是說循環(huán)初始化所有物理地址是以 PAGE_OFFSET 為起點的。繼續(xù)觀察我們可以看到在 PMD 被初始化后,所有地址計算均是以 PAGE_OFFSET 作為標記來遞增的。分析到這里已經很明顯的可以看出,物理地址被映射到以 PAGE_OFFSET開始的虛擬地址空間。這樣以上所有疑問就都有了答案。kmalloc() 與__get_free_page() 所分配的物理頁面被映射到了 PAGE_OFFSET 開始的虛擬地址,也就是說實際物理地址與虛擬地址有一組一一對應的關系,正是因為有了這種映射關系,對內核以 PAGE_OFFSET 啟始的虛擬地址的分配也就是對物理地址的分配(當然這有一定的范圍,應該在 PAGE_OFFSET
與 VMALLOC_START 之間,后者為vmalloc() 函數(shù)分配內存的啟始地址)。這也就解釋了為什么 virt_to_phys() 與phys_to_virt() 函數(shù)的實現(xiàn)僅僅是加/減 PAGE_OFFSET 即可在虛擬地址與物理地址之間轉換,正是
因為了有了這種映射,且固定不變,所以才不用去查頁表進行轉換。這也同樣回答了開始的問題,即 kmalloc() / __get_free_page() 分配的是物理地址,而返回的則是虛擬地址(雖然這聽上去有些別扭)。正是因為有了這種映射關系,所以需要將它們的返回地址減去 PAGE_OFFSET 才可以得到真正的物理地址。
虛擬地址和物理地址的概念
CPU通過地址來訪問內存中的單元,地址有虛擬地址和物理地址之分,如果CPU沒有MMU(Memory Management Unit,內存管理單元),或者有MMU但沒有啟用,CPU核在取指令或訪問內存時發(fā)出的地址將直接傳到CPU芯片的外部地址引腳上,直接被內存芯片(以下稱為物理內存,以便與虛擬內存區(qū)分)接收,這稱為物理地址(Physical Address,以下簡稱PA),如下圖所示。
物理地址示意圖
如果CPU啟用了MMU,CPU核發(fā)出的地址將被MMU截獲,從CPU到MMU的地址稱為虛擬地址(Virtual Address,以下簡稱VA),而MMU將這個地址翻譯成另一個地址發(fā)到CPU芯片的外部地址引腳上,也就是將虛擬地址映射成物理地址,如下圖所示[1]。
虛擬地址示意圖
MMU將虛擬地址映射到物理地址是以頁(Page)為單位的,對于32位CPU通常一頁為4K。例如,虛擬地址0xb700 1000~0xb700 1fff是一個頁,可能被MMU映射到物理地址0x2000~0x2fff,物理內存中的一個物理頁面也稱為一個頁框(Page Frame)。
內核也不能直接訪問物理地址.但因為內核的虛擬地址和物理地址之間只是一個差值0xc0000000的區(qū)別,所以從物理地址求虛擬地址或從虛擬地址求物理地址很容易,+-這個差就行了
物理地址(physical address)
用于內存芯片級的單元尋址,與處理器和CPU連接的地址總線相對應。
——這個概念應該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把物理地址理解成插在機器上那根內存本身,把內存看成一個從0字節(jié)一直到最大空量逐字節(jié)的編號的大數(shù)組,然后把這個數(shù)組叫做物理地址,但是事實上,這只是一個硬件提供給軟件的抽像,內存的尋址方式并不是這樣。所以,說它是“與地址總線相對應”,是更貼切一些,不過拋開對物理內存尋址方式的考慮,直接把物理地址與物理的內存一一對應,也是可以接受的。也許錯誤的理解更利于形而上的抽像。
虛擬內存(virtual memory)
這是對整個內存(不要與機器上插那條對上號)的抽像描述。它是相對于物理內存來講的,可以直接理解成“不直實的”,“假的”內存,例如,一個0x08000000內存地址,它并不對就物理地址上那個大數(shù)組中0x08000000 - 1那個地址元素;
之所以是這樣,是因為現(xiàn)代操作系統(tǒng)都提供了一種內存管理的抽像,即虛擬內存(virtual memory)。進程使用虛擬內存中的地址,由操作系統(tǒng)協(xié)助相關硬件,把它“轉換”成真正的物理地址。這個“轉換”,是所有問題討論的關鍵。
有了這樣的抽像,一個程序,就可以使用比真實物理地址大得多的地址空間。(拆東墻,補西墻,銀行也是這樣子做的),甚至多個進程可以使用相同的地址。不奇怪,因為轉換后的物理地址并非相同的。
——可以把連接后的程序反編譯看一下,發(fā)現(xiàn)連接器已經為程序分配了一個地址,例如,要調用某個函數(shù)A,代碼不是call A,而是call 0x0811111111 ,也就是說,函數(shù)A的地址已經被定下來了。沒有這樣的“轉換”,沒有虛擬地址的概念,這樣做是根本行不通的。
Linux下獲取虛擬地址對應的物理地址的方法
* /proc/pid/pagemap. This file lets a userspace process find out which
physical frame each virtual page is mapped to. It contains one 64-bit
value for each virtual page, containing the following data (from
fs/proc/task_mmu.c, above pagemap_read):
* Bits 0-54 page frame number (PFN) if present
* Bits 0-4 swap type if swapped
* Bits 5-54 swap offset if swapped
* Bit 55 pte is soft-dirty (see Documentation/vm/soft-dirty.txt)
* Bits 56-60 zero
* Bit 61 page is file-page or shared-anon
* Bit 62 page swapped
* Bit 63 page present
If the page is not present but in swap, then the PFN contains an
encoding of the swap file number and the page's offset into the
swap. Unmapped pages return a null PFN. This allows determining
precisely which pages are mapped (or in swap) and comparing mapped
pages between processes.
接下來,我們根據(jù)上述描述,給出獲取虛擬地址對應的物理地址的代碼
#include <stdio.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define page_map_file "/proc/self/pagemap"
#define PFN_MASK ((((uint64_t)1)<<55)-1)
#define PFN_PRESENT_FLAG (((uint64_t)1)<<63)
int mem_addr_vir2phy(unsigned long vir, unsigned long *phy)
{
int fd;
int page_size=getpagesize();
unsigned long vir_page_idx = vir/page_size;
unsigned long pfn_item_offset = vir_page_idx*sizeof(uint64_t);
uint64_t pfn_item;
fd = open(page_map_file, O_RDONLY);
if (fd<0)
{
printf("open %s failed", page_map_file);
return -1;
}
if ((off_t)-1 == lseek(fd, pfn_item_offset, SEEK_SET))
{
printf("lseek %s failed", page_map_file);
return -1;
}
if (sizeof(uint64_t) != read(fd, &pfn_item, sizeof(uint64_t)))
{
printf("read %s failed", page_map_file);
return -1;
}
if (0==(pfn_item & PFN_PRESENT_FLAG))
{
printf("page is not present");
return -1;
}
*phy = (pfn_item & PFN_MASK)*page_size + vir % page_size;
return 0;
}
如果擔心vir地址對應的頁面不在內存中,可以在調用mem_addr_vir2phy之前,先訪問一下此地址。
例如, int a=*(int *)(void *)vir;
如果擔心Linux的swap功能將進程的頁面交換到硬盤上從而導致頁面的物理地址變化,可以關閉swap功能。
下面兩個C庫函數(shù)可以阻止Linux將當前進程的部分或全部頁面交換到硬盤上。
int mlock(const void *addr, size_t len);
int mlockall(int flags);
看過“虛擬地址物理地址linux ”的人還看了: