你是否想過如何在Linux系統中為你的裝置編寫驅動程式?你是否想過如何在Linux系統中讓你的驅動程式有效地使用記憶體資源?你是否想過如何在Linux系統中讓你的驅動程式實現一些進階的功能,例如記憶體映射、記憶體分配、記憶體保護等?如果你對這些問題感興趣,那麼本文將為你介紹一種實現這些目標的有效方法——Linux裝置驅動之記憶體管理。記憶體管理是一種用來描述和控制記憶體資源的資料結構和演算法,它可以讓你用一種簡單而統一的方式,將記憶體的資訊和屬性傳遞給內核,從而實現記憶體的分配和釋放。記憶體管理也是一種用來實現有效使用的機制,它可以讓你用一種標準而通用的方式,定義和使用各種記憶體的操作和命令,從而實現記憶體的映射、拷貝、保護等功能。記憶體管理也是一種用於實現高級功能的框架,它可以讓你用一種靈活而可擴展的方式,定義和使用各種內存的接口和協議,從而實現內存的共享、鎖定、緩衝等功能。本文將從記憶體管理的基本概念、語法規則、編寫方法、呼叫過程、操作方式等方面,為你詳細地介紹記憶體管理在Linux裝置驅動中的應用和作用,幫助你掌握這種有用且強大的方法。
對於包含 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()申请的虚拟内存和物理内存之间也没有简单的换算关系。
void *kmalloc(size_t size, int flags);
给 kmalloc()的第一个参数是要分配的块的大小,第二个参数为分配标志,用于控制 kmalloc()的行为。
flags
使用 kmalloc()申请的内存应使用 kfree()释放,这个函数的用法和用户空间的 free()类似。
__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()远大于__get_free_pages()的开销,为了完成 vmalloc(),新的页表需要被建立。因此,只是调用 vmalloc()来分配少量的内存(如 1 页)是不妥的。
vmalloc()申请的内存应使用 vfree()释放, vmalloc()和 vfree()的函数原型如下:
void *vmalloc(unsigned long size); void vfree(void * addr);
vmalloc()不能用在原子上下文中,因为它的内部实现使用了标志为 GFP_KERNEL 的 kmalloc()。
一方面,完全使用页为单元申请和释放内存容易导致浪费(如果要申请少量字节也需要 1 页);另一方面,在操作系统的运作过程中,经常会涉及大量对象的重复生成、使用和释放内存问题。在Linux 系统中所用到的对象,比较典型的例子是 inode、 task_struct 等。如果我们能够用合适的方法使得在对象前后两次被使用时分配在同一块内存或同一类内存空间且保留了基本的数据结构,就可以大大提高效率。 内核的确实现了这种类型的内存池,通常称为后备高速缓存(lookaside cache)。内核对高速缓存的管理称为slab分配器。实际上 kmalloc()即是使用 slab 机制实现的。
注意, slab 不是要代替__get_free_pages(),其在最底层仍然依赖于__get_free_pages(), slab在底层每次申请 1 页或多页,之后再分隔这些页为更小的单元进行管理,从而节省了内存,也提高了 slab 缓冲对象的访问效率。
#include /* 创建一个新的高速缓存对象,其中可容纳任意数目大小相同的内存区域 */ 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很容易浪费大量内存,应尽量避免使用。
#include /* 创建 */ 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 "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 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 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 映射,其调用发生在缺页异常时。
透過本文,我們了解了記憶體管理在Linux裝置驅動程式中的應用和作用,學習如何編寫、呼叫、操作、修改和調試記憶體管理。我們發現,記憶體管理是一種非常適合嵌入式系統開發的方法,它可以讓我們方便地描述和控制記憶體資源,實現有效使用和進階功能。當然,記憶體管理也有一些注意事項和限制,例如需要遵循語法規格、需要注意權限問題、需要注意效能影響等。因此,在使用記憶體管理時,我們需要有一定的硬體知識和經驗,以及良好的程式設計習慣和調試技巧。希望本文能為你提供一個入門級的指導,讓你對記憶體管理有初步的認識和理解。如果你想深入學習記憶體管理,建議你參考更多的資料和範例,以及自己動手實作和探索。
以上是詳解Linux裝置驅動之記憶體管理的詳細內容。更多資訊請關注PHP中文網其他相關文章!