mmap底层驱动实现方法总结

  最近在调试视频卡,虽然通了,但是公司CPU主频在300MHZ,对于采集D1格式图像显示到fb中并不是很流畅,分析原因,视频采集卡是PCI插槽,PCI的带宽没有问题,有一个想法,视频卡做DMA将采集到的图像放到内存中,CPU将图像再搬运到framebuffer中,这是视频采集的大体流程,在这个过程中最费CPU的是数据的搬运,在我自己写的小测试程序中就是循环的拷贝从videobuf到framebuffer。如果能将视频卡的DMA缓冲区直接映射到framebuffer中,就直接省掉了数据搬运的过程,视频卡采集数据和fb读取数据都是DMA操作,CPU不需要参与,这样视频的流畅度肯定能大幅度的提高,而且CPU占用率会很低。

  实现这个想法需要做的工作是能实现视频卡DMA缓冲区和fb缓冲区的映射。说白了就是将做fb缓冲区的那段内存和视频卡DMA缓冲区的虚拟地址之间建立页表,这需要修改视频卡的mmap驱动实现,正好之前调视频卡DMA学习了LDD15章内存映射和DMA的后半部DMA,现在结合这个问题学习一下前半部内存映射。最终这个想法也实现了,这里记录一下,以备以后查看。

  之前在刚学习PCI总线的时候,对于PCI设备的I/O内存的访问,内核采用的ioremap的方式,这也是将设备内存映射到内核虚拟地址,前面我也写过一篇总结ioremap的文章,这里主要介绍mmap的实现。

  在现代unix系统中最吸引人的地方就是内存映射,对于驱动程序,内存映射提供了用户程序直接访问设备内存的能力,映射一个设备也就意味着将用户空间的一段内存与设备内存关联起来,无论何时访问这段地址范围实际是直接对设备的访问,这是LDD3上的一句话,这的确很令人心动!

  在驱动程序中,最终传给底层mmap实现函数的参数是struct file和struct vm_area_struct,从系统调用mmap经过内核的处理,vm_area_struct代表着需要做映射的一段用户空间虚拟地址,驱动程序需要做的是为vma重新建立页表,以及填充vma->vm_ops操作。

  mmap驱动实现中重新建立页表有2种方式,第一种方式调用remap_pfn_range一次性完成指定用户空间虚拟地址和指定物理地址之间的映射,建立页表,第二种方式是在mmap实现中仅仅填充vma->vm_ops,当访问vma中的虚拟空间时,因为没有建立页表映射,引发缺页异常,执行do_page_fault,最终会执行到相应vma中的vm->vm_ops->fault函数(2.6.33内核是这个,21内核是nopage函数),为该段虚拟地址建立一个页表。这2种方式各有利弊,我在实现videobuf到framebuffer映射中2种方法都试过,下面详细记录一下。


   1 remap_pfn_range一次性完成映射

  起初最先想到的方法是修改remap_pfn_range函数,在saa7134视频采集卡的驱动中,跟踪到最底层的mmap实现函数是videobuf-dma-sg.c中函数__videobuf_mmap_mapper,先找到相应的一帧videobuf,然后给vma的vm_ops vm_flags vm_private_data赋值,也就是说在这里没有使用remap_pfn_range来一次性完成页表的映射,而是在缺页异常中一页一页的处理。

  我要采用remap_pfn_range的方法来做映射,因为公司处理器的framebuffer分配在固定的逻辑地址0xade00000,物理地址就是0xde00000(mips处理器),remap_pfn_range如下:

  int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t prot);

  参数vma以及prot pfn在调用驱动中mmap实现之前内核已经做了分配处理,prot是vma这段虚拟地址空间的保护权限,pfn是页帧号,一般是用vma->vm_pgoff,内核也做了分配,不过在驱动中可以修改,我们就可以修改这个pfn来让它映射到我们指定的内存区域中。修改如下:

  vma->vm_pgoff = 0xde00000 >> PAGE_SHIFT;

  if(remap_pfn_range(vma, vma->start, vma->vm_pgoff, vma->vm_end -vma->vm_start, vma->vm_page_prot)){

      printk("remap_pfn_range failed !\n");

      return -EAGAIN;

  }

  这样修改的确是将视频卡的DMA缓冲区映射到了fb上,但是在测试的时候报错,qbuf:bad address。跟踪代码发现在v4l2的qbuf过程中底层需要为mmap中准备的缓冲区做DMA映射,流程如下,需要了解saa7134视频采集卡的可以看一下:

  qbuf-----》buffer_prepare ------>videobuf_iolock ------->videobuf_dma_init_user_locked --------> get_user_pages

  get_user_pages获取指定用户地址空间的所有物理页,然后再调用videobuf_dma_map来做DMA映射。

  问题就出在get_user_pages函数,查看remap_pfn_range的实现,在其注释中就可以看到,remap_pfn_range只是实现了指定用户空间虚拟地址和指定物理地址之间页表的重新建立,具体该物理地址是否有相应的page结构体并不关心,也就是不涉及到到page结构体,在remap_pfn_range实现中为vm_flags添加VM_PFNMAP标志位就是这个意思。而get_user_pages是来获取对应物理地址的page结构体的,因此内核实现规则中是不允许这2个函数先后使用的,在get_user_pages中有多处对vm_flags做了检查,如果有VM_PFNMAP标志位,则出错返回。

  LDD3上15.2.6中说remap_pfn_range限制只能访问保留页和物理内存之外的物理地址,不允许重新映射传统地址,比如get_free_pages获取到的地址。

  我的理解这句话的意思还是说remap_pfn_range不涉及到page结构体,所以可以访问物理内存之外的地址,我前面的操作说明物理内存也是可以采用这个函数的,我采用的物理地址是fb做DMA的地址,fb分配DMA的时候调用的是dma_alloc_coherent,实现中就调用了get_free_pages,remap_pfn_range也没有出现什么问题。但是后面get_user_pages中就出现了问题,我思考了一下这个问题,如果想办法将remap_pfn_range映射的物理地址所代表的页都能伪装成正常的物理页也就没有什么问题了吧,在驱动中remap_pfn_range除了重新建立映射,也就是修改了vm_flags,然后我在调用了remap_pfn_range后将在remap_pfn_range中添加的vm_flags的标志位全部去掉,如下:

vma->vm_flags &= ~(VM_IO | VM_PFNMAP | VM_RESERVED);

  我的想法就是让这段物理地址上的物理页看起来是“正常”的,而不是用remap_pfn_range重新映射过,这样修改之后跑测试程序的确是正常了! 图像直接采集到framebuffer中,非常流畅,但是fb分辨率必须要跟采集的图像一致,这个也是这种想法的一大缺点。但是在程序退出的时候出现了错误,现在还在查找原因,我的感觉是因为内核规则不允许remap_pfn_range和get_free_pages以及get_user_pages共存,可能还有我没看到的限制条件在退出的时候检查,所以出错。

 虽然这种方法现在看来因为内核的限制还有些缺陷,但是起码证明,这种想法是可以实现的,并且性能的提升非常大!


  2 缺页异常调用fault函数单页映射

   LDD上也说明将实际的物理内存映射到用户空间最好的方法就是调用vm_ops->fault一次处理一页的映射。在LDD3中同样说明这种情况下get_free_pages的order必须为0,也就是获取一个页,因为一次缺页异常只能完成一页的映射。

   而在我的例子中,视频卡如果在mmap中不做映射,就在get_user_pages中去建立映射,但并没有引起缺页异常,查看get_user_pages实现可以看到,实现中会针对每一页的虚拟地址调用follow_page来查找相应的物理页,因为mmap没有做映射所以找不到实际的物理页,就会调用handle_mm_fault,这个函数在缺页异常中就会调用到,这里针对每一个缺页都调用了这个函数,最终就会调用到vm_ops->fault函数,所以在get_user_pages中是相当于一页一页的产生缺页异常,修改fault来实现映射应该没有问题。

   fault函数的原型如下:

   static int fault(struct vm_area_struct *vma, struct vm_fault *vmf);

   vma就不用说了,vmf我们顺着handle_mm_fault函数找下去,

    handle_mm_fault -----> handle_pte_fault ----------> do_linear_fault -----------> __do_fault

    在__do_fault中vmf.virtual_address和pgoff被赋值,pgoff值是((address & PAGE_MASK)- vma->start)>>PAGE_SHIFT + vma->vm_pgoff,也就是要做映射的页帧号,这样的话我的修改如下:

    在_videobuf_mmap_mapper中:

    vma->vm_pgoff = 0xde00000 >> PAGE_SHIFT;

    在fault函数中直接将页帧号转换成page结构体:

    vmf->page = pfn_to_page(vmf->pgoff);

   这样修改之后跑测试程序没有问题了,视频显示非常流畅,在主频只有300MHz情况下实现D1格式(720x576)视频流畅播放!


   上面的2种修改方法有一个前提就是linux内核在启动初始化的过程中为物理内存每一页都建立了page结构体,超出物理内存的没有page结构体,所以可以调用remap_pfn_range.

你可能感兴趣的:(driver,debug,summary)