對於包含 MMU 的處理器而言, Linux 系統提供了複雜的存儲管理系統,使得進程所能訪問的記憶體達到 4GB。進程的 4GB 記憶體空間被分為兩個部分—用戶空間與內核空間。用戶空間地址一般分佈為 0~3GB(即 PAGE_OFFSET),這樣,剩下的 3~4GB 為內核空間。內核空間申請記憶體涉及的函... ...
對於包含 MMU 的處理器而言, Linux 系統提供了複雜的存儲管理系統,使得進程所能訪問的記憶體達到 4GB。進程的 4GB 記憶體空間被分為兩個部分—用戶空間與內核空間。用戶空間地址一般分佈為 0~3GB(即 PAGE_OFFSET),這樣,剩下的 3~4GB 為內核空間。
內核空間申請記憶體涉及的函數主要包括 kmalloc()、__get_free_pages()和 vmalloc()等。
通過記憶體映射,用戶進程可以在用戶空間直接訪問設備。
內核地址空間
每個進程的用戶空間都是完全獨立、互不相干的,用戶進程各自有不同的頁表。而內核空間是由內核負責映射,它並不會跟著進程改變,是固定的。內核空間地址有自己對應的頁表,內核的虛擬空間獨立於其他程式。用戶進程只有通過系統調用(代表用戶進程在內核態執行)等方式才可以訪問到內核空間。
Linux 中 1GB 的內核地址空間又被劃分為物理記憶體映射區、虛擬記憶體分配區、高端頁面映射區、專用頁面映射區和系統保留映射區這幾個區域,如圖所示。
保留區
Linux 保留內核空間最頂部 FIXADDR_TOP~4GB 的區域作為保留區。
專用頁面映射區
緊接著最頂端的保留區以下的一段區域為專用頁面映射區(FIXADDR_START~FIXADDR_TOP),它的總尺寸和每一頁的用途由 fixed_address 枚舉結構在編譯時預定義,用__fix_to_virt(index)可獲取專用區內預定義頁面的邏輯地址。
高端記憶體映射區
當系統物理記憶體大於 896MB 時,超過物理記憶體映射區的那部分記憶體稱為高端記憶體(而未超過物理記憶體映射區的記憶體通常被稱為常規記憶體),內核在存取高端記憶體時必須將它們映射到高端頁面映射區。
虛存記憶體分配區
用於 vmalloc()函數,它的前部與物理記憶體映射區有一個隔離帶,後部與高端映射區也有一個隔離帶。
物理記憶體映射區
一般情況下,物理記憶體映射區最大長度為 896MB,系統的物理記憶體被順序映射在內核空間的這個區域中。
虛擬地址與物理地址關係
對於內核物理記憶體映射區的虛擬記憶體,使用 virt_to_phys()可以實現內核虛擬地址轉化為物理地址, virt_to_phys()的實現是體繫結構相關的,對於 ARM 而言, virt_to_phys()的定義如代碼:
static inline unsigned long virt_to_phys(void *x)
{
return __virt_to_phys((unsigned long)(x));
}
/* PAGE_OFFSET 通常為 3GB,而 PHYS_OFFSET 則定於為系統 DRAM 記憶體的基地址 */
#define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)
記憶體分配
在 Linux 內核空間申請記憶體涉及的函數主要包括 kmalloc()、__get_free_pages()和 vmalloc()等。kmalloc()和__get_free_pages()( 及其類似函數) 申請的記憶體位於物理記憶體映射區域,而且在物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,因此存在較簡單的轉換關係。而vmalloc()在虛擬記憶體空間給出一塊連續的記憶體區,實質上,這片連續的虛擬記憶體在物理記憶體中並不一定連續,而 vmalloc()申請的虛擬記憶體和物理記憶體之間也沒有簡單的換算關係。
kmalloc()
void *kmalloc(size_t size, int flags);
給 kmalloc()的第一個參數是要分配的塊的大小,第二個參數為分配標誌,用於控制 kmalloc()的行為。
flags
- 最常用的分配標誌是 GFP_KERNEL,其含義是在內核空間的進程中申請記憶體。 kmalloc()的底層依賴__get_free_pages()實現,分配標誌的首碼 GFP 正好是這個底層函數的縮寫。使用 GFP_KERNEL 標誌申請記憶體時,若暫時不能滿足,則進程會睡眠等待頁,即會引起阻塞,因此不能在中斷上下文或持有自旋鎖的時候使用 GFP_KERNEL 申請記憶體。
- 在中斷處理函數、 tasklet 和內核定時器等非進程上下文中不能阻塞,此時驅動應當使用GFP_ATOMIC 標誌來申請記憶體。當使用 GFP_ATOMIC 標誌申請記憶體時,若不存在空閑頁,則不等待,直接返回。
- 其他的相對不常用的申請標誌還包括 GFP_USER(用來為用戶空間頁分配記憶體,可能阻塞)、GFP_HIGHUSER(類似 GFP_USER,但是從高端記憶體分配)、 GFP_NOIO(不允許任何 I/O 初始化)、 GFP_NOFS(不允許進行任何文件系統調用)、 __GFP_DMA(要求分配在能夠 DMA 的記憶體區)、 __GFP_HIGHMEM(指示分配的記憶體可以位於高端記憶體)、 __GFP_COLD(請求一個較長時間不訪問的頁)、 __GFP_NOWARN(當一個分配無法滿足時,阻止內核發出警告)、 __GFP_HIGH(高優先順序請求,允許獲得被內核保留給緊急狀況使用的最後的記憶體頁)、 __GFP_REPEAT(分配失敗則儘力重覆嘗試)、 __GFP_NOFAIL(標誌只許申請成功,不推薦)和__GFP_NORETRY(若申請不到,則立即放棄)。
使用 kmalloc()申請的記憶體應使用 kfree()釋放,這個函數的用法和用戶空間的 free()類似。
__get_free_pages ()
__get_free_pages()系列函數/巨集是 Linux 內核本質上最底層的用於獲取空閑記憶體的方法,因為底層的伙伴演算法以 page 的 2 的 n 次冪為單位管理空閑記憶體,所以最底層的記憶體申請總是以頁為單位的。
__get_free_pages()系列函數/巨集包括 get_zeroed_page()、 __get_free_page()和__get_free_pages()。
/* 該函數返回一個指向新頁的指針並且將該頁清零 */
get_zeroed_page(unsigned int flags);
/* 該巨集返回一個指向新頁的指針但是該頁不清零 */
__get_free_page(unsigned int flags);
/* 該函數可分配多個頁並返回分配記憶體的首地址,分配的頁數為 2^order,分配的頁也不清零 */
__get_free_pages(unsigned int flags, unsigned int order);
/* 釋放 */
void free_page(unsigned long addr);
void free_pages(unsigned long addr, unsigned long order);
__get_free_pages 等函數在使用時,其申請標誌的值與 kmalloc()完全一樣,各標誌的含義也與kmalloc()完全一致,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。
vmalloc()
vmalloc()一般用在為只存在於軟體中(沒有對應的硬體意義)的較大的順序緩衝區分配記憶體,vmalloc()遠大於__get_free_pages()的開銷,為了完成 vmalloc(),新的頁表需要被建立。因此,只是調用 vmalloc()來分配少量的記憶體(如 1 頁)是不妥的。
vmalloc()申請的記憶體應使用 vfree()釋放, vmalloc()和 vfree()的函數原型如下:
void *vmalloc(unsigned long size);
void vfree(void * addr);
vmalloc()不能用在原子上下文中,因為它的內部實現使用了標誌為 GFP_KERNEL 的 kmalloc()。
slab
一方面,完全使用頁為單元申請和釋放記憶體容易導致浪費(如果要申請少量位元組也需要 1 頁);另一方面,在操作系統的運作過程中,經常會涉及大量對象的重覆生成、使用和釋放記憶體問題。在Linux 系統中所用到的對象,比較典型的例子是 inode、 task_struct 等。如果我們能夠用合適的方法使得在對象前後兩次被使用時分配在同一塊記憶體或同一類記憶體空間且保留了基本的數據結構,就可以大大提高效率。 內核的確實現了這種類型的記憶體池,通常稱為後備高速緩存(lookaside cache)。內核對高速緩存的管理稱為slab分配器。實際上 kmalloc()即是使用 slab 機制實現的。
註意, slab 不是要代替__get_free_pages(),其在最底層仍然依賴於__get_free_pages(), slab在底層每次申請 1 頁或多頁,之後再分隔這些頁為更小的單元進行管理,從而節省了記憶體,也提高了 slab 緩衝對象的訪問效率。
#include <linux/slab.h>
/* 創建一個新的高速緩存對象,其中可容納任意數目大小相同的記憶體區域 */
struct kmem_cache *kmem_cache_create(const char *name, /* 一般為將要高速緩存的結構類型的名字 */
size_t size, /* 每個記憶體區域的大小 */
size_t offset, /* 第一個對象的偏移量,一般為0 */
unsigned long flags, /* 一個位掩碼:
SLAB_NO_REAP 即使記憶體緊縮也不自動收縮這塊緩存,不建議使用
SLAB_HWCACHE_ALIGN 每個數據對象被對齊到一個緩存行
SLAB_CACHE_DMA 要求數據對象在DMA記憶體區分配
*/
/* 可選參數,用於初始化新分配的對象,多用於一組對象的記憶體分配時使用 */
void (*constructor)(void*, struct kmem_cache *, unsigned long),
void (*destructor)(void*, struct kmem_cache *, unsigned long)
);
/* 在 kmem_cache_create()創建的 slab 後備緩衝中分配一塊並返迴首地址指針 */
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
/* 釋放 slab 緩存 */
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
/* 收回 slab 緩存,如果失敗則說明記憶體泄漏 */
int kmem_cache_destroy(struct kmem_cache *cachep);
Tip: 高速緩存的使用統計情況可以從/proc/slabinfo獲得。
記憶體池(mempool)
內核中有些地方的記憶體分配是不允許失敗的,內核開發者建立了一種稱為記憶體池的抽象。記憶體池其實就是某種形式的高速後備緩存,它試圖始終保持空閑的記憶體以便在緊急狀態下使用。mempool很容易浪費大量記憶體,應儘量避免使用。
#include <linux/mempool.h>
/* 創建 */
mempool_t *mempool_create(int min_nr, /* 需要預分配對象的數目 */
mempool_alloc_t *alloc_fn, /* 分配函數,一般直接使用內核提供的mempool_alloc_slab */
mempool_free_t *free_fn, /* 釋放函數,一般直接使用內核提供的mempool_free_slab */
void *pool_data); /* 傳給alloc_fn/free_fn的參數,一般為kmem_cache_create創建的cache */
/* 分配釋放 */
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
/* 回收 */
void mempool_destroy(mempool_t *pool);
記憶體映射
一般情況下,用戶空間是不可能也不應該直接訪問設備的,但是,設備驅動程式中可實現mmap()函數,這個函數可使得用戶空間直能接訪問設備的物理地址。
這種能力對於顯示適配器一類的設備非常有意義,如果用戶空間可直接通過記憶體映射訪問顯存的話,屏幕幀的各點的像素將不再需要一個從用戶空間到內核空間的複製的過程。
從 file_operations 文件操作結構體可以看出,驅動中 mmap()函數的原型如下:
int(*mmap)(struct file *, struct vm_area_struct*);
驅動程式中 mmap()的實現機制是建立頁表,並填充 VMA 結構體中 vm_operations_struct 指針。VMA 即 vm_area_struct,用於描述一個虛擬記憶體區域:
struct vm_area_struct {
unsigned long vm_start; /* 開始虛擬地址 */
unsigned long vm_end; /* 結束虛擬地址 */
unsigned long vm_flags; /* VM_IO 設置一個記憶體映射I/O區域;
VM_RESERVED 告訴記憶體管理系統不要將VMA交換出去 */
struct vm_operations_struct *vm_ops; /* 操作 VMA 的函數集指針 */
unsigned long vm_pgoff; /* 偏移(頁幀號) */
void *vm_private_data;
...
}
struct vm_operations_struct {
void(*open)(struct vm_area_struct *area); /*打開 VMA 的函數*/
void(*close)(struct vm_area_struct *area); /*關閉 VMA 的函數*/
struct page *(*nopage)(struct vm_area_struct *area, unsigned long address, int *type); /*訪問的頁不在記憶體時調用*/
/* 當用戶訪問頁前,該函數允許內核將這些頁預先裝入記憶體。驅動程式一般不必實現 */
int(*populate)(struct vm_area_struct *area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
...
建立頁表的方法有兩種:使用remap_pfn_range函數一次全部建立或者通過nopage VMA方法每次建立一個頁表。
remap_pfn_range
remap_pfn_range負責為一段物理地址建立新的頁表,原型如下:int remap_pfn_range(struct vm_area_struct *vma, /* 虛擬記憶體區域,一定範圍的頁將被映射到該區域 */ unsigned long addr, /* 重新映射時的起始用戶虛擬地址。該函數為處於addr和addr+size之間的虛擬地址建立頁表 */ unsigned long pfn, /* 與物理記憶體對應的頁幀號,實際上就是物理地址右移 PAGE_SHIFT 位 */ unsigned long size, /* 被重新映射的區域大小,以位元組為單位 */ pgprot_t prot); /* 新頁所要求的保護屬性 */
demo:
static int xxx_mmap(struct file *filp, struct vm_area_struct *vma) { if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->vm_start, vma->vm_page_prot)) /* 建立頁表 */ return - EAGAIN; vma->vm_ops = &xxx_remap_vm_ops; xxx_vma_open(vma); return 0; } /* VMA 打開函數 */ void xxx_vma_open(struct vm_area_struct *vma) { ... printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %lx\n", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT); } /* VMA 關閉函數 */ void xxx_vma_close(struct vm_area_struct *vma) { ... printk(KERN_NOTICE "xxx VMA close.\n"); } static struct vm_operations_struct xxx_remap_vm_ops = { /* VMA 操作結構體 */ .open = xxx_vma_open, .close = xxx_vma_close, ... };
nopage
除了 remap_pfn_range()以外,在驅動程式中實現 VMA 的 nopage()函數通常可以為設備提供更加靈活的記憶體映射途徑。當訪問的頁不在記憶體,即發生缺頁異常時, nopage()會被內核自動調用。static int xxx_mmap(struct file *filp, struct vm_area_struct *vma) { unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; if (offset >= _ _pa(high_memory) || (filp->f_flags &O_SYNC)) vma->vm_flags |= VM_IO; vma->vm_flags |= VM_RESERVED; /* 預留 */ vma->vm_ops = &xxx_nopage_vm_ops; xxx_vma_open(vma); return 0; } struct page *xxx_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type) { struct page *pageptr; unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; unsigned long physaddr = address - vma->vm_start + offset; /* 物理地址 */ unsigned long pageframe = physaddr >> PAGE_SHIFT; /* 頁幀號 */ if (!pfn_valid(pageframe)) /* 頁幀號有效? */ return NOPAGE_SIGBUS; pageptr = pfn_to_page(pageframe); /* 頁幀號->頁描述符 */ get_page(pageptr); /* 獲得頁,增加頁的使用計數 */ if (type) *type = VM_FAULT_MINOR; return pageptr; /*返回頁描述符 */ }
上述函數對常規記憶體進行映射, 返回一個頁描述符,可用於擴大或縮小映射的記憶體區域。
由此可見, nopage()與 remap_pfn_range()的一個較大區別在於 remap_pfn_range()一般用於設備記憶體映射,而 nopage()還可用於 RAM 映射,其調用發生在缺頁異常時。
References
1. Linux Device Drivers
2. Linux設備驅動開發詳解(宋寶華第二版)
Copyright (C) 2016 archiexie@cnblogs. All Rights Reserved.