轉一篇文章,比較kmalloc/vmalloc/get_free_pages

來源:互聯網
上載者:User

對於提供了MMU(儲存管理器,輔助作業系統進行記憶體管理,提供虛真實位址轉換等硬體支援)的處理器而言,Linux提供了複雜的儲存管理系統,使得進程所能訪問的記憶體達到4GB。

進程的4GB記憶體空間被人為的分為兩個部分--使用者空間與核心空間。使用者空間地址分布從0到3GB(PAGE_OFFSET,在0x86中它等於0xC0000000),3GB到4GB為核心空間。

核心空間中,從3G到vmalloc_start這段地址是實體記憶體映射地區(該地區中包含了核心鏡像、物理頁框表mem_map等等),比如我們 使用 的 VMware虛擬系統記憶體是160M,那麼3G~3G+160M這片記憶體就應該映射實體記憶體。在實體記憶體映射區之後,就是vmalloc地區。對於 160M的系統而言,vmalloc_start位置應在3G+160M附近(在實體記憶體映射區與vmalloc_start期間還存在一個8M的gap 來防止躍界),vmalloc_end的位置接近4G(最後位置系統會保留一片128k大小的地區用於專用頁面映射)

kmalloc和get_free_page申請的記憶體位於實體記憶體映射地區,而且在物理上也是連續的,它們與真實的物理地址只有一個固定的位移,因此存在較簡單的轉換關係,virt_to_phys()可以實現核心虛擬位址轉化為物理地址:
   #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
   extern inline unsigned long virt_to_phys(volatile void * address)
   {
        return __pa(address);
   }
上面轉換過程是將虛擬位址減去3G(PAGE_OFFSET=0XC000000)。

與之對應的函數為phys_to_virt(),將核心物理地址轉化為虛擬位址:
   #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
   extern inline void * phys_to_virt(unsigned long address)
   {
        return __va(address);
   }
virt_to_phys()和phys_to_virt()都定義在include\asm-i386\io.h中。

而vmalloc申請的記憶體則位於vmalloc_start~vmalloc_end之間,與物理地址沒有簡單的轉換關係,雖然在邏輯上它們也是連續的,但是在物理上它們不要求連續。

我們用下面的程式來示範kmalloc、get_free_page和vmalloc的區別:
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");
unsigned char *pagemem;
unsigned char *kmallocmem;
unsigned char *vmallocmem;

int __init mem_module_init(void)
{
//最好每次記憶體申請都檢查申請是否成功
//下面這段僅僅作為示範的代碼沒有檢查
pagemem = (unsigned char*)get_free_page(0);
printk("<1>pagemem addr=%x", pagemem);

kmallocmem = (unsigned char*)kmalloc(100, 0);
printk("<1>kmallocmem addr=%x", kmallocmem);

vmallocmem = (unsigned char*)vmalloc(1000000);
printk("<1>vmallocmem addr=%x", vmallocmem);

return 0;
}

void __exit mem_module_exit(void)
{
free_page(pagemem);
kfree(kmallocmem);
vfree(vmallocmem);
}

module_init(mem_module_init);
module_exit(mem_module_exit);

我們的系統上有160MB的記憶體空間,運行一次上述程式,發現pagemem的地址在0xc7997000(約3G+121M)、 kmallocmem 地址在0xc9bc1380(約3G+155M)、vmallocmem的地址在0xcabeb000(約3G+171M)處,符合前文所述的記憶體布局。

1、kmalloc() 分配連續的物理地址,用於小記憶體配置。

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

至於為什麼說以上函數分配的是連續的物理地址和返回的到底是物理地址還是虛擬位址,下面的記錄會做出解釋。

  kmalloc() 函數本身是基於 slab 實現的。slab 是為分配小記憶體提供的一種高效機制。但 slab 這種分配機制又不是獨立的,它本身也是在頁分配器的基礎上來劃分更細粒度的記憶體供調用者使用。也就是說系統先用頁分配器分配以頁為最小單位的連續物理地 址,然後 kmalloc() 再在這上面根據調用者的需要進行切分。

  關於以上論述,我們可以查看 kmalloc() 的實現,kmalloc()函數的實現是在 __do_kmalloc() 中,可以看到在 __do_kmalloc()代碼裡最終調用了 __cache_alloc() 來分配一個 slab,其實kmem_cache_alloc() 等函數的實現也是調用了這個函數來分配新的 slab。我們按照 __cache_alloc()函數的調用路徑一直跟蹤下去會發現在 cache_grow()
函數中使用了 kmem_getpages()函數來分配一個物理頁面,kmem_getpages() 函數中調用的alloc_pages_node() 最終是使用 __alloc_pages() 來返回一個struct page 結構,而這個結構正是系統用來描述物理頁面的。這樣也就證實了上面所說的,slab 是在物理頁面基礎上實現的。kmalloc() 分配的是物理地址。

  __get_free_page() 是頁面分配器提供給調用者的最底層的記憶體配置函數。它分配連續的實體記憶體。__get_free_page() 函數本身是基於 buddy 實現的。在使用 buddy 實現的實體記憶體管理中最小分配粒度是以頁為單位的。關於以上論述,我們可以查看__get_free_page()的實現,可以看到 __get_free_page()函數只是一個非常簡單的封狀,它的整個函數實現就是無條件的調用 __alloc_pages() 函數來分配實體記憶體,上面記錄 kmalloc()實現時也提到過是在調用
__alloc_pages() 函數來分配物理頁面的前提下進行的 slab 管理。那麼這個函數是如何分配到物理頁面又是在什麼地區中進行分配的?回答這個問題只能看下相關的實現。可以看到在 __alloc_pages() 函數中,多次嘗試調用get_page_from_freelist() 函數從 zonelist 中取得相關 zone,並從其中返回一個可用的 struct page 頁面(這裡的有些調用分支是因為標誌不同)。至此,可以知道一個物理頁面的分配是從 zonelist(一個 zone 的結構數組)中的
zone 返回的。那麼 zonelist/zone 是如何與物理頁面關聯,又是如何初始化的呢?繼續來看 free_area_init_nodes() 函數,此函數在系統初始化時由 zone_sizes_init() 函數間接調用的,zone_sizes_init()函數填充了三個地區:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。並把他 們作為參數調用 free_area_init_nodes(),在這個函數中會分配一個 pglist_data 結構,此結構中包含了 zonelist/zone結構和一個
struct page 的物理頁結構,在函數最後用此結構作為參數調用了 free_area_init_node() 函數,在這個函數中首先使用 calculate_node_totalpages() 函數標記 pglist_data 相關地區,然後調用 alloc_node_mem_map() 函數初始化 pglist_data結構中的 struct page 物理頁。最後使用 free_area_init_core()函數關聯 pglist_data 與 zonelist。現在通以上分析已經明確了__get_free_page()
函數分配實體記憶體的流程。但這裡又引出了幾個新問題,那就是此函數分配的物理頁面是如何映射的?映射到了什麼位置?到這裡不得不去看下與 VMM 相關的引導代碼。

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

  直接從 start_kernel() 核心引導部分來尋找 VMM 相關內容。可以看到第一個應該關注的函數是 setup_arch(),在這個函數當中使用paging_init() 函數來初始化和映射硬體頁表(在初始化前已有 8M記憶體被映射,在這裡不做記錄),而 paging_init() 則是調用的pagetable_init() 來完成核心物理地址的映射以及相關記憶體的初始化。在 pagetable_init() 函數中,首先是一些 PAE/PSE/PGE 相關判斷與設定,然後使用 kernel_physical_mapping_init()
函數來實現核心實體記憶體的映射。在這個函數中可以很清楚的看到,pgd_idx 是以PAGE_OFFSET 為啟始地址進行映射的,也就是說迴圈初始化所有物理地址是以 PAGE_OFFSET 為起點的。繼續觀察我們可以看到在 PMD 被初始化後,所有地址計算均是以 PAGE_OFFSET 作為標記來遞增的。分析到這裡已經很明顯的可以看出,物理地址被映射到以 PAGE_OFFSET 開始的虛擬位址空間。這樣以上所有疑問就都有了答案。kmalloc() 與__get_free_page() 所分配的物理頁面被映射到了
PAGE_OFFSET 開始的虛擬位址,也就是說實際物理地址與虛擬位址有一組一一對應的關係,

  正是因為有了這種映射關係,對核心以 PAGE_OFFSET 啟始的虛擬位址的分配也就是對物理地址的分配(當然這有一定的範圍,應該在 PAGE_OFFSET與 VMALLOC_START 之間,後者為 vmalloc() 函數分配記憶體的啟始地址)。這也就解釋了為什麼 virt_to_phys() 與 phys_to_virt() 函數的實現僅僅是加/減 PAGE_OFFSET 即可在虛擬位址與物理地址之間轉換,正是因為了有了這種映射,且固定不變,所以才不用去查頁表進行轉換。這也同樣回答了開始的問題,即 kmalloc()
/ __get_free_page() 分配的是物理地址,而返回的則是虛擬位址(雖然這聽上去有些彆扭)。正是因為有了這種映射關係,所以需要將它們的返回地址減去 PAGE_OFFSET 才可以得到真正的物理地址。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.