linux驱动程序设计中的内存与I/O访问


1.内核空间内存的动态申请

linux内核空间中申请内存涉及的函数主要有kmlloc()、__get_free_pages()和vmalloc()等。kmalloc()和__get_free_pages()申请的内存位于常规内存区和DMA的映射区,并且在物理上是连续的,它们与真是的物理区只存在一个固定的偏移量,存在着较为简单的转换关系。而vmalloc()分配的内存位于虚拟内存空间的连续的内存区域,实际上连续的虚拟内存在物理内存上并不连续。vmalloc在进行内存分配时会改变页表项,而kmlloc用的是开机就项映射好的常规内存和DMA区域的页表项。

(1) kmlloc()

void *kmlloc(size_t size,int flags);//size分配内存的大小,flags分配标志用来控制kmlloc的行为。

分配标志有以下几种:

GFP_KERNEL(常用):在内核空间的进程中分配内存,再申请内存时如果暂时不能满足进程进入睡眠等待页,将会阻塞,因此该标志不能用于中断上下文或持有自旋锁的情况下。

GFP_ATOMIC:在申请内存时如果不存在空闲页,则立即返回,因此该标志可以用于中断上下文或持有自旋锁的情况下。

GFP_USER: 为用户空间分配内存,可能会引起阻塞。

GFP_HIGHUSER:类似GFP_USER但是其从高端内存分配。

GFP_DMA:从DMA区域分配内存。

GFP_NOIO:不允许任何I/O初始化。

GFP_NOFS:不允许任何文件系统调用。

使用kfree()释放kmalloc()分配的内存。

(2) __get_free_pages()

__get_free_pages()系类函数/宏是linux内核最底层获取空闲空间的方法,因为底层使用buddy算法一2^n页为单位管理空闲内存,所以最底层内存的申请总是以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);//该宏返回一个新页的指针但是不清零。

__get_free_pages(unsigned int flags,unsigned int order);//该函数分配order页并返回内存的首地址。

释放内存的函数有:void free_pages(unsigned long addr),void free_page(unsigned long addr,unsigned long order)。

(3)vmalloc()

vmalloc()一般为只存在软件的较大顺序缓冲区分配内存,其在分配内存时需要建立新的页表项,代价较大,并且其不能用在中断上下文和持有自旋锁的进程中。

void vmalloc(unsigned long size);//分配内存

void vfree(void *addr);//释放内存

(4)slab

如果完全使用以页为单位的内存分配回收机制容易造成内存的浪费,还有就是操作系统在运行过程中经常会有大量的重复的对象的建立和销毁。如果有合适的方法使相同类型的对象在前后两次的使用同一块内存空间或同类的内存空间 并且保留了基本的数据,则可以大大的提高效率。而slab算法就是针对上述特点设计。

①创建slab缓存

struct kmem_cache *kmem_cache_craete(const char *name,size_t size,size_t align,unsigned long flags, \

void (*ctor)(void*,struct kmem_cache*,unsigned long), \

void (*dtor)(void*,struct kmem_cache*,unsigned long));

kmem_cache_craete创建一个任意数目的全部同样大小的后备缓存。size要分被的每个数据块的大小,flags控制如何进行分配。

② 分配salb缓存

void *kmem_cache_alloc(struct kmem_cache *cachep,gfp_t flags);//在①创建的slab缓存区中分配一块内存并返回首指针。

③ 释放salb缓存

void kmem_cache_free(struct kmem_cache *cachep,void *objp);//释放有②分配的内存。

④ 回收sable缓存

int kmem_cache_destory(struct kmem_cache *cachep);//释放有①创建的缓存区。

(5)内存池

内存池是一种典型的用于分配大量小对象的后备缓存技术。

① mempool_t结构体

typedef struct mempool_s {

    spinlock_t lock; /*保护内存池的自旋锁*/
    int min_nr; /*内存池中最少可分配的元素数目*/
    int curr_nr; /*尚余可分配的元素数目*/
    void **elements; /*指向元素池的指针*/
    void *pool_data; /*内存源,即池中元素真实的分配处*/
    mempool_alloc_t *alloc; /*分配元素的方法*/
    mempool_free_t *free; /*回收元素的方法*/
    wait_queue_head_t wait; /*被阻塞的等待队列*/
} mempool_t;

②创建内存池

mempool_t *mempool_create(int min_nr,mempool_alloc_t *alloc_fn,mempool_free_t *free_fn,void *pool_data);

min_tr:需要分配对象的数目。

alloc_fn和free_fn分别指向内存池机制提供的标准提供的对象分配和回收函数的指针。

pool_data:是分配和回收函数用到的指针,gfp_mask是分配标志。

③分配和回收对象

void *mempool_alloc(mempool_t *pool,int pfg_mask);

void mempool_free(void *element,mempool_t *pool);

④回收内存池

void mempool_destory(mempool_t *pool);

2. I/O内存

(1)I/O内存的映射及读写
在内核访问I/O内存(通常是芯片内部的各个I^2C、spi、usb等控制器的寄存器或外部总线上的设备)之前,需要先将设备的物理地址映射到内核的虚拟地址上。
①映射到虚拟地址
void *ioremap(unsigned long offset,unsigned long size);//返回一个虚拟地址对虚拟地址的操作就是对设备物理地址的操作。
ioremap()有个devm_ioremap的变体,类似其他以devm_开头的函数,实用devm_ioremap函数映射通常在失败时不需要驱动退出和出错处理的时候使用iounmap。
其原型为: void __iomem *devm_ioremap(struct device *dev,resource_size_t offset,unsigned long size);
②解除映射
void iounmap(void *addr);
③ 读写设备内存。
一般完成映射后就可以直接使用指针对其进行读写,但是内核推荐使用readb_relaxed()、readb_relaxew()、readb_relaxel()、readb()、readw()、readl()和writeb_relaxed()、writew_relaxed()、writel_relaxed()、writeb()、writew()、writel()函数,对映射到虚拟地址的物理地址进行读写。没加_relaxed有内存屏障,b、w、l分别是读/写8位、16位、32位寄存器。
(2)I/O内存申请
①申请(只是告诉内核驱动要访问这片内存,实际什么也不做)
struct resource *request_mem_region(unsigned long satrt,unsigned long end,char *name);
②释放
void release_mem_region(unsigned long satrt,unsigned long end);

3.将设备地址映射到用户空间

(1) 内存映射与VMA
一般情况下,用户空间不能也不应该直接访问设备的寄存器或地址空间,但是可以在设备驱动程序中实现mmap()函数,此函数可以把设备寄存器或地址空间和用户空间相关联,实际就是一个映射过程,当用户访问用户空间时转换为对设备的访问。
内核空间mmap()的原型如下:
int (*mmap)(struct file *,struct vm_area_strucr *);
如果在驱动层实现了该函数,在应用层调用同名的mmap函数则可以完成地址映射。
(2) vma结构体

struct vm_area_struct

         struct mm_struct * vm_mm;       /*虚拟区间所在的地址空间*/

         unsigned long vm_start;         /*在vm_mm中的起始地址*/

         unsigned long vm_end;           /*在vm_mm中的结束地址 */

 

         /* linked list of VM areas per task, sorted by address */

         struct vm_area_struct *vm_next;

 

         pgprot_t vm_page_prot;          /*对这个虚拟区间的存取权限 */

         unsigned long vm_flags;         /*虚拟区间的标志. */

 

         rb_node_t vm_rb;

 

         /*

          * For areas with an address space and backing store,

          * one of the address_space->i_mmap{,shared} lists,

          * for shm areas, the list of attaches, otherwise unused.

          */

         struct vm_area_struct *vm_next_share;

         struct vm_area_struct **vm_pprev_share;

 

         /*对这个区间进行操作的函数 */

         struct vm_operations_struct * vm_ops;

 

         /* Information about our backing store: */

         unsigned long vm_pgoff;         /* Offset (within vm_file) in PAGE_SIZE

                                           units, *not* PAGE_CACHE_SIZE */

         struct file * vm_file;          /* File we map to (can be NULL). */

         unsigned long vm_raend;         /* XXX: put full readahead info here. */

         void * vm_private_data;         /* was vm_pte (shared mem) */

 };

(3) vma_operations_struct结构体
struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);
 
    /* notification that a previously read-only page is about to become
     * writable, if an error is returned it will cause a SIGBUS */
    int (*page_mkwrite)(struct vm_area_struct *vma, struct page *page);
 
    /* called by access_process_vm when get_user_pages() fails, typically
     * for use by special VMAs that can switch between memory and hardware
     */
    int (*access)(struct vm_area_struct *vma, unsigned long addr,
              void *buf, int len, int write);
};
在用户调用mmap()时调用open(),在用户调用munmap()时调用close();

4.DMA

DMA是无需CPU参与就可以完成外设和内存间的双向数据交换。DMA可以使CPU在繁忙的I/O操作中解脱出来,极大的提高了系统的吞吐率。
(1) DMA与cache的一致性
cache是为了解决cpu与内存速度的不一致的问题,cache利用程序的局部性原理,把CPU经常访问或即将访问的数据存储到cache中。DMA提高了外设和内存间数据交换的效率,提高CPU的工作效率。如果DMA存入到内存的数据和Cache中存放的内存数据没有交集则没有任何问题。但是如果DMA存入内存的数据和Cache中存放的内存的数据有交集,那么通过DMA存入的新的数据,而Cache中的数据就与内存的数据不一致并且处理器依然访问cache的旧数据,这就是Cache与DMA不一致。
(2) linux下的DMA编程
①DMA区域
使用kmalloc()、__get_free_pages(),使用GFP_DMA标志可以申请到内存中DMA区域,并具有DMA的能力,也可以使用dma_mem_alloc()获取DMA区域。
现代的嵌入式处理器的DMA操作可以是整个的常规内存区域。
②虚拟地、物理地址和总线地址
基于DMA的硬件使用的是总线地址而不是物理地址,总线地址是从设备角度看到的内存地址,物理地址是从CPU(虚拟地址)和MMU看到的内存地址。
虚拟地址和物理地址之间的转换:
unsigned long virt_to_bus(volatile void *address); //虚拟地址到总线地址
void *bus_to_virt(unsigned long address);//总线地址到物理地址的转换
③DMA掩码
int dma_set_mask(struct device *dev,u64 mask);//其本质是修改device中的dma_mask成员。mask就是DMA能访问的内存范围。
④一致DMA缓冲区
DMA的映射包含两方面的工作:分配一片DMA缓冲区,为这片缓冲区产生设备可以访问的地址,DMA映射必须考虑cache一致性问题。
void *dma_alloc_coherenet(struct device *dev,size_t size,dma_addr_t *handle,gfp_t gfp);//返回DMA缓冲区的虚拟地址,handle为带回的总线地址。
void dma_free_coherenet(struct device *dev,size_t size,dma_addr_t *handle,gfp_t gfp); //释放DMA缓冲区
dma_alloc_xxx函数分配的不一定是DMA区。
⑤流式DMA映射


⑥dmaengine标准API

 



你可能感兴趣的:(linux驱动程序设计)