學(xué)習(xí)啦>學(xué)習(xí)電腦>網(wǎng)絡(luò)知識>網(wǎng)絡(luò)基礎(chǔ)知識> 虛擬地址物理地址linux

虛擬地址物理地址linux

時間: 春健736 分享

虛擬地址物理地址linux

  ?學(xué)習(xí)啦小編整理了linux環(huán)境下虛擬地址物理地址的相關(guān)資料。供大家參考!

  虛擬地址物理地址linux

  內(nèi)核從3G開始的那一段是連續(xù)映射

  而且這種固定映射最大到896M的地址范圍,也即從0xc0000000-0xf7ffffff的虛擬地址采用固定映射,稱為內(nèi)核邏輯地址.剩下的1G-896=128M范圍的虛擬地址可以映射到任意物理地址.稱為內(nèi)核虛擬地址.當(dāng)實際內(nèi)存大于1G時(實際上是> 896M時),用這塊地址空間做映射.

  實際的計算機(jī)體系結(jié)構(gòu)有硬件的制約,這限制了頁框可以使用的方式。尤其是,Linux內(nèi)核必須處理80x86體系結(jié)構(gòu)的兩種硬件約束:

  ISA總線的直接存儲器(DMA)處理器有一個嚴(yán)格的限制:它們只能對RAM的前16MB尋址。

  在具有大容量RAM的現(xiàn)代32位計算機(jī)中,CPU不能直接訪問所有的物理存儲器,因為線形地址空間太小。

  為了應(yīng)付這兩種限制,Linux把物理存儲器劃分為三個管理區(qū)(zone):

  ZONE_DMA:包含低于16MB的存儲器頁

  ZONE_NORMAL:包含高于16MB且低于896MB的存儲器頁

  ZONE_HIGHMEM:包含高于896MB的存儲器頁

  ZONE_DMA區(qū)包含的頁可以由老式基于ISA的設(shè)備通過DMA使用。

  ZONE_DMA和ZONE_NORMAL和區(qū)包含的存儲器的“常規(guī)”頁,通過把它們線性地映射到線性地址空間的第4個GB,內(nèi)核就可以直接進(jìn)行訪問。相反,包含的存儲器頁不能由內(nèi)核直接訪問,但它們也線性映射到了線性地址空間的第4個GB。在64位體系結(jié)構(gòu)上沒有使用在64位體系結(jié)構(gòu)上沒有使用ZONE_NORMAL。

  這里只分析分配連續(xù)物理地址的函數(shù)。對于 vmalloc() 這種分配非連續(xù)物理地址的函數(shù)不在本記錄范圍之內(nèi)。

  1、kmalloc() 分配連續(xù)的物理地址,用于小內(nèi)存分配。

  2、__get_free_page() 分配連續(xù)的物理地址,用于整頁分配。

  至于為什么說以上函數(shù)分配的是連續(xù)的物理地址和返回的到底是物理地址還是虛擬地址,下面的記錄會做出解釋。

  kmalloc() 函數(shù)本身是基于 slab 實現(xiàn)的。slab是為分配小內(nèi)存提供的一種高效機(jī)制。但 slab 這種分配機(jī)制又不是獨立的,它本身也是在頁分配器的基礎(chǔ)上來劃分更細(xì)粒度的內(nèi)存供調(diào)用者使用。也就是說系統(tǒng)先用頁分配器分配以頁為最小單位的連續(xù)物理地址,然后 kmalloc() 再在這上面根據(jù)調(diào)用者的需要進(jìn)行切分。關(guān)于以上論述,我們可以查看malloc() 的實現(xiàn),kmalloc()函數(shù)的實現(xiàn)是在 __do_kmalloc() 中,可以看到在__do_kmalloc() 代碼里最終調(diào)用了_cache_alloc() 來分配一個slab,其實kmem_cache_alloc() 等函數(shù)的實現(xiàn)也是調(diào)用了這個函數(shù)來分配新的 slab。我們按_cache_alloc() 函數(shù)的調(diào)用路徑一直跟蹤下去會發(fā)現(xiàn)在 cache_grow() 函數(shù)中使用了kmem_getpages() 函數(shù)來分配一個物理,kmem_getpages() 函數(shù)中調(diào)用的alloc_pages_node() 最終是使用 __alloc_pages() 來返回一個struct page 結(jié)構(gòu),而這個結(jié)構(gòu)正是系統(tǒng)用來描述物理頁面的。這樣也就證實了上面所說的,slab 是在物理頁面基礎(chǔ)上實現(xiàn)的。kmalloc() 分配的是物理地址。

  __get_free_page() 是頁面分配器提供給調(diào)用者的最底層的內(nèi)存分配函數(shù)。它分配連續(xù)的物理內(nèi)。__get_free_page() 函數(shù)本身是基于 buddy 實現(xiàn)的。在使用 buddy 實現(xiàn)的物理內(nèi)存管理中最小分配粒度是以頁為單位的。關(guān)于以上論述,我們可以查看__get_free_page() 的實現(xiàn),可以看到 __get_free_page() 函數(shù)只是一個非常簡單的封狀,它的整個函數(shù)實現(xiàn)就是無條件的調(diào)用 __alloc_pages() 函數(shù)來分配物理內(nèi)存,上面記錄 kmalloc()實現(xiàn)時也提到過是在調(diào)用_alloc_pages() 函數(shù)來分配物理頁面的前提下進(jìn)行的 slab 管理。那么這個函數(shù)是如何分配到物理頁面又是在什么區(qū)域中進(jìn)行分配的?回答這個問題只能看下相關(guān)的實現(xiàn)??梢钥吹皆?__alloc_pages() 函數(shù)中,多次嘗試調(diào)用get_page_from_freelist() 函數(shù)從 zonelist 中取得相關(guān) zone,并從其中返回一個可用的 struct page 頁面(這里的有些調(diào)用分支是因為標(biāo)志不同)。至此,可以知道一個物理頁面的分配是從 zonelist(一個 zone 的結(jié)構(gòu)數(shù)組)中的 zone 返回的。那么 zonelist/zone 是如何與物理頁面關(guān)聯(lián),又是如何初始化的呢?繼續(xù)來看 free_area_init_nodes() 函數(shù),此函數(shù)在系統(tǒng)初始化時由 zone_sizes_init() 函數(shù)間接調(diào)用的,zone_sizes_init()

  函數(shù)填充了三個區(qū)域:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。并把他們作為參數(shù)調(diào)用 free_area_init_nodes(),在這個函數(shù)中會分配一個 pglist_data 結(jié)構(gòu),此結(jié)構(gòu)中包含了zonelist/zone結(jié)構(gòu)和一個 struct page 的物理頁結(jié)構(gòu),在函數(shù)最后用此結(jié)構(gòu)作為參數(shù)調(diào)用了 free_area_init_node() 函數(shù),在這個函數(shù)中首先使用 calculate_node_totalpages() 函數(shù)標(biāo)記 pglist_data 相關(guān)區(qū)域,然后調(diào)用 alloc_node_mem_map() 函數(shù)初始化 pglist_data結(jié)構(gòu)中的 struct page 物理頁。最后使用free_area_init_core()函數(shù)關(guān)聯(lián) pglist_data 與 zonelist。可見__get_free_page()是從buddy systems分配的頁框?,F(xiàn)在通以上分析已經(jīng)明確了__get_free_page() 函數(shù)分配物理內(nèi)存的流程。但這里又引出了幾個新問題,那就是此函數(shù)分配的物理頁面是如何映射的?映射到了什么位置?到這里不得不去看下與 VMM 相關(guān)的引導(dǎo)代碼。

  在看 VMM 相關(guān)的引導(dǎo)代碼前,先來看一下virt_to_phys() 與phys_to_virt 這兩個函數(shù)。顧名思義,即是虛擬地址到物理地址和物理地址到虛擬地址的轉(zhuǎn)換。函數(shù)實現(xiàn)十分簡單,前者調(diào)用了__pa( address ) 轉(zhuǎn)換虛擬地址到物理地址,后者調(diào)用 __va( addrress ) 將物理地址轉(zhuǎn)換為虛擬地址。再看下 __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ìn)行轉(zhuǎn)換。那么為什么要有這一步呢?我們不分配的不就是物理地址么?怎么分配完成還需要轉(zhuǎn)換?如果返回的是虛擬地址,那么根據(jù)如上對 virt_to_phys() 的分析,為什么僅僅對 PAGE_OFFSET 操作就能實現(xiàn)地址轉(zhuǎn)換呢?虛擬地址與物理地址之間的轉(zhuǎn)換不需要查頁表么?代著以上諸多疑問來看 VMM 相關(guān)的引導(dǎo)代碼。

  直接從 start_kernel() 內(nèi)核引導(dǎo)部分來查找VMM 相關(guān)內(nèi)容。可以看到第一個應(yīng)該關(guān)注的函數(shù)是 setup_arch(),在這個函數(shù)當(dāng)中使用paging_init() 函數(shù)來初始化和映射硬件頁表(在初始化前已有 8M內(nèi)存被映射,在這里不做記錄),而 paging_init() 則是調(diào)用的pagetable_init() 來完成內(nèi)核物理地址的映射以及相關(guān)內(nèi)存的初始化。在pagetable_init() 函數(shù)中,首先是一些PAE/PSE/PGE 相關(guān)判斷與設(shè)置,然后使用 kernel_physical_mapping_init() 函數(shù)來實現(xiàn)內(nèi)核物理內(nèi)存的映射。在這個函數(shù)中可以很清楚的看到,pgd_idx 是以PAGE_OFFSET 為啟始地址進(jìn)行映射的,也就是說循環(huán)初始化所有物理地址是以 PAGE_OFFSET 為起點的。繼續(xù)觀察我們可以看到在 PMD 被初始化后,所有地址計算均是以 PAGE_OFFSET 作為標(biāo)記來遞增的。分析到這里已經(jīng)很明顯的可以看出,物理地址被映射到以 PAGE_OFFSET開始的虛擬地址空間。這樣以上所有疑問就都有了答案。kmalloc() 與__get_free_page() 所分配的物理頁面被映射到了 PAGE_OFFSET 開始的虛擬地址,也就是說實際物理地址與虛擬地址有一組一一對應(yīng)的關(guān)系,正是因為有了這種映射關(guān)系,對內(nèi)核以 PAGE_OFFSET 啟始的虛擬地址的分配也就是對物理地址的分配(當(dāng)然這有一定的范圍,應(yīng)該在 PAGE_OFFSET

  與 VMALLOC_START 之間,后者為vmalloc() 函數(shù)分配內(nèi)存的啟始地址)。這也就解釋了為什么 virt_to_phys() 與phys_to_virt() 函數(shù)的實現(xiàn)僅僅是加/減 PAGE_OFFSET 即可在虛擬地址與物理地址之間轉(zhuǎn)換,正是

  因為了有了這種映射,且固定不變,所以才不用去查頁表進(jìn)行轉(zhuǎn)換。這也同樣回答了開始的問題,即 kmalloc() / __get_free_page() 分配的是物理地址,而返回的則是虛擬地址(雖然這聽上去有些別扭)。正是因為有了這種映射關(guān)系,所以需要將它們的返回地址減去 PAGE_OFFSET 才可以得到真正的物理地址。

  虛擬地址和物理地址的概念

  CPU通過地址來訪問內(nèi)存中的單元,地址有虛擬地址和物理地址之分,如果CPU沒有MMU(Memory Management Unit,內(nèi)存管理單元),或者有MMU但沒有啟用,CPU核在取指令或訪問內(nèi)存時發(fā)出的地址將直接傳到CPU芯片的外部地址引腳上,直接被內(nèi)存芯片(以下稱為物理內(nèi)存,以便與虛擬內(nèi)存區(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,物理內(nèi)存中的一個物理頁面也稱為一個頁框(Page Frame)。

  內(nèi)核也不能直接訪問物理地址.但因為內(nèi)核的虛擬地址和物理地址之間只是一個差值0xc0000000的區(qū)別,所以從物理地址求虛擬地址或從虛擬地址求物理地址很容易,+-這個差就行了

  物理地址(physical address)

  用于內(nèi)存芯片級的單元尋址,與處理器和CPU連接的地址總線相對應(yīng)。

  ——這個概念應(yīng)該是這幾個概念中最好理解的一個,但是值得一提的是,雖然可以直接把物理地址理解成插在機(jī)器上那根內(nèi)存本身,把內(nèi)存看成一個從0字節(jié)一直到最大空量逐字節(jié)的編號的大數(shù)組,然后把這個數(shù)組叫做物理地址,但是事實上,這只是一個硬件提供給軟件的抽像,內(nèi)存的尋址方式并不是這樣。所以,說它是“與地址總線相對應(yīng)”,是更貼切一些,不過拋開對物理內(nèi)存尋址方式的考慮,直接把物理地址與物理的內(nèi)存一一對應(yīng),也是可以接受的。也許錯誤的理解更利于形而上的抽像。

  虛擬內(nèi)存(virtual memory)

  這是對整個內(nèi)存(不要與機(jī)器上插那條對上號)的抽像描述。它是相對于物理內(nèi)存來講的,可以直接理解成“不直實的”,“假的”內(nèi)存,例如,一個0x08000000內(nèi)存地址,它并不對就物理地址上那個大數(shù)組中0x08000000 - 1那個地址元素;

  之所以是這樣,是因為現(xiàn)代操作系統(tǒng)都提供了一種內(nèi)存管理的抽像,即虛擬內(nèi)存(virtual memory)。進(jìn)程使用虛擬內(nèi)存中的地址,由操作系統(tǒng)協(xié)助相關(guān)硬件,把它“轉(zhuǎn)換”成真正的物理地址。這個“轉(zhuǎn)換”,是所有問題討論的關(guān)鍵。

  有了這樣的抽像,一個程序,就可以使用比真實物理地址大得多的地址空間。(拆東墻,補(bǔ)西墻,銀行也是這樣子做的),甚至多個進(jìn)程可以使用相同的地址。不奇怪,因為轉(zhuǎn)換后的物理地址并非相同的。

  ——可以把連接后的程序反編譯看一下,發(fā)現(xiàn)連接器已經(jīng)為程序分配了一個地址,例如,要調(diào)用某個函數(shù)A,代碼不是call A,而是call 0x0811111111 ,也就是說,函數(shù)A的地址已經(jīng)被定下來了。沒有這樣的“轉(zhuǎn)換”,沒有虛擬地址的概念,這樣做是根本行不通的。

  Linux下獲取虛擬地址對應(yīng)的物理地址的方法

  * /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ù)上述描述,給出獲取虛擬地址對應(yīng)的物理地址的代碼

  #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;

  }

  如果擔(dān)心vir地址對應(yīng)的頁面不在內(nèi)存中,可以在調(diào)用mem_addr_vir2phy之前,先訪問一下此地址。

  例如, int a=*(int *)(void *)vir;

  如果擔(dān)心Linux的swap功能將進(jìn)程的頁面交換到硬盤上從而導(dǎo)致頁面的物理地址變化,可以關(guān)閉swap功能。

  下面兩個C庫函數(shù)可以阻止Linux將當(dāng)前進(jìn)程的部分或全部頁面交換到硬盤上。

  int mlock(const void *addr, size_t len);

  int mlockall(int flags);

  看過“虛擬地址物理地址linux ”的人還看了:

1.linux虛擬地址怎么映射物理地址

2.物理地址與虛擬地址映射

3.物理地址與虛擬地址怎么轉(zhuǎn)換

4.Linux關(guān)于虛擬內(nèi)存

5.物理地址和邏輯地址的區(qū)別

604436