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