内核内存有时需要重新映射,从内核到用户空间,或者从内核到内核空间。常见的用例是将内核内存重新映射到用户空间,但是当您需要访问高端内存时,也有其他情况。
kmap
Linux内核将其地址空间的896 MB永久地映射到低896 MB的物理内存(低端内存)。在一个4 GB的系统上,内核只剩下128 MB来映射剩余的3.2 GB物理内存(高端内存)。低端内存是直接由内核寻址,因为是永久的和一对一的映射。当涉及到高端内存(大于896 MB的内存)时,内核必须将请求的高端内存区域映射到其地址空间,前面提到的128 MB是专门为此预留的。用于执行此技巧的函数kmap()。kmap()用于将给定的页面映射到内核地址空间:
void *kmap(struct page *page);
page是一个指向要映射的页面结构体的指针。当分配一个高端内存页时,不能直接寻址它。kmap()是必须调用的函数,用于将高端内存临时映射到内核地址空间。这个映射将持续到 kunmap() 被调用:
void kunmap(struct page *page);
所谓临时,我的意思是映射应该在不再需要时立即撤消。记住,128MB不足以映射3.2 GB。最佳编程实践是在不再需要时取消对高端内存的映射。这就是为什么每次访问高端内存页面时【kmap() - kunmap()】序列都被调用的原因。
此函数可在高端内存和低端内存上工作。也就是说,如果页面结构驻留在低端内存中,那么只返回页面的虚拟地址(因为低端内存页面已经有永久的映射)。如果该页属于高端内存,则在内核的页表中创建一个永久映射,并返回地址:
1 void *kmap(struct page *page) 2 { 3 BUG_ON(in_interrupt()); 4 if (!PageHighMem(page)) 5 return page_address(page); 6 7 return kmap_high(page); 8 }
将内核内存映射到用户空间
物理地址映射是最有用的功能之一,特别是在嵌入式系统中。有时,您可能希望与用户空间共享部分内核内存。如前所述,CPU在用户空间中运行时以非特权模式运行。为了让一个进程访问一个内核内存区域,我们需要将该区域重新映射到进程地址空间。
使用remap_pfn_range
remap_pfn_range() 将物理内存(通过内核逻辑地址)映射到用户空间进程。它对于实现mmap()系统调用特别有用。
在对一个文件(无论它是否是一个设备文件)调用mmap()系统调用之后,CPU将切换到特权模式并运行相应的 file_operations.mmap() 内核函数,该内核函数将依次调用 remap_pfn_range()。映射区域的内核PTE将被导出并赋予进程,当然使用不同的保护标志。使用一个新的VMA条目(具有适当的属性)更新进程的VMA列表,该进程将使用PTE访问相同的内存。
因此,内核只是复制PTEs,而不是通过复制来浪费内存。但是,内核和用户空间PTEs具有不同的属性。remap_pfn_range() 的原型如下:
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, unsigned long pfn, unsigned long size, pgprot_t flags);
一个成功的调用将返回0,如果失败将返回一个否定的错误代码。remap_pfn_range()的大多数参数是在mmap()方法被调用时提供的:
- vma: 这是内核在调用 file_operations.mmap() 时提供的虚拟内存区域。它对应于应该进行映射的用户进程vma。
- addr: 这是VMA应当开始的用户虚拟地址(vma->vm_start),这将完成 addr 和 addr + size 之间的虚拟地址范围的映射。
- pfn: 表示要映射的内核内存区域的PFN。它对应于 PAGE_SHIFT 位右移的物理地址。应该考虑vma偏移量(映射必须开始的对象偏移量)以产生PFN。因为VMA结构的 vm_pgoff 字段包含页面数形式的偏移值,所以它正是您需要(使用PAGE_SHIFT左移)以字节形式提取偏移值的:offset = VMA ->vm_pgoff << PAGE_SHIFT)。最后,pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT。
- size: 这是以字节为单位的被重映射的区域的大小。
- flags: 表示新VMA请求的保护。驱动程序可以破坏默认值,但是应该使用在vma->vm_page_prot中找到的值作为使用OR操作符的框架,因为它的一些位已经由用户空间设置了。其中一些flags是:
- VM_IO,它指定设备的内存映射I/O
- VM_DONTCOPY,它告诉内核不要在fork上复制这个vma
- VM_DONTEXPAND,它阻止vma使用mremap(2)展开
- VM_DONTDUMP,它阻止vma包含在核心转储中
你可能需要修改这个值,以便禁用缓存,如果使用这个I/O内存(vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);)。
使用io_remap_pfn_range
当涉及到将I/O内存映射到用户空间时,remap_pfn_range()函数将不再适用。合适的函数是 io_remap_pfn_range(),它们的参数相同。唯一改变的是PFN的来源。它的原型如下:
int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long phys_addr, unsigned long size, pgprot_t prot);
当尝试将I/O内存映射到用户空间时,不需要使用 ioremap()。ioremap()用于内核目的映射(将I/O内存映射到内核地址空间),而 io_remap_pfn_range 用于用户空间目的。
只需将实际的物理I/O地址(通过PAGE_SHIFT向下移动以产生PFN)直接传递给io_remap_pfn_range()。即使在某些体系结构中io_remap_pfn_range()被定义为remap_pfn_range(),但在其他体系结构中却不是这样。出于可移植性的原因,您应该只在PFN参数指向RAM的情况下使用remap_pfn_range(),而在phys_addr指向I/O内存的情况下使用io_remap_pfn_range()。
mmap文件操作
内核 mmap 函数是 struct file_operations 结构的一部分,当用户执行用于将物理内存映射到用户虚拟地址的mmap(2)系统调用时执行该回调函数。内核通过通常的指针解引用将对内存映射区域的任何访问转换为文件操作。甚至可以将设备物理内存直接映射到用户空间(参见/dev/mem)。本质上,写内存就像写文件一样。这只是调用write()的一种更方便的方式。
一般情况下,出于安全考虑,用户空间进程不能直接访问设备内存。因此,用户空间进程使用mmap()系统调用请求内核将设备映射到调用进程的虚拟地址空间。映射完成后,用户空间进程可以通过返回的地址直接写入设备内存。
mmap系统调用声明如下:
mmap (void *addr, size_t len, int prot, int flags, int fd, off_t offset);
驱动程序应该定义mmap文件操作(file_operations.mmap)以支持mmap(2)。在内核方面,驱动程序的文件操作结构(struct file_operations结构)中的mmap字段有以下原型:
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
- filp 是一个指向驱动程序打开的设备文件的指针,该文件是由fd参数转换产生的。
- vma 是由内核作为参数分配和给出的。它是一个指向用户进程vma的指针,映射应该去哪里。为了理解内核是如何创建新的vma的,让我们回顾一下mmap(2)系统调用的原型:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
这个函数的参数以某种方式影响vma的一些字段:
- addr: 这是用户空间的虚拟地址,映射应该从这里开始。它对vma>vm_start有影响。如果指定NULL(最可移植的方式),自动确定正确的地址。
- length: 这指定映射的长度,并间接影响vma->vm_end。记住,vma的大小总是PAGE_SIZE的倍数。换句话说,PAGE_SIZE总是vma可以拥有的最小大小。内核将总是改变vma的大小,因此它是PAGE_SIZE的倍数。
If length <= PAGE_SIZE vma->vm_end - vma->vm_start == PAGE_SIZE. If PAGE_SIZE < length <= (N * PAGE_SIZE) vma->vm_end - vma->vm_start == (N * PAGE_SIZE)
- prot: 这会影响VMA的权限,驱动程序可以在VMA ->vm_pro中找到。如前所述,驱动程序可以更新这些值,但不能更改它们。
- flags: 这决定了驱动程序可以在vma->vm_flags中找到的映射类型。映射可以是私有的,也可以是共享的。
- offset: 指定映射区域内的偏移量,从而改写vma->vm_pgoff的值。
在内核中实现mmap
由于用户空间代码不能访问内核内存,mmap() 函数的目的是派生一个或多个受保护的内核页表项(对应于要映射的内存),并复制用户空间页表,删除内核标志保护,并且设置允许用户访问与内核相同的内存而不需要特殊权限的权限标志。
写mmap文件操作的步骤如下:
- 获取映射偏移量,并检查它是否超出我们的缓冲区大小:
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; if (offset >= buffer_size) return -EINVAL;
2. 检查映射大小是否大于我们的缓冲区大小:
unsigned long size = vma->vm_end - vma->vm_start; if (size > (buffer_size - offset)) return -EINVAL;
3. 获取与缓冲区偏移位置所在页面的PFN对应的PFN:
unsigned long pfn; /* we can use page_to_pfn on the struct page structure * returned by virt_to_page */ /* pfn = page_to_pfn (virt_to_page (buffer + offset)); */ /* Or make PAGE_SHIFT bits right-shift on the physical * address returned by virt_to_phys */ pfn = virt_to_phys(buffer + offset) >> PAGE_SHIFT;
4. 设置适当的标志,无论I/O内存是否存在:
- 使用 vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot) 禁用缓存.
- 设置VM_IO标志: vma->vm_flags |= VM_IO 。
- 防止VMA被换出: vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP 。在3.7以上的内核版本中,应该只使用 VM_RESERVED 标志。
5. 调用 remap_pfn_range,计算PFN、大小和保护标志:
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot)) { return -EAGAIN; } return 0;
6. 将你的mmap函数传递给struct file_operations结构:
static const struct file_operations my_fops = { .owner = THIS_MODULE, [...] .mmap = my_mmap, [...] };