深入linux设备驱动程序内核机制(第十章) 读书笔记

2019-07-13 01:56发布

第十章 内存映射与DMA
      本文欢迎转载, 请标明出处
      本文出处http://blog.csdn.net/dyron

    本章讨论驱动如何实现内存映射和进行DMA操作, 内存映射的任务是将设备的地址空间映射到用户空间或直接
    使用用户空间的地址, 这样做的目的显然是从提升系统性能的角度出发.

10.1 设备缓存与设备内存

    设备缓存是由驱动管理位于系统主存RAM中的一段内存区域, 则设备内存则是设备所固有的一段存储空间(如
    某设备的fifo, 显卡设备的frame buffer). 从设备驱动的角度来看, 它属于特定设备的硬件范畴.

    linux系统下设备缓存与设备内存的典型用法是在两者之间建立DMA通道, 这样当设备内存中收到的数据达到
    一定的阈值时,设备将启动DMA将数据从设备内存传输到位于主布中的设备缓存中,发送数据正好相反.

10.2 mmap

    ioremap主要用来将内核空间的一段虚拟地址映射到外部设备的存储区中.
    mmap用来将用户空间的一段虚拟地址映射到设备的I/O空间中. 这样用户程序就可以直接访问设备内存了.

    10.2.1 struct vm_area_struct:

struct file_operations { ... int (*mmap)(struct file *, struct vm_area_struct *); ... }    结构体vm_area_struct中的一些关键成员定义如下:

struct vm_area_struct { struct mm_struct *vm_mm; unsigned long vm_start; unsigned long vm_end; struct vm_area_struct *vm_next, *vm_prev; pgprot_t vm_page_prot; unsigned long vm_flags; const struct vm_operations_struct *vm_ops; ... }    vm_mm: 当前结构对象所表示的虚拟地址段所归属的进程虚拟地址空间.
    vm_start: 当前对象所表示的虚拟地址段的起段地址.
    vm_end: 当前对象所表示的虚拟地址段的结束地址.
    *vm_next, *vm_prev: 用来将一系列struct vm_area_struct构成链表.
    pgprot_t vm_page_prot: 在将当前对象所表示的虚拟地址段映射到设备内存时的页保护属性, 主要体现在页
    目录项的映射属性当中.
    vm_flags: 当前对象所表示的虚拟地址段的访问属性,如VM_READ, VM_WRITE, VM_EXEC, VM_SHARED等.
    *vm_ops: 用来定义对当前对象所表示的虚拟地址段上的一组操作集.

    内核中的每个struct vm_area_struct对象都表示用户进程地址窨的一段区域, 它是访问用户进程mmap地址空
    间的最小单元, 内核为管理这些struct vm_area_struct对象准备了大量的代码.

    10.2.2 用户窨虚拟地址布局

    此处按照经典的x86结构的3G/1G方式展开, 即用户空间虚拟地址大小是3G, 内核空间虚拟地址大小是1G, 此
    处理布局是指在3G的进程虚拟地址空间中规划出进程的代码段(text), 存储全局变量和动态分配变量地址的
    堆, 以及用于保存局部变量和实现函数调用的栈等存储块的起始地址和大小.

    在系统运行一个应用程序时, 系统调用exec通过调用load_elf_binary来将应用程序对应的elf二进制文件加
    载到进程3G大小的虚拟地址空间中, 布局由此产生.

    load_elf_binary函数建立布局相关的函数调用链是:
    load_elf_binary->setup_new_exec->arch_pick_mmap_layout.

    arch_pick_mmap_layout的参数mm是一类型为struct mm_struct的对象指针, linux系统中每个进程都拥有一
    个struct mm_struct类型的对象, 保存了进程中与内存管理相关的信息. 内核为虚拟空间提供了两种布局方案
????????两种布局方案有什么不同呢, 看样很雷同.区别在于mmap区域是向上增长还是向下增长这一点吗?
    参数mmap_is_legacy来判断是采用哪种布局.
    
    对于传统布局, mmap的起始地址从0x40000000开始, mm->get_unmapped_area表示用来获得mmap区域尚未被映
    射的一段内存的函数, 内核提供了一个通用的函数, 用来在用户进程的MMAP区分配尚未被映射的内存块. 该
    函数将从低地址向高地址方向分配空闲的struct vm_area_struct对象, 每个对象代表一段连续的虚拟地址空
    间.

    对于新式布局. MMAP的起始地址由mmap_base函数来决定.
    函数首先通过rlimit(RLIMIT_STACK)获得当前进程栈空间的最大值, 然后通过PAGE_ALIGN(TASK_SIZE -gap -
    mmap_rnd())来获得新式布局下MMAP空间的起始地址.

    对于驱动而言, 重点是要知道映射的地址区出自3G大小的用户空间的MMAP区域, 内核会很好地管理MMAP区域,
    管理该区域的最小单位是struct vm_area_struct, 此处管理的主义是分配和释放一个vm_area_struct对象.

    映射过程概括为: 内核先在进程虚拟地址窨的MMAP区分配一个空闲的struct vm_area_struct对象, 然后通过
    页目录表项的方式将struct vm_area_struct对象所代表的虚拟地址空间映射到设备的存储窨中. 如此, 用户
    进程就可以直接访问设备的存储区,从而提高系统性能.
.......页目录项的介入意味着每个vm_area_struct对象表示的地址窨应该是而对齐的, 大小是页的整数.

    10.2.3 mmap系统调用过程

    void *mmap(void *start, size_t length, int port, int flags, int fd, off_t offset);

    start表示映射区的起始地址, length是映射区的长度, port表示用户进程在映射区被映射时所期望的保护方
    式, flags指定映射区的类型, fd是当前正在操作的文件的描述符. offset是实际数据在映射区中的偏移值.
........实际使用中start常为NULL, 让系统在MMAP区域找一个合适的空闲区域.

    用户空间调用mmap时, linux系统将通过sys_mmap_pgoff进入内核, 由当前设备文件中实现的mmap方法来完成
    用户程序要求的映射.

    sys_mmap_pgoff除了作一些错误检查外, 主要做两个件事:
    1. 通过fget由文件描述符获得对应的struct file对象指针.
    2. 调用do_mmap_pgoff来完成后续的内存映射工作.

    do_mmap_pgoff对比一下驱动中的mmap方法, 可以推测出do_mmap_pgoff的主体应该是根据用户窨进程调用mmap
    API时传入的参数构造一个struct vm_area_struct对象的实例, 然后调用file->f_op->mmap().

    10.2.4 驱动程序中mmap方法的实现

    mmap方法的主要功能是将内核提供的用户进程空间中来自mmap区域的一段内存映射到设备内存上.
    到目前为止, 内核为我们在MMAP区域分配了一个空闲的vma对象, 然后调用file对应的设备驱动中的mmap方法
    , 驱动需要在它的mmap方法的实现里将vma对象代表的用户空间地址映射到设备内存中.

    . remap_pfn_range: 这个函数可以将参数addr开始的大小为size的虚拟地址空间映射到pfn表示的一组连续
    的物理页面上, pfn是页帧号,

    通常, 将用户空间的地址通过remap_pfn_range映射到设备内存上, 尤其是设备的寄存嚣所在的地址空间,都不
    希望cache机制发挥作用, 驱动可以通过最后一个参数prot来影响页表项中属性位的建立.

    pgd_offset函数用来获得某一虚拟地址在页目录表中的对应单元的地址pgd.
    flush_cache_range是体系架构相关的函数, 用来将(addr, end)地址范围对应的cache内容同步到主存中.

    remap_pte_range将首先通过__pte_alloc分配一个物理页作为第二级映射的页表:

    remap_pfn_range首先根据需要映射的虚拟地址块的首地址的前10位得到第一级映射在页目录表中的entry,
    接着分配一块物理页面作为新的二级页表, 并将该页表的物理地址填入前面的entry中, 最后通过虚拟地址首
    地址的中间10位来确定对应的4K大小的映射在新页表中的entry, 找到之后将要映射的物理页的起始地址放到
    该entry中.

    SetPageReserved用来设置内核虚拟地址所对应的struct page对象的PG_reserved属性, 被reserved的都是一
    些特殊的页面, 这些页面最显著的特性是脱离了内核VM组件的管理, 因此这些页面可以确保不会被交换出去.

    . io_remap_pfn_range:
    它与remap_pfn_range的区别是, 它用来将用户地址映射到设备的I/O空间.

    10.2.6 munmap

    驱动中并没有对应的munmap方法, 内核就把这件事做完了.

    int munmap(void *start, size_t length);
    
    start是mmap返回的地址, 表示要拆除映射的虚拟地址段的起始地址.
    调用链是: do_munmap->unmap_region->unmap_region->free_pgtables->free_pgd_range->free_pud_range
    ->free_pmd_range->free_pte_range.

    总体思想是通过虚拟地址找到对应的页目录项和页表项, 然后清除这些页目录表项中的相应内容, 从而撤销
    掉mmap建立的虚拟地址到物理页面的映射关系.
???????????但是设备了reserver的内存会怎么处理呢???? 浪费掉了??

10.3 DMA

    10.3.1 内核中的DMA层

    一致性映射: dma_alloc_coherenr --> DMA Layer --> map_page....
    流式映射: dma_map_single --> DMA Layer --> dma_map_single....

    按照Linux内核对DMA层的架构设计, 各平台DMA缓冲区映射之间的差异应该由内核定义的一个DMA映射操作集
    struct dma_map_ops对象来完成, 换句话说不同平台应该提供各自的struct dma_map_ops对象来实现相应的
    DMA映射. 但是ARM平台似乎不按这个规矩来.

struct dma_map_ops { void* (*alloc_coherent)(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp); void (*free_coherent)(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle); dma_addr_t (*map_page)(struct device *dev, struct page *page, unsigned long offset, size_t size, enum dma_data_direction dir, struct dma_attrs *attrs); void (*unmap_page)(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir, struct dma_attrs *attrs); int (*map_sg)(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir, struct dma_attrs *attrs); void (*unmap_sg)(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir, struct dma_attrs *attrs); void (*sync_single_for_cpu)(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir); void (*sync_single_for_device)(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir); void (*sync_sg_for_cpu)(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir); void (*sync_sg_for_device)(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir); int (*mapping_error)(struct device *dev, dma_addr_t dma_addr); int (*dma_supported)(struct device *dev, u64 mask); int (*set_dma_mask)(struct device *dev, u64 mask); int is_phys; };     10.3.2 物理地址和总线地址

    所谓CPU物理地址, 即CPU的地址信号线上产生的地址. 在有MMU的系统中, CPU执行的程序指令中的地址是虚
    似地址, 经过MMU的转换, 虚拟地址转变为物理地址用来驱动CPU的地址信号线,或者用来访问系统主存RAM,或
    者用来访问设备I/O空间.

    而总线地址, 可以简单认为是从设备角度看到的地址, 不同类型的总线具有不同类型的总线地址.
    DMA地址使用的数据结构是dma_addr_t.

    10.3.3 dma_set_mask

    该函数用来查询设备的DMA寻址范围,如果设备dev的DMA操作支持参数mask指定的范围,函数返回0, 否则返回
     一负的错误代码. 设备dev在其成员dma_mask中来标识其DMA寻址范围.

static inline int dma_set_mask(struct device *dev, u64 dma_mask) { #ifdef CONFIG_DMABOUNCE if (dev->archdata.dmabounce) { if (dma_mask >= ISA_DMA_THRESHOLD) return 0; else return -EIO; } #endif if (!dev->dma_mask || !dma_supported(dev, dma_mask)) return -EIO; *dev->dma_mask = dma_mask; return 0; }     其中dma_supported的内部通过获得设备dev上定义的struct dma_map_ops对象的dma_supported成员来获得设
    备上DMA的寻址范围信息, 如果设备没有实现dma_supported方法, 内核将采用默认值.

static inline int dma_supported(struct device *dev, u64 mask) { if (mask < ISA_DMA_THRESHOLD) return 0; return 1; }   10.3.4 DMA映射

    DMA映射主要为在设备与主存之间建立DMA数据传输通道时,在主存中为该DMA通道分配内存空间的行为,也称为
    DMA缓冲区.

    . 一致性DMA映射:
    dma_alloc_coherent. 函数分配的一致性MDA缓冲区的总线地址由参数dma_handle带回,函数返回的则是映射
    到DMA缓冲区的虚拟地址的起始地址.

    函数首先试图通过dma_alloc_from_coherent在per-device的一致性存储区域中分配所需的DMA缓冲区, 具体
    per-device的一致性存储区放在dev->dma_mem中. 这种方法不常见, 所以dma_alloc_coherent实际会通过ops
    ->alloc_coherent指向的实际函数为DMA传输分配缓冲区,后者根据指定的需要分配缓冲区的大小size调用
    alloc_pages_node来获得一组连续的物理页,如果分配成员, 将会把这种物理页的起始物理地址放到dma_addr
    中供后续DMA通道传输数据时使用, 同时把该组物理页面对应的虚拟地址作为返回值返回.

    下面分析arm架构下的DMA申请内存方法, 函数首先调用__dma_alloc_buffer来分配大小为size的连续物理内存
    帷幕,因ARM不是通过硬件来保证cache一致性, 所以在__dma_alloc_buffer中, 除了分配连续的物理页外, 还
    会对这段物理页页对应的虚拟地址调用dma_flush_range, 使其对应的cache和write buffer无效, 这样无论
    CPU对主存进行读或者写操作, 都不会因为cache和write buffer的存在而导致DMA操作时出现cache一致性的
    问题.

    上边是ARM平台在软件层面保证cache一致性的第一道工序, __dma_alloc中的arch_is_coherent返回0, 表明
    ARM不是一种硬件保证cache一致性的平台, 所城机要调用__dma_alloc_remap来确保cache一致性,这个函数要
    做的工作是重新建立页表项来映射__dma_alloc_buffer分配的一组物理页面,通过在新建的页表项并闭映射区
    的cache功能来解决cache一致性问题, 更进一步的细节是ARM在虚拟地址窨的[0xFFC00000, 0xFFE00000]这2
    MB的虚拟地址空间保留做uncached的DMA映射窨, 这个敬意的映射都是将cache功能关闭的.

    所以__dma_alloc_remap的功能是在[0xFFC00000, 0XFFE00000]区间寻找一段虚拟地址段,将其重新映射到__
    dma_alloc_buffer分配的一组物理页面, 由于这种映射关闭了cache功能, 所以保证了DMA操作时不会出现cac
    he一致性的问题.

    一致性DMA映射的根据操作是获得一组连续的物理页用做DMA操作的缓冲区. 对于ARM而言,从软件层面上通过
    重新映射新获得的物理地址窨,在页目录和页表项中关闭了这段映射敬意上的cache功能, 使得cache一致性也
    不再成为问题.  注意, 一致性映射所获得的DMA缓冲区大小都是页面的整数倍, 如果需要更小的一致性DMA映
    射区, 应该使用内核提供的DMA池(pool)机制.

    卸载一致性DMA缓冲区使用dma_free_coherent.

    如果驱动程序中使用到的DMA缓冲区并非由驱动程序分配, 而是来自其它模块, 此时需要使用另一种DMA映射
    的方法: 流式DMA映射.

    . 流式DMA映射:
    这种映射的特点为是, DMA传输通道所使用的缓冲区往往不是由当前驱动自身分配的, 而往往对每次DMA传输
    都会重新建立一个流式映射的缓冲区,此外由于无法确定外部模块传入的DMA缓冲区的映射情况, 所以使用流
    式DMA映射时, 驱动必须小心负责处理可能出现的cache一致性问题.

    流式DMA映射的函数为:dma_map_single
    #define dma_map_single(d,a,s,r) dma_map_single_attrs(d,a,s,r,NULL);

    dma_map_single内部用来完成实际的流式映射操作的代码也是体系架构相关的, 内核通过struct dma_map_op
    s对象来屏蔽这种平台的差异, 具体的平台需要提供其特有的struct dma_map_ops对象来供内核中的DMA层使
    用.

    调用ops中的map_page方法, 注意此处调用map_page时通过virt_to_page把要映射的cpu虚拟地址转化成了对应
    的物理页面的struct page指针.

    要想通过dma_map_single来对某一虚拟地址段作流式映射, 必须保证传进来的虚拟地址是通过kmalloc获得的
    
    对于ARM平台来说, dma_map_single的主要实现是由__dma_single_cpu_to_dev来实现的, 其中重点函数如下
    outer_inv_range, 使ARM的cache失效, outer_clean_range, 使写入的数据不会只写到cache中.

    一致性DMA与流式DMA映射分析, 当驱动主要去分配一个DMA缓冲区并且该缓冲区的存在周期与所在的驱动模块
    一样长时,就用一致性DMA映射, 这种映射在一开始为DMA操作分配缓冲区时就解决了cache一致性的问题.
    如果驱动需要使用从别的模块传进来的地址空间作为DMA缓冲区,那就需要考虑使用流式DMA映射,这种映射对传
    入的地址空间要求是,必须位于内核窨的线性映射区中, 驱动的处理主要是确保每次DMA操作前后cache的一致
    性问题.

    建立流式DMA映射的关键点有两个:
    1. 确保CPU侧的虛拟地址所对应的物理地址能够被设备DMA正确访问;
    2. 要确保cache一致性的问题.

    struct dma_map_ops对象中的sync_single_for_cpu, sync_single_for_device, sync_sg_for_cpu和sync_sg
    _for_device方法就是用来处理cache一致性的问题.

    sync_single_for_cpu方法用于数据从设备传到主存这种情况: 当DMA无成时,设备已将数据放到了位于主存的
    缓冲区中,CPU需要读该数据, 为了避免cache的介入导致CPU读到的只是cache中的老数据, 驱动需要在cpu读之
    前调用该函数. 在ARM平台上, 该函数是一个invalidate, 使cache无效, 这样CPU将直接从主存获得数据.

    sync_single_for_device用于数据从主存传到设备中:在启动DMA前, CPU将数据放到位于主存的DMA缓冲区中,
    为了防止write buffer的介入,导致数据只是临时写到write buffer中, 驱动需要在CPU往主存写数据这后启
    DMA操作前调用该函数. 在ARM平台上操作是一个"flush/clean", 把write buffer中的数据冲到主存中.

    . 分散/聚集映射(scatter/gather map)
    
    所谓分散/聚集映射通过将虚拟地址上分散的DMA缓冲区通过一个类型为struct catterlist的数组或者链表组
    织起来, 然后通过一次的DMA传输操作在主存RAM与设备之间传输数据.

struct scatterlist { #ifdef CONFIG_DEBUG_SG unsigned long sg_magic; #endif unsigned long page_link; unsigned int offset; unsigned int length; dma_addr_t dma_address; #ifdef CONFIG_NEED_SG_DMA_LENGTH unsigned int dma_length; #endif };    从CPU的角度来看这种分散/聚集映射, 对应的需求是有三块数据,分别放在三段分散的虚拟地址空间中,需要和
    设备进行交互,通过建立struct scatterlist类型的数组/链表在一起DMA传输中完成所有的数据传递.

    在struct scatterlist结构中, page_link指明虚拟地址所对应的物理页面struct page对象的地址.
    offset是数据在DMA缓冲区中的偏移地址, length是传输数据块的大小, dma_address是设备DMA操作要使用的
    DMA地址.

    内核中的DMA层为分散/聚集映射所提供的接口为dma_map_sg;
    通过上面的讨论可以看到,分散/聚集DMA映射本质上是通过一次DMA操作把主存中分散的数据块在主存与设备
    之间进行传输, 对于每个数据块内核都会建立对应的一个流式DMA映射.

    10.3.5 回弹缓冲区(bounce buffer)

    如果CPU侧虚拟地址对应的物理地址不适合设备的DMA操作, 就需要建立回弹缓冲区,它相当于一个中转站的作
    用,在把数据往设备方向传输时,以区动程序需要把CPU给的数据拷贝到回弹缓冲区, 然后再启动DMA操作,反之
    也一样. 所以回弹缓冲区必豌豆是可以直接与设备进行DMA传输的当传输结束时, 再通过CPU的介入把回弹缓冲
    区中的数据搬移到最终的目标, 所以除非外部传入的地址不可进行DMA传输,否则不应该使用它.

    10.3.6 DMA池

    DMA池机制非常类似于linux内存管理中的slab机制, 它的实现建立在一致性DMA映射所获得的连续物理页面之
    上,通过DMA池接口函数在物理页面上分配所谓块大小的DMA缓冲区, 以下称为DMA缓冲块, 以区别于一致性DMA
    映射中页面级大小的缓冲区.

    为了管理跟踪物理页面中DMA缓冲块的分配和余下空闲空间的多少,内核需要引入对应的管理数据结构, struct
     dma_pool就是内核用来完成该任务的数据结构.
struct dma_pool { /* the pool */ struct list_head page_list; //用来将一致性DMA映射建立的页面组织成链表. spinlock_t lock; size_t size; //DMA池用来分配一致性DMA映射的缓冲区的大小, 也称块大小 struct device *dev; //进行DMA操作的设备对象指针 size_t allocation; size_t boundary; char name[32]; //DMA池的名称,主要在调试或者诊断时使用 wait_queue_head_t waitq; struct list_head pools; //用来将当前DMA池对象加到dev->dma_pools链表中. };    在利用DMA池进行缓冲区分配之前, 首先需要创建一个DMA池,通过dma_poll_create来完成. 函数的核心工作
    就是分配一个struct dma_poll对象并初始化, 入参name用于指定即将创建的DMA池的名称, size用于指定在
    该DMA池中分配缓冲块的大小, align用于指定当前DMA池分配操作所遵守的对齐方式.

    如果要在缓冲池中分配一个一致性映射的DMA缓冲区块, 应该使用dma_poll_alloc函数, 函数的主线框加是,
    如果当前DMA池中有页面满足接下来的缓冲块分配需求, 那么就在该页面上分配, 否则通过调用pool_alloc_
    page来重新分配一段连续物理页. DMA池中每段这样的页面都用一个struct dma_page的对象来表示.

struct dma_page { /* cacheable header for 'allocation' bytes */ struct list_head page_list; void *vaddr; dma_addr_t dma; unsigned int in_use; unsigned int offset; };    dma_pool_alloc函数返回DMA池中某一段物理页面中空闲块的虚拟地址,对应的DMA地址由参数handle返回,如
    果调用dma_pool_alloc时在mem_flags中指定了__GFP_WAIT樗,那么在系统中暂时没有一段连续的物理页面满
    足分配需求时, 函数会进入睡眠等待状态, 等POLL_TIMEOUT_JIFFIES指定的时间到期.

    释放一个DMA缓冲块应该调用dma_poll_free函数. 销毁一个DMA池应该调用dma_pool_destroy;

10.4 本章小结
      本文欢迎转载, 请标明出处
      本文出处http://blog.csdn.net/dyron
    本章讲座了两个话题, 一个是如何将用户空间的地址映射到设备内存中, 将用户空间的地址直接映射到设备
    地址上可以使得应用程序直接使用设备内存, 绕过内核部分的介入, 提高程序的性能.

    另一个放晴与DMA操作相关, 主要集中在如何为一个DMA传输建立DMA缓冲区,所为缓冲区的建立, 主要是在系
    统主存中为DMA操作分配一段内存区域, 因为cache的存在使得原本单纯的任务变得有些不那么坦荡,而且还应
    该注意并不是主存中所有的区域都适合DMA传输.
    
    内核中关于DMA缓冲区的建立主要有三种方式:
    1. 一致性DMA映射, 一致性DMA映射在建立之初就解决了cache一致性的问题, 所以后续DMA操作无须再关心了.
    2. 流式DMA映射, 基本上这种映射的缓冲区都不是驱动程序自身所分配,因此驱动需要确认传入的虚拟地址映
    射的物理地址范围可以进行DMA操作, 然后将这些虚拟地址转化为物理地址作为后结DMA操作的缓冲区. 因为
    驱动程序在此只是简单地做虚拟地址到DMA地址的转换工作, 所以后续的每次DMA操作时都要小心采取措施解决
    cache一致性问题.
    3. 如果有更小的DMA一致性缓冲区分配需求, 可以使用内核提供的DMA池机制.