1. 程式人生 > >linux 記憶體管理之kmalloc

linux 記憶體管理之kmalloc

在裝置驅動程式中動態開闢記憶體,不是用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)處,符合前文所述的記憶體佈局。