内存与IO访问函数实例

一般情况下,用户空间是不可能也不应该直接访问设备的,但是设备驱动程序可实现mmap()函数,这个函数可使得用户空间能直接访问设备的物理地址。
实际上,mmap()实现了这样的一个映射过程:它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。
而根据mmap()函数的定义可知,mmap()函数必须以PAGE_SIZE为单位进行映射,但是实际上内存只能以页为单位进行映射。这也就说明如果要映射非PAGE_SIZE整数倍的地址范围,需要先进性页对齐,然后再强制以PAGE_SIZE的倍数大小进行映射
mmap()函数的原型:

 int (*mmp)(struct file *, struct vm_area_struct *)

它的实现机制是建立页表,并填充VMA结构体中的vm_operations_struct指针,m_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;

   pgprot_t vm_page_prot;      //访问权限    
   unsigned long vm_flags;     //标志
   ...
   struct vm_operations_struct * vm_ops;   //操作VMA的函数集指针
   unsigned long vm_pgoff;                 //偏移(页帧号)
   struct file * vm_file;
   void * vm_private_data;
   ...
   };


在上述结构体中,vm_ops成员指向这个VMA的操作集,结构体定义如下:

 struct vm_operations_struct {
    void (*open)(struct vm_area_struct * area);
    void (*close)(struct vm_area_struct * area);
    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);
    ...
    };


在内核生成一个VMA后,它就会调用该VMAopen()函数。
而驱动中的mmap()函数将在用户进行mmap()系统调用时最终被调用,当用户调用mmap()时候内核会进行如下处理:

  • 在进程的虚拟空间查找一块VMA.
  • 将这块VMA进行映射。
  • 如果设备驱动程序或文件系统的file_operations定义了mmap()操作,则调用它。
  • 将这个VMA插入到进程的VMA链表中。

file_operationsmmap()函数的第一个参数就是步骤1中找的VMA.由mmap()系统调用映射的内存可由munmap()解除映射。这个函数原型如下:

int munmap(caddr_t addr, size_t len);

:
但是,需要注意的是:当用户进行mmap()系统调用后,尽管VMA在设备驱动文件操作结构体的mmap()被调用前就已经产生,内核却不会调用VMA的open()函数,通常需要在驱动的mmap()函数中先上调用vma->vm_ops->open()。为了说明问题,给出一个vm_operations_struct操作范例:

static int xxx_mmp(struct file *filp, struct vm_area_struct *vma)
{
if(remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma->start, vma->vm_page_prot))   //建立页表
 return - EAGAIN;
vma->vm_ops = &xxx_remap_vm_ops;
xxx_vma_open(vma);
return 0;
}
void xxx_vma_open(struct vm_area_struct *vma) //VMA打开函数
{
  ...
  printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %1x\n",vma->vm_start, vma->vm_pgoff 《PAGE_SHIFT);
}
void xxx_vma_close(struct vm_area_struct *vma)  //VMA关闭函数
{
  ...
  printk(KERN_NOTICE "xxx VMA close. \n");
}
static struct vm_operation_struct xxx_remap_vm_ops = //VM操作结构体
{
.open=xxx_vma_open,
.close=xxx_vma_close,
...
}

上面代码中调用了remap_pfn_range()创建页表,但是我们知道在内核空间用kmalloc申请内存时,这部分内存如果要映射到用户空间可以通过mem_map_reserve()调用设置为保留后进行,具体操作可以参考如下:

// 内核模块加载函数
int __init kmalloc_map_init(void)
{
  ../申请设备号,添加cedv结构体
  buffer = kmalloc(BUF_SIZE, GFP_KERNEL); //申请buffer
  for(page = virt_to_page(buffer); page < virt_to_page(buffer+BUF_SIZE); page++)
  {
     mem_map_reserve(page);  //保留
  }
}
//mmap()函数
static int kmalloc_map_mmap(struct file *filp, struct vm_area_struct *vma)
{
    unsigned long page, pos;
    unsigned long start = (unsigned long)vma->start;
    unsigned long size = (unsigned long)(vma->end - vma->start);
    printk(KERN_INFO, "mmaptest_mmap called\n");
    if(size > BUF_SIZE)  //用户要映射的区域太大
        return - EINVAL;
    pos = (unsigned long)buffer;
    while(size > 0)   //映射buffer中的所有页
    {
        page = virt_to_phys((void *)pos);
        if(remap_page_range(start, page, PAGE_SIZE, PAGE_SHARRED))
            return -EAGAIN;
        start += PAGE_SIZE;
        pos +=PAGE_SIZE;
        size -= PAGE_SIZE;
    }
    return 0;
}


另外通常,IO内存被映射时需要是noncache的,这个时候应该对vma->vm_page_prot设置noncache标志。如下:

 static int xxx_noncache_mmap(struct file *filp, struct vm_area_struct *vma)
    {
      vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);   //赋noncache标志
      vma->vm_pgoff = ((u32)map_start >> PAGE_SHIFT);
      if(remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vm_start, vma->vm_page_prot));
         return - EAGGIN;
      return 0;
    }


这段代码中的pgprot_noncached()是一个宏,它实际上禁止了相关页的cache和写缓冲(write buffer),另外一个稍微少的一些限制的宏是:

#define pgprot_writecombine(prot)  __pgprot(pgprot_val (prot) & –L_PTE_CACHEABLE);    //它则没有禁止写缓冲


而除了remap_pfn_range()外,在驱动程序中实现VMA的nopage()函数通常可以为设备提供更加灵活的内存映射途径。
当发生缺页时,nopage()会被内核自动调用。这是因为,当发生缺页异常时,系统会经过如下处理过程:

  • 找到缺页的虚拟地址所在的VMA
  • 如果必要,分配中间页目录表和页表
  • 如果页表项对应的物理页表不存在,则调用这个VMA的nopage()方法,它返回物理页面的页描述符。
  • 将物理页面的地址填充到页表中。

实现nopage后,用户空间可以通过mremap()系统调用重新绑定映射区所绑定的地址,下面给出一个在设备驱动中使用nopage()的典型范例:

static int xxx_mmap(struct file *filp, struct vm_area_struct *vma);
{
     unsigned long offset = vma->vm_pgoff << PAGE_OFFSET;
     if(offset >= _ _pa(high_memory) || (filp->flags &O_SYNC))
             vma->vm_flags |=VM_IO;
     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;
   unsigned long physaddr = address - vma->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_pf_range()一般用于设备内存映射,而nopage()还可以用于RAM映射。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值