在設備驅動程序中動態開辟內存,不是用malloc,而是kmalloc,或者用get_free_pages直接申請頁。釋放內存用的是kfree,或free_pages.
對於提供了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中。
-------------------------------------------------------------------------------------
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 才可以得到真正的物理地址。
另一篇更容易理解的:
kmalloc, vmalloc分配的內存結構 zz2008-01-20 16:05進程空間:| <-用戶空間-> | <-內核空間-> |
內核空間:| <-物理內存映射區-> | <-vmalloc區域-> |
==============原文================================
對於提供了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)處,符合前文所述的內存布局。