需要了解Linux设备驱动的内存管理办法

来源:
导读 大家好,我是本期栏目编辑小友,现在为大家讲解需要了解Linux设备驱动的内存管理办法问题。 对于包含MMU的处理器,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)。它的总大小和每页的用途是由固定地址枚举结构在编译期间预定义的。可以通过使用__fix_to_virt(索引)获得专用区域中预定义页面的逻辑地址。

高端内存映射区

当系统物理内存大于896MB时,超出物理内存映射区的内存称为高端内存(而未超出物理内存映射区的内存通常称为常规内存),内核在访问高端内存时必须将其映射到高端页面映射区。

虚拟内存分配区

在vmalloc()函数中使用,它在前端和物理内存映射区之间有一个屏障,在后端和高端映射区之间有一个屏障。

物理内存映射区

通常,物理内存映射区的最大长度为896MB,系统的物理内存顺序映射在内核空间的这个区域。

虚拟地址与物理地址的关系。

对于内核物理内存映射区的虚拟内存,可以使用virt_to_phys()将内核的虚拟地址转换为物理地址。virt_to_phys()的实现与架构有关。对于ARM,virt_to_phys()的定义如下:

静态内联无符号长virt _ to _ phys(void * x){ return _ _ virt _ to _ phys((无符号长)(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 fla

gs);

给 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 /* 创建一个新的高速缓存对象,其中可容纳任意数目大小相同的内存区域 */ 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 /* 创建 */ 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 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; /* 页帧号 */ 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 映射,其调用发生在缺页异常时。

 

标签:

版权声明:转载此文是出于传递更多信息之目的。若有来源标注错误或侵犯了您的合法权益,请作者持权属证明与本网联系,我们将及时更正、删除,谢谢您的支持与理解。