天天看點

Linux記憶體管理之kmalloc 與 __get_free_page()

      在裝置驅動程式中動态開辟記憶體,不是用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 才可以得到真正的實體位址。

 

繼續閱讀