四
一般情况下,用户空间是不可能也不应该直接访问设备的,但是设备驱动程序可实现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
后,它就会调用该VMA
的open()
函数。
而驱动中的mmap()
函数将在用户进行mmap()系统调用时最终被调用,当用户调用mmap()
时候内核会进行如下处理:
- 在进程的虚拟空间查找一块VMA.
- 将这块VMA进行映射。
- 如果设备驱动程序或文件系统的file_operations定义了mmap()操作,则调用它。
- 将这个VMA插入到进程的VMA链表中。
file_operations
中mmap()
函数的第一个参数就是步骤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映射。