14-将设备地址映射到用户空间
11.4.4 将设备地址映射到用户空间
1.内存映射与VMA
一般情况下,用户空间是不可能也不应该直接访问设备的,但是,设备驱动程序中可实现mmap()函数,这个函数可使得用户空间直能接访问设备的物理地址。实际上,mmap()实现了这样的一个映射过程:它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。
这种能力对于显示适配器一类的设备非常有意义,如果用户空间可直接通过内存映射访问显存的话,屏幕帧的各点的像素将不再需要一个从用户空间到内核空间的复制的过程。
mmap()必须以PAGE_SIZE为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行页对齐,强行以PAGE_SIZE的倍数大小进行映射。
从file_operations文件操作结构体可以看出,驱动中mmap()函数的原型如下:
int(mmap)(struct file , struct vm_area_struct*);
驱动中的mmap()函数将在用户进行mmap()系统调用时最终被调用,mmap()系统调用的原型与file_operations中mmap()的原型区别很大,如下所示:
caddr_t mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);
参数fd为文件描述符,一般由open()返回,fd也可以指定为−1,此时需指定flags参数中的MAP_ANON,表明进行的是匿名映射。
len是映射到调用用户空间的字节数,它从被映射文件开头offset个字节开始算起,offset参数一般设为0,表示从文件头开始映射。
prot参数指定访问权限,可取如下几个值的“或”:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)和PROT_NONE(不可访问)。
参数addr指定文件应被映射到用户空间的起始地址,一般被指定为NULL,这样,选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。其类型caddr_t实际上就是void *。
当用户调用mmap()的时候,内核会进行如下处理。
① 在进程的虚拟空间查找一块VMA。
② 将这块VMA进行映射。
③ 如果设备驱动程序或者文件系统的file_operations定义了mmap()操作,则调用它。
④ 将这个VMA插入进程的VMA链表中。
file_operations中mmap()函数的第一个参数就是步骤①中找到的VMA。
由mmap()系统调用映射的内存可由munmap()解除映射,这个函数的原型如下:
int munmap(caddr_t addr, size_t len );
驱动程序中mmap()的实现机制是建立页表,并填充VMA结构体中vm_operations_struct指针。VMA即vm_area_struct,用于描述一个虚拟内存区域,VMA结构体的定义如代码清单11.5所示。
代码清单11.5 VMA结构体
1 struct vm_area_struct {
2 struct mm_struct vm_mm; / 所处的地址空间 */
3 unsigned long vm_start; / 开始虚拟地址 /
4 unsigned long vm_end; / 结束虚拟地址 /
5
6 pgprot_t vm_page_prot; / 访问权限 /
7 unsigned long vm_flags; / 标志,VM_READ/VM_WRITE/VM_EXEC/VM_SHARED等 /
8 ...
9 / 操作VMA的函数集指针 /
10 struct vm_operations_struct *vm_ops;
11
12 unsigned long vm_pgoff; / 偏移(页帧号) /
13 struct file *vm_file;
14 void *vm_private_data;
15 ...
16 };
VMA结构体描述的虚地址介于vm_start和vm_end之间,而其vm_ops成员指向这个VMA的操作集。针对VMA的操作都被包含在vm_operations_struct结构体中,vm_operations_struct结构体的定义如代码清单11.6所示。
代码清单11.6 vm_operations_struct结构体
1 struct vm_operations_struct {
2 void(open)(struct vm_area_struct area); /打开VMA的函数/
3 void(close)(struct vm_area_struct area); /关闭VMA的函数/
4 struct page (nopage)(struct vm_area_struct *area, unsigned long address,
5 int type); /访问的页不在内存时调用*/
6 int(populate)(struct vm_area_struct area, unsigned long address, unsigned
7 long len, pgprot_t prot, unsigned long pgoff, int nonblock);
8 ...
9 };
在内核生成一个VMA后,它会调用该VMA的open()函数,例如fork一个继承父继承资源的子进程时。但是,当用户进行mmap()系统调用后,尽管VMA在设备驱动文件操作结构体的mmap()被调用前就已产生,内核却不会调用VMA的open()函数,通常需要在驱动的mmap()函数中显示调用vma->vm_ops->open()。代码清单11.7给出了一个vm_operations_struct的操作范例。
代码清单11.7 vm_operations_struct操作范例
1 static int xxx_mmap(struct file filp, struct vm_area_struct vma)
2 {
3 if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff, vma->vm_end - vma
4 ->vm_start, vma->vm_page_prot)) / 建立页表 /
5 return - EAGAIN;
6 vma->vm_ops = &xxx_remap_vm_ops;
7 xxx_vma_open(vma);
8 return 0;
9 }
10
11 void xxx_vma_open(struct vm_area_struct vma) / VMA打开函数 */
12 {
13 ...
14 printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %lx\n", vma->vm_start,
15 vma->vm_pgoff << PAGE_SHIFT);
16 }
17
18 void xxx_vma_close(struct vm_area_struct *vma) //VMA关闭函数
19 {
20 ...
21 printk(KERN_NOTICE "xxx VMA close.\n");
22 }
23
24 static struct vm_operations_struct xxx_remap_vm_ops = { / VMA操作结构体 /
25 .open = xxx_vma_open,
26 .close = xxx_vma_close,
27 ...
28 };
第3行调用的remap_pfn_range()创建页表,以VMA结构体的成员(VMA的数据成员是内核根据用户的请求自己填充的)作为remap_pfnrange()的参数,映射的虚拟地址范围是vma->vm start至vma->vm_end。
remap_pfn_range()函数的原型如下:
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot);
其中的addr参数表示内存映射开始处的虚拟地址。remap_pfn_range()函数为addr~addr+size之间的虚拟地址构造页表。
pfn是虚拟地址应该映射到的物理地址的页帧号,实际上就是物理地址右移PAGE_SHIFT位。若PAGE_SIZE为4KB,则PAGE_SHIFT为12,因为PAGE_SIZE等于1<<PAGE_SHIFT。
prot是新页所要求的保护属性。
在驱动程序中,我们能使用remap_pfn_range()映射内存中的保留页(如X86系统中的640KB~1MB区域)和设备I/O内存,另外,kmalloc()申请的内存若要被映射到用户空间可以通过memmap reserve()设置为保留后进行。代码清单11.8给出了映射kmalloc()申请的内存到用户空间的典型范例。
代码清单11.8 映射kmalloc()申请的内存到用户空间范例
1 /内核模块加载函数/
2 int _ _init kmalloc_map_init(void)
3 {
4 ...
5 / 申请设备号、添加cdev结构体 /
6 buffer = kmalloc(BUF_SIZE, GFP_KERNEL); //申请buffer
7
8 for (page = virt_to_page(buffer); page < virt_to_page(buffer + BUF_SIZE);
9 page++)
10 mem_map_reserve(page); / 置页为保留 /
11 }
12 /mmap()函数/
13 static int kmalloc_map_mmap(struct file filp, struct vm_area_struct vma)
14 {
15 unsigned long page, pos;
16 unsigned long start = (unsigned long)vma->vm_start;
17 unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
18 printk(KERN_INFO "mmaptest_mmap called\n");
19 / 用户要映射的区域太大 /
20 if (size > BUF_SIZE)
21 return - EINVAL;
22
23 pos = (unsigned long)buffer;
24 / 映射buffer中的所有页 /
25 while (size > 0) {
26 / 每次映射一页 /
27 page = virt_to_phys((void*)pos);
28 if (remap_page_range(start, page, PAGE_SIZE, PAGE_SHARED))
29 return - EAGAIN;
30 start += PAGE_SIZE;
31 pos += PAGE_SIZE;
32 size -= PAGE_SIZE;
33 } return 0;
34 }
第28行调用remap_page_range(start, page, PAGE_SIZE, PAGESHARED)的第4个参数PAGE SHARED实际上是_PAGE_PRESENT | _PAGE_USER | _PAGE_RW,表明可读写并映射到用户空间。
通常,I/O内存被映射时需要是nocache的,这时候,我们应该对vma->vm_page_prot设置nocache标志之后再映射,如代码清单11.9所示。
代码清单11.9 以nocache方式将内核空间映射到用户空间
1 static int xxx_nocache_mmap(struct file filp, struct vm_area_struct vma)
2 {
3 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);/ 赋nocache标志 /
4 vma->vm_pgoff = ((u32)map_start >> PAGE_SHIFT);
5 / 映射 /
6 if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma
7 ->vm_start, vma->vm_page_prot))
8 return - EAGAIN;
9 return 0;
10 }
上述代码第3行的pgprot_noncached()是一个宏,它高度依赖于CPU体系结构,ARM的pgprot_noncached()定义如下:
define pgprot_noncached(prot) __pgprot(pgprot_val(prot) & ~(L_PTE_CACHEABLE | L_PTE_BUFFERABLE))
另一个比pgprot_noncached()稍微少一些限制的宏是pgprot_writecombine(),它的定义如下:
define pgprot_writecombine(prot) __pgprot(pgprot_val(prot) & ~L_PTE_CACHEABLE)
pgprot_noncached()实际禁止了相关页的Cache和写缓冲(write buffer),pgprot_writecombine()则没有禁止写缓冲。ARM的写缓冲器是一个非常小的FIFO存储器,位于处理器核与主存之间,其目的在于将处理器核和Cache从较慢的主存写操作中解脱出来。写缓冲区与Cache在存储层次上处于同一层次,但是它只作用于写主存。
2.nopage()函数
除了remap_pfn_range()以外,在驱动程序中实现VMA的nopage()函数通常可以为设备提供更加灵活的内存映射途径。当访问的页不在内存,即发生缺页异常时,nopage()会被内核自动调用。这是因为,当发生缺页异常时,系统会经过如下处理过程。
(1)找到缺页的虚拟地址所在的VMA。
(2)如果必要,分配中间页目录表和页表。
(3)如果页表项对应的物理页面不存在,则调用这个VMA的nopage()方法,它返回物理页面的页描述符。
(4)将物理页面的地址填充到页表中。
实现nopage()后,用户空间可以通过mremap()系统调用重新绑定映射区域所绑定的地址,代码清单11.10给出了一个设备驱动中使用nopage()的典型范例。
代码清单11.10 nopage()函数使用范例
1 static int xxx_mmap(struct file filp, struct vm_area_struct vma)
2 {
3 unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
4 if (offset >= _ _pa(high_memory) || (filp->f_flags &O_SYNC))
5 vma->vm_flags |= VM_IO;
6 vma->vm_flags |= VM_RESERVED; / 预留 /
7 vma->vm_ops = &xxx_nopage_vm_ops;
8 xxx_vma_open(vma);
9 return 0;
10 }
11
12 struct page xxx_vma_nopage(struct vm_area_struct vma, unsigned long
13 address, int *type)
14 {
15 struct page *pageptr;
16 unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
17 unsigned long physaddr = address - vma->vm_start + offset; / 物理地址 /
18 unsigned long pageframe = physaddr >> PAGE_SHIFT; / 页帧号 /
19 if (!pfn_valid(pageframe)) / 页帧号有效? /
20 return NOPAGE_SIGBUS;
21 pageptr = pfn_to_page(pageframe); / 页帧号->页描述符 /
22 get_page(pageptr); / 获得页,增加页的使用计数 /
23 if (type)
24 *type = VM_FAULT_MINOR;
25 return pageptr; /返回页描述符 /
26 }
上述函数对常规内存进行映射,返回一个页描述符,可用于扩大或缩小映射的内存区域。
由此可见,nopage()与remap_pfn_range()的一个较大区别在于remap_pfn_range()一般用于设备内存映射,而nopage()还可用于RAM映射,其调用发生在缺页异常时。