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